zorch.commit.merkle¶
Layer-by-layer k-ary Merkle commitment — scheme-agnostic, on Sponge + Compression.
commit hashes each matrix row to a leaf digest (Sponge), then folds sibling
groups per layer (Compression, whose arity sets the tree's) down to a single
root, returning (raw_root, digest_layers) (leaf digests first, root last).
It adds NO domain separator — that, the proof layout, and the verify error codes
are scheme-specific and live in the consumer (its commitment scheme).
A level whose node count is not a multiple of the arity is completed with
zero digests — the only convention that keeps a k-ary tree well-defined on
power-of-two heights (e.g. 2^k leaves under arity 4 leave a 2-node top level),
and the one the k-ary schemes this seam serves use. The padded form is what
digest_layers stores, so an opening near the boundary can read its zero
siblings like any others. A binary tree on a power-of-two height never pads,
so the arity-2 layout is exactly the historical one.
Each layer is one vmap over its nodes: an internal layer batches one
compress = one permute; the leaf layer batches one hash, which lowers as one
hash_frx.sponge_hash region per leaf (the whole rate-block absorb fused
into a single register-resident kernel, not a per-block permute chain). Those
collapse to one GPU kernel per node-batch once the permutation is captured to a
kernel (the poseidon2 fusion path, #25). The tree folds the layers one
right-sized level at a time (_fold_to_root) — see _build for why this beats a
full-width scan.
commit lowers each leaf hash to a hash_frx.sponge_hash marker and each
fold layer's compress to a hash_frx.poseidon2 permute marker, which the vendor
lowers to kernels directly. Committing by this plain vmap/fold body keeps the fast
per-permute kernels and lowers under symbolic dims for recompile-free export.
Opening
dataclass
¶
A single leaf's authentication path: the committed matrix row plus the
sibling digest at each level (leaf-first, excluding the root).
A pytree (leaves: row and each path sibling) so open / reconstruct_root
batch under frx.vmap and trace under jit.
Source code in zorch/commit/merkle.py
46 47 48 49 50 51 52 53 54 55 56 | |
MerkleTree ¶
A k-ary Merkle commitment over a single matrix.
leaf_hasher squeezes each row to a digest_elems-element leaf; compressor
folds compressor.arity digests into one — the tree's arity follows it.
They must agree on digest size (leaf_hasher.out == compressor.chunk).
Arity 2 keeps the historical path layout (each path entry one sibling,
shape (digest_elems,)); a wider arity carries the whole sibling group per
level, shape (arity-1, digest_elems). The batched reconstruct_roots
fast path stays binary-only — its sole consumer is the binary fold-PCS
query machinery.
column_major (keyword-only, default False) selects the COMMIT-side leaf
layout: False reads a leaf as a matrix row (commit takes
[num_leaves, leaf_width]), True as a matrix column ([leaf_width,
num_leaves]), so a producer whose data is already column-per-leaf commits
it without transposing to leaf-major. It changes only the commit leaf
gather — open/verify/reconstruct are always leaf-major (one leaf per
row), so a column-major consumer hands those the leaf-major matrix (the
commit input's transpose).
Source code in zorch/commit/merkle.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | |
commit ¶
commit(matrix: Array) -> tuple[Array, list[Array]]
Commit a matrix: leaf-major (num_leaves, leaf_width), or
(leaf_width, num_leaves) when column_major (a leaf is a column).
Returns (raw_root (digest_elems,), digest_layers), where digest_layers
runs leaf digests -> ... -> root, each (nodes_at_level, digest_elems)
in the zero-padded form (see the module docstring).
A binary tree keeps its historical power-of-two-height contract — its pad-free layout is what the fold-PCS query machinery indexes; k-ary consumers commit any height via the per-level padding.
hash_leaves + fold_digests are this commit as two halves, for a
consumer that cuts a jit-zone boundary between them (only the leaf
hash's shapes carry the leaf width; see zorch.pcs.jagged.commit).
Source code in zorch/commit/merkle.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
hash_leaves ¶
hash_leaves(matrix: Array) -> Array
Hash each leaf of matrix (layout per column_major, see commit)
to its digest — the (num_leaves, digest_elems) leaf layer of
digest_layers. Validation lives here, where the bad shape enters.
Source code in zorch/commit/merkle.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
fold_digests ¶
fold_digests(
leaf_digests: Array,
) -> tuple[Array, list[Array]]
Fold a (num_leaves, digest_elems) leaf-digest layer (from
hash_leaves) to (raw_root, digest_layers) — commit's second half.
Compresses only each level's live nodes, one right-sized level at a
time — a single scan would carry a full-width buffer and recompress
the zero padding every level (~height× the work, the dominant commit
runtime). The cost is an O(depth) compile: the per-level compresses are
distinct shapes, so they don't share a cubin (#163 traded the other
way); amortized once under the polymorphic compile-many-shards path.
Leaf-layout-independent: column_major affects only the leaf hash.
Source code in zorch/commit/merkle.py
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
open ¶
open(
matrix: Array,
digest_layers: list[Array],
index: int | Array,
) -> Opening
Authentication path for leaf index: its row plus each level's sibling.
matrix is always leaf-major ([num_leaves, leaf_width], one leaf per
row), even when column_major (a commit-side-only flag): a column-major
consumer passes the leaf-major transpose of its commit input here.
Single-index by construction; batch by frx.vmap-ing over index (the
sibling gather is orchestration outside the fused permute, like commit).
Index validity is a prover-side precondition — enforced eagerly for any
concrete index (Python int or 0-d Array), skipped only under tracing,
where the value is unknown and JAX would silently clamp an out-of-range
gather; there verify owns out-of-range rejection.
Source code in zorch/commit/merkle.py
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | |
reconstruct_root ¶
reconstruct_root(
index: int | Array, opening: Opening
) -> Array
Rebuild the raw root from an opening's row + path (leaf-first).
Returns the root Array, not a verdict — a separator-binding consumer
(e.g. SP1's SMCS) rebinds the raw root before comparing, which
verify's plain equality can't express. Single-index; batch by
frx.vmap-ing over (index, opening).
Source code in zorch/commit/merkle.py
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | |
reconstruct_roots ¶
reconstruct_roots(
rows: Array, indices: Array, paths: Array, valid: Array
) -> Array
Rebuild a whole batch of roots in one vmap + one scan.
rows (B, row_width), indices (B,), paths (B, depth, digest)
leaf-first, valid (B, depth) true for the real levels of each path
and false for trailing don't-care padding. Paths shorter than depth
are zero-padded and masked, so a batch can mix Merkle trees of different
height: a masked step keeps the running node, so element b rebuilds the
same root reconstruct_root would on its first valid[b].sum() levels.
Tracing the compress body once for the whole batch — instead of once per
reconstruct_root call — is the point: the folding verifiers reconstruct
one pair per fold layer, and that per-layer loop dominated their
trace+lower (#163).
Source code in zorch/commit/merkle.py
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | |
verify ¶
verify(root: Array, index: int, opening: Opening) -> bool
Rebuild the root from the row + path; compare to the committed root.
Source code in zorch/commit/merkle.py
327 328 329 330 331 | |