diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 267f0b62..7d8323a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,56 @@ jobs: with: rust-version: ${{ env.RUST_VERSION }} + # flock-stage3 is intentionally an isolated Cargo workspace, so the root + # workspace jobs above do not discover it. Keep both its no-prove relation + # and native/circuit differential suite and its real proof vectors on every + # pull request. The proof vectors run serially to bound peak memory while + # exercising heterogeneous relation shapes against Flock's recycled buffers. + flock-stage3-test: + runs-on: warp-ubuntu-latest-x64-16x + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup-rust-toolchain + with: + cache-workspaces: flock-stage3 + - name: Check Stage 3 Rustfmt style + run: cargo fmt --manifest-path flock-stage3/Cargo.toml --all -- --check + - name: Check Stage 3 clippy warnings + run: cargo clippy --release --locked --manifest-path flock-stage3/Cargo.toml --workspace --all-targets -- -D warnings + - name: Test Stage 3 relation and differential checks + run: cargo test --release --locked --manifest-path flock-stage3/Cargo.toml --workspace --lib + - name: Test Stage 3 cryptographic proof vectors + run: cargo test --release --locked --manifest-path flock-stage3/Cargo.toml -p flock-stage3-host --lib -- --ignored --test-threads=1 + + # The isolated host job cannot catch drift in the Lean extern signature or + # failures that appear only when the CLI combines its net and flock features. + # Reuse the base Lake/Cargo work from `build` and link that exact opt-in path. + flock-stage3-link: + needs: [build, flock-stage3-test] + runs-on: warp-ubuntu-latest-x64-16x + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup-rust-toolchain + - uses: actions/cache/restore@v6 + with: + path: ./.lake + key: lake-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lake-manifest.json') }}-${{ github.sha }} + restore-keys: lake-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('lake-manifest.json') }}- + - uses: leanprover/lean-action@v1 + with: + auto-config: false + use-github-cache: false + - name: Link Flock-enabled ix CLI and fixture harness + run: IX_FLOCK=1 lake build ix bench-flock-root-fixture + - name: Verify persisted current-protocol aggregate fixture + run: | + .lake/build/bin/bench-flock-root-fixture --verify Tests/Fixtures/Aggregate/singleton-2026-09-05 + .lake/build/bin/bench-flock-root-fixture --min-opening-width --verify Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05 + - name: Test Flock CLI JSONL and artifact safety + run: | + ulimit -v 16777216 + lake env lean --run Tests/FlockRootCli.lean + # Compile and link the opt-in backend without requiring a GPU. Runtime and # proof-byte equivalence are covered by multi-stark's NVIDIA smoke suite. cuda-compile: diff --git a/Benchmarks/FlockRootFixture.lean b/Benchmarks/FlockRootFixture.lean new file mode 100644 index 00000000..71514927 --- /dev/null +++ b/Benchmarks/FlockRootFixture.lean @@ -0,0 +1,297 @@ +import Ix.Cli.FlockRootCmd +import Ix.Aiur.Statistics +import Ix.Benchmark.Bench +import Ix.TracingTexray + +/-! +A small, genuine production-protocol `ix_aggr` root for Stage 3 measurements. +The environment has one well-formed axiom declaration, not a toy verifier +circuit. Both systems use the CLI's canonical parameters. No store or cache +is accessed. Outputs go into a newly created directory, never an existing one. + +The default run proves the tiny IxVM child and only EXECUTES its aggregate +wrap. `--prove` also proves, verifies, and persists the aggregate. Require a +finite Linux address-space limit of at most 64 GiB in either mode: recursive +prover RAM estimates were calibrated on an older protocol, so they are not a +safe absolute budget for this measurement. Allocation failure can terminate +the subprocess; an incomplete directory is not a completed fixture. +`--verify DIRECTORY` checks a completed fixture in a fresh process without +proving, modifying files, or requiring the generation-only memory cap. +`--min-opening-width` explicitly selects the experimental aggregate lookup +packing profile in any mode. The default profile/keys remain unchanged. +`--count DIRECTORY` verifies the fixture, then counts Stage 3 with a one-byte +witness admission limit, so it cannot compile wiring or invoke a prover. +-/ + +open Lean (Json toJson) + +namespace Benchmarks.FlockRootFixture + +private def require (condition : Bool) (message : String) : IO Unit := do + unless condition do throw <| IO.userError message + +private def timed (label : String) (action : Unit → Except String α) : IO (α × Nat) := do + IO.eprintln s!"[flock-root-fixture] {label}" + let start ← IO.monoNanosNow + let result ← IO.ofExcept (← blackBoxIO action ()) + return (result, ((← IO.monoNanosNow) - start) / 1000) + +private def memoryCap : IO Nat := do + let limits ← IO.FS.readFile "/proc/self/limits" + let some line := (limits.splitOn "\n").find? (·.startsWith "Max address space") + | throw <| IO.userError "could not read the process address-space limit" + let fields := line.splitOn " " |>.filter (!·.isEmpty) + let cap := (fields[3]?).bind String.toNat? + let some cap := cap + | throw <| IO.userError "set a finite process memory cap: ulimit -v 67108864 (64 GiB)" + require (cap > 0 && cap ≤ 64 * 1024 ^ 3) + "the process address-space limit must be positive and at most 64 GiB" + return cap + +private def singletonEnv : Ixon.Env × Address := + let constant : Ixon.Constant := + ⟨.axio ⟨false, 0, .sort 0⟩, #[], #[], #[.succ .zero]⟩ + let address := Address.blake3 (Ixon.serConstant constant) + (({} : Ixon.Env).storeConst address constant, address) + +private def compile (source : Except Aiur.Global Aiur.Source.Toplevel) : + Except String Aiur.CompiledToplevel := do + let top ← source.mapError toString + top.compile.mapError toString + +private def writeBytes (directory : System.FilePath) (name : String) + (bytes : ByteArray) : IO Json := do + IO.FS.writeBinFile (directory / name) bytes + return Json.mkObj [("file", toJson name), ("bytes", toJson bytes.size), + ("blake3", toJson (toString (Address.blake3 bytes)))] + +private def circuitRow (stats : Aiur.CircuitStats) : Json := + Json.mkObj [("name", toJson stats.name), ("height", toJson stats.height), + ("committed_width", toJson stats.width), ("cache_hits", toJson stats.cacheHits)] + +private def lookupPolicy (minOpeningWidth : Bool) : String := + if minOpeningWidth then "min-opening-width-v1" else "legacy" + +private def aggregateSystem (compiled : Aiur.CompiledToplevel) + (minOpeningWidth : Bool) : Aiur.AiurSystem := + let recursion := MultiStark.defaultRecursionParameters + if minOpeningWidth then + Aiur.AiurSystem.buildMinOpeningWidth compiled.bytecode recursion.commitment recursion.fri + else MultiStark.buildRecursionSystem compiled.bytecode recursion + +/-- Fresh-process verification is deliberately cheap enough for CI. Validate +the persisted transport and the exact singleton subject, not just a proof of +an arbitrary self-reported bundled claim. -/ +private def verify (directory : System.FilePath) (minOpeningWidth count : Bool) : IO Unit := do + if count then discard memoryCap + let manifestBytes ← Ix.Cli.FlockRootCmd.readBounded (directory / "fixture.json") (1024 ^ 2) + let some manifestText := String.fromUTF8? manifestBytes + | throw <| IO.userError "fixture manifest is not UTF-8" + let manifest ← IO.ofExcept (Json.parse manifestText) + require ((← IO.ofExcept (manifest.getObjValAs? String "schema")) == "ix.flock-stage3.root-fixture") + "unexpected fixture schema" + require ((← IO.ofExcept (manifest.getObjValAs? Nat "version")) == 1) "unexpected fixture version" + require (← IO.ofExcept (manifest.getObjValAs? Bool "aggregate_proven")) "fixture is execution-only" + let policy ← match manifest.getObjVal? "aggregate_lookup_policy" with + | .ok json => IO.ofExcept (Lean.fromJson? json : Except String String) + | .error _ => pure "legacy" + require (policy == lookupPolicy minOpeningWidth) + "fixture lookup policy differs from the explicitly requested profile" + let inputs ← IO.ofExcept (manifest.getObjValAs? (Array Json) "inputs") + let records := inputs.push (← IO.ofExcept (manifest.getObjVal? "child")) + |>.push (← IO.ofExcept (manifest.getObjVal? "root")) + let names := #["environment.ixe", "check-env.claim", "subjects.tree", "ixvm.vk", + "aggr.vk", "outer-claim.bin", "ixvm.ixon-proof", "root.ixon-proof"] + require (records.size == names.size) "unexpected fixture file count" + let mut files : Std.HashMap String ByteArray := {} + for (record, name) in records.zip names do + -- Only literal filenames may select a read; never follow a manifest path. + require ((← IO.ofExcept (record.getObjValAs? String "file")) == name) "unexpected fixture filename" + let bytes ← Ix.Cli.FlockRootCmd.readBounded (directory / name) (64 * 1024 ^ 2) + require ((← IO.ofExcept (record.getObjValAs? Nat "bytes")) == bytes.size) s!"{name}: size changed" + require ((← IO.ofExcept (record.getObjValAs? String "blake3")) == toString (Address.blake3 bytes)) + s!"{name}: digest changed" + files := files.insert name bytes + -- Verify the same bounded, digest-checked bytes; do not re-open mutable + -- paths between transport validation and native proof verification. + let read := fun name => files[name]! + let (env, owned) := singletonEnv + require (read "environment.ixe" == (← IO.ofExcept (Ixon.serEnv env))) + "fixture environment is not the expected singleton" + let (claim, trees) ← IO.ofExcept (IxVM.ClaimHarness.shardCheckEnvClaimTrees env #[owned]) + let statement ← IO.ofExcept (MultiStark.CheckEnvTrees.ofClaim claim trees) + require (read "subjects.tree" == statement.subjects.ser) + "fixture subject tree changed" + let claimBytes := Ix.Claim.ser claim + require (read "check-env.claim" == claimBytes) "fixture claim changed" + let (ixvmSystem, compiled) ← IO.ofExcept (← Ix.Cli.VerifyCmd.buildBackend) + let some verifyIdx := compiled.getFuncIdx `verify_claim + | throw <| IO.userError "missing verify_claim entrypoint" + require (read "ixvm.vk" == ixvmSystem.vkBytes) "IxVM key changed" + let aggrCompiled ← IO.ofExcept (compile Aggr.ixAggr) + let some aggrIdx := aggrCompiled.getFuncIdx `ix_aggr + | throw <| IO.userError "missing ix_aggr entrypoint" + let aggrSystem := aggregateSystem aggrCompiled minOpeningWidth + require (read "aggr.vk" == aggrSystem.vkBytes) "aggregate key changed" + let allowed := Aggr.allowedBlob ixvmSystem.vkBytes verifyIdx aggrSystem.vkBytes aggrIdx + let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim allowed aggrIdx claim + require (read "outer-claim.bin" == + Ix.Cli.FlockRootCmd.outerClaimBytes outerClaim) "aggregate outer claim changed" + let innerClaim := Aiur.buildClaim verifyIdx + (IxVM.ClaimHarness.packedDigestKey (Address.blake3 claimBytes)) #[] + for (name, system, expected) in + [("ixvm.ixon-proof", ixvmSystem, innerClaim), ("root.ixon-proof", aggrSystem, outerClaim)] do + let wrapper ← IO.ofExcept (Ixon.Proof.de (read name)) + require (wrapper.claim == claim) s!"{name}: bundled claim changed" + let proof ← IO.ofExcept (Aiur.Proof.ofBytesChecked wrapper.proof) + IO.ofExcept (system.verify expected proof) + let verified := "Flock root fixture digests, subjects, keys, and native proofs verified" + if count then IO.eprintln verified else IO.println verified + if count then + let wrapper ← IO.ofExcept (Ixon.Proof.de (read "root.ixon-proof")) + let outcome : Except String String ← try + pure (.ok (← Aiur.flockStage3AggregateRoot + aggrSystem.vkBytes (Ix.Cli.FlockRootCmd.outerClaimBytes outerClaim) + wrapper.proof MultiStark.defaultRecursionParameters.fri "preflight" "" "" + "{\"max_table_capacity\":4294967296,\"max_union_witness_bytes\":1}")) + catch error => pure (.error error.toString) + match outcome with + | .ok _ => throw <| IO.userError "count-only mode unexpectedly passed one-byte witness admission" + | .error message => + require (message.startsWith "Stage 3 padded union witness requires ") message + IO.println <| (Json.mkObj [("schema", toJson "ix.flock-stage3.fixture-count"), + ("version", toJson (1 : Nat)), ("aggregate_lookup_policy", toJson policy), + ("compiled", toJson false), ("admission_error", toJson message)]).compress + +private def generate (directory : System.FilePath) (prove minOpeningWidth : Bool) : IO Unit := do + let cap ← memoryCap + -- createDir is exclusive: an existing directory, including a symlink, + -- fails before any output is written. Never create the user's store. + IO.FS.createDir directory + TracingTexray.startSampler 10 + let (env, owned) := singletonEnv + let envBytes ← IO.ofExcept (Ixon.serEnv env) + let handle ← IO.ofExcept (Aiur.EnvHandle.fromBytes envBytes) + let (claim, trees) ← IO.ofExcept (IxVM.ClaimHarness.shardCheckEnvClaimTrees env #[owned]) + IO.ofExcept (Ix.Cli.FlockRootCmd.validateBundledClaim claim) + let statement ← IO.ofExcept (MultiStark.CheckEnvTrees.ofClaim claim trees) + let audited ← IO.ofExcept <| Ix.Cli.VerifyCmd.auditAggregateConstants env + (Ix.Cli.AggregateCmd.toAggrCheckEnvTrees statement) + require (audited == 1) "fixture must certify exactly one environment constant" + let claimBytes := Ix.Claim.ser claim + let environmentFile ← writeBytes directory "environment.ixe" envBytes + let claimFile ← writeBytes directory "check-env.claim" claimBytes + let subjectsFile ← writeBytes directory "subjects.tree" statement.subjects.ser + let (ixvmCompiled, ixvmCompileUs) ← timed "compile IxVM" fun _ => compile IxVM.ixVM + let (aggrCompiled, aggrCompileUs) ← timed "compile ix_aggr" fun _ => compile Aggr.ixAggr + let some verifyIdx := ixvmCompiled.getFuncIdx `verify_claim + | throw <| IO.userError "missing verify_claim entrypoint" + let some aggrIdx := aggrCompiled.getFuncIdx `ix_aggr + | throw <| IO.userError "missing ix_aggr entrypoint" + let recursion := MultiStark.defaultRecursionParameters + let ixvmSystem := Aiur.AiurSystem.build ixvmCompiled.bytecode + Aiur.defaultCommitmentParameters Aiur.defaultFriParameters + let aggrSystem := aggregateSystem aggrCompiled minOpeningWidth + let ixvmVk := ixvmSystem.vkBytes + let aggrVk := aggrSystem.vkBytes + let allowed := Aggr.allowedBlob ixvmVk verifyIdx aggrVk aggrIdx + let ixvmVkFile ← writeBytes directory "ixvm.vk" ixvmVk + let aggrVkFile ← writeBytes directory "aggr.vk" aggrVk + let innerClaim := Aiur.buildClaim verifyIdx + (IxVM.ClaimHarness.packedDigestKey (Address.blake3 claimBytes)) #[] + TracingTexray.resetPeakTreeRss + let (inner, ixvmProveUs) ← timed "prove IxVM child (2 GiB model budget)" fun _ => + ixvmSystem.shardProveWithEnv verifyIdx handle owned.hash (2 * 1024 ^ 3) + require (inner.claimBytes == claimBytes) "native shard claim differs from host reconstruction" + let some innerProof := inner.proof + | throw <| IO.userError s!"IxVM child exceeds budget: predicted {inner.peakBytes} bytes" + let ixvmPeak ← TracingTexray.peakTreeRssBytes + let (_, ixvmVerifyUs) ← timed "verify IxVM child" fun _ => + ixvmSystem.verify innerClaim innerProof + let childFile ← writeBytes directory "ixvm.ixon-proof" + (Ixon.Proof.ser { claim, proof := innerProof.toBytes }) + let advice ← IO.ofExcept (ixvmSystem.proofToAdviceBytes innerClaim innerProof) + let childClaims := MultiStark.serializeClaims #[innerClaim] + let pubInput := Aggr.pubInput allowed claimBytes + let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim allowed aggrIdx claim + let outerFile ← writeBytes directory "outer-claim.bin" + (Ix.Cli.FlockRootCmd.outerClaimBytes outerClaim) + TracingTexray.resetPeakTreeRss + let ((output, queries), aggrExecuteUs) ← timed "execute ix_aggr wrap (no aggregate proof yet)" fun _ => + aggrCompiled.bytecode.executeIxAggr aggrIdx pubInput 0 + advice ByteArray.empty ixvmVk aggrVk childClaims ByteArray.empty + claimBytes allowed (Aggr.preimagesBlob #[]) (Aggr.treesBlob #[]) (Aggr.pathsBlob #[]) + require (Aiur.buildClaim aggrIdx pubInput output == outerClaim) + "aggregate execution produced an unexpected outer claim" + let aggrExecutePeak ← TracingTexray.peakTreeRssBytes + let stats := Aiur.computeStats aggrCompiled queries aggrSystem.circuitShapes + recursion.commitment.logBlowup + let base := [("schema", toJson "ix.flock-stage3.root-fixture"), ("version", toJson (1 : Nat)), + ("lean_toolchain", toJson Lean.versionString), + ("description", toJson "single well-formed axiom declaration; production ix_aggr shape-0 wrap"), + ("aggregate_lookup_policy", toJson (lookupPolicy minOpeningWidth)), + ("active_committed_width", toJson <| stats.circuits.foldl + (fun total circuit => total + if circuit.height == 0 then 0 else circuit.width) 0), + ("subject", toJson (toString owned)), ("bundled_claim", toJson (toString claim)), + ("address_space_limit_bytes", toJson cap), + ("fri", Json.mkObj [("num_queries", toJson recursion.fri.numQueries), + ("query_pow_bits", toJson recursion.fri.queryProofOfWorkBits), + ("commit_pow_bits", toJson recursion.fri.commitProofOfWorkBits), + ("max_log_arity", toJson recursion.fri.maxLogArity), + ("log_final_poly_len", toJson recursion.fri.logFinalPolyLen), + ("log_blowup", toJson recursion.commitment.logBlowup)]), + ("inputs", Json.arr #[environmentFile, claimFile, subjectsFile, ixvmVkFile, aggrVkFile, outerFile]), + ("child", childFile), ("ixvm_compile_us", toJson ixvmCompileUs), + ("aggr_compile_us", toJson aggrCompileUs), ("ixvm_prove_us", toJson ixvmProveUs), + ("ixvm_verify_us", toJson ixvmVerifyUs), ("ixvm_predicted_peak_bytes", toJson inner.peakBytes), + ("ixvm_sampled_peak_rss_bytes", toJson ixvmPeak), + ("aggr_execute_us", toJson aggrExecuteUs), + ("aggr_execute_sampled_peak_rss_bytes", toJson aggrExecutePeak), + ("aggregate_circuits", Json.arr (stats.circuits.map circuitRow))] + IO.FS.writeFile (directory / "execution.json") + ((Json.mkObj (base ++ [("aggregate_proven", toJson false)])).pretty ++ "\n") + unless prove do + IO.println s!"Aggregate execution passed; measurements: {directory / "execution.json"}" + return + TracingTexray.resetPeakTreeRss + let ((provedClaim, proof), aggrProveUs) ← timed "prove ix_aggr wrap (process memory cap enforced)" fun _ => + aggrSystem.proveIxAggr aggrIdx pubInput 0 + advice ByteArray.empty ixvmVk aggrVk childClaims ByteArray.empty + claimBytes allowed (Aggr.preimagesBlob #[]) (Aggr.treesBlob #[]) (Aggr.pathsBlob #[]) + let aggrProvePeak ← TracingTexray.peakTreeRssBytes + require (provedClaim == outerClaim) "aggregate proof produced an unexpected outer claim" + let (_, aggrVerifyUs) ← timed "verify ix_aggr wrap" fun _ => aggrSystem.verify outerClaim proof + let rootFile ← writeBytes directory "root.ixon-proof" + (Ixon.Proof.ser { claim, proof := proof.toBytes }) + -- Re-read the persisted transport, not just the in-memory proof handle. + let wrapper ← IO.ofExcept (Ixon.Proof.de (← IO.FS.readBinFile (directory / "root.ixon-proof"))) + require (wrapper.claim == claim) "persisted aggregate claim changed" + let rereadProof ← IO.ofExcept (Aiur.Proof.ofBytesChecked wrapper.proof) + IO.ofExcept (aggrSystem.verify outerClaim rereadProof) + IO.FS.writeFile (directory / "fixture.json") + ((Json.mkObj (base ++ [("aggregate_proven", toJson true), ("root", rootFile), + ("aggr_prove_us", toJson aggrProveUs), ("aggr_verify_us", toJson aggrVerifyUs), + ("aggr_prove_sampled_peak_rss_bytes", toJson aggrProvePeak)])).pretty ++ "\n") + IO.println s!"Verified current-protocol aggregate: {directory / "root.ixon-proof"}" + +end Benchmarks.FlockRootFixture + +def main (args : List String) : IO UInt32 := do + try + let minOpeningWidth := args.contains "--min-opening-width" + let args := args.erase "--min-opening-width" + if let ["--verify", directory] := args then + Benchmarks.FlockRootFixture.verify directory minOpeningWidth false + return 0 + if let ["--count", directory] := args then + Benchmarks.FlockRootFixture.verify directory minOpeningWidth true + return 0 + let (directory, prove) ← match args with + | ["--output", directory] => pure (directory, false) + | ["--output", directory, "--prove"] => pure (directory, true) + | _ => throw (IO.userError "usage: bench-flock-root-fixture [--min-opening-width] (--output NEW_DIRECTORY [--prove] | --verify DIRECTORY | --count DIRECTORY)") + Benchmarks.FlockRootFixture.generate directory prove minOpeningWidth + return 0 + catch error => + IO.eprintln s!"flock-root-fixture: {error}" + return 1 diff --git a/Cargo.lock b/Cargo.lock index 2d7e0208..1a278fb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -54,7 +54,7 @@ dependencies = [ "indexmap", "libc", "multi-stark", - "num-bigint 0.4.6", + "num-bigint 0.4.8", "rayon", "rustc-hash", "tracing", @@ -69,18 +69,18 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ar_archive_writer" @@ -91,27 +91,21 @@ dependencies = [ "object", ] -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -154,9 +148,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backon" @@ -192,7 +186,16 @@ name = "bignat" version = "0.1.0" source = "git+https://github.com/argumentcomputer/lean-ffi.git?rev=93c7e52952ae94546be08313f4ff3922984c84d5#93c7e52952ae94546be08313f4ff3922984c84d5" dependencies = [ - "num-bigint 0.4.6", + "num-bigint 0.4.8", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", ] [[package]] @@ -231,28 +234,27 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", - "syn", + "shlex 1.3.0", + "syn 2.0.119", ] [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "blake3" -version = "1.8.5" +version = "1.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" dependencies = [ - "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", ] [[package]] @@ -284,9 +286,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -296,18 +298,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.61" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -327,9 +329,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -338,15 +340,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -366,9 +368,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", @@ -407,9 +409,9 @@ dependencies = [ [[package]] name = "cordyceps" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" dependencies = [ "loom", "tracing", @@ -432,9 +434,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -447,18 +449,18 @@ checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -475,9 +477,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -497,9 +499,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -539,7 +541,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -563,7 +565,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.119", ] [[package]] @@ -574,14 +576,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -594,15 +596,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "pem-rfc7468", @@ -614,9 +616,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_builder" @@ -636,7 +635,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -646,7 +645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.119", ] [[package]] @@ -668,7 +667,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.119", "unicode-xid", ] @@ -696,7 +695,7 @@ checksum = "afa94b64bfc6549e6e4b5a3216f22593224174083da7a90db47e951c4fb31725" dependencies = [ "block-buffer 0.11.0", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] @@ -713,13 +712,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -771,9 +770,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "embedded-io" @@ -796,18 +795,18 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "enum-assoc" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +checksum = "0590c4a94da3372e83493b956755a6e2266830b6e4e3b101afe66e3f39477b91" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -843,7 +842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -854,15 +853,15 @@ checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" dependencies = [ "getrandom 0.3.4", "libm", - "rand 0.9.4", + "rand 0.9.5", "siphasher", ] [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fiat-crypto" @@ -872,9 +871,52 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flock-core" +version = "0.1.0" +source = "git+https://github.com/succinctlabs/flock?rev=b310f35f35f68095537150a1c8c0a43caca9a29e#b310f35f35f68095537150a1c8c0a43caca9a29e" +dependencies = [ + "bincode 1.3.3", + "blake3", + "rand_core 0.9.5", + "rayon", + "serde", + "sha2 0.10.9", + "toml", +] + +[[package]] +name = "flock-prover" +version = "0.1.0" +source = "git+https://github.com/succinctlabs/flock?rev=b310f35f35f68095537150a1c8c0a43caca9a29e#b310f35f35f68095537150a1c8c0a43caca9a29e" +dependencies = [ + "bincode 1.3.3", + "blake3", + "flock-core", + "rayon", + "serde", + "sha2 0.10.9", +] + +[[package]] +name = "flock-stage3-host" +version = "0.1.0" +dependencies = [ + "aiur", + "anyhow", + "bincode 1.3.3", + "blake3", + "flock-prover", + "ix-terminal", + "multi-stark", + "rayon", + "serde", + "serde_json", +] [[package]] name = "fnv" @@ -905,9 +947,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -933,9 +975,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -943,15 +985,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -960,9 +1002,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -979,32 +1021,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1019,9 +1061,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" dependencies = [ "cc", "cfg-if", @@ -1071,17 +1113,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", "wasm-bindgen", ] @@ -1097,9 +1137,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "gloo-timers" @@ -1115,9 +1155,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.16" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -1171,9 +1211,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heapless" @@ -1214,7 +1254,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.4", + "rand 0.9.5", "ring", "rustls", "thiserror", @@ -1238,7 +1278,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.4", + "rand 0.9.5", "resolv-conf", "rustls", "smallvec", @@ -1250,9 +1290,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1260,9 +1300,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -1270,9 +1310,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1295,18 +1335,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -1389,9 +1429,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1403,9 +1443,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1416,9 +1456,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1430,16 +1470,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1450,15 +1491,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1469,12 +1510,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1523,7 +1558,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.9.4", + "rand 0.9.5", "tokio", "url", "xmltree", @@ -1531,15 +1566,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "rayon", - "serde", - "serde_core", ] [[package]] @@ -1566,19 +1599,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "iroh" @@ -1613,7 +1636,7 @@ dependencies = [ "pkcs8", "portable-atomic", "portmapper", - "rand 0.9.4", + "rand 0.9.5", "reqwest", "rustc-hash", "rustls", @@ -1678,7 +1701,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1709,7 +1732,7 @@ dependencies = [ "pin-project", "pkarr", "postcard", - "rand 0.9.4", + "rand 0.9.5", "reqwest", "rustls", "rustls-pki-types", @@ -1794,7 +1817,7 @@ dependencies = [ "ix-common", "ix-kernel", "ixon", - "num-bigint 0.4.6", + "num-bigint 0.4.8", "rayon", "rustc-hash", "sha2 0.10.9", @@ -1807,10 +1830,12 @@ dependencies = [ "aiur", "anyhow", "bignat", - "bincode", + "bincode 2.0.1", "blake3", "bytes", "dashmap", + "ed25519", + "flock-stage3-host", "getrandom 0.3.4", "indexmap", "iroh", @@ -1827,12 +1852,14 @@ dependencies = [ "mimalloc", "multi-stark", "n0-error", - "num-bigint 0.4.6", + "num-bigint 0.4.8", + "pkcs8", "rayon", "rustc-hash", "serde", "serde_json", "sha2 0.10.9", + "signature", "tiny-keccak", "tokio", "tracing", @@ -1859,13 +1886,24 @@ dependencies = [ "ix-common", "ixon", "log", - "num-bigint 0.4.6", + "num-bigint 0.4.8", "quickcheck", "quickcheck_macros", "rayon", "rustc-hash", ] +[[package]] +name = "ix-terminal" +version = "0.1.0" +dependencies = [ + "aiur", + "anyhow", + "bincode 2.0.1", + "blake3", + "multi-stark", +] + [[package]] name = "ixon" version = "0.1.0" @@ -1877,7 +1915,7 @@ dependencies = [ "ix-common", "memmap2", "nom", - "num-bigint 0.4.6", + "num-bigint 0.4.8", "quickcheck", "quickcheck_macros", "rayon", @@ -1895,7 +1933,7 @@ dependencies = [ "ix-common", "ixon", "multi-stark", - "num-bigint 0.4.6", + "num-bigint 0.4.8", "rayon", "rustc-hash", "stacker", @@ -1903,13 +1941,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1927,20 +1964,14 @@ dependencies = [ "bignat", "bindgen", "cc", - "num-bigint 0.4.6", + "num-bigint 0.4.8", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -1960,9 +1991,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libmimalloc-sys" -version = "0.1.47" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", ] @@ -1975,9 +2006,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -1996,9 +2027,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "loom" @@ -2045,9 +2076,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -2060,9 +2091,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -2075,9 +2106,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -2086,9 +2117,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -2104,9 +2135,9 @@ dependencies = [ [[package]] name = "multi-stark" version = "0.1.0" -source = "git+https://github.com/argumentcomputer/multi-stark.git?rev=a8aab731af8d2a5e15f390cd2ef14af4fc42d7d3#a8aab731af8d2a5e15f390cd2ef14af4fc42d7d3" +source = "git+https://github.com/argumentcomputer/multi-stark.git?rev=6ad074c1f2983ecdd7a56984d333441d6b38186a#6ad074c1f2983ecdd7a56984d333441d6b38186a" dependencies = [ - "bincode", + "bincode 2.0.1", "itertools 0.14.0", "p3-air", "p3-blake3", @@ -2144,7 +2175,7 @@ checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2203,9 +2234,9 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +checksum = "b897d7bd4f0af82e68d40d0344cf37e97f9c97ddf74a098de3e4da05e96ca395" dependencies = [ "paste", ] @@ -2224,12 +2255,13 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +checksum = "93af8261786086024cd5e96e0a991dd65ced07bbf7c233a487bbc96b971d5539" dependencies = [ "bytes", - "futures", + "futures-channel", + "futures-util", "log", "netlink-packet-core", "netlink-sys", @@ -2330,7 +2362,7 @@ dependencies = [ "getrandom 0.3.4", "identity-hash", "lru-slab", - "rand 0.9.4", + "rand 0.9.5", "ring", "rustc-hash", "rustls", @@ -2377,14 +2409,14 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -2402,15 +2434,15 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2443,7 +2475,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2604,7 +2636,7 @@ dependencies = [ "p3-maybe-rayon", "p3-util", "paste", - "rand 0.10.1", + "rand 0.10.2", "serde", "tracing", ] @@ -2623,7 +2655,7 @@ dependencies = [ "p3-maybe-rayon", "p3-security", "p3-util", - "rand 0.10.1", + "rand 0.10.2", "serde", "spin 0.12.3", "thiserror", @@ -2644,7 +2676,7 @@ dependencies = [ "p3-symmetric", "p3-util", "paste", - "rand 0.10.1", + "rand 0.10.2", "serde", "spin 0.12.3", ] @@ -2668,7 +2700,7 @@ dependencies = [ "p3-field", "p3-maybe-rayon", "p3-util", - "rand 0.10.1", + "rand 0.10.2", "serde", "tracing", ] @@ -2690,7 +2722,7 @@ dependencies = [ "p3-field", "p3-symmetric", "p3-util", - "rand 0.10.1", + "rand 0.10.2", ] [[package]] @@ -2705,7 +2737,7 @@ dependencies = [ "p3-maybe-rayon", "p3-symmetric", "p3-util", - "rand 0.10.1", + "rand 0.10.2", "serde", "spin 0.12.3", "thiserror", @@ -2722,7 +2754,7 @@ dependencies = [ "p3-matrix", "p3-maybe-rayon", "p3-util", - "rand 0.10.1", + "rand 0.10.2", "serde", "tracing", ] @@ -2735,7 +2767,7 @@ dependencies = [ "p3-field", "p3-mds", "p3-symmetric", - "rand 0.10.1", + "rand 0.10.2", ] [[package]] @@ -2747,7 +2779,7 @@ dependencies = [ "p3-mds", "p3-symmetric", "p3-util", - "rand 0.10.1", + "rand 0.10.2", ] [[package]] @@ -2784,9 +2816,9 @@ dependencies = [ [[package]] name = "papaya" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +checksum = "da2442474a9404698c42509b8967f437249dbc7b50493e83020333d3943ec0ae" dependencies = [ "equivalent", "seize", @@ -2854,22 +2886,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -2889,7 +2921,7 @@ dependencies = [ "cfg_aliases", "document-features", "ed25519-dalek", - "getrandom 0.4.2", + "getrandom 0.4.3", "ntimestamp", "self_cell", "serde", @@ -2909,9 +2941,9 @@ dependencies = [ [[package]] name = "plist" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64", "indexmap", @@ -2934,9 +2966,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" dependencies = [ "serde", ] @@ -2959,7 +2991,7 @@ dependencies = [ "n0-error", "netwatch", "num_enum", - "rand 0.9.4", + "rand 0.9.5", "serde", "smallvec", "socket2", @@ -2993,14 +3025,14 @@ checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3027,7 +3059,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] @@ -3036,14 +3068,14 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3060,9 +3092,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -3075,7 +3107,7 @@ checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" dependencies = [ "env_logger", "log", - "rand 0.10.1", + "rand 0.10.2", ] [[package]] @@ -3086,14 +3118,14 @@ checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -3111,14 +3143,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", @@ -3132,23 +3165,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3167,9 +3200,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -3177,12 +3210,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3211,6 +3244,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rayon" version = "1.12.0" @@ -3242,9 +3284,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3254,9 +3296,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3265,9 +3307,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -3332,9 +3374,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -3355,14 +3397,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -3375,9 +3417,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -3385,9 +3427,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -3396,9 +3438,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -3423,16 +3465,12 @@ name = "seize" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -3448,9 +3486,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3468,29 +3506,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3499,6 +3537,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3520,6 +3567,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest 0.10.7", + "sha2-asm", ] [[package]] @@ -3533,6 +3581,15 @@ dependencies = [ "digest 0.11.0-rc.10", ] +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3548,6 +3605,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3581,9 +3644,9 @@ dependencies = [ [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -3593,15 +3656,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3621,7 +3684,7 @@ checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3701,7 +3764,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3712,9 +3775,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -3738,7 +3812,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -3754,46 +3828,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "js-sys", "libc", "num-conv", @@ -3806,15 +3879,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3831,9 +3904,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -3841,9 +3914,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -3856,9 +3929,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -3872,13 +3945,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] @@ -3893,9 +3966,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -3905,14 +3978,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", + "libc", "pin-project-lite", "tokio", ] @@ -3930,7 +4004,7 @@ dependencies = [ "getrandom 0.3.4", "http", "httparse", - "rand 0.9.4", + "rand 0.9.5", "ring", "rustls-pki-types", "simdutf8", @@ -3939,6 +4013,27 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3950,25 +4045,45 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "toml_datetime", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow", + "winnow 1.0.4", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.3" @@ -3986,20 +4101,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4034,7 +4149,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4096,9 +4211,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -4108,9 +4223,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -4161,11 +4276,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -4253,27 +4368,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4284,9 +4390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4294,9 +4400,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4304,48 +4410,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -4359,23 +4443,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4393,9 +4465,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -4481,7 +4553,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4492,7 +4564,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -4642,20 +4714,20 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.2" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "winnow" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ - "wit-bindgen-rust-macro", + "memchr", ] [[package]] @@ -4664,85 +4736,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "wmi" version = "0.18.4" @@ -4760,9 +4753,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "ws_stream_wasm" @@ -4800,9 +4793,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4817,7 +4810,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] @@ -4829,29 +4822,29 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -4864,35 +4857,35 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -4901,9 +4894,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -4912,17 +4905,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 6b683b95..65cc3abd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,12 @@ members = [ "crates/ixvm-codegen", "crates/ixon", "crates/kernel", + "crates/terminal", ] -# `zisk/` and `sp1/` are their own Cargo workspaces (guest + host) built via -# the respective zkVM toolchains; excluded so host workspace ops don't pick -# them up. -exclude = ["zisk", "sp1", "multi-stark"] +# `zisk/`, `sp1/`, and `flock-stage3/` are isolated Cargo workspaces with +# heavyweight or specialized toolchains; keep normal host workspace operations +# from picking them up. +exclude = ["zisk", "sp1", "flock-stage3", "multi-stark"] resolver = "2" [profile.dev] @@ -40,6 +41,7 @@ ix-common = { path = "crates/common" } ix-compile = { path = "crates/compile" } ixon = { path = "crates/ixon" } ix-kernel = { path = "crates/kernel" } +ix-terminal = { path = "crates/terminal" } # lean-ffi tree (lean-ffi crate + factored-out bignat sub-crate) bignat = { git = "https://github.com/argumentcomputer/lean-ffi.git", rev = "93c7e52952ae94546be08313f4ff3922984c84d5" } @@ -47,6 +49,7 @@ lean-ffi = { git = "https://github.com/argumentcomputer/lean-ffi.git", rev = "93 # External shared deps anyhow = "1" +bincode = { version = "2.0.1", features = ["serde"] } blake3 = "1.8.4" dashmap = "6.1.0" hashbrown = "0.15" @@ -56,7 +59,7 @@ libc = "0.2" log = "0.4" memmap2 = "0.9" mimalloc = { version = "0.1", default-features = false } -multi-stark = { git = "https://github.com/argumentcomputer/multi-stark.git", rev = "a8aab731af8d2a5e15f390cd2ef14af4fc42d7d3" } +multi-stark = { git = "https://github.com/argumentcomputer/multi-stark.git", rev = "6ad074c1f2983ecdd7a56984d333441d6b38186a" } nom = "7.1.3" num-bigint = "0.4.6" quickcheck = "1.0.3" diff --git a/Ix/Aiur/Protocol.lean b/Ix/Aiur/Protocol.lean index cbb7ae89..1f47da0a 100644 --- a/Ix/Aiur/Protocol.lean +++ b/Ix/Aiur/Protocol.lean @@ -108,6 +108,14 @@ namespace AiurSystem @[extern "rs_aiur_system_build"] opaque build : @&Bytecode.Toplevel → @&CommitmentParameters → @&FriParameters → AiurSystem +/-- Experimental, opt-in key profile minimizing lookup-accumulator plus +quotient opening width within the existing blowup. FRI parameters and user +constraints are unchanged, but the verifying key changes. Larger quotients +may make native proving slower; this is not the deployment default. -/ +@[extern "rs_aiur_system_build_min_opening_width"] +opaque buildMinOpeningWidth : + @&Bytecode.Toplevel → @&CommitmentParameters → @&FriParameters → AiurSystem + /-- Serialize the verifying key (`System`) to bytes. -/ @[extern "rs_aiur_system_vk_bytes"] opaque vkBytes : @& AiurSystem → ByteArray @@ -418,6 +426,24 @@ abbrev functionChannel : G := .ofNat 0 def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) := #[functionChannel, .ofNat funIdx] ++ input ++ output +-- Keep the operation effectful, but transport backend failures as Except. +-- The pinned Rust lean-ffi helper hardcodes an IO.Error constructor tag that +-- does not match this Lean toolchain; let Lean construct IO.userError instead. +@[extern "rs_flock_stage3_aggregate_root"] +private opaque flockStage3AggregateRootRaw : @& ByteArray → @& ByteArray → @& ByteArray → + @& FriParameters → @& String → @& String → @& String → @& String → IO (Except String String) + +/-- Compile/evaluate (preflight), prove, or independently verify the complete +no-RISC-V Flock Stage 3 relation for one Aiur aggregate root. Proving requires +an output path; verification requires an artifact path. Without the Cargo flock +feature, this binding returns a descriptive error while remaining linkable. +The final argument is a JSON object of host resource-limit overrides. The +result is a JSON diagnostic object; progress is written only to stderr. -/ +def flockStage3AggregateRoot (vkBytes claimBytes proofBytes : ByteArray) + (friParameters : FriParameters) (mode artifactPath output limitsJson : String) : IO String := do + IO.ofExcept (← flockStage3AggregateRootRaw vkBytes claimBytes proofBytes + friParameters mode artifactPath output limitsJson) + end Aiur end diff --git a/Ix/Cli/FlockRootCmd.lean b/Ix/Cli/FlockRootCmd.lean new file mode 100644 index 00000000..1411d608 --- /dev/null +++ b/Ix/Cli/FlockRootCmd.lean @@ -0,0 +1,194 @@ +/- +Preflight closed aggregate roots, prove one root, or verify its Stage 3 +artifact. JSONL stdout contains one versioned result per requested root; +progress and pre-prove reports go to stderr. A batch reuses only the Aiur +backend, never a root-specific Flock relation. +-/ +module +public import Cli +public import Ix.Address +public import Ix.Aiur.Protocol +public import Ix.Aggr +public import Ix.Cli.AggregateCmd +public import Ix.Cli.VerifyCmd +public import Ix.Common +public import Ix.Ixon +public import Ix.MultiStark +public import Ix.Store +public import Ix.Unsigned + +public section + +namespace Ix.Cli.FlockRootCmd + +def outerClaimBytes (claim : Array Aiur.G) : ByteArray := + claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes + +/-- Flock Stage 3 accepts only closed aggregate roots. -/ +def validateBundledClaim (claim : Ix.Claim) : Except String Unit := do + let .checkEnv _ assumptions := claim + | throw "aggregate root wrapper does not contain a CheckEnv claim" + if assumptions.isSome then + throw "aggregate root retains assumptions; Flock Stage 3 requires a closed root" + +/-- Bound reads even if a file grows after opening; do not allocate from an +untrusted file length. The extra byte detects oversized wrappers. -/ +def readBounded (path : System.FilePath) (maximum : Nat) : IO ByteArray := do + let handle ← IO.FS.Handle.mk path .read + let mut bytes := ByteArray.empty + repeat + let chunk ← handle.read (min (1024 * 1024) (maximum + 1 - bytes.size)).toUSize + if chunk.isEmpty then return bytes + bytes := bytes ++ chunk + if bytes.size > maximum then + throw <| IO.userError s!"{path} exceeds the {maximum}-byte input limit" + +/-- Read the canonical wrapper without changing the store. File mode derives +its address from the bytes; address mode also checks the requested identity. -/ +def loadAggregateWrapper (rootHex rootFile : String) : IO (Address × Ixon.Proof) := do + let maximum := 64 * 1024 * 1024 + 4096 + let (address, bytes) ← if rootFile.isEmpty then do + let some address := Address.fromString rootHex + | throw <| IO.userError s!"aggregate root: expected a 64-char hex address, got {rootHex}" + let path ← StoreIO.toIO (Store.existingPath address) + pure (address, ← readBounded path maximum) + else do + let bytes ← readBounded rootFile maximum + pure (Address.blake3 bytes, bytes) + let wrapper ← IO.ofExcept <| VerifyCmd.decodeAggregateWrapperAt address bytes + IO.ofExcept <| validateBundledClaim wrapper.claim + return (address, wrapper) + +abbrev CachedBackend := Option (Except String VerifyCmd.AggregateBackend) + +def runRoot (rootHex rootFile mode artifact output limits : String) + (backendRef : IO.Ref CachedBackend) : IO Lean.Json := do + let started ← IO.monoNanosNow + let (address, wrapper) ← loadAggregateWrapper rootHex rootFile + let recursionParameters := MultiStark.defaultRecursionParameters + let backendStarted ← IO.monoNanosNow + let cached ← backendRef.get + let backend ← match cached with + | some result => IO.ofExcept result + | none => do + let result ← VerifyCmd.buildAggregateBackend recursionParameters + backendRef.set (some result) + IO.ofExcept result + let backendUs := ((← IO.monoNanosNow) - backendStarted) / 1000 + let outerClaim := AggregateCmd.aggregateOuterClaim + backend.allowed backend.aggrIdx wrapper.claim + if outerClaim.size != 18 then + throw <| IO.userError s!"internal ix_aggr claim width is {outerClaim.size}, expected 18" + IO.eprintln s!"Flock Stage 3 {mode}: aggregate root {address}" + let report ← Aiur.flockStage3AggregateRoot + backend.system.vkBytes (outerClaimBytes outerClaim) wrapper.proof + recursionParameters.fri mode artifact output limits + let details ← IO.ofExcept (Lean.Json.parse report) + return Lean.Json.mkObj + [ ("root_address", Lean.toJson (toString address)) + , ("bundled_claim", Lean.toJson (toString wrapper.claim)) + , ("backend_prepare_us", Lean.toJson backendUs) + , ("backend_cache", Lean.toJson (if cached.isSome then "hit" else "miss")) + , ("root_total_us", Lean.toJson (((← IO.monoNanosNow) - started) / 1000)) + , ("details", details) ] + +def resourceLimitsJson (p : Cli.Parsed) : Except String String := do + let mut fields := [] + for (flag, field, scale) in + [("max-advice-mib", "max_advice_bytes", 1024 * 1024), + ("max-witness-mib", "max_union_witness_bytes", 1024 * 1024), + ("max-table-capacity", "max_table_capacity", 1)] do + if let some value := p.flag? flag then + let text := value.as! String + let some n := text.toNat? + | throw s!"--{flag} requires a positive integer" + if n == 0 || n * scale > 18446744073709551615 then + throw s!"--{flag} is outside the positive u64 range after scaling" + fields := fields ++ [(field, Lean.toJson (n * scale))] + return (Lean.Json.mkObj fields).compress + +def runFlockRootCmd (p : Cli.Parsed) : IO UInt32 := do + let flag (name : String) := (p.flag? name).map (·.as! String) |>.getD "" + let mode := if (flag "mode").isEmpty then "preflight" else flag "mode" + let artifact := flag "artifact" + let output := flag "output" + let rootFile := flag "root-file" + let rootsFile := flag "roots-file" + let jsonl := p.hasFlag "jsonl" + try + if mode != "preflight" && mode != "prove" && mode != "verify" then + throw <| IO.userError s!"unknown Flock mode {mode} (expected preflight|prove|verify)" + if mode == "preflight" && (!artifact.isEmpty || !output.isEmpty) then + throw <| IO.userError "--artifact and --output are not valid with --mode preflight" + if mode == "prove" && (!artifact.isEmpty || output.isEmpty) then + throw <| IO.userError "Flock proving requires --output and forbids --artifact" + if mode == "verify" && (artifact.isEmpty || !output.isEmpty) then + throw <| IO.userError "Flock verification requires --artifact and forbids --output" + let limits ← IO.ofExcept (resourceLimitsJson p) + let mut roots := p.variableArgsAs! String + if !rootsFile.isEmpty then + let bytes ← readBounded rootsFile (1024 * 1024) + let some contents := String.fromUTF8? bytes + | throw <| IO.userError "--roots-file must be UTF-8" + roots := roots ++ (contents.splitOn "\n" |>.filterMap fun line => + let line := line.trimAscii.toString + if line.isEmpty || line.startsWith "#" then none else some line).toArray + if !rootFile.isEmpty then + if !roots.isEmpty || !rootsFile.isEmpty then + throw <| IO.userError "--root-file cannot be combined with addresses or --roots-file" + roots := #[rootFile] + if roots.isEmpty then + throw <| IO.userError "expected an aggregate root address, --roots-file, or --root-file" + if mode != "preflight" && roots.size != 1 then + throw <| IO.userError "prove and verify require exactly one root; batches use preflight" + let backendRef ← IO.mkRef (none : CachedBackend) + let mut failed := false + for source in roots do + let common := + [("schema", Lean.toJson "ix.flock-stage3.root"), ("version", Lean.toJson (1 : Nat)), + ("ix_version", Lean.toJson Ix.versionString), ("lean_toolchain", Lean.toJson Lean.versionString), + ("mode", Lean.toJson mode), ("source", Lean.toJson source)] + try + let result ← runRoot source rootFile mode artifact output limits backendRef + if jsonl then + IO.println (Lean.Json.mkObj (common ++ + [("status", Lean.toJson "ok"), ("result", result)])).compress + else + IO.println s!"ok: Flock Stage 3 {mode} accepted {source}" + if !output.isEmpty then IO.println s!" artifact saved to {output}" + catch error => + failed := true + if jsonl then + IO.println (Lean.Json.mkObj (common ++ + [("status", Lean.toJson "error"), ("error", Lean.toJson error.toString)])).compress + else + IO.eprintln s!"error: Flock Stage 3 {mode} failed for {source}: {error}" + (← IO.getStdout).flush + return if failed then 1 else 0 + catch error => + IO.eprintln s!"error: {error}" + return 1 + +end Ix.Cli.FlockRootCmd + +open Ix.Cli.FlockRootCmd in +def flockRootCmd : Cli.Cmd := `[Cli| + "flock-root" VIA runFlockRootCmd; + "Preflight closed ix_aggr roots, or prove/verify one root with Flock Stage 3 (IX_FLOCK=1)" + + FLAGS: + "mode" : String; "preflight | prove | verify (default: preflight)." + "artifact" : String; "Read a Stage3ArtifactV1 (required for verify)." + "output" : String; "Atomically save the verified artifact (required for prove; no overwrite)." + "jsonl"; "Emit one versioned JSON result per root on stdout; progress stays on stderr." + "roots-file" : String; "Append addresses from a UTF-8 file, one per line (# comments allowed)." + "root-file" : String; "Read one offline Ixon proof wrapper instead of a store address." + "max-advice-mib" : String; "Host expanded-advice bound in MiB (default: 256)." + "max-witness-mib" : String; "Padded z/a/b union bound in MiB, excluding scratch (default: 32768)." + "max-table-capacity" : String; "Maximum rows/table before wiring compilation (default: 4194304)." + + ARGS: + ...root : String; "32-byte store addresses; multiple roots are supported in preflight mode." +] + +end diff --git a/Ix/Cli/VerifyCmd.lean b/Ix/Cli/VerifyCmd.lean index 1b6bc5fe..cb617d23 100644 --- a/Ix/Cli/VerifyCmd.lean +++ b/Ix/Cli/VerifyCmd.lean @@ -149,7 +149,7 @@ def auditAggregateConstants (env : Ixon.Env) (statement : Aggr.CheckEnvTrees) : /-- Build the two deterministic systems whose identities are committed by an aggregate root: the IxVM vk and the single-entrypoint recursion vk. -/ -private def buildAggregateBackend +def buildAggregateBackend (recursionParameters : MultiStark.RecursionParameters) : IO (Except String AggregateBackend) := do let ixvmCompiled ← match IxVM.ixVM with diff --git a/Ix/Store.lean b/Ix/Store.lean index 50a6155a..988d33c6 100644 --- a/Ix/Store.lean +++ b/Ix/Store.lean @@ -48,18 +48,24 @@ def cacheDir (namespace' : String) : StoreIO FilePath := do IO.toEIO .ioError (IO.FS.createDirAll path) return path -def storePath (addr: Address): StoreIO FilePath := do - let store <- storeDir +/-- Resolve an object path without creating directories. Read-only consumers +can use this before bounded reads, including when the address is missing. -/ +def existingPath (addr : Address) : StoreIO FilePath := do + let store := (← getHomeDir) / ".ix" / "store" let hex := hexOfBytes addr.hash let s := hex.toSlice let dir1 := (s.take 2).toString let dir2 := (s.drop 2 |>.take 2).toString let dir3 := (s.drop 4 |>.take 2).toString let file := (s.drop 6).toString - let path := store / dir1 / dir2 / dir3 - if !(<- path.pathExists) then - IO.toEIO .ioError (IO.FS.createDirAll path) - return path / file + return store / dir1 / dir2 / dir3 / file + +def storePath (addr: Address): StoreIO FilePath := do + let path ← existingPath addr + let parent := path.parent.getD path + if !(← parent.pathExists) then + IO.toEIO .ioError (IO.FS.createDirAll parent) + return path def write (bytes: ByteArray) : StoreIO Address := do let addr := Address.blake3 bytes diff --git a/Main.lean b/Main.lean index 6963f1ad..c0ccb7dc 100644 --- a/Main.lean +++ b/Main.lean @@ -13,6 +13,7 @@ import Ix.Cli.CatalogCmd import Ix.Cli.CompileCmd import Ix.Cli.DecompileCmd import Ix.Cli.DiffCmd +import Ix.Cli.FlockRootCmd import Ix.Cli.IngressCmd import Ix.Cli.MergeCmd import Ix.Cli.NameOfCmd @@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli| treeCmd; profileCmd; proveCmd; + flockRootCmd; shardCmd; codegenCmd; verifyCmd; diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/PROVENANCE.md b/Tests/Fixtures/Aggregate/singleton-2026-09-05/PROVENANCE.md new file mode 100644 index 00000000..db2d740c --- /dev/null +++ b/Tests/Fixtures/Aggregate/singleton-2026-09-05/PROVENANCE.md @@ -0,0 +1,102 @@ +# Current-protocol singleton aggregate + +This is a genuine `ix_aggr` shape-0 wrap of an IxVM `CheckEnv` proof, not a +toy verifier circuit. Its environment contains one well-formed axiom +declaration. The certificate checks that declaration's well-formedness; it +does not prove the proposition postulated by the axiom. Both native proofs +use the production defaults: blowup log 2, cap height 0, 100 binary-FRI +queries, 20-bit query grinding, no commitment grinding, constant final +polynomial. The canonical subject tree has no assumptions. + +The fixture was generated on 2026-09-05 by +`Benchmarks/FlockRootFixture.lean`, in the uncommitted Stage 3 worktree based +on `40cf786ac78990108701ac2f1ec5e3b5867f4410`. It uses Lean 4.33.1, +multi-stark `6ad074c1f2983ecdd7a56984d333441d6b38186a`, and Plonky3 +`3152b14a89067c83775a8076cc262ffc48a1fd7c`. No personal store/cache or +formalization workspace was used. This is a minimal current-protocol +measurement, not a representative full-environment corpus. + +## Identities + +- Subject: `f7a3722c3ab8ad9d45a8cecf412c1d1cae92e2e453a8e923b7f466bfecd345a8` +- Subject-tree root: `71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c` +- Bundled claim: `CheckEnv(71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c, none)` +- Root wrapper BLAKE3: `254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406` +- Root wrapper SHA-256: `6ea804f334bf5a4f126c7f2713acc6636740a6789f10c23b42ea860857f8fada` +- Child wrapper BLAKE3: `9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618` +- Child wrapper SHA-256: `0795bff320f62034e76bb37e1ebf2e6da7968302a828242cc8e0041ba1236001` + +`fixture.json` records every input/key/claim/proof file's byte length and +BLAKE3 digest, native circuit heights, timings, and memory measurements. +`execution.json` is the checkpoint written before aggregate proving. The +fixture is complete only when `fixture.json` exists and verification passes. +The 116-byte environment and its canonical subject tree are included; no +external manifest is needed for this singleton certificate. + +## Reproduction and verification + +From the repository root, build and generate into a **new** directory: + +```sh +IX_FLOCK=1 lake build ix bench-flock-root-fixture +flock_fixture_parent=$(mktemp -d /tmp/ix-flock-fixture.XXXXXX) +( + ulimit -v 67108864 + RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture \ + --output "$flock_fixture_parent/root" --prove +) +.lake/build/bin/bench-flock-root-fixture --verify "$flock_fixture_parent/root" +``` + +Without `--prove`, the harness proves/verifies the tiny IxVM child and only +executes the aggregate. Generation requires a Linux process address-space +limit of at most 64 GiB. This is an OS allocation guard, not a prediction of +RSS; an allocation failure can terminate the subprocess and leave an +incomplete output directory. Existing destinations are refused before writes. + +To verify these saved files without proving or writing anything: + +```sh +.lake/build/bin/bench-flock-root-fixture \ + --verify Tests/Fixtures/Aggregate/singleton-2026-09-05 +``` + +Verification checks bounded, digest-checked bytes, the exact singleton +environment and subject tree, both current keys, the expected outer claim, +and both native proofs. CI runs this fresh-process verification; it does not +regenerate the high-memory native aggregate proof. + +## Measured costs + +AMD Ryzen 9 7950X3D, 128 GiB-class RAM (`MemTotal=134128111616` bytes), +8 Rayon workers, 64 GiB address-space cap, 10 ms process-tree RSS sampling: + +| Operation | Time | Sampled peak RSS | +| --- | ---: | ---: | +| IxVM child proof | 0.189 s | 0.87 GiB | +| Aggregate execution alone, in the proving run | 1.652 s | 2.68 GiB | +| Aggregate proof, including its own execution | 75.813 s | 25.26 GiB | +| Aggregate native verification | 0.121 s | Not separately sampled | + +RSS includes retained system/runtime state, not just incremental prover +allocation. The aggregate has 175 active circuits. The root wrapper is +8,565,030 bytes and the child wrapper 4,485,008 bytes. + +The initial Stage 3 count required `nu=26`, 67,108,864 rows of uniform capacity, +and **3,298,534,883,328 bytes (3 TiB) of padded z/a/b alone**. The subsequent +shared-PCS/weighted-quotient compiler reduces the same unchanged root to +`nu=24`, 16,777,216 rows and **824,633,720,832 bytes (768 GiB)**. Query-point +and denominator sharing subsequently reduces it to `nu=23`, 8,388,608 rows +and **412,316,860,416 bytes (384 GiB)**, with 6,283,484 canonicality rows. +Packing two canonicality requests into the same 512-column row reduces it +again to `nu=22`, 4,194,304 rows and **206,158,430,208 bytes (192 GiB)**, +with 3,162,519 packed canonicality rows. The other table row counts and all +native fixture bytes are unchanged. The latest count took 0.155 s with a +350 MiB process peak under a 16 GiB address-space cap. Native +validation and counting pass, but production admission still correctly refuses +the root before wiring compilation. No Flock proof or evaluated relation +was produced for this fixture. See the retained +[baseline](../../../../flock-stage3/measurements/persisted-singleton-2026-09-05.json) +[PCS follow-up](../../../../flock-stage3/measurements/persisted-singleton-pcs-2026-09-05.json) +[query-sharing measurement](../../../../flock-stage3/measurements/pcs-query-sharing-2026-09-05.json) +and [canonicality-packing measurement](../../../../flock-stage3/measurements/packed-canonicality-2026-09-05.json). diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/aggr.vk b/Tests/Fixtures/Aggregate/singleton-2026-09-05/aggr.vk new file mode 100644 index 00000000..9d2c284b Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/aggr.vk differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/check-env.claim b/Tests/Fixtures/Aggregate/singleton-2026-09-05/check-env.claim new file mode 100644 index 00000000..19871069 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/check-env.claim differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/environment.ixe b/Tests/Fixtures/Aggregate/singleton-2026-09-05/environment.ixe new file mode 100644 index 00000000..d44c554a Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/environment.ixe differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/execution.json b/Tests/Fixtures/Aggregate/singleton-2026-09-05/execution.json new file mode 100644 index 00000000..6441bda2 --- /dev/null +++ b/Tests/Fixtures/Aggregate/singleton-2026-09-05/execution.json @@ -0,0 +1,783 @@ +{"version": 1, + "subject": "f7a3722c3ab8ad9d45a8cecf412c1d1cae92e2e453a8e923b7f466bfecd345a8", + "schema": "ix.flock-stage3.root-fixture", + "lean_toolchain": "4.33.1", + "ixvm_verify_us": 16618, + "ixvm_sampled_peak_rss_bytes": 931577856, + "ixvm_prove_us": 188766, + "ixvm_predicted_peak_bytes": 228450779, + "ixvm_compile_us": 63619, + "inputs": + [{"file": "environment.ixe", + "bytes": 116, + "blake3": + "bed282f9db4dee56f1746870478aad7223ce2a325f7e6baf24dacde3aa06e0e8"}, + {"file": "check-env.claim", + "bytes": 34, + "blake3": + "ddef67870a4517be2f996e2eeb6f7fdcd3871ba527190f17d45b93308a40825f"}, + {"file": "subjects.tree", + "bytes": 34, + "blake3": + "9014207ff8cef20d1f8f64934fef14d4114e8afe43f7a35f930dfcb70175d67e"}, + {"file": "ixvm.vk", + "bytes": 709741, + "blake3": + "e3b0aff0da508c305da9048b113e586502d35e75dadc4f7647d89efb320fbc78"}, + {"file": "aggr.vk", + "bytes": 181630, + "blake3": + "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e"}, + {"file": "outer-claim.bin", + "bytes": 144, + "blake3": + "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713"}], + "fri": + {"query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0}, + "description": + "single well-formed axiom declaration; production ix_aggr shape-0 wrap", + "child": + {"file": "ixvm.ixon-proof", + "bytes": 4485008, + "blake3": "9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618"}, + "bundled_claim": + "CheckEnv(71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c, none)", + "aggregate_proven": false, + "aggregate_circuits": + [{"name": "blake3_compress", + "height": 332248, + "committed_width": 925, + "cache_hits": 30}, + {"name": "blake3_compress_chunks", + "height": 910476, + "committed_width": 29, + "cache_hits": 0}, + {"name": "memory[3]", + "height": 1781328, + "committed_width": 12, + "cache_hits": 2531861}, + {"name": "read_u64_vec_n", + "height": 499217, + "committed_width": 28, + "cache_hits": 0}, + {"name": "aggr_read_byte", + "height": 709741, + "committed_width": 12, + "cache_hits": 1}, + {"name": "ro_fold", + "height": 192396, + "committed_width": 49, + "cache_hits": 741}, + {"name": "read_nodes_n", + "height": 124768, + "committed_width": 59, + "cache_hits": 0}, + {"name": "bucket_update", + "height": 168721, + "committed_width": 42, + "cache_hits": 33549}, + {"name": "select_rows", + "height": 111490, + "committed_width": 46, + "cache_hits": 2735}, + {"name": "read_vk_u16", + "height": 287822, + "committed_width": 15, + "cache_hits": 0}, + {"name": "rows_pop", + "height": 103656, + "committed_width": 46, + "cache_hits": 11313}, + {"name": "b3_rows_chunks", + "height": 13889, + "committed_width": 282, + "cache_hits": 9}, + {"name": "select_rows_le", + "height": 48377, + "committed_width": 68, + "cache_hits": 3009}, + {"name": "bytes_to_block", + "height": 13089, + "committed_width": 265, + "cache_hits": 1152}, + {"name": "memory[7]", + "height": 166607, + "committed_width": 16, + "cache_hits": 172539}, + {"name": "blake3_compress_block", + "height": 14209, + "committed_width": 211, + "cache_hits": 0}, + {"name": "memory[6]", + "height": 153362, + "committed_width": 15, + "cache_hits": 104373}, + {"name": "list_length.U8_8", + "height": 92339, + "committed_width": 25, + "cache_hits": 38901}, + {"name": "memory[10]", + "height": 102225, + "committed_width": 19, + "cache_hits": 863505}, + {"name": "memory[32]", + "height": 47672, + "committed_width": 41, + "cache_hits": 56371}, + {"name": "ri_apply", "height": 38900, "committed_width": 44, "cache_hits": 0}, + {"name": "Bytes2", "height": 65536, "committed_width": 24, "cache_hits": 0}, + {"name": "read_vk_tag", + "height": 124768, + "committed_width": 11, + "cache_hits": 0}, + {"name": "read_node_ids_n", + "height": 63409, + "committed_width": 23, + "cache_hits": 0}, + {"name": "mmcs_compress", + "height": 11919, + "committed_width": 111, + "cache_hits": 0}, + {"name": "eval_at", + "height": 16958, + "committed_width": 55, + "cache_hits": 17366}, + {"name": "cons8", "height": 25251, "committed_width": 33, "cache_hits": 31}, + {"name": "frontier_level", + "height": 10798, + "committed_width": 70, + "cache_hits": 10}, + {"name": "list_drop.G", + "height": 31910, + "committed_width": 20, + "cache_hits": 66505}, + {"name": "accs_onto", "height": 9679, "committed_width": 68, "cache_hits": 2}, + {"name": "open_batch_2pt", + "height": 15600, + "committed_width": 38, + "cache_hits": 0}, + {"name": "list_length.Ptr.U8_8_4", + "height": 30013, + "committed_width": 18, + "cache_hits": 715}, + {"name": "read_u64_vec_vec_n", + "height": 23700, + "committed_width": 23, + "cache_hits": 0}, + {"name": "read_count_at", + "height": 25993, + "committed_width": 19, + "cache_hits": 0}, + {"name": "read_digest_vec_at_n", + "height": 9245, + "committed_width": 58, + "cache_hits": 0}, + {"name": "blake3_next_layer", + "height": 3191, + "committed_width": 191, + "cache_hits": 0}, + {"name": "list_lookup.BatchOpening", + "height": 27956, + "committed_width": 16, + "cache_hits": 86659}, + {"name": "open_2pt_mat", + "height": 15600, + "committed_width": 30, + "cache_hits": 0}, + {"name": "read_ext_vec_n", + "height": 12881, + "committed_width": 37, + "cache_hits": 0}, + {"name": "frontier_merge", + "height": 10686, + "committed_width": 43, + "cache_hits": 605}, + {"name": "blake3_compress_layer", + "height": 3257, + "committed_width": 157, + "cache_hits": 9}, + {"name": "rev_onto", + "height": 17695, + "committed_width": 22, + "cache_hits": 38800}, + {"name": "list_drop.SysNode", + "height": 16958, + "committed_width": 23, + "cache_hits": 16881}, + {"name": "read_u64_vec", + "height": 23301, + "committed_width": 15, + "cache_hits": 0}, + {"name": "list_lookup.SysNode", + "height": 16958, + "committed_width": 19, + "cache_hits": 0}, + {"name": "open_prep", "height": 7800, "committed_width": 45, "cache_hits": 0}, + {"name": "logup_fingerprint", + "height": 6738, + "committed_width": 49, + "cache_hits": 601}, + {"name": "open_quotient", + "height": 7800, + "committed_width": 41, + "cache_hits": 0}, + {"name": "inject_maybe", + "height": 10698, + "committed_width": 23, + "cache_hits": 0}, + {"name": "memory[34]", + "height": 5984, + "committed_width": 43, + "cache_hits": 14017}, + {"name": "frontier_split", + "height": 6774, + "committed_width": 37, + "cache_hits": 1388}, + {"name": "list_is_empty.Ptr.U8_32", + "height": 14193, + "committed_width": 15, + "cache_hits": 13340}, + {"name": "memory[4]", + "height": 15175, + "committed_width": 13, + "cache_hits": 229269}, + {"name": "memo_u32_less_than", + "height": 6753, + "committed_width": 28, + "cache_hits": 2604}, + {"name": "list_length.CommitPhaseProofStep", + "height": 9431, + "committed_width": 19, + "cache_hits": 40384}, + {"name": "read_sys_lookups_n", + "height": 5866, + "committed_width": 31, + "cache_hits": 0}, + {"name": "verify_query", + "height": 1231, + "committed_width": 170, + "cache_hits": 96}, + {"name": "compress_ordered", + "height": 9187, + "committed_width": 16, + "cache_hits": 0}, + {"name": "limbs_onto", + "height": 5357, + "committed_width": 29, + "cache_hits": 0}, + {"name": "leaf_hash_at", + "height": 2752, + "committed_width": 58, + "cache_hits": 469}, + {"name": "exp_by_bits", + "height": 6010, + "committed_width": 23, + "cache_hits": 40029}, + {"name": "pair_mul", + "height": 4383, + "committed_width": 32, + "cache_hits": 4214}, + {"name": "frontier_sort", + "height": 3465, + "committed_width": 40, + "cache_hits": 423}, + {"name": "list_drop.G_2", + "height": 5894, + "committed_width": 21, + "cache_hits": 4471}, + {"name": "read_sys_circuits_n", + "height": 765, + "committed_width": 185, + "cache_hits": 0}, + {"name": "list_lookup.G_2", + "height": 5883, + "committed_width": 17, + "cache_hits": 9004}, + {"name": "logup_steps_fold", + "height": 678, + "committed_width": 126, + "cache_hits": 0}, + {"name": "frontier_leaves", + "height": 2018, + "committed_width": 35, + "cache_hits": 2}, + {"name": "fold_roots", + "height": 1374, + "committed_width": 45, + "cache_hits": 0}, + {"name": "read_ext_vec_vec_n", + "height": 2238, + "committed_width": 23, + "cache_hits": 0}, + {"name": "rows_at_round", + "height": 1616, + "committed_width": 31, + "cache_hits": 0}, + {"name": "fri_fold2", + "height": 1131, + "committed_width": 44, + "cache_hits": 96}, + {"name": "step_views_at", + "height": 1700, + "committed_width": 27, + "cache_hits": 0}, + {"name": "take_bits", "height": 1921, "committed_width": 23, "cache_hits": 0}, + {"name": "drop_index_bits", + "height": 1702, + "committed_width": 25, + "cache_hits": 15}, + {"name": "list_concat.Ptr.U8_32", + "height": 1889, + "committed_width": 22, + "cache_hits": 15}, + {"name": "rollin", "height": 1128, "committed_width": 35, "cache_hits": 99}, + {"name": "read_vk_u16_limb", + "height": 2300, + "committed_width": 15, + "cache_hits": 0}, + {"name": "read_ext_vec", + "height": 1991, + "committed_width": 15, + "cache_hits": 0}, + {"name": "has_height", + "height": 1199, + "committed_width": 23, + "cache_hits": 10673}, + {"name": "memory[8]", + "height": 1534, + "committed_width": 17, + "cache_hits": 63077}, + {"name": "circ_has_height", + "height": 1027, + "committed_width": 25, + "cache_hits": 0}, + {"name": "select_active_circuits", + "height": 765, + "committed_width": 34, + "cache_hits": 0}, + {"name": "bits_to_num", + "height": 1247, + "committed_width": 18, + "cache_hits": 23282}, + {"name": "select_active_prep", + "height": 765, + "committed_width": 30, + "cache_hits": 0}, + {"name": "ood_fold", + "height": 1038, + "committed_width": 18, + "cache_hits": 1419}, + {"name": "list_length.SysCircuit", + "height": 840, + "committed_width": 23, + "cache_hits": 2}, + {"name": "pad_block", "height": 1029, "committed_width": 18, "cache_hits": 0}, + {"name": "read_opt_idx_n", + "height": 765, + "committed_width": 25, + "cache_hits": 0}, + {"name": "log_degrees_onto", + "height": 843, + "committed_width": 22, + "cache_hits": 0}, + {"name": "read_active_n", + "height": 843, + "committed_width": 21, + "cache_hits": 0}, + {"name": "u64_is_zero", + "height": 694, + "committed_width": 25, + "cache_hits": 27436}, + {"name": "relaxed_u64_succ", + "height": 693, + "committed_width": 25, + "cache_hits": 819}, + {"name": "read_vk_u32_limb", + "height": 764, + "committed_width": 21, + "cache_hits": 0}, + {"name": "assert_bits", + "height": 765, + "committed_width": 19, + "cache_hits": 0}, + {"name": "ch_sample8", + "height": 143, + "committed_width": 138, + "cache_hits": 0}, + {"name": "points_onto", + "height": 622, + "committed_width": 22, + "cache_hits": 0}, + {"name": "assert_blowup_zero", + "height": 466, + "committed_width": 30, + "cache_hits": 99}, + {"name": "ood_loop", "height": 78, "committed_width": 251, "cache_hits": 0}, + {"name": "batch_views_at", + "height": 500, + "committed_width": 26, + "cache_hits": 0}, + {"name": "read_u64_vec_vec_vec_n", + "height": 404, + "committed_width": 27, + "cache_hits": 0}, + {"name": "b3_w4_onto", "height": 376, "committed_width": 21, "cache_hits": 0}, + {"name": "verify_one_query", + "height": 100, + "committed_width": 96, + "cache_hits": 0}, + {"name": "read_vk_u64", + "height": 209, + "committed_width": 33, + "cache_hits": 0}, + {"name": "blake3_finish", + "height": 49, + "committed_width": 190, + "cache_hits": 0}, + {"name": "lookup_groups_count", + "height": 308, + "committed_width": 18, + "cache_hits": 761}, + {"name": "ch_sample_bits", + "height": 101, + "committed_width": 69, + "cache_hits": 100}, + {"name": "read_opened_round_n", + "height": 237, + "committed_width": 23, + "cache_hits": 0}, + {"name": "frontier_fold", + "height": 244, + "committed_width": 22, + "cache_hits": 0}, + {"name": "round_onto", "height": 237, "committed_width": 22, "cache_hits": 0}, + {"name": "query_loop", "height": 101, "committed_width": 54, "cache_hits": 0}, + {"name": "open_prep_batch", + "height": 100, + "committed_width": 44, + "cache_hits": 0}, + {"name": "read_field", "height": 205, "committed_width": 18, "cache_hits": 0}, + {"name": "read_ext_vec_vec", + "height": 233, + "committed_width": 15, + "cache_hits": 0}, + {"name": "Bytes1", "height": 256, "committed_width": 11, "cache_hits": 0}, + {"name": "query_views", + "height": 101, + "committed_width": 26, + "cache_hits": 0}, + {"name": "sample_query_indices", + "height": 101, + "committed_width": 25, + "cache_hits": 0}, + {"name": "last_acc_is_zero", + "height": 77, + "committed_width": 30, + "cache_hits": 0}, + {"name": "heights_prep", + "height": 78, + "committed_width": 29, + "cache_hits": 100}, + {"name": "read_u8", "height": 108, "committed_width": 18, "cache_hits": 4}, + {"name": "heights_all", + "height": 78, + "committed_width": 23, + "cache_hits": 100}, + {"name": "quotient_eval", + "height": 57, + "committed_width": 32, + "cache_hits": 68}, + {"name": "ch_sample_field", + "height": 42, + "committed_width": 41, + "cache_hits": 0}, + {"name": "ext_exp_pow2", + "height": 70, + "committed_width": 21, + "cache_hits": 512}, + {"name": "reconstruct_ext_row", + "height": 46, + "committed_width": 32, + "cache_hits": 76}, + {"name": "cap_onto", "height": 23, "committed_width": 65, "cache_hits": 17}, + {"name": "flatten_u64", + "height": 70, + "committed_width": 14, + "cache_hits": 25927}, + {"name": "pcs_betas", "height": 17, "committed_width": 64, "cache_hits": 0}, + {"name": "mmcs_verify_multi", + "height": 20, + "committed_width": 41, + "cache_hits": 0}, + {"name": "verify_commit_multi", + "height": 17, + "committed_width": 51, + "cache_hits": 0}, + {"name": "read_digest_vec_at", + "height": 39, + "committed_width": 15, + "cache_hits": 0}, + {"name": "ch_sample_ext", + "height": 21, + "committed_width": 32, + "cache_hits": 0}, + {"name": "two_adic_gen", + "height": 33, + "committed_width": 17, + "cache_hits": 55783}, + {"name": "from_ext_basis", + "height": 44, + "committed_width": 10, + "cache_hits": 1}, + {"name": "pcs_check_witness", + "height": 17, + "committed_width": 34, + "cache_hits": 0}, + {"name": "read_commit_phase_step_vec_n", + "height": 17, + "committed_width": 33, + "cache_hits": 0}, + {"name": "build_buckets", + "height": 19, + "committed_width": 25, + "cache_hits": 99}, + {"name": "read_merkle_cap_vec_n", + "height": 17, + "committed_width": 23, + "cache_hits": 0}, + {"name": "snoc_b8", "height": 17, "committed_width": 22, "cache_hits": 0}, + {"name": "obs_log_arities", + "height": 17, + "committed_width": 22, + "cache_hits": 0}, + {"name": "list_length.CommitPhaseMultiStep", + "height": 17, + "committed_width": 20, + "cache_hits": 0}, + {"name": "read_u64", "height": 12, "committed_width": 33, "cache_hits": 0}, + {"name": "fingerprint_vals", + "height": 11, + "committed_width": 32, + "cache_hits": 0}, + {"name": "snoc_cap", "height": 18, "committed_width": 15, "cache_hits": 0}, + {"name": "read_claim_vals_n", + "height": 11, + "committed_width": 30, + "cache_hits": 0}, + {"name": "pow2", "height": 17, "committed_width": 14, "cache_hits": 76}, + {"name": "memory[5]", + "height": 17, + "committed_width": 14, + "cache_hits": 1734}, + {"name": "list_drop.U8_8", + "height": 10, + "committed_width": 27, + "cache_hits": 9}, + {"name": "list_lookup.U8_8", + "height": 10, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_concat.U8_8", + "height": 8, + "committed_width": 29, + "cache_hits": 0}, + {"name": "aggr_claim_field", + "height": 10, + "committed_width": 18, + "cache_hits": 0}, + {"name": "aggr_verify_child", + "height": 1, + "committed_width": 500, + "cache_hits": 0}, + {"name": "read_batch_opening_vec_n", + "height": 5, + "committed_width": 31, + "cache_hits": 0}, + {"name": "aggr_read_address", + "height": 2, + "committed_width": 138, + "cache_hits": 0}, + {"name": "heights_max", + "height": 3, + "committed_width": 44, + "cache_hits": 100}, + {"name": "aggr_load_sys", + "height": 1, + "committed_width": 227, + "cache_hits": 0}, + {"name": "ix_aggr", "height": 1, "committed_width": 202, "cache_hits": 0}, + {"name": "read_opened_round", + "height": 4, + "committed_width": 15, + "cache_hits": 0}, + {"name": "read_vk_cap_n", + "height": 2, + "committed_width": 66, + "cache_hits": 0}, + {"name": "quotient_degree_of", + "height": 2, + "committed_width": 45, + "cache_hits": 75}, + {"name": "aggr_pack_address", + "height": 2, + "committed_width": 41, + "cache_hits": 0}, + {"name": "aggr_child_check_env_digest", + "height": 1, + "committed_width": 89, + "cache_hits": 0}, + {"name": "claims_acc", "height": 2, "committed_width": 37, "cache_hits": 0}, + {"name": "read_claims_n", + "height": 2, + "committed_width": 27, + "cache_hits": 0}, + {"name": "verify_input_multi", + "height": 1, + "committed_width": 62, + "cache_hits": 0}, + {"name": "read_count", "height": 2, "committed_width": 21, "cache_hits": 0}, + {"name": "aggr_wrap", "height": 1, "committed_width": 53, "cache_hits": 0}, + {"name": "aggr_assert_digest", + "height": 2, + "committed_width": 22, + "cache_hits": 0}, + {"name": "aggr_claim_digest", + "height": 1, + "committed_width": 26, + "cache_hits": 0}, + {"name": "read_opt_commit", + "height": 1, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_length.Bucket", + "height": 1, + "committed_width": 22, + "cache_hits": 3}, + {"name": "list_length.FrontierNode", + "height": 1, + "committed_width": 21, + "cache_hits": 19}, + {"name": "read_preprocessed", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_only_claim", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "prep_count", "height": 1, "committed_width": 11, "cache_hits": 100}, + {"name": "aggr_parse_check_env", + "height": 0, + "committed_width": 23, + "cache_hits": 0}, + {"name": "aggr_node_hash", + "height": 0, + "committed_width": 19, + "cache_hits": 0}, + {"name": "aggr_leaf_hash", + "height": 0, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_parse_tree_body", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_put_address", + "height": 0, + "committed_width": 106, + "cache_hits": 0}, + {"name": "aggr_load_preimage", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_leaf_hashes", + "height": 0, + "committed_width": 24, + "cache_hits": 0}, + {"name": "aggr_pair", "height": 0, "committed_width": 137, "cache_hits": 0}, + {"name": "aggr_pair_hashes", + "height": 0, + "committed_width": 33, + "cache_hits": 0}, + {"name": "bytes_to_addr", + "height": 0, + "committed_width": 53, + "cache_hits": 0}, + {"name": "aggr_reduce_hashes", + "height": 0, + "committed_width": 27, + "cache_hits": 0}, + {"name": "aggr_canonical_root", + "height": 0, + "committed_width": 13, + "cache_hits": 0}, + {"name": "address_eq_tail", + "height": 0, + "committed_width": 84, + "cache_hits": 0}, + {"name": "aggr_load_canonical_tree", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_load_optional_tree", + "height": 0, + "committed_width": 14, + "cache_hits": 0}, + {"name": "aggr_assert_same_list", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_union", + "height": 0, + "committed_width": 40, + "cache_hits": 0}, + {"name": "aggr_next_assumption", + "height": 0, + "committed_width": 30, + "cache_hits": 0}, + {"name": "aggr_seek_subject", + "height": 0, + "committed_width": 25, + "cache_hits": 0}, + {"name": "aggr_assert_difference", + "height": 0, + "committed_width": 35, + "cache_hits": 0}, + {"name": "aggr_fold_path", + "height": 0, + "committed_width": 35, + "cache_hits": 0}, + {"name": "aggr_pair_structural", + "height": 0, + "committed_width": 134, + "cache_hits": 0}, + {"name": "aggr_address_order", + "height": 0, + "committed_width": 120, + "cache_hits": 0}, + {"name": "aggr_discharge_choice", + "height": 0, + "committed_width": 78, + "cache_hits": 0}, + {"name": "aggr_assert_strict_sorted", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_structural_difference", + "height": 0, + "committed_width": 34, + "cache_hits": 0}, + {"name": "address_eq", "height": 0, "committed_width": 82, "cache_hits": 0}, + {"name": "aggr_get_opt_address", + "height": 0, + "committed_width": 18, + "cache_hits": 0}], + "aggr_execute_us": 1652316, + "aggr_execute_sampled_peak_rss_bytes": 2873384960, + "aggr_compile_us": 24606, + "address_space_limit_bytes": 68719476736} diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json b/Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json new file mode 100644 index 00000000..409077a9 --- /dev/null +++ b/Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json @@ -0,0 +1,790 @@ +{"version": 1, + "subject": "f7a3722c3ab8ad9d45a8cecf412c1d1cae92e2e453a8e923b7f466bfecd345a8", + "schema": "ix.flock-stage3.root-fixture", + "root": + {"file": "root.ixon-proof", + "bytes": 8565030, + "blake3": "254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406"}, + "lean_toolchain": "4.33.1", + "ixvm_verify_us": 16618, + "ixvm_sampled_peak_rss_bytes": 931577856, + "ixvm_prove_us": 188766, + "ixvm_predicted_peak_bytes": 228450779, + "ixvm_compile_us": 63619, + "inputs": + [{"file": "environment.ixe", + "bytes": 116, + "blake3": + "bed282f9db4dee56f1746870478aad7223ce2a325f7e6baf24dacde3aa06e0e8"}, + {"file": "check-env.claim", + "bytes": 34, + "blake3": + "ddef67870a4517be2f996e2eeb6f7fdcd3871ba527190f17d45b93308a40825f"}, + {"file": "subjects.tree", + "bytes": 34, + "blake3": + "9014207ff8cef20d1f8f64934fef14d4114e8afe43f7a35f930dfcb70175d67e"}, + {"file": "ixvm.vk", + "bytes": 709741, + "blake3": + "e3b0aff0da508c305da9048b113e586502d35e75dadc4f7647d89efb320fbc78"}, + {"file": "aggr.vk", + "bytes": 181630, + "blake3": + "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e"}, + {"file": "outer-claim.bin", + "bytes": 144, + "blake3": + "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713"}], + "fri": + {"query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0}, + "description": + "single well-formed axiom declaration; production ix_aggr shape-0 wrap", + "child": + {"file": "ixvm.ixon-proof", + "bytes": 4485008, + "blake3": "9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618"}, + "bundled_claim": + "CheckEnv(71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c, none)", + "aggregate_proven": true, + "aggregate_circuits": + [{"name": "blake3_compress", + "height": 332248, + "committed_width": 925, + "cache_hits": 30}, + {"name": "blake3_compress_chunks", + "height": 910476, + "committed_width": 29, + "cache_hits": 0}, + {"name": "memory[3]", + "height": 1781328, + "committed_width": 12, + "cache_hits": 2531861}, + {"name": "read_u64_vec_n", + "height": 499217, + "committed_width": 28, + "cache_hits": 0}, + {"name": "aggr_read_byte", + "height": 709741, + "committed_width": 12, + "cache_hits": 1}, + {"name": "ro_fold", + "height": 192396, + "committed_width": 49, + "cache_hits": 741}, + {"name": "read_nodes_n", + "height": 124768, + "committed_width": 59, + "cache_hits": 0}, + {"name": "bucket_update", + "height": 168721, + "committed_width": 42, + "cache_hits": 33549}, + {"name": "select_rows", + "height": 111490, + "committed_width": 46, + "cache_hits": 2735}, + {"name": "read_vk_u16", + "height": 287822, + "committed_width": 15, + "cache_hits": 0}, + {"name": "rows_pop", + "height": 103656, + "committed_width": 46, + "cache_hits": 11313}, + {"name": "b3_rows_chunks", + "height": 13889, + "committed_width": 282, + "cache_hits": 9}, + {"name": "select_rows_le", + "height": 48377, + "committed_width": 68, + "cache_hits": 3009}, + {"name": "bytes_to_block", + "height": 13089, + "committed_width": 265, + "cache_hits": 1152}, + {"name": "memory[7]", + "height": 166607, + "committed_width": 16, + "cache_hits": 172539}, + {"name": "blake3_compress_block", + "height": 14209, + "committed_width": 211, + "cache_hits": 0}, + {"name": "memory[6]", + "height": 153362, + "committed_width": 15, + "cache_hits": 104373}, + {"name": "list_length.U8_8", + "height": 92339, + "committed_width": 25, + "cache_hits": 38901}, + {"name": "memory[10]", + "height": 102225, + "committed_width": 19, + "cache_hits": 863505}, + {"name": "memory[32]", + "height": 47672, + "committed_width": 41, + "cache_hits": 56371}, + {"name": "ri_apply", "height": 38900, "committed_width": 44, "cache_hits": 0}, + {"name": "Bytes2", "height": 65536, "committed_width": 24, "cache_hits": 0}, + {"name": "read_vk_tag", + "height": 124768, + "committed_width": 11, + "cache_hits": 0}, + {"name": "read_node_ids_n", + "height": 63409, + "committed_width": 23, + "cache_hits": 0}, + {"name": "mmcs_compress", + "height": 11919, + "committed_width": 111, + "cache_hits": 0}, + {"name": "eval_at", + "height": 16958, + "committed_width": 55, + "cache_hits": 17366}, + {"name": "cons8", "height": 25251, "committed_width": 33, "cache_hits": 31}, + {"name": "frontier_level", + "height": 10798, + "committed_width": 70, + "cache_hits": 10}, + {"name": "list_drop.G", + "height": 31910, + "committed_width": 20, + "cache_hits": 66505}, + {"name": "accs_onto", "height": 9679, "committed_width": 68, "cache_hits": 2}, + {"name": "open_batch_2pt", + "height": 15600, + "committed_width": 38, + "cache_hits": 0}, + {"name": "list_length.Ptr.U8_8_4", + "height": 30013, + "committed_width": 18, + "cache_hits": 715}, + {"name": "read_u64_vec_vec_n", + "height": 23700, + "committed_width": 23, + "cache_hits": 0}, + {"name": "read_count_at", + "height": 25993, + "committed_width": 19, + "cache_hits": 0}, + {"name": "read_digest_vec_at_n", + "height": 9245, + "committed_width": 58, + "cache_hits": 0}, + {"name": "blake3_next_layer", + "height": 3191, + "committed_width": 191, + "cache_hits": 0}, + {"name": "list_lookup.BatchOpening", + "height": 27956, + "committed_width": 16, + "cache_hits": 86659}, + {"name": "open_2pt_mat", + "height": 15600, + "committed_width": 30, + "cache_hits": 0}, + {"name": "read_ext_vec_n", + "height": 12881, + "committed_width": 37, + "cache_hits": 0}, + {"name": "frontier_merge", + "height": 10686, + "committed_width": 43, + "cache_hits": 605}, + {"name": "blake3_compress_layer", + "height": 3257, + "committed_width": 157, + "cache_hits": 9}, + {"name": "rev_onto", + "height": 17695, + "committed_width": 22, + "cache_hits": 38800}, + {"name": "list_drop.SysNode", + "height": 16958, + "committed_width": 23, + "cache_hits": 16881}, + {"name": "read_u64_vec", + "height": 23301, + "committed_width": 15, + "cache_hits": 0}, + {"name": "list_lookup.SysNode", + "height": 16958, + "committed_width": 19, + "cache_hits": 0}, + {"name": "open_prep", "height": 7800, "committed_width": 45, "cache_hits": 0}, + {"name": "logup_fingerprint", + "height": 6738, + "committed_width": 49, + "cache_hits": 601}, + {"name": "open_quotient", + "height": 7800, + "committed_width": 41, + "cache_hits": 0}, + {"name": "inject_maybe", + "height": 10698, + "committed_width": 23, + "cache_hits": 0}, + {"name": "memory[34]", + "height": 5984, + "committed_width": 43, + "cache_hits": 14017}, + {"name": "frontier_split", + "height": 6774, + "committed_width": 37, + "cache_hits": 1388}, + {"name": "list_is_empty.Ptr.U8_32", + "height": 14193, + "committed_width": 15, + "cache_hits": 13340}, + {"name": "memory[4]", + "height": 15175, + "committed_width": 13, + "cache_hits": 229269}, + {"name": "memo_u32_less_than", + "height": 6753, + "committed_width": 28, + "cache_hits": 2604}, + {"name": "list_length.CommitPhaseProofStep", + "height": 9431, + "committed_width": 19, + "cache_hits": 40384}, + {"name": "read_sys_lookups_n", + "height": 5866, + "committed_width": 31, + "cache_hits": 0}, + {"name": "verify_query", + "height": 1231, + "committed_width": 170, + "cache_hits": 96}, + {"name": "compress_ordered", + "height": 9187, + "committed_width": 16, + "cache_hits": 0}, + {"name": "limbs_onto", + "height": 5357, + "committed_width": 29, + "cache_hits": 0}, + {"name": "leaf_hash_at", + "height": 2752, + "committed_width": 58, + "cache_hits": 469}, + {"name": "exp_by_bits", + "height": 6010, + "committed_width": 23, + "cache_hits": 40029}, + {"name": "pair_mul", + "height": 4383, + "committed_width": 32, + "cache_hits": 4214}, + {"name": "frontier_sort", + "height": 3465, + "committed_width": 40, + "cache_hits": 423}, + {"name": "list_drop.G_2", + "height": 5894, + "committed_width": 21, + "cache_hits": 4471}, + {"name": "read_sys_circuits_n", + "height": 765, + "committed_width": 185, + "cache_hits": 0}, + {"name": "list_lookup.G_2", + "height": 5883, + "committed_width": 17, + "cache_hits": 9004}, + {"name": "logup_steps_fold", + "height": 678, + "committed_width": 126, + "cache_hits": 0}, + {"name": "frontier_leaves", + "height": 2018, + "committed_width": 35, + "cache_hits": 2}, + {"name": "fold_roots", + "height": 1374, + "committed_width": 45, + "cache_hits": 0}, + {"name": "read_ext_vec_vec_n", + "height": 2238, + "committed_width": 23, + "cache_hits": 0}, + {"name": "rows_at_round", + "height": 1616, + "committed_width": 31, + "cache_hits": 0}, + {"name": "fri_fold2", + "height": 1131, + "committed_width": 44, + "cache_hits": 96}, + {"name": "step_views_at", + "height": 1700, + "committed_width": 27, + "cache_hits": 0}, + {"name": "take_bits", "height": 1921, "committed_width": 23, "cache_hits": 0}, + {"name": "drop_index_bits", + "height": 1702, + "committed_width": 25, + "cache_hits": 15}, + {"name": "list_concat.Ptr.U8_32", + "height": 1889, + "committed_width": 22, + "cache_hits": 15}, + {"name": "rollin", "height": 1128, "committed_width": 35, "cache_hits": 99}, + {"name": "read_vk_u16_limb", + "height": 2300, + "committed_width": 15, + "cache_hits": 0}, + {"name": "read_ext_vec", + "height": 1991, + "committed_width": 15, + "cache_hits": 0}, + {"name": "has_height", + "height": 1199, + "committed_width": 23, + "cache_hits": 10673}, + {"name": "memory[8]", + "height": 1534, + "committed_width": 17, + "cache_hits": 63077}, + {"name": "circ_has_height", + "height": 1027, + "committed_width": 25, + "cache_hits": 0}, + {"name": "select_active_circuits", + "height": 765, + "committed_width": 34, + "cache_hits": 0}, + {"name": "bits_to_num", + "height": 1247, + "committed_width": 18, + "cache_hits": 23282}, + {"name": "select_active_prep", + "height": 765, + "committed_width": 30, + "cache_hits": 0}, + {"name": "ood_fold", + "height": 1038, + "committed_width": 18, + "cache_hits": 1419}, + {"name": "list_length.SysCircuit", + "height": 840, + "committed_width": 23, + "cache_hits": 2}, + {"name": "pad_block", "height": 1029, "committed_width": 18, "cache_hits": 0}, + {"name": "read_opt_idx_n", + "height": 765, + "committed_width": 25, + "cache_hits": 0}, + {"name": "log_degrees_onto", + "height": 843, + "committed_width": 22, + "cache_hits": 0}, + {"name": "read_active_n", + "height": 843, + "committed_width": 21, + "cache_hits": 0}, + {"name": "u64_is_zero", + "height": 694, + "committed_width": 25, + "cache_hits": 27436}, + {"name": "relaxed_u64_succ", + "height": 693, + "committed_width": 25, + "cache_hits": 819}, + {"name": "read_vk_u32_limb", + "height": 764, + "committed_width": 21, + "cache_hits": 0}, + {"name": "assert_bits", + "height": 765, + "committed_width": 19, + "cache_hits": 0}, + {"name": "ch_sample8", + "height": 143, + "committed_width": 138, + "cache_hits": 0}, + {"name": "points_onto", + "height": 622, + "committed_width": 22, + "cache_hits": 0}, + {"name": "assert_blowup_zero", + "height": 466, + "committed_width": 30, + "cache_hits": 99}, + {"name": "ood_loop", "height": 78, "committed_width": 251, "cache_hits": 0}, + {"name": "batch_views_at", + "height": 500, + "committed_width": 26, + "cache_hits": 0}, + {"name": "read_u64_vec_vec_vec_n", + "height": 404, + "committed_width": 27, + "cache_hits": 0}, + {"name": "b3_w4_onto", "height": 376, "committed_width": 21, "cache_hits": 0}, + {"name": "verify_one_query", + "height": 100, + "committed_width": 96, + "cache_hits": 0}, + {"name": "read_vk_u64", + "height": 209, + "committed_width": 33, + "cache_hits": 0}, + {"name": "blake3_finish", + "height": 49, + "committed_width": 190, + "cache_hits": 0}, + {"name": "lookup_groups_count", + "height": 308, + "committed_width": 18, + "cache_hits": 761}, + {"name": "ch_sample_bits", + "height": 101, + "committed_width": 69, + "cache_hits": 100}, + {"name": "read_opened_round_n", + "height": 237, + "committed_width": 23, + "cache_hits": 0}, + {"name": "frontier_fold", + "height": 244, + "committed_width": 22, + "cache_hits": 0}, + {"name": "round_onto", "height": 237, "committed_width": 22, "cache_hits": 0}, + {"name": "query_loop", "height": 101, "committed_width": 54, "cache_hits": 0}, + {"name": "open_prep_batch", + "height": 100, + "committed_width": 44, + "cache_hits": 0}, + {"name": "read_field", "height": 205, "committed_width": 18, "cache_hits": 0}, + {"name": "read_ext_vec_vec", + "height": 233, + "committed_width": 15, + "cache_hits": 0}, + {"name": "Bytes1", "height": 256, "committed_width": 11, "cache_hits": 0}, + {"name": "query_views", + "height": 101, + "committed_width": 26, + "cache_hits": 0}, + {"name": "sample_query_indices", + "height": 101, + "committed_width": 25, + "cache_hits": 0}, + {"name": "last_acc_is_zero", + "height": 77, + "committed_width": 30, + "cache_hits": 0}, + {"name": "heights_prep", + "height": 78, + "committed_width": 29, + "cache_hits": 100}, + {"name": "read_u8", "height": 108, "committed_width": 18, "cache_hits": 4}, + {"name": "heights_all", + "height": 78, + "committed_width": 23, + "cache_hits": 100}, + {"name": "quotient_eval", + "height": 57, + "committed_width": 32, + "cache_hits": 68}, + {"name": "ch_sample_field", + "height": 42, + "committed_width": 41, + "cache_hits": 0}, + {"name": "ext_exp_pow2", + "height": 70, + "committed_width": 21, + "cache_hits": 512}, + {"name": "reconstruct_ext_row", + "height": 46, + "committed_width": 32, + "cache_hits": 76}, + {"name": "cap_onto", "height": 23, "committed_width": 65, "cache_hits": 17}, + {"name": "flatten_u64", + "height": 70, + "committed_width": 14, + "cache_hits": 25927}, + {"name": "pcs_betas", "height": 17, "committed_width": 64, "cache_hits": 0}, + {"name": "mmcs_verify_multi", + "height": 20, + "committed_width": 41, + "cache_hits": 0}, + {"name": "verify_commit_multi", + "height": 17, + "committed_width": 51, + "cache_hits": 0}, + {"name": "read_digest_vec_at", + "height": 39, + "committed_width": 15, + "cache_hits": 0}, + {"name": "ch_sample_ext", + "height": 21, + "committed_width": 32, + "cache_hits": 0}, + {"name": "two_adic_gen", + "height": 33, + "committed_width": 17, + "cache_hits": 55783}, + {"name": "from_ext_basis", + "height": 44, + "committed_width": 10, + "cache_hits": 1}, + {"name": "pcs_check_witness", + "height": 17, + "committed_width": 34, + "cache_hits": 0}, + {"name": "read_commit_phase_step_vec_n", + "height": 17, + "committed_width": 33, + "cache_hits": 0}, + {"name": "build_buckets", + "height": 19, + "committed_width": 25, + "cache_hits": 99}, + {"name": "read_merkle_cap_vec_n", + "height": 17, + "committed_width": 23, + "cache_hits": 0}, + {"name": "snoc_b8", "height": 17, "committed_width": 22, "cache_hits": 0}, + {"name": "obs_log_arities", + "height": 17, + "committed_width": 22, + "cache_hits": 0}, + {"name": "list_length.CommitPhaseMultiStep", + "height": 17, + "committed_width": 20, + "cache_hits": 0}, + {"name": "read_u64", "height": 12, "committed_width": 33, "cache_hits": 0}, + {"name": "fingerprint_vals", + "height": 11, + "committed_width": 32, + "cache_hits": 0}, + {"name": "snoc_cap", "height": 18, "committed_width": 15, "cache_hits": 0}, + {"name": "read_claim_vals_n", + "height": 11, + "committed_width": 30, + "cache_hits": 0}, + {"name": "pow2", "height": 17, "committed_width": 14, "cache_hits": 76}, + {"name": "memory[5]", + "height": 17, + "committed_width": 14, + "cache_hits": 1734}, + {"name": "list_drop.U8_8", + "height": 10, + "committed_width": 27, + "cache_hits": 9}, + {"name": "list_lookup.U8_8", + "height": 10, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_concat.U8_8", + "height": 8, + "committed_width": 29, + "cache_hits": 0}, + {"name": "aggr_claim_field", + "height": 10, + "committed_width": 18, + "cache_hits": 0}, + {"name": "aggr_verify_child", + "height": 1, + "committed_width": 500, + "cache_hits": 0}, + {"name": "read_batch_opening_vec_n", + "height": 5, + "committed_width": 31, + "cache_hits": 0}, + {"name": "aggr_read_address", + "height": 2, + "committed_width": 138, + "cache_hits": 0}, + {"name": "heights_max", + "height": 3, + "committed_width": 44, + "cache_hits": 100}, + {"name": "aggr_load_sys", + "height": 1, + "committed_width": 227, + "cache_hits": 0}, + {"name": "ix_aggr", "height": 1, "committed_width": 202, "cache_hits": 0}, + {"name": "read_opened_round", + "height": 4, + "committed_width": 15, + "cache_hits": 0}, + {"name": "read_vk_cap_n", + "height": 2, + "committed_width": 66, + "cache_hits": 0}, + {"name": "quotient_degree_of", + "height": 2, + "committed_width": 45, + "cache_hits": 75}, + {"name": "aggr_pack_address", + "height": 2, + "committed_width": 41, + "cache_hits": 0}, + {"name": "aggr_child_check_env_digest", + "height": 1, + "committed_width": 89, + "cache_hits": 0}, + {"name": "claims_acc", "height": 2, "committed_width": 37, "cache_hits": 0}, + {"name": "read_claims_n", + "height": 2, + "committed_width": 27, + "cache_hits": 0}, + {"name": "verify_input_multi", + "height": 1, + "committed_width": 62, + "cache_hits": 0}, + {"name": "read_count", "height": 2, "committed_width": 21, "cache_hits": 0}, + {"name": "aggr_wrap", "height": 1, "committed_width": 53, "cache_hits": 0}, + {"name": "aggr_assert_digest", + "height": 2, + "committed_width": 22, + "cache_hits": 0}, + {"name": "aggr_claim_digest", + "height": 1, + "committed_width": 26, + "cache_hits": 0}, + {"name": "read_opt_commit", + "height": 1, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_length.Bucket", + "height": 1, + "committed_width": 22, + "cache_hits": 3}, + {"name": "list_length.FrontierNode", + "height": 1, + "committed_width": 21, + "cache_hits": 19}, + {"name": "read_preprocessed", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_only_claim", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "prep_count", "height": 1, "committed_width": 11, "cache_hits": 100}, + {"name": "aggr_parse_check_env", + "height": 0, + "committed_width": 23, + "cache_hits": 0}, + {"name": "aggr_node_hash", + "height": 0, + "committed_width": 19, + "cache_hits": 0}, + {"name": "aggr_leaf_hash", + "height": 0, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_parse_tree_body", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_put_address", + "height": 0, + "committed_width": 106, + "cache_hits": 0}, + {"name": "aggr_load_preimage", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_leaf_hashes", + "height": 0, + "committed_width": 24, + "cache_hits": 0}, + {"name": "aggr_pair", "height": 0, "committed_width": 137, "cache_hits": 0}, + {"name": "aggr_pair_hashes", + "height": 0, + "committed_width": 33, + "cache_hits": 0}, + {"name": "bytes_to_addr", + "height": 0, + "committed_width": 53, + "cache_hits": 0}, + {"name": "aggr_reduce_hashes", + "height": 0, + "committed_width": 27, + "cache_hits": 0}, + {"name": "aggr_canonical_root", + "height": 0, + "committed_width": 13, + "cache_hits": 0}, + {"name": "address_eq_tail", + "height": 0, + "committed_width": 84, + "cache_hits": 0}, + {"name": "aggr_load_canonical_tree", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_load_optional_tree", + "height": 0, + "committed_width": 14, + "cache_hits": 0}, + {"name": "aggr_assert_same_list", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_union", + "height": 0, + "committed_width": 40, + "cache_hits": 0}, + {"name": "aggr_next_assumption", + "height": 0, + "committed_width": 30, + "cache_hits": 0}, + {"name": "aggr_seek_subject", + "height": 0, + "committed_width": 25, + "cache_hits": 0}, + {"name": "aggr_assert_difference", + "height": 0, + "committed_width": 35, + "cache_hits": 0}, + {"name": "aggr_fold_path", + "height": 0, + "committed_width": 35, + "cache_hits": 0}, + {"name": "aggr_pair_structural", + "height": 0, + "committed_width": 134, + "cache_hits": 0}, + {"name": "aggr_address_order", + "height": 0, + "committed_width": 120, + "cache_hits": 0}, + {"name": "aggr_discharge_choice", + "height": 0, + "committed_width": 78, + "cache_hits": 0}, + {"name": "aggr_assert_strict_sorted", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_structural_difference", + "height": 0, + "committed_width": 34, + "cache_hits": 0}, + {"name": "address_eq", "height": 0, "committed_width": 82, "cache_hits": 0}, + {"name": "aggr_get_opt_address", + "height": 0, + "committed_width": 18, + "cache_hits": 0}], + "aggr_verify_us": 121037, + "aggr_prove_us": 75812960, + "aggr_prove_sampled_peak_rss_bytes": 27118215168, + "aggr_execute_us": 1652316, + "aggr_execute_sampled_peak_rss_bytes": 2873384960, + "aggr_compile_us": 24606, + "address_space_limit_bytes": 68719476736} diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/ixvm.ixon-proof b/Tests/Fixtures/Aggregate/singleton-2026-09-05/ixvm.ixon-proof new file mode 100644 index 00000000..1be669e3 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/ixvm.ixon-proof differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/ixvm.vk b/Tests/Fixtures/Aggregate/singleton-2026-09-05/ixvm.vk new file mode 100644 index 00000000..bfc2cf6b Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/ixvm.vk differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/outer-claim.bin b/Tests/Fixtures/Aggregate/singleton-2026-09-05/outer-claim.bin new file mode 100644 index 00000000..f896dfc8 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/outer-claim.bin differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/root.ixon-proof b/Tests/Fixtures/Aggregate/singleton-2026-09-05/root.ixon-proof new file mode 100644 index 00000000..61893ea5 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/root.ixon-proof differ diff --git a/Tests/Fixtures/Aggregate/singleton-2026-09-05/subjects.tree b/Tests/Fixtures/Aggregate/singleton-2026-09-05/subjects.tree new file mode 100644 index 00000000..4850027e Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-2026-09-05/subjects.tree differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/PROVENANCE.md b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/PROVENANCE.md new file mode 100644 index 00000000..99bf19d3 --- /dev/null +++ b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/PROVENANCE.md @@ -0,0 +1,139 @@ +# Experimental minimum-opening-width singleton aggregate + +This is a genuine `ix_aggr` shape-0 wrap using the explicit +`min-opening-width-v1` lookup-packing profile. Its IxVM child proof, +environment, subject tree, CheckEnv claim and IxVM key are byte-identical to +the [default-profile fixture](../singleton-2026-09-05/PROVENANCE.md). The +environment contains one well-formed axiom declaration: the certificate +checks its well-formedness, not a proof of the postulated proposition. The +canonical subject tree has no assumptions. + +Only the aggregate system uses the new profile. It chooses circuit-local +lookup groups minimizing accumulator plus quotient opening width, subject to +the existing blowup/degree bound. User constraints, lookup order, main-trace layouts, +preprocessed commitments and the 175 active circuit heights are unchanged. +Both proofs still use blowup log 2, cap height 0, 100 binary-FRI queries, +20-bit query grinding, no commitment grinding and a constant final polynomial. +The aggregate key, allowed-key digest, outer claim and root are different. +Default system construction and the production CLI have **not** adopted them. + +Generated on 2026-09-05 by `Benchmarks/FlockRootFixture.lean` in the +uncommitted Stage 3 worktree based on +`40cf786ac78990108701ac2f1ec5e3b5867f4410`, using Lean 4.33.1, multi-stark +`6ad074c1f2983ecdd7a56984d333441d6b38186a` and Plonky3 +`3152b14a89067c83775a8076cc262ffc48a1fd7c`. No personal store/cache or +formalization workspace was used. This is one minimal fixture, not a +representative corpus or an independently audited profile. + +## Identities + +- Subject: `f7a3722c3ab8ad9d45a8cecf412c1d1cae92e2e453a8e923b7f466bfecd345a8` +- Subject-tree root: `71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c` +- Bundled claim: `CheckEnv(71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c, none)` +- Root wrapper BLAKE3: `635c8f79af8cf1a913cb9291fbd1aa1a3f2c4ff221e6ec1339795989c649910f` +- Root wrapper SHA-256: `4cab2c294c1ba5ed80f3481c8fe2854cf7842b9ef95be64bcbbc9edf3c44fb95` +- Aggregate key BLAKE3: `3c740f5b7645b1f7cdf361b7de3e20628c18ea3bf9cfafd1bd8aac4aca2bf7ba` +- Outer claim BLAKE3: `72e37e5dda38c176b1caaa377ea779ca285d8e614ca268aeb9130812fdc2216d` +- Child wrapper BLAKE3: `9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618` +- Child wrapper SHA-256: `0795bff320f62034e76bb37e1ebf2e6da7968302a828242cc8e0041ba1236001` + +`fixture.json` records every input/key/claim/proof file's length and BLAKE3 +digest, circuit heights, native timings and memory measurements. +`execution.json` is the checkpoint from before aggregate proving. A directory +is complete only when `fixture.json` exists and explicit-profile verification +passes. Fixture metadata alone never selects a trusted key. + +## Reproduction and verification + +From the repository root, generate into a **new** directory: + +```sh +IX_FLOCK=1 lake build ix bench-flock-root-fixture +flock_fixture_parent=$(mktemp -d /tmp/ix-flock-min-opening.XXXXXX) +( + ulimit -v 67108864 + RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture \ + --min-opening-width --output "$flock_fixture_parent/root" --prove +) +.lake/build/bin/bench-flock-root-fixture --min-opening-width \ + --verify "$flock_fixture_parent/root" +``` + +Without `--prove`, the harness proves/verifies the IxVM child and executes +the aggregate without proving it. Generation requires a Linux address-space +limit of at most 64 GiB and refuses existing destinations before writes. The +cap is an OS allocation guard, not an RSS prediction; allocation failure can +terminate the subprocess and leave an incomplete directory. + +Verify these saved files without proving or writing anything: + +```sh +.lake/build/bin/bench-flock-root-fixture --min-opening-width \ + --verify Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05 +``` + +Verification checks bounded, digest-checked bytes, the exact singleton and +subject tree, both explicitly rebuilt keys, the expected outer claim and +both native proofs. Omitting the profile flag is an error. The normal +`ix flock-root` path rejects this experimental root with `InvalidProofShape` +under its unchanged production key. CI checks these boundaries and verifies +both fixtures in fresh processes; it does not regenerate native proofs. + +## Measured costs + +AMD Ryzen 9 7950X3D, 128 GiB-class RAM (`MemTotal=134128111616` bytes), +8 Rayon workers, 64 GiB address-space cap, 10 ms process-tree RSS sampling: + +| Measurement | Default profile | This profile | +| --- | ---: | ---: | +| Active committed column widths, summed | 9,025 | 8,367 | +| Root wrapper bytes | 8,565,030 | 8,002,662 | +| Aggregate proof time, including execution | 75.813 s | 75.386 s | +| Aggregate proof sampled peak RSS | 25.26 GiB | 22.61 GiB | +| Native aggregate verification | 0.121 s | 0.228 s | +| Stage 3 canonicality rows, packed-check compiler | 3,162,519 | 3,056,107 | +| Stage 3 padded z/a/b, packed-check compiler | 192 GiB | 192 GiB | + +These are single local runs. RSS includes retained system/runtime state, +not only incremental prover allocations. Widths are not height-weighted. +The native RSS model now uses the field's true extension degree instead of +inferring it from grouped lookup width; its historical calibration remains +approximate. The changed child prediction did not change its key/proof bytes. +Smaller native proof bytes and sampled proving RSS do not imply faster +verification or proportionally smaller Stage 3 memory. + +Count this root safely without allocating its Flock witness: + +```sh +( + ulimit -v 16777216 + IX_FLOCK_TIMING=1 RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture \ + --min-opening-width --count Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05 +) +``` + +Count mode first verifies the fixture, then sets the table limit to `2^32` +and the padded-witness limit to **one byte**. It exits zero only after the +expected witness-admission rejection, emitting `ix.flock-stage3.fixture-count` +JSON on stdout and the exact shape count on stderr. No wiring is compiled and +no prover is started; `compiled: false` is intentional. Production admission +defaults remain unchanged. This count took 0.149 s with a 347 MiB process +high-water RSS, excluding native validation from the reported count time. + +The initial Stage 2 packing experiment shrank Stage 3's largest table only +1.65%, leaving 768 GiB padded z/a/b. The query-point/denominator-sharing +compiler then reduced both profiles to `nu=23`, 8,388,608 rows and +**412,316,860,416 bytes (384 GiB)** of padded z/a/b alone. Packed canonicality +checks subsequently reduce both profiles to `nu=22`, 4,194,304 rows and +**206,158,430,208 bytes (192 GiB)**. This profile now has 3,056,107 packed +canonicality rows, versus 3,162,519 under the default Stage 2 key. All native +fixture bytes and other Stage 3 table row counts are unchanged. +No Flock proof or evaluated relation was produced for this real aggregate. +See the [paired measurement](../../../../flock-stage3/measurements/stage2-lookup-packing-2026-09-05.json) +for the original paired counts and circuit-width changes, and the +[query-sharing measurement](../../../../flock-stage3/measurements/pcs-query-sharing-2026-09-05.json) +for the preceding compiler and the +[canonicality-packing measurement](../../../../flock-stage3/measurements/packed-canonicality-2026-09-05.json) +for current per-gate counts on the same unchanged native fixtures. Deployment +adoption requires explicit key/relation pins, further corpus measurements and +independent review; this fixture is not automatic authorization to adopt it. diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/aggr.vk b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/aggr.vk new file mode 100644 index 00000000..b2080571 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/aggr.vk differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/check-env.claim b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/check-env.claim new file mode 100644 index 00000000..19871069 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/check-env.claim differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/environment.ixe b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/environment.ixe new file mode 100644 index 00000000..d44c554a Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/environment.ixe differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/execution.json b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/execution.json new file mode 100644 index 00000000..fa982002 --- /dev/null +++ b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/execution.json @@ -0,0 +1,785 @@ +{"version": 1, + "subject": "f7a3722c3ab8ad9d45a8cecf412c1d1cae92e2e453a8e923b7f466bfecd345a8", + "schema": "ix.flock-stage3.root-fixture", + "lean_toolchain": "4.33.1", + "ixvm_verify_us": 16870, + "ixvm_sampled_peak_rss_bytes": 953339904, + "ixvm_prove_us": 182085, + "ixvm_predicted_peak_bytes": 237549820, + "ixvm_compile_us": 49810, + "inputs": + [{"file": "environment.ixe", + "bytes": 116, + "blake3": + "bed282f9db4dee56f1746870478aad7223ce2a325f7e6baf24dacde3aa06e0e8"}, + {"file": "check-env.claim", + "bytes": 34, + "blake3": + "ddef67870a4517be2f996e2eeb6f7fdcd3871ba527190f17d45b93308a40825f"}, + {"file": "subjects.tree", + "bytes": 34, + "blake3": + "9014207ff8cef20d1f8f64934fef14d4114e8afe43f7a35f930dfcb70175d67e"}, + {"file": "ixvm.vk", + "bytes": 709741, + "blake3": + "e3b0aff0da508c305da9048b113e586502d35e75dadc4f7647d89efb320fbc78"}, + {"file": "aggr.vk", + "bytes": 181630, + "blake3": + "3c740f5b7645b1f7cdf361b7de3e20628c18ea3bf9cfafd1bd8aac4aca2bf7ba"}, + {"file": "outer-claim.bin", + "bytes": 144, + "blake3": + "72e37e5dda38c176b1caaa377ea779ca285d8e614ca268aeb9130812fdc2216d"}], + "fri": + {"query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0}, + "description": + "single well-formed axiom declaration; production ix_aggr shape-0 wrap", + "child": + {"file": "ixvm.ixon-proof", + "bytes": 4485008, + "blake3": "9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618"}, + "bundled_claim": + "CheckEnv(71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c, none)", + "aggregate_proven": false, + "aggregate_lookup_policy": "min-opening-width-v1", + "aggregate_circuits": + [{"name": "blake3_compress", + "height": 332248, + "committed_width": 735, + "cache_hits": 30}, + {"name": "blake3_compress_chunks", + "height": 910476, + "committed_width": 29, + "cache_hits": 0}, + {"name": "memory[3]", + "height": 1781328, + "committed_width": 12, + "cache_hits": 2531861}, + {"name": "read_u64_vec_n", + "height": 499217, + "committed_width": 28, + "cache_hits": 0}, + {"name": "aggr_read_byte", + "height": 709741, + "committed_width": 12, + "cache_hits": 1}, + {"name": "ro_fold", + "height": 192396, + "committed_width": 49, + "cache_hits": 741}, + {"name": "bucket_update", + "height": 168721, + "committed_width": 42, + "cache_hits": 33549}, + {"name": "read_nodes_n", + "height": 124768, + "committed_width": 57, + "cache_hits": 0}, + {"name": "select_rows", + "height": 111490, + "committed_width": 44, + "cache_hits": 2735}, + {"name": "read_vk_u16", + "height": 287822, + "committed_width": 15, + "cache_hits": 0}, + {"name": "rows_pop", + "height": 103656, + "committed_width": 44, + "cache_hits": 11313}, + {"name": "b3_rows_chunks", + "height": 13889, + "committed_width": 264, + "cache_hits": 9}, + {"name": "memory[7]", + "height": 166607, + "committed_width": 16, + "cache_hits": 172539}, + {"name": "select_rows_le", + "height": 48377, + "committed_width": 60, + "cache_hits": 3009}, + {"name": "bytes_to_block", + "height": 13089, + "committed_width": 237, + "cache_hits": 1152}, + {"name": "memory[6]", + "height": 153362, + "committed_width": 15, + "cache_hits": 104373}, + {"name": "blake3_compress_block", + "height": 14209, + "committed_width": 201, + "cache_hits": 0}, + {"name": "list_length.U8_8", + "height": 92339, + "committed_width": 25, + "cache_hits": 38901}, + {"name": "memory[10]", + "height": 102225, + "committed_width": 19, + "cache_hits": 863505}, + {"name": "memory[32]", + "height": 47672, + "committed_width": 41, + "cache_hits": 56371}, + {"name": "ri_apply", "height": 38900, "committed_width": 44, "cache_hits": 0}, + {"name": "Bytes2", "height": 65536, "committed_width": 24, "cache_hits": 0}, + {"name": "read_vk_tag", + "height": 124768, + "committed_width": 11, + "cache_hits": 0}, + {"name": "read_node_ids_n", + "height": 63409, + "committed_width": 23, + "cache_hits": 0}, + {"name": "mmcs_compress", + "height": 11919, + "committed_width": 111, + "cache_hits": 0}, + {"name": "eval_at", + "height": 16958, + "committed_width": 55, + "cache_hits": 17366}, + {"name": "cons8", "height": 25251, "committed_width": 33, "cache_hits": 31}, + {"name": "list_drop.G", + "height": 31910, + "committed_width": 20, + "cache_hits": 66505}, + {"name": "frontier_level", + "height": 10798, + "committed_width": 62, + "cache_hits": 10}, + {"name": "list_length.Ptr.U8_8_4", + "height": 30013, + "committed_width": 18, + "cache_hits": 715}, + {"name": "read_u64_vec_vec_n", + "height": 23700, + "committed_width": 23, + "cache_hits": 0}, + {"name": "open_batch_2pt", + "height": 15600, + "committed_width": 36, + "cache_hits": 0}, + {"name": "accs_onto", "height": 9679, "committed_width": 60, "cache_hits": 2}, + {"name": "read_count_at", + "height": 25993, + "committed_width": 19, + "cache_hits": 0}, + {"name": "read_digest_vec_at_n", + "height": 9245, + "committed_width": 58, + "cache_hits": 0}, + {"name": "blake3_next_layer", + "height": 3191, + "committed_width": 189, + "cache_hits": 0}, + {"name": "list_lookup.BatchOpening", + "height": 27956, + "committed_width": 16, + "cache_hits": 86659}, + {"name": "open_2pt_mat", + "height": 15600, + "committed_width": 30, + "cache_hits": 0}, + {"name": "read_ext_vec_n", + "height": 12881, + "committed_width": 37, + "cache_hits": 0}, + {"name": "frontier_merge", + "height": 10686, + "committed_width": 41, + "cache_hits": 605}, + {"name": "blake3_compress_layer", + "height": 3257, + "committed_width": 155, + "cache_hits": 9}, + {"name": "rev_onto", + "height": 17695, + "committed_width": 22, + "cache_hits": 38800}, + {"name": "list_drop.SysNode", + "height": 16958, + "committed_width": 23, + "cache_hits": 16881}, + {"name": "read_u64_vec", + "height": 23301, + "committed_width": 15, + "cache_hits": 0}, + {"name": "list_lookup.SysNode", + "height": 16958, + "committed_width": 19, + "cache_hits": 0}, + {"name": "open_prep", "height": 7800, "committed_width": 43, "cache_hits": 0}, + {"name": "logup_fingerprint", + "height": 6738, + "committed_width": 49, + "cache_hits": 601}, + {"name": "open_quotient", + "height": 7800, + "committed_width": 39, + "cache_hits": 0}, + {"name": "inject_maybe", + "height": 10698, + "committed_width": 23, + "cache_hits": 0}, + {"name": "memory[34]", + "height": 5984, + "committed_width": 43, + "cache_hits": 14017}, + {"name": "frontier_split", + "height": 6774, + "committed_width": 35, + "cache_hits": 1388}, + {"name": "list_is_empty.Ptr.U8_32", + "height": 14193, + "committed_width": 15, + "cache_hits": 13340}, + {"name": "memory[4]", + "height": 15175, + "committed_width": 13, + "cache_hits": 229269}, + {"name": "memo_u32_less_than", + "height": 6753, + "committed_width": 28, + "cache_hits": 2604}, + {"name": "list_length.CommitPhaseProofStep", + "height": 9431, + "committed_width": 19, + "cache_hits": 40384}, + {"name": "read_sys_lookups_n", + "height": 5866, + "committed_width": 29, + "cache_hits": 0}, + {"name": "compress_ordered", + "height": 9187, + "committed_width": 16, + "cache_hits": 0}, + {"name": "limbs_onto", + "height": 5357, + "committed_width": 29, + "cache_hits": 0}, + {"name": "leaf_hash_at", + "height": 2752, + "committed_width": 58, + "cache_hits": 469}, + {"name": "verify_query", + "height": 1231, + "committed_width": 142, + "cache_hits": 96}, + {"name": "exp_by_bits", + "height": 6010, + "committed_width": 23, + "cache_hits": 40029}, + {"name": "pair_mul", + "height": 4383, + "committed_width": 32, + "cache_hits": 4214}, + {"name": "frontier_sort", + "height": 3465, + "committed_width": 38, + "cache_hits": 423}, + {"name": "list_drop.G_2", + "height": 5894, + "committed_width": 21, + "cache_hits": 4471}, + {"name": "list_lookup.G_2", + "height": 5883, + "committed_width": 17, + "cache_hits": 9004}, + {"name": "read_sys_circuits_n", + "height": 765, + "committed_width": 155, + "cache_hits": 0}, + {"name": "frontier_leaves", + "height": 2018, + "committed_width": 33, + "cache_hits": 2}, + {"name": "logup_steps_fold", + "height": 678, + "committed_width": 116, + "cache_hits": 0}, + {"name": "fold_roots", + "height": 1374, + "committed_width": 45, + "cache_hits": 0}, + {"name": "read_ext_vec_vec_n", + "height": 2238, + "committed_width": 23, + "cache_hits": 0}, + {"name": "rows_at_round", + "height": 1616, + "committed_width": 29, + "cache_hits": 0}, + {"name": "fri_fold2", + "height": 1131, + "committed_width": 44, + "cache_hits": 96}, + {"name": "step_views_at", + "height": 1700, + "committed_width": 27, + "cache_hits": 0}, + {"name": "take_bits", "height": 1921, "committed_width": 23, "cache_hits": 0}, + {"name": "drop_index_bits", + "height": 1702, + "committed_width": 25, + "cache_hits": 15}, + {"name": "list_concat.Ptr.U8_32", + "height": 1889, + "committed_width": 22, + "cache_hits": 15}, + {"name": "rollin", "height": 1128, "committed_width": 35, "cache_hits": 99}, + {"name": "read_vk_u16_limb", + "height": 2300, + "committed_width": 15, + "cache_hits": 0}, + {"name": "read_ext_vec", + "height": 1991, + "committed_width": 15, + "cache_hits": 0}, + {"name": "has_height", + "height": 1199, + "committed_width": 23, + "cache_hits": 10673}, + {"name": "memory[8]", + "height": 1534, + "committed_width": 17, + "cache_hits": 63077}, + {"name": "circ_has_height", + "height": 1027, + "committed_width": 25, + "cache_hits": 0}, + {"name": "select_active_circuits", + "height": 765, + "committed_width": 34, + "cache_hits": 0}, + {"name": "bits_to_num", + "height": 1247, + "committed_width": 18, + "cache_hits": 23282}, + {"name": "select_active_prep", + "height": 765, + "committed_width": 30, + "cache_hits": 0}, + {"name": "ood_fold", + "height": 1038, + "committed_width": 18, + "cache_hits": 1419}, + {"name": "list_length.SysCircuit", + "height": 840, + "committed_width": 23, + "cache_hits": 2}, + {"name": "pad_block", "height": 1029, "committed_width": 18, "cache_hits": 0}, + {"name": "read_opt_idx_n", + "height": 765, + "committed_width": 25, + "cache_hits": 0}, + {"name": "log_degrees_onto", + "height": 843, + "committed_width": 22, + "cache_hits": 0}, + {"name": "read_active_n", + "height": 843, + "committed_width": 21, + "cache_hits": 0}, + {"name": "u64_is_zero", + "height": 694, + "committed_width": 25, + "cache_hits": 27436}, + {"name": "relaxed_u64_succ", + "height": 693, + "committed_width": 25, + "cache_hits": 819}, + {"name": "read_vk_u32_limb", + "height": 764, + "committed_width": 21, + "cache_hits": 0}, + {"name": "assert_bits", + "height": 765, + "committed_width": 19, + "cache_hits": 0}, + {"name": "points_onto", + "height": 622, + "committed_width": 22, + "cache_hits": 0}, + {"name": "assert_blowup_zero", + "height": 466, + "committed_width": 30, + "cache_hits": 99}, + {"name": "ch_sample8", + "height": 143, + "committed_width": 116, + "cache_hits": 0}, + {"name": "batch_views_at", + "height": 500, + "committed_width": 26, + "cache_hits": 0}, + {"name": "ood_loop", "height": 78, "committed_width": 211, "cache_hits": 0}, + {"name": "read_u64_vec_vec_vec_n", + "height": 404, + "committed_width": 27, + "cache_hits": 0}, + {"name": "b3_w4_onto", "height": 376, "committed_width": 21, "cache_hits": 0}, + {"name": "verify_one_query", + "height": 100, + "committed_width": 90, + "cache_hits": 0}, + {"name": "read_vk_u64", + "height": 209, + "committed_width": 33, + "cache_hits": 0}, + {"name": "blake3_finish", + "height": 49, + "committed_width": 186, + "cache_hits": 0}, + {"name": "lookup_groups_count", + "height": 308, + "committed_width": 18, + "cache_hits": 761}, + {"name": "ch_sample_bits", + "height": 101, + "committed_width": 67, + "cache_hits": 100}, + {"name": "read_opened_round_n", + "height": 237, + "committed_width": 23, + "cache_hits": 0}, + {"name": "frontier_fold", + "height": 244, + "committed_width": 22, + "cache_hits": 0}, + {"name": "round_onto", "height": 237, "committed_width": 22, "cache_hits": 0}, + {"name": "query_loop", "height": 101, "committed_width": 52, "cache_hits": 0}, + {"name": "read_field", "height": 205, "committed_width": 18, "cache_hits": 0}, + {"name": "open_prep_batch", + "height": 100, + "committed_width": 42, + "cache_hits": 0}, + {"name": "read_ext_vec_vec", + "height": 233, + "committed_width": 15, + "cache_hits": 0}, + {"name": "Bytes1", "height": 256, "committed_width": 11, "cache_hits": 0}, + {"name": "query_views", + "height": 101, + "committed_width": 26, + "cache_hits": 0}, + {"name": "sample_query_indices", + "height": 101, + "committed_width": 25, + "cache_hits": 0}, + {"name": "last_acc_is_zero", + "height": 77, + "committed_width": 30, + "cache_hits": 0}, + {"name": "heights_prep", + "height": 78, + "committed_width": 29, + "cache_hits": 100}, + {"name": "read_u8", "height": 108, "committed_width": 18, "cache_hits": 4}, + {"name": "heights_all", + "height": 78, + "committed_width": 23, + "cache_hits": 100}, + {"name": "quotient_eval", + "height": 57, + "committed_width": 32, + "cache_hits": 68}, + {"name": "ch_sample_field", + "height": 42, + "committed_width": 41, + "cache_hits": 0}, + {"name": "ext_exp_pow2", + "height": 70, + "committed_width": 21, + "cache_hits": 512}, + {"name": "reconstruct_ext_row", + "height": 46, + "committed_width": 30, + "cache_hits": 76}, + {"name": "cap_onto", "height": 23, "committed_width": 61, "cache_hits": 17}, + {"name": "flatten_u64", + "height": 70, + "committed_width": 14, + "cache_hits": 25927}, + {"name": "pcs_betas", "height": 17, "committed_width": 60, "cache_hits": 0}, + {"name": "mmcs_verify_multi", + "height": 20, + "committed_width": 41, + "cache_hits": 0}, + {"name": "verify_commit_multi", + "height": 17, + "committed_width": 45, + "cache_hits": 0}, + {"name": "read_digest_vec_at", + "height": 39, + "committed_width": 15, + "cache_hits": 0}, + {"name": "ch_sample_ext", + "height": 21, + "committed_width": 32, + "cache_hits": 0}, + {"name": "two_adic_gen", + "height": 33, + "committed_width": 17, + "cache_hits": 55783}, + {"name": "from_ext_basis", + "height": 44, + "committed_width": 10, + "cache_hits": 1}, + {"name": "pcs_check_witness", + "height": 17, + "committed_width": 34, + "cache_hits": 0}, + {"name": "read_commit_phase_step_vec_n", + "height": 17, + "committed_width": 31, + "cache_hits": 0}, + {"name": "build_buckets", + "height": 19, + "committed_width": 25, + "cache_hits": 99}, + {"name": "read_merkle_cap_vec_n", + "height": 17, + "committed_width": 23, + "cache_hits": 0}, + {"name": "snoc_b8", "height": 17, "committed_width": 22, "cache_hits": 0}, + {"name": "obs_log_arities", + "height": 17, + "committed_width": 22, + "cache_hits": 0}, + {"name": "list_length.CommitPhaseMultiStep", + "height": 17, + "committed_width": 20, + "cache_hits": 0}, + {"name": "read_u64", "height": 12, "committed_width": 33, "cache_hits": 0}, + {"name": "fingerprint_vals", + "height": 11, + "committed_width": 32, + "cache_hits": 0}, + {"name": "snoc_cap", "height": 18, "committed_width": 15, "cache_hits": 0}, + {"name": "read_claim_vals_n", + "height": 11, + "committed_width": 30, + "cache_hits": 0}, + {"name": "pow2", "height": 17, "committed_width": 14, "cache_hits": 76}, + {"name": "memory[5]", + "height": 17, + "committed_width": 14, + "cache_hits": 1734}, + {"name": "list_drop.U8_8", + "height": 10, + "committed_width": 27, + "cache_hits": 9}, + {"name": "list_lookup.U8_8", + "height": 10, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_concat.U8_8", + "height": 8, + "committed_width": 29, + "cache_hits": 0}, + {"name": "aggr_claim_field", + "height": 10, + "committed_width": 18, + "cache_hits": 0}, + {"name": "read_batch_opening_vec_n", + "height": 5, + "committed_width": 29, + "cache_hits": 0}, + {"name": "aggr_verify_child", + "height": 1, + "committed_width": 394, + "cache_hits": 0}, + {"name": "aggr_read_address", + "height": 2, + "committed_width": 126, + "cache_hits": 0}, + {"name": "heights_max", + "height": 3, + "committed_width": 40, + "cache_hits": 100}, + {"name": "aggr_load_sys", + "height": 1, + "committed_width": 201, + "cache_hits": 0}, + {"name": "ix_aggr", "height": 1, "committed_width": 174, "cache_hits": 0}, + {"name": "read_vk_cap_n", + "height": 2, + "committed_width": 62, + "cache_hits": 0}, + {"name": "read_opened_round", + "height": 4, + "committed_width": 15, + "cache_hits": 0}, + {"name": "quotient_degree_of", + "height": 2, + "committed_width": 45, + "cache_hits": 75}, + {"name": "aggr_pack_address", + "height": 2, + "committed_width": 41, + "cache_hits": 0}, + {"name": "aggr_child_check_env_digest", + "height": 1, + "committed_width": 83, + "cache_hits": 0}, + {"name": "claims_acc", "height": 2, "committed_width": 37, "cache_hits": 0}, + {"name": "read_claims_n", + "height": 2, + "committed_width": 27, + "cache_hits": 0}, + {"name": "verify_input_multi", + "height": 1, + "committed_width": 54, + "cache_hits": 0}, + {"name": "read_count", "height": 2, "committed_width": 21, "cache_hits": 0}, + {"name": "aggr_wrap", "height": 1, "committed_width": 53, "cache_hits": 0}, + {"name": "aggr_assert_digest", + "height": 2, + "committed_width": 22, + "cache_hits": 0}, + {"name": "aggr_claim_digest", + "height": 1, + "committed_width": 26, + "cache_hits": 0}, + {"name": "read_opt_commit", + "height": 1, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_length.Bucket", + "height": 1, + "committed_width": 22, + "cache_hits": 3}, + {"name": "list_length.FrontierNode", + "height": 1, + "committed_width": 21, + "cache_hits": 19}, + {"name": "read_preprocessed", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_only_claim", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "prep_count", "height": 1, "committed_width": 11, "cache_hits": 100}, + {"name": "aggr_parse_check_env", + "height": 0, + "committed_width": 23, + "cache_hits": 0}, + {"name": "aggr_node_hash", + "height": 0, + "committed_width": 19, + "cache_hits": 0}, + {"name": "aggr_leaf_hash", + "height": 0, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_parse_tree_body", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_put_address", + "height": 0, + "committed_width": 94, + "cache_hits": 0}, + {"name": "aggr_load_preimage", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_leaf_hashes", + "height": 0, + "committed_width": 24, + "cache_hits": 0}, + {"name": "aggr_pair", "height": 0, "committed_width": 131, "cache_hits": 0}, + {"name": "aggr_pair_hashes", + "height": 0, + "committed_width": 31, + "cache_hits": 0}, + {"name": "bytes_to_addr", + "height": 0, + "committed_width": 53, + "cache_hits": 0}, + {"name": "aggr_reduce_hashes", + "height": 0, + "committed_width": 27, + "cache_hits": 0}, + {"name": "aggr_canonical_root", + "height": 0, + "committed_width": 13, + "cache_hits": 0}, + {"name": "address_eq_tail", + "height": 0, + "committed_width": 84, + "cache_hits": 0}, + {"name": "aggr_load_canonical_tree", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_load_optional_tree", + "height": 0, + "committed_width": 14, + "cache_hits": 0}, + {"name": "aggr_assert_same_list", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_union", + "height": 0, + "committed_width": 38, + "cache_hits": 0}, + {"name": "aggr_next_assumption", + "height": 0, + "committed_width": 30, + "cache_hits": 0}, + {"name": "aggr_seek_subject", + "height": 0, + "committed_width": 25, + "cache_hits": 0}, + {"name": "aggr_assert_difference", + "height": 0, + "committed_width": 33, + "cache_hits": 0}, + {"name": "aggr_fold_path", + "height": 0, + "committed_width": 33, + "cache_hits": 0}, + {"name": "aggr_pair_structural", + "height": 0, + "committed_width": 128, + "cache_hits": 0}, + {"name": "aggr_address_order", + "height": 0, + "committed_width": 116, + "cache_hits": 0}, + {"name": "aggr_discharge_choice", + "height": 0, + "committed_width": 72, + "cache_hits": 0}, + {"name": "aggr_assert_strict_sorted", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_structural_difference", + "height": 0, + "committed_width": 32, + "cache_hits": 0}, + {"name": "address_eq", "height": 0, "committed_width": 82, "cache_hits": 0}, + {"name": "aggr_get_opt_address", + "height": 0, + "committed_width": 18, + "cache_hits": 0}], + "aggr_execute_us": 1443624, + "aggr_execute_sampled_peak_rss_bytes": 2855505920, + "aggr_compile_us": 22978, + "address_space_limit_bytes": 68719476736, + "active_committed_width": 8367} diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/fixture.json b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/fixture.json new file mode 100644 index 00000000..0f5362c5 --- /dev/null +++ b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/fixture.json @@ -0,0 +1,792 @@ +{"version": 1, + "subject": "f7a3722c3ab8ad9d45a8cecf412c1d1cae92e2e453a8e923b7f466bfecd345a8", + "schema": "ix.flock-stage3.root-fixture", + "root": + {"file": "root.ixon-proof", + "bytes": 8002662, + "blake3": "635c8f79af8cf1a913cb9291fbd1aa1a3f2c4ff221e6ec1339795989c649910f"}, + "lean_toolchain": "4.33.1", + "ixvm_verify_us": 16870, + "ixvm_sampled_peak_rss_bytes": 953339904, + "ixvm_prove_us": 182085, + "ixvm_predicted_peak_bytes": 237549820, + "ixvm_compile_us": 49810, + "inputs": + [{"file": "environment.ixe", + "bytes": 116, + "blake3": + "bed282f9db4dee56f1746870478aad7223ce2a325f7e6baf24dacde3aa06e0e8"}, + {"file": "check-env.claim", + "bytes": 34, + "blake3": + "ddef67870a4517be2f996e2eeb6f7fdcd3871ba527190f17d45b93308a40825f"}, + {"file": "subjects.tree", + "bytes": 34, + "blake3": + "9014207ff8cef20d1f8f64934fef14d4114e8afe43f7a35f930dfcb70175d67e"}, + {"file": "ixvm.vk", + "bytes": 709741, + "blake3": + "e3b0aff0da508c305da9048b113e586502d35e75dadc4f7647d89efb320fbc78"}, + {"file": "aggr.vk", + "bytes": 181630, + "blake3": + "3c740f5b7645b1f7cdf361b7de3e20628c18ea3bf9cfafd1bd8aac4aca2bf7ba"}, + {"file": "outer-claim.bin", + "bytes": 144, + "blake3": + "72e37e5dda38c176b1caaa377ea779ca285d8e614ca268aeb9130812fdc2216d"}], + "fri": + {"query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0}, + "description": + "single well-formed axiom declaration; production ix_aggr shape-0 wrap", + "child": + {"file": "ixvm.ixon-proof", + "bytes": 4485008, + "blake3": "9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618"}, + "bundled_claim": + "CheckEnv(71cbf135649af05f7cce8ba201a48e46556de51483f690cc6ec500a65530059c, none)", + "aggregate_proven": true, + "aggregate_lookup_policy": "min-opening-width-v1", + "aggregate_circuits": + [{"name": "blake3_compress", + "height": 332248, + "committed_width": 735, + "cache_hits": 30}, + {"name": "blake3_compress_chunks", + "height": 910476, + "committed_width": 29, + "cache_hits": 0}, + {"name": "memory[3]", + "height": 1781328, + "committed_width": 12, + "cache_hits": 2531861}, + {"name": "read_u64_vec_n", + "height": 499217, + "committed_width": 28, + "cache_hits": 0}, + {"name": "aggr_read_byte", + "height": 709741, + "committed_width": 12, + "cache_hits": 1}, + {"name": "ro_fold", + "height": 192396, + "committed_width": 49, + "cache_hits": 741}, + {"name": "bucket_update", + "height": 168721, + "committed_width": 42, + "cache_hits": 33549}, + {"name": "read_nodes_n", + "height": 124768, + "committed_width": 57, + "cache_hits": 0}, + {"name": "select_rows", + "height": 111490, + "committed_width": 44, + "cache_hits": 2735}, + {"name": "read_vk_u16", + "height": 287822, + "committed_width": 15, + "cache_hits": 0}, + {"name": "rows_pop", + "height": 103656, + "committed_width": 44, + "cache_hits": 11313}, + {"name": "b3_rows_chunks", + "height": 13889, + "committed_width": 264, + "cache_hits": 9}, + {"name": "memory[7]", + "height": 166607, + "committed_width": 16, + "cache_hits": 172539}, + {"name": "select_rows_le", + "height": 48377, + "committed_width": 60, + "cache_hits": 3009}, + {"name": "bytes_to_block", + "height": 13089, + "committed_width": 237, + "cache_hits": 1152}, + {"name": "memory[6]", + "height": 153362, + "committed_width": 15, + "cache_hits": 104373}, + {"name": "blake3_compress_block", + "height": 14209, + "committed_width": 201, + "cache_hits": 0}, + {"name": "list_length.U8_8", + "height": 92339, + "committed_width": 25, + "cache_hits": 38901}, + {"name": "memory[10]", + "height": 102225, + "committed_width": 19, + "cache_hits": 863505}, + {"name": "memory[32]", + "height": 47672, + "committed_width": 41, + "cache_hits": 56371}, + {"name": "ri_apply", "height": 38900, "committed_width": 44, "cache_hits": 0}, + {"name": "Bytes2", "height": 65536, "committed_width": 24, "cache_hits": 0}, + {"name": "read_vk_tag", + "height": 124768, + "committed_width": 11, + "cache_hits": 0}, + {"name": "read_node_ids_n", + "height": 63409, + "committed_width": 23, + "cache_hits": 0}, + {"name": "mmcs_compress", + "height": 11919, + "committed_width": 111, + "cache_hits": 0}, + {"name": "eval_at", + "height": 16958, + "committed_width": 55, + "cache_hits": 17366}, + {"name": "cons8", "height": 25251, "committed_width": 33, "cache_hits": 31}, + {"name": "list_drop.G", + "height": 31910, + "committed_width": 20, + "cache_hits": 66505}, + {"name": "frontier_level", + "height": 10798, + "committed_width": 62, + "cache_hits": 10}, + {"name": "list_length.Ptr.U8_8_4", + "height": 30013, + "committed_width": 18, + "cache_hits": 715}, + {"name": "read_u64_vec_vec_n", + "height": 23700, + "committed_width": 23, + "cache_hits": 0}, + {"name": "open_batch_2pt", + "height": 15600, + "committed_width": 36, + "cache_hits": 0}, + {"name": "accs_onto", "height": 9679, "committed_width": 60, "cache_hits": 2}, + {"name": "read_count_at", + "height": 25993, + "committed_width": 19, + "cache_hits": 0}, + {"name": "read_digest_vec_at_n", + "height": 9245, + "committed_width": 58, + "cache_hits": 0}, + {"name": "blake3_next_layer", + "height": 3191, + "committed_width": 189, + "cache_hits": 0}, + {"name": "list_lookup.BatchOpening", + "height": 27956, + "committed_width": 16, + "cache_hits": 86659}, + {"name": "open_2pt_mat", + "height": 15600, + "committed_width": 30, + "cache_hits": 0}, + {"name": "read_ext_vec_n", + "height": 12881, + "committed_width": 37, + "cache_hits": 0}, + {"name": "frontier_merge", + "height": 10686, + "committed_width": 41, + "cache_hits": 605}, + {"name": "blake3_compress_layer", + "height": 3257, + "committed_width": 155, + "cache_hits": 9}, + {"name": "rev_onto", + "height": 17695, + "committed_width": 22, + "cache_hits": 38800}, + {"name": "list_drop.SysNode", + "height": 16958, + "committed_width": 23, + "cache_hits": 16881}, + {"name": "read_u64_vec", + "height": 23301, + "committed_width": 15, + "cache_hits": 0}, + {"name": "list_lookup.SysNode", + "height": 16958, + "committed_width": 19, + "cache_hits": 0}, + {"name": "open_prep", "height": 7800, "committed_width": 43, "cache_hits": 0}, + {"name": "logup_fingerprint", + "height": 6738, + "committed_width": 49, + "cache_hits": 601}, + {"name": "open_quotient", + "height": 7800, + "committed_width": 39, + "cache_hits": 0}, + {"name": "inject_maybe", + "height": 10698, + "committed_width": 23, + "cache_hits": 0}, + {"name": "memory[34]", + "height": 5984, + "committed_width": 43, + "cache_hits": 14017}, + {"name": "frontier_split", + "height": 6774, + "committed_width": 35, + "cache_hits": 1388}, + {"name": "list_is_empty.Ptr.U8_32", + "height": 14193, + "committed_width": 15, + "cache_hits": 13340}, + {"name": "memory[4]", + "height": 15175, + "committed_width": 13, + "cache_hits": 229269}, + {"name": "memo_u32_less_than", + "height": 6753, + "committed_width": 28, + "cache_hits": 2604}, + {"name": "list_length.CommitPhaseProofStep", + "height": 9431, + "committed_width": 19, + "cache_hits": 40384}, + {"name": "read_sys_lookups_n", + "height": 5866, + "committed_width": 29, + "cache_hits": 0}, + {"name": "compress_ordered", + "height": 9187, + "committed_width": 16, + "cache_hits": 0}, + {"name": "limbs_onto", + "height": 5357, + "committed_width": 29, + "cache_hits": 0}, + {"name": "leaf_hash_at", + "height": 2752, + "committed_width": 58, + "cache_hits": 469}, + {"name": "verify_query", + "height": 1231, + "committed_width": 142, + "cache_hits": 96}, + {"name": "exp_by_bits", + "height": 6010, + "committed_width": 23, + "cache_hits": 40029}, + {"name": "pair_mul", + "height": 4383, + "committed_width": 32, + "cache_hits": 4214}, + {"name": "frontier_sort", + "height": 3465, + "committed_width": 38, + "cache_hits": 423}, + {"name": "list_drop.G_2", + "height": 5894, + "committed_width": 21, + "cache_hits": 4471}, + {"name": "list_lookup.G_2", + "height": 5883, + "committed_width": 17, + "cache_hits": 9004}, + {"name": "read_sys_circuits_n", + "height": 765, + "committed_width": 155, + "cache_hits": 0}, + {"name": "frontier_leaves", + "height": 2018, + "committed_width": 33, + "cache_hits": 2}, + {"name": "logup_steps_fold", + "height": 678, + "committed_width": 116, + "cache_hits": 0}, + {"name": "fold_roots", + "height": 1374, + "committed_width": 45, + "cache_hits": 0}, + {"name": "read_ext_vec_vec_n", + "height": 2238, + "committed_width": 23, + "cache_hits": 0}, + {"name": "rows_at_round", + "height": 1616, + "committed_width": 29, + "cache_hits": 0}, + {"name": "fri_fold2", + "height": 1131, + "committed_width": 44, + "cache_hits": 96}, + {"name": "step_views_at", + "height": 1700, + "committed_width": 27, + "cache_hits": 0}, + {"name": "take_bits", "height": 1921, "committed_width": 23, "cache_hits": 0}, + {"name": "drop_index_bits", + "height": 1702, + "committed_width": 25, + "cache_hits": 15}, + {"name": "list_concat.Ptr.U8_32", + "height": 1889, + "committed_width": 22, + "cache_hits": 15}, + {"name": "rollin", "height": 1128, "committed_width": 35, "cache_hits": 99}, + {"name": "read_vk_u16_limb", + "height": 2300, + "committed_width": 15, + "cache_hits": 0}, + {"name": "read_ext_vec", + "height": 1991, + "committed_width": 15, + "cache_hits": 0}, + {"name": "has_height", + "height": 1199, + "committed_width": 23, + "cache_hits": 10673}, + {"name": "memory[8]", + "height": 1534, + "committed_width": 17, + "cache_hits": 63077}, + {"name": "circ_has_height", + "height": 1027, + "committed_width": 25, + "cache_hits": 0}, + {"name": "select_active_circuits", + "height": 765, + "committed_width": 34, + "cache_hits": 0}, + {"name": "bits_to_num", + "height": 1247, + "committed_width": 18, + "cache_hits": 23282}, + {"name": "select_active_prep", + "height": 765, + "committed_width": 30, + "cache_hits": 0}, + {"name": "ood_fold", + "height": 1038, + "committed_width": 18, + "cache_hits": 1419}, + {"name": "list_length.SysCircuit", + "height": 840, + "committed_width": 23, + "cache_hits": 2}, + {"name": "pad_block", "height": 1029, "committed_width": 18, "cache_hits": 0}, + {"name": "read_opt_idx_n", + "height": 765, + "committed_width": 25, + "cache_hits": 0}, + {"name": "log_degrees_onto", + "height": 843, + "committed_width": 22, + "cache_hits": 0}, + {"name": "read_active_n", + "height": 843, + "committed_width": 21, + "cache_hits": 0}, + {"name": "u64_is_zero", + "height": 694, + "committed_width": 25, + "cache_hits": 27436}, + {"name": "relaxed_u64_succ", + "height": 693, + "committed_width": 25, + "cache_hits": 819}, + {"name": "read_vk_u32_limb", + "height": 764, + "committed_width": 21, + "cache_hits": 0}, + {"name": "assert_bits", + "height": 765, + "committed_width": 19, + "cache_hits": 0}, + {"name": "points_onto", + "height": 622, + "committed_width": 22, + "cache_hits": 0}, + {"name": "assert_blowup_zero", + "height": 466, + "committed_width": 30, + "cache_hits": 99}, + {"name": "ch_sample8", + "height": 143, + "committed_width": 116, + "cache_hits": 0}, + {"name": "batch_views_at", + "height": 500, + "committed_width": 26, + "cache_hits": 0}, + {"name": "ood_loop", "height": 78, "committed_width": 211, "cache_hits": 0}, + {"name": "read_u64_vec_vec_vec_n", + "height": 404, + "committed_width": 27, + "cache_hits": 0}, + {"name": "b3_w4_onto", "height": 376, "committed_width": 21, "cache_hits": 0}, + {"name": "verify_one_query", + "height": 100, + "committed_width": 90, + "cache_hits": 0}, + {"name": "read_vk_u64", + "height": 209, + "committed_width": 33, + "cache_hits": 0}, + {"name": "blake3_finish", + "height": 49, + "committed_width": 186, + "cache_hits": 0}, + {"name": "lookup_groups_count", + "height": 308, + "committed_width": 18, + "cache_hits": 761}, + {"name": "ch_sample_bits", + "height": 101, + "committed_width": 67, + "cache_hits": 100}, + {"name": "read_opened_round_n", + "height": 237, + "committed_width": 23, + "cache_hits": 0}, + {"name": "frontier_fold", + "height": 244, + "committed_width": 22, + "cache_hits": 0}, + {"name": "round_onto", "height": 237, "committed_width": 22, "cache_hits": 0}, + {"name": "query_loop", "height": 101, "committed_width": 52, "cache_hits": 0}, + {"name": "read_field", "height": 205, "committed_width": 18, "cache_hits": 0}, + {"name": "open_prep_batch", + "height": 100, + "committed_width": 42, + "cache_hits": 0}, + {"name": "read_ext_vec_vec", + "height": 233, + "committed_width": 15, + "cache_hits": 0}, + {"name": "Bytes1", "height": 256, "committed_width": 11, "cache_hits": 0}, + {"name": "query_views", + "height": 101, + "committed_width": 26, + "cache_hits": 0}, + {"name": "sample_query_indices", + "height": 101, + "committed_width": 25, + "cache_hits": 0}, + {"name": "last_acc_is_zero", + "height": 77, + "committed_width": 30, + "cache_hits": 0}, + {"name": "heights_prep", + "height": 78, + "committed_width": 29, + "cache_hits": 100}, + {"name": "read_u8", "height": 108, "committed_width": 18, "cache_hits": 4}, + {"name": "heights_all", + "height": 78, + "committed_width": 23, + "cache_hits": 100}, + {"name": "quotient_eval", + "height": 57, + "committed_width": 32, + "cache_hits": 68}, + {"name": "ch_sample_field", + "height": 42, + "committed_width": 41, + "cache_hits": 0}, + {"name": "ext_exp_pow2", + "height": 70, + "committed_width": 21, + "cache_hits": 512}, + {"name": "reconstruct_ext_row", + "height": 46, + "committed_width": 30, + "cache_hits": 76}, + {"name": "cap_onto", "height": 23, "committed_width": 61, "cache_hits": 17}, + {"name": "flatten_u64", + "height": 70, + "committed_width": 14, + "cache_hits": 25927}, + {"name": "pcs_betas", "height": 17, "committed_width": 60, "cache_hits": 0}, + {"name": "mmcs_verify_multi", + "height": 20, + "committed_width": 41, + "cache_hits": 0}, + {"name": "verify_commit_multi", + "height": 17, + "committed_width": 45, + "cache_hits": 0}, + {"name": "read_digest_vec_at", + "height": 39, + "committed_width": 15, + "cache_hits": 0}, + {"name": "ch_sample_ext", + "height": 21, + "committed_width": 32, + "cache_hits": 0}, + {"name": "two_adic_gen", + "height": 33, + "committed_width": 17, + "cache_hits": 55783}, + {"name": "from_ext_basis", + "height": 44, + "committed_width": 10, + "cache_hits": 1}, + {"name": "pcs_check_witness", + "height": 17, + "committed_width": 34, + "cache_hits": 0}, + {"name": "read_commit_phase_step_vec_n", + "height": 17, + "committed_width": 31, + "cache_hits": 0}, + {"name": "build_buckets", + "height": 19, + "committed_width": 25, + "cache_hits": 99}, + {"name": "read_merkle_cap_vec_n", + "height": 17, + "committed_width": 23, + "cache_hits": 0}, + {"name": "snoc_b8", "height": 17, "committed_width": 22, "cache_hits": 0}, + {"name": "obs_log_arities", + "height": 17, + "committed_width": 22, + "cache_hits": 0}, + {"name": "list_length.CommitPhaseMultiStep", + "height": 17, + "committed_width": 20, + "cache_hits": 0}, + {"name": "read_u64", "height": 12, "committed_width": 33, "cache_hits": 0}, + {"name": "fingerprint_vals", + "height": 11, + "committed_width": 32, + "cache_hits": 0}, + {"name": "snoc_cap", "height": 18, "committed_width": 15, "cache_hits": 0}, + {"name": "read_claim_vals_n", + "height": 11, + "committed_width": 30, + "cache_hits": 0}, + {"name": "pow2", "height": 17, "committed_width": 14, "cache_hits": 76}, + {"name": "memory[5]", + "height": 17, + "committed_width": 14, + "cache_hits": 1734}, + {"name": "list_drop.U8_8", + "height": 10, + "committed_width": 27, + "cache_hits": 9}, + {"name": "list_lookup.U8_8", + "height": 10, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_concat.U8_8", + "height": 8, + "committed_width": 29, + "cache_hits": 0}, + {"name": "aggr_claim_field", + "height": 10, + "committed_width": 18, + "cache_hits": 0}, + {"name": "read_batch_opening_vec_n", + "height": 5, + "committed_width": 29, + "cache_hits": 0}, + {"name": "aggr_verify_child", + "height": 1, + "committed_width": 394, + "cache_hits": 0}, + {"name": "aggr_read_address", + "height": 2, + "committed_width": 126, + "cache_hits": 0}, + {"name": "heights_max", + "height": 3, + "committed_width": 40, + "cache_hits": 100}, + {"name": "aggr_load_sys", + "height": 1, + "committed_width": 201, + "cache_hits": 0}, + {"name": "ix_aggr", "height": 1, "committed_width": 174, "cache_hits": 0}, + {"name": "read_vk_cap_n", + "height": 2, + "committed_width": 62, + "cache_hits": 0}, + {"name": "read_opened_round", + "height": 4, + "committed_width": 15, + "cache_hits": 0}, + {"name": "quotient_degree_of", + "height": 2, + "committed_width": 45, + "cache_hits": 75}, + {"name": "aggr_pack_address", + "height": 2, + "committed_width": 41, + "cache_hits": 0}, + {"name": "aggr_child_check_env_digest", + "height": 1, + "committed_width": 83, + "cache_hits": 0}, + {"name": "claims_acc", "height": 2, "committed_width": 37, "cache_hits": 0}, + {"name": "read_claims_n", + "height": 2, + "committed_width": 27, + "cache_hits": 0}, + {"name": "verify_input_multi", + "height": 1, + "committed_width": 54, + "cache_hits": 0}, + {"name": "read_count", "height": 2, "committed_width": 21, "cache_hits": 0}, + {"name": "aggr_wrap", "height": 1, "committed_width": 53, "cache_hits": 0}, + {"name": "aggr_assert_digest", + "height": 2, + "committed_width": 22, + "cache_hits": 0}, + {"name": "aggr_claim_digest", + "height": 1, + "committed_width": 26, + "cache_hits": 0}, + {"name": "read_opt_commit", + "height": 1, + "committed_width": 23, + "cache_hits": 0}, + {"name": "list_length.Bucket", + "height": 1, + "committed_width": 22, + "cache_hits": 3}, + {"name": "list_length.FrontierNode", + "height": 1, + "committed_width": 21, + "cache_hits": 19}, + {"name": "read_preprocessed", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_only_claim", + "height": 1, + "committed_width": 17, + "cache_hits": 0}, + {"name": "prep_count", "height": 1, "committed_width": 11, "cache_hits": 100}, + {"name": "aggr_parse_check_env", + "height": 0, + "committed_width": 23, + "cache_hits": 0}, + {"name": "aggr_node_hash", + "height": 0, + "committed_width": 19, + "cache_hits": 0}, + {"name": "aggr_leaf_hash", + "height": 0, + "committed_width": 17, + "cache_hits": 0}, + {"name": "aggr_parse_tree_body", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_put_address", + "height": 0, + "committed_width": 94, + "cache_hits": 0}, + {"name": "aggr_load_preimage", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_leaf_hashes", + "height": 0, + "committed_width": 24, + "cache_hits": 0}, + {"name": "aggr_pair", "height": 0, "committed_width": 131, "cache_hits": 0}, + {"name": "aggr_pair_hashes", + "height": 0, + "committed_width": 31, + "cache_hits": 0}, + {"name": "bytes_to_addr", + "height": 0, + "committed_width": 53, + "cache_hits": 0}, + {"name": "aggr_reduce_hashes", + "height": 0, + "committed_width": 27, + "cache_hits": 0}, + {"name": "aggr_canonical_root", + "height": 0, + "committed_width": 13, + "cache_hits": 0}, + {"name": "address_eq_tail", + "height": 0, + "committed_width": 84, + "cache_hits": 0}, + {"name": "aggr_load_canonical_tree", + "height": 0, + "committed_width": 62, + "cache_hits": 0}, + {"name": "aggr_load_optional_tree", + "height": 0, + "committed_width": 14, + "cache_hits": 0}, + {"name": "aggr_assert_same_list", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_union", + "height": 0, + "committed_width": 38, + "cache_hits": 0}, + {"name": "aggr_next_assumption", + "height": 0, + "committed_width": 30, + "cache_hits": 0}, + {"name": "aggr_seek_subject", + "height": 0, + "committed_width": 25, + "cache_hits": 0}, + {"name": "aggr_assert_difference", + "height": 0, + "committed_width": 33, + "cache_hits": 0}, + {"name": "aggr_fold_path", + "height": 0, + "committed_width": 33, + "cache_hits": 0}, + {"name": "aggr_pair_structural", + "height": 0, + "committed_width": 128, + "cache_hits": 0}, + {"name": "aggr_address_order", + "height": 0, + "committed_width": 116, + "cache_hits": 0}, + {"name": "aggr_discharge_choice", + "height": 0, + "committed_width": 72, + "cache_hits": 0}, + {"name": "aggr_assert_strict_sorted", + "height": 0, + "committed_width": 26, + "cache_hits": 0}, + {"name": "aggr_assert_structural_difference", + "height": 0, + "committed_width": 32, + "cache_hits": 0}, + {"name": "address_eq", "height": 0, "committed_width": 82, "cache_hits": 0}, + {"name": "aggr_get_opt_address", + "height": 0, + "committed_width": 18, + "cache_hits": 0}], + "aggr_verify_us": 228214, + "aggr_prove_us": 75386250, + "aggr_prove_sampled_peak_rss_bytes": 24276983808, + "aggr_execute_us": 1443624, + "aggr_execute_sampled_peak_rss_bytes": 2855505920, + "aggr_compile_us": 22978, + "address_space_limit_bytes": 68719476736, + "active_committed_width": 8367} diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/ixvm.ixon-proof b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/ixvm.ixon-proof new file mode 100644 index 00000000..1be669e3 Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/ixvm.ixon-proof differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/ixvm.vk b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/ixvm.vk new file mode 100644 index 00000000..bfc2cf6b Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/ixvm.vk differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/outer-claim.bin b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/outer-claim.bin new file mode 100644 index 00000000..a423ba8f Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/outer-claim.bin differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/root.ixon-proof b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/root.ixon-proof new file mode 100644 index 00000000..b5f73d4b Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/root.ixon-proof differ diff --git a/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/subjects.tree b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/subjects.tree new file mode 100644 index 00000000..4850027e Binary files /dev/null and b/Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/subjects.tree differ diff --git a/Tests/FlockRootCli.lean b/Tests/FlockRootCli.lean new file mode 100644 index 00000000..b537efaf --- /dev/null +++ b/Tests/FlockRootCli.lean @@ -0,0 +1,112 @@ +import Lean + +/- Run after `IX_FLOCK=1 lake build ix`. These checks invoke the linked binary, +so the JSONL contract, feature-enabled FFI, and early file handling are covered. +The current singleton aggregate exercises count-before-compile admission; +the historical fixture remains rejected at the protocol boundary. -/ + +private def runIx (args : Array String) (trace : Bool := false) : IO IO.Process.Output := + IO.Process.output { + cmd := ".lake/build/bin/ix" + args := #["flock-root"] ++ args + env := #[("IX_FLOCK_TIMING", if trace then some "1" else none)] } + +private def require (condition : Bool) (message : String) : IO Unit := do + unless condition do throw <| IO.userError message + +private def jsonErrors (args : Array String) (fragments : Array String) + (exact : Bool := false) (trace : Bool := false) : IO Unit := do + let output ← runIx (args ++ #["--jsonl"]) trace + require (output.exitCode == 1) s!"expected failure: {output.stdout}\n{output.stderr}" + let lines := output.stdout.splitOn "\n" |>.filter (!·.isEmpty) |>.toArray + require (lines.size == fragments.size) s!"stdout is not one record per root: {output.stdout}" + for (line, fragment) in lines.zip fragments do + let json ← IO.ofExcept <| Lean.Json.parse line + require ((← IO.ofExcept <| json.getObjValAs? String "schema") == "ix.flock-stage3.root") line + require ((← IO.ofExcept <| json.getObjValAs? Nat "version") == 1) line + require ((← IO.ofExcept <| json.getObjValAs? String "status") == "error") line + let error ← IO.ofExcept <| json.getObjValAs? String "error" + require (if exact then error == fragment else (error.splitOn fragment).length > 1) + s!"expected {fragment}: {error}" + if trace then + require ((output.stderr.splitOn "\"schema\":\"ix.flock-stage3.shape-count\"").length > 1) + "admission failure did not emit its count-only diagnostic on stderr" + require ((output.stderr.splitOn "\"compiled\":false").length > 1) + "count-only diagnostic must not claim the relation was compiled" + +def main : IO Unit := do + let temp ← IO.Process.output { cmd := "mktemp", args := #["-d", "/tmp/ix-flock-cli-test.XXXXXX"] } + require (temp.exitCode == 0) "create CLI test directory" + let directory : System.FilePath := temp.stdout.trimAscii.toString + let rootsFile := directory / "roots.txt" + let occupied := directory / "occupied.flock" + let abandoned := directory / "abandoned.flock" + let historical := "Tests/Fixtures/Aggregate/mathlib-2026-09-03/c2fdce660eb66899efa303b41d4ca1611a62a688ef20684fdc327739d38bd67f.ixon-proof" + let protocolError := "expand verified Aiur proof: Verification(\"InvalidProofShape\")" + let current := "Tests/Fixtures/Aggregate/singleton-2026-09-05/root.ixon-proof" + let compactDirectory := "Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05" + try + jsonErrors #["invalid-one", "invalid-two"] #["64-char hex", "64-char hex"] + IO.FS.writeFile rootsFile "# a comment\n\ninvalid-two\ninvalid-three\n" + jsonErrors #["invalid-one", "--roots-file", rootsFile.toString] + #["64-char hex", "64-char hex", "64-char hex"] + jsonErrors #["--root-file", (directory / "missing.ixon").toString] #["no such file"] + -- Compare the complete backend error across fresh processes. A mismatched + -- Rust IO.Error constructor tag used to attach a garbage OS error code. + for _ in [:2] do + jsonErrors #["--root-file", historical] #[protocolError] (exact := true) + -- This is a genuine, current-protocol production aggregate, natively + -- verified in the fixture job. Count its full relation without allocating + -- its 192 GiB padded witness or compiling its capacity-sized wiring. + jsonErrors #["--root-file", current] + #["Stage 3 padded union witness requires 206158430208 bytes; admission limit is 34359738368 (PCS/compiler scratch is additional)"] + (exact := true) (trace := true) + jsonErrors #["--root-file", current, "--max-table-capacity", "2097152"] + #["Stage 3 table capacity 4194304 (nu=22) exceeds admission limit 2097152"] + (exact := true) + -- The experiment has a distinct aggregate key. Default production + -- verification must never infer/adopt that key from fixture metadata. + jsonErrors #["--root-file", compactDirectory ++ "/root.ixon-proof"] + #[protocolError] (exact := true) + let wrongProfile ← IO.Process.output { + cmd := ".lake/build/bin/bench-flock-root-fixture" + args := #["--verify", compactDirectory] } + require (wrongProfile.exitCode == 1 && + (wrongProfile.stderr.splitOn "explicitly requested profile").length > 1) + "experimental fixture verified without its explicit key profile" + let compactCount ← IO.Process.output { + cmd := ".lake/build/bin/bench-flock-root-fixture" + args := #["--min-opening-width", "--count", compactDirectory] + env := #[("IX_FLOCK_TIMING", some "1")] } + require (compactCount.exitCode == 0) compactCount.stderr + let countJson ← IO.ofExcept (Lean.Json.parse compactCount.stdout) + require ((← IO.ofExcept <| countJson.getObjValAs? String "schema") == + "ix.flock-stage3.fixture-count") compactCount.stdout + require (!(← IO.ofExcept <| countJson.getObjValAs? Bool "compiled")) compactCount.stdout + require ((← IO.ofExcept <| countJson.getObjValAs? String "admission_error") == + "Stage 3 padded union witness requires 206158430208 bytes; admission limit is 1 (PCS/compiler scratch is additional)") + compactCount.stdout + require ((compactCount.stderr.splitOn "\"schema\":\"ix.flock-stage3.shape-count\"").length > 1) + "experimental count did not emit its table census on stderr" + + IO.FS.writeFile occupied "retained artifact" + jsonErrors #["--root-file", historical, "--mode", "prove", "--output", occupied.toString] + #["refusing to overwrite"] + require ((← IO.FS.readFile occupied) == "retained artifact") "existing artifact changed" + jsonErrors #["--root-file", historical, "--mode", "prove", "--output", abandoned.toString] + #[protocolError] (exact := true) + require (!(← abandoned.pathExists)) "failed preflight installed an artifact" + require ((← directory.readDir).size == 2) "failed preflight left temporary files" + + for args in [#["invalid", "--max-witness-mib", "0"], + #["invalid", "--max-advice-mib", "18446744073709551615"], + #["invalid", "--mode", "verify"], + #["one", "two", "--mode", "prove", "--output", abandoned.toString], + #["invalid", "--root-file", historical]] do + let output ← runIx args + require (output.exitCode == 1 && output.stdout.isEmpty) s!"invalid options started root work: {output.stdout}\n{output.stderr}" + finally + for path in [rootsFile, occupied] do + if ← path.pathExists then IO.FS.removeFile path + IO.FS.removeDir directory + IO.println "Flock Stage 3 CLI regressions passed" diff --git a/Tests/MultiStark.lean b/Tests/MultiStark.lean index 0e0f9242..a2935fe8 100644 --- a/Tests/MultiStark.lean +++ b/Tests/MultiStark.lean @@ -103,11 +103,9 @@ def selfTestSuite : IO UInt32 := do -- ════════════════════════════════════════════════════════════════════════════ /-- A tiny Aiur program: a BRANCHLESS entrypoint (single selector, no match) -that routes its argument through store/load before calling `factorial`. Its -circuit has 4 lookups (return, store, load, call) with raw degree-1 -arguments, so synthesis groups them 2 per chained-accumulator step -(`lookup_group_size = 2`) — the recursive verifier's grouped logUp fold is -exercised end-to-end alongside the k = 1 branching/memory circuits. -/ +with enough degree-1 lookups to benefit from the opt-in four-message packing. +The recursive verifier exercises k = 4 alongside the k = 1 branching/memory +and k = 2 gadget circuits, with the same commitment/FRI parameters. -/ def factorialProgram : Source.Toplevel := ⟦ pub fn factorial(n: G) -> G { match n { @@ -117,7 +115,13 @@ def factorialProgram : Source.Toplevel := ⟦ } pub fn fact_entry(n: G) -> G { - factorial(load(store(n))) + let v = load(store(n)); + assert_eq!(load(store(n + 1)), n + 1); + assert_eq!(load(store(n + 2)), n + 2); + assert_eq!(load(store(n + 3)), n + 3); + assert_eq!(load(store(n + 4)), n + 4); + assert_eq!(load(store(n + 5)), n + 5); + factorial(v) } ⟧ @@ -152,7 +156,8 @@ def endToEndSuite : IO UInt32 := do let facCompiled ← match factorialProgram.compile with | .error e => IO.eprintln s!"factorial compilation failed: {e}"; return 1 | .ok c => pure c - let facSystem := AiurSystem.build facCompiled.bytecode recCommitParams innerFri + let baselineSystem := AiurSystem.build facCompiled.bytecode recCommitParams innerFri + let facSystem := AiurSystem.buildMinOpeningWidth facCompiled.bytecode recCommitParams innerFri let facIdx ← match facCompiled.getFuncIdx `fact_entry with | some i => pure i | none => IO.eprintln "fact_entry entrypoint not found"; return 1 @@ -237,7 +242,12 @@ def endToEndSuite : IO UInt32 := do vCompiled.bytecode.executeMultiStark vIdx badClaimInput proofBytes vkBytes badClaimBytes lspecIO (.ofList [("recursive-verifier", [ test "factorial(5) claim = #[functionChannel, facIdx, 5, 120]" (claim == expectedClaim), + test "lookup packing changes the key and reduces total opening width" + (facSystem.vkBytes != baselineSystem.vkBytes && + (facSystem.circuitShapes.foldl (fun n s => n + s.committedWidth) 0) < + (baselineSystem.circuitShapes.foldl (fun n s => n + s.committedWidth) 0)), expectOk "inner factorial proof verifies" innerVerify, + expectErr "baseline key rejects the regrouped proof" (baselineSystem.verify claim proof), expectOk "verifier accepts honest proof (vk digest bound + OOD + FRI)" honest, test "codegen'd verifier matches interpreter (output + query counts)" parity, expectErr "tampered proof advice rejected (verification checks)" tamperedProof, diff --git a/crates/aiur/src/lib.rs b/crates/aiur/src/lib.rs index 3acf6666..0628419f 100644 --- a/crates/aiur/src/lib.rs +++ b/crates/aiur/src/lib.rs @@ -2,6 +2,7 @@ pub mod bytecode; pub mod constraints; pub mod execute; pub mod gadgets; +mod lookup_grouping; pub mod memory; pub mod querymap; pub mod synthesis; diff --git a/crates/aiur/src/lookup_grouping.rs b/crates/aiur/src/lookup_grouping.rs new file mode 100644 index 00000000..440889d9 --- /dev/null +++ b/crates/aiur/src/lookup_grouping.rs @@ -0,0 +1,228 @@ +//! Opt-in lookup packing for a smaller recursively verified proof. +//! +//! The pinned protocol already supports circuit-local lookup groups. Larger +//! groups reduce committed accumulator columns but can require more quotient +//! columns. Minimize their SUM, within the existing PCS degree budget; do not +//! change FRI queries, grinding, blowup, lookup order or user constraints. + +use multi_stark::{ + graph::ConstraintGraph, + lookup::{MAX_LOOKUP_GROUP, stage2_width}, + p3_field::BasedVectorSpace, + system::Circuit, + types::ExtVal, +}; + +use crate::G; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct LookupPacking { + pub group_size: usize, + pub stage2_width: usize, + pub max_constraint_degree: usize, + pub quotient_degree: usize, +} + +impl LookupPacking { + fn opening_width(self) -> usize { + self.stage2_width + Self::extension_degree() * self.quotient_degree + } + + fn extension_degree() -> usize { + >::DIMENSION + } + + fn candidate( + graph: &ConstraintGraph, + group_size: usize, + max_quotient_degree: usize, + ) -> Option { + // Analytic degree of the EXISTING grouped logUp constraints, as in + // multi_stark::lookup::logup_max_degree. Use u64 here: a decoded graph + // can have valid u32 node degrees whose regrouped sum exceeds u32. + let mut degree = u64::from(graph.max_constraint_degree).max(1); + for group in graph.lookups.chunks(group_size) { + let messages: Vec<_> = group + .iter() + .map(|lookup| { + lookup + .args + .iter() + .map(|id| u64::from(graph.degrees[id.index()])) + .max() + .unwrap_or(0) + }) + .collect(); + let sum: u64 = messages.iter().sum(); + degree = degree.max(sum + 1); + for (lookup, message_degree) in group.iter().zip(messages) { + degree = degree.max( + u64::from(graph.degrees[lookup.multiplicity.index()]) + sum + - message_degree, + ); + } + } + let quotient_degree = (degree.max(2) - 1).checked_next_power_of_two()?; + if quotient_degree > u64::try_from(max_quotient_degree).ok()? + || degree > u64::from(u32::MAX) + { + return None; + } + Some(Self { + group_size, + stage2_width: stage2_width( + graph.lookups.len(), + group_size, + Self::extension_degree(), + ), + max_constraint_degree: usize::try_from(degree).ok()?, + quotient_degree: usize::try_from(quotient_degree).ok()?, + }) + } +} + +/// Returns only a STRICT opening-width improvement. Preserve the existing +/// key on ties; among improvements prefer smaller degree, then smaller groups. +pub(crate) fn smaller_opening_packing( + circuit: &Circuit, + max_quotient_degree: usize, +) -> Option { + let current_width = circuit.stage_2_width + + LookupPacking::extension_degree() * circuit.quotient_degree(); + (1..=MAX_LOOKUP_GROUP) + .filter_map(|group| { + LookupPacking::candidate(&circuit.graph, group, max_quotient_degree) + }) + .filter(|plan| plan.opening_width() < current_width) + .min_by_key(|plan| { + (plan.opening_width(), plan.max_constraint_degree, plan.group_size) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use multi_stark::{ + expr::{ColRef, RowOffset, Source}, + graph::{Node, NodeId}, + lookup::Lookup, + }; + + fn linear_lookup_circuit(count: usize) -> Circuit { + let graph = ConstraintGraph { + nodes: (0..2) + .map(|index| { + Node::Var(ColRef { + source: Source::Main, + offset: RowOffset::Current, + index, + }) + }) + .collect(), + degrees: vec![1, 1], + zeros: vec![], + lookups: vec![ + Lookup { multiplicity: NodeId(1), args: vec![NodeId(0)] }; + count + ], + lookup_prefix_len: 2, + max_constraint_degree: 0, + }; + let plan = LookupPacking::candidate(&graph, 2, 2).unwrap(); + Circuit { + graph, + main_width: 2, + preprocessed: None, + preprocessed_width: 0, + preprocessed_height: 0, + num_lookups: count, + stage_2_width: plan.stage2_width, + num_publics: 8, + lookup_group_size: 2, + constraint_count: plan.stage2_width, + max_constraint_degree: plan.max_constraint_degree, + } + } + + #[test] + fn packing_accounts_for_quotient_cost_and_preserves_ties() { + // Twelve degree-1 messages: k=2 costs 12+4 columns, k=4 costs 6+8. + let circuit = linear_lookup_circuit(12); + let plan = smaller_opening_packing(&circuit, 4).unwrap(); + assert_eq!(plan.group_size, 4); + assert_eq!(plan.opening_width(), 14); + // No higher quotient budget is silently introduced at blowup two. + assert_eq!(smaller_opening_packing(&circuit, 2), None); + // Eight messages tie at 12 columns: keep the original key/profile. + assert_eq!(smaller_opening_packing(&linear_lookup_circuit(8), 4), None); + // Empty lookup sets retain a pass-through accumulator. + let empty = linear_lookup_circuit(0); + assert_eq!(empty.stage_2_width, 2); + assert_eq!(smaller_opening_packing(&empty, 4), None); + } + + #[test] + fn packing_checks_multiplicity_degree_and_overflow_before_admission() { + let mut circuit = linear_lookup_circuit(12); + // Degree-only adversarial vectors: multiplicity, not the product term, + // is now limiting. A bound looking only at argument degrees is unsound. + circuit.graph.degrees[1] = 3; + assert_eq!(LookupPacking::candidate(&circuit.graph, 4, 4), None); + circuit.graph.degrees[0] = u32::MAX; + circuit.graph.degrees[1] = u32::MAX; + assert_eq!(LookupPacking::candidate(&circuit.graph, 8, usize::MAX), None); + } + + #[test] + fn persisted_aggregate_key_lookup_packing_census() { + let bytes = std::fs::read(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../Tests/Fixtures/Aggregate/singleton-2026-09-05/aggr.vk" + )) + .unwrap(); + let (system, commitment, _) = crate::vk_codec::from_bytes(&bytes).unwrap(); + let mut before = 0; + let mut after = 0; + let mut changes = 0; + let mut histogram = [0; MAX_LOOKUP_GROUP + 1]; + for (index, circuit) in system.circuits.iter().enumerate() { + let old_width = circuit.main_width + + circuit.stage_2_width + + 2 * circuit.quotient_degree(); + before += old_width; + let Some(plan) = + smaller_opening_packing(circuit, 1 << commitment.log_blowup) + else { + after += old_width; + continue; + }; + let new_width = circuit.main_width + plan.opening_width(); + assert!(new_width < old_width); + assert!(plan.quotient_degree <= 1 << commitment.log_blowup); + assert_eq!( + plan.max_constraint_degree, + circuit.graph.max_constraint_degree.max( + multi_stark::lookup::logup_max_degree( + &circuit.graph, + plan.group_size + ) + ) as usize + ); + eprintln!( + "circuit {index}: width {old_width} -> {new_width}, group {} -> {}, quotient {} -> {}", + circuit.lookup_group_size, + plan.group_size, + circuit.quotient_degree(), + plan.quotient_degree, + ); + after += new_width; + changes += 1; + histogram[plan.group_size] += 1; + } + assert!(changes > 0); + eprintln!( + "Aggregate key, all {} circuits: committed width {before} -> {after}; {changes} changed; new group histogram {histogram:?}", + system.circuits.len() + ); + } +} diff --git a/crates/aiur/src/synthesis.rs b/crates/aiur/src/synthesis.rs index f0be342b..34e91355 100644 --- a/crates/aiur/src/synthesis.rs +++ b/crates/aiur/src/synthesis.rs @@ -1,12 +1,13 @@ use multi_stark::{ expr::Expr, lookup::Lookup, - p3_field::PrimeCharacteristicRing, + p3_field::{BasedVectorSpace, PrimeCharacteristicRing}, p3_matrix::dense::RowMajorMatrix, prover::Proof, system::{CircuitInputs, ProverKey, System, SystemWitness}, types::{ - CommitmentParameters, FriParameters, GoldilocksBlake3Config, PcsError, + CommitmentParameters, ExtVal, FriParameters, GoldilocksBlake3Config, + PcsError, }, verifier::VerificationError, }; @@ -216,6 +217,45 @@ impl AiurSystem { } } + /// Experimental, explicit alternative to `build`: choose circuit-local + /// lookup groups minimizing accumulator + quotient opening width within + /// the SAME PCS blowup. This changes the verifying key, not the bytecode, + /// constraints, lookup order or FRI parameters. It is not a runtime/memory + /// optimum: higher-degree quotients can cost more to prove. Default system + /// construction and deployment keys deliberately remain unchanged. + pub fn build_min_opening_width( + toplevel: Toplevel, + commitment_parameters: CommitmentParameters, + fri_parameters: FriParameters, + ) -> Self { + let mut system = + Self::build(toplevel, commitment_parameters, fri_parameters); + let max_quotient_degree = 1usize << commitment_parameters.log_blowup; + for circuit in &mut system.system.circuits { + let Some(plan) = crate::lookup_grouping::smaller_opening_packing( + circuit, + max_quotient_degree, + ) else { + continue; + }; + // Aiur's user graph references only the original main/preprocessed + // traces and selectors. Grouping changes the protocol-owned logUp + // layout, not that graph or the preprocessed data/commitment/prover key. + assert!(circuit.graph.nodes.iter().all(|node| { + !matches!(node, multi_stark::graph::Node::Var(column) + if column.source == multi_stark::expr::Source::Stage2) + })); + circuit.lookup_group_size = plan.group_size; + circuit.stage_2_width = plan.stage2_width; + circuit.max_constraint_degree = plan.max_constraint_degree; + // Both counts are one base-coordinate constraint per committed + // accumulator coordinate, including the lookup-free pass-through. + circuit.constraint_count = circuit.graph.zeros.len() + plan.stage2_width; + assert_eq!(circuit.quotient_degree(), plan.quotient_degree); + } + system + } + /// The circuit list in system order: constrained functions (ascending /// index), then memories, then `Bytes1`, then `Bytes2`. This matches the /// order the circuits were chained in [`AiurSystem::build`], so index `i` @@ -323,7 +363,9 @@ impl AiurSystem { } let n = raw.next_power_of_two(); let c = &self.system.circuits[i]; - let d = c.stage_2_width / (1 + c.num_lookups); // extension degree + // Extension degree is a FIELD property, not inferable from the packed + // lookup-column count. Grouping can make that old quotient even zero. + let d = >::DIMENSION; let args: usize = self.slot_widths[i].iter().sum(); let q = c.quotient_degree(); witness += @@ -590,8 +632,25 @@ impl AiurSystem { claim: &[G], proof: &AiurProof, ) -> Result, String> { - self.verify(claim, proof).map_err(|e| format!("{e:?}"))?; - proof.to_bytes().map_err(|e| format!("{e:?}")) + self.verify(claim, proof).map_err(|error| format!("{error:?}"))?; + proof.to_bytes().map_err(|error| format!("{error:?}")) + } + + /// Verify and expand the native multiproof for a per-query terminal + /// verifier such as Flock Stage 3. + pub fn proof_to_per_query_advice_bytes( + &self, + claim: &[G], + proof: &AiurProof, + ) -> Result, String> { + multi_stark::advice::proof_to_advice_bytes( + &self.system, + self.commitment_parameters, + self.fri_parameters, + &[claim], + proof, + ) + .map_err(|error| format!("{error:?}")) } } @@ -684,6 +743,83 @@ mod tests { Toplevel { functions: vec![function], memory_sizes: vec![] } } + #[test] + fn min_opening_width_profile_proves_and_is_key_bound() { + let toplevel = || { + let mut top = xor_splits_toplevel(); + let function = &mut top.functions[0]; + function.body.ops = (0..12) + .map(|i| { + if i % 2 == 0 { Op::U8XorSplit7(0, 1) } else { Op::U8XorSplit4(0, 1) } + }) + .collect(); + function.body.ctrl = Ctrl::Return(0, vec![24, 25]); + function.layout.auxiliaries = 25; + function.layout.lookups = 13; + top + }; + let (mut cp, fp) = test_parameters(); + cp.log_blowup = 2; + let baseline = AiurSystem::build(toplevel(), cp, fp); + let compact = AiurSystem::build_min_opening_width(toplevel(), cp, fp); + let old = &baseline.system.circuits[0]; + let new = &compact.system.circuits[0]; + assert_eq!(old.lookup_group_size, 2); + assert_eq!(new.lookup_group_size, 4); + assert_eq!(old.quotient_degree(), 2); + assert_eq!(new.quotient_degree(), 4); + assert!( + new.stage_2_width + 2 * new.quotient_degree() + < old.stage_2_width + 2 * old.quotient_degree() + ); + assert_eq!(new.graph, old.graph, "user constraints/lookup order unchanged"); + assert_eq!(new.main_width, old.main_width); + assert_eq!( + compact.system.preprocessed_commit, + baseline.system.preprocessed_commit, + ); + for system in [&baseline, &compact] { + let circuit = &system.system.circuits[0]; + let height = 8; + let blowup = 1 << cp.log_blowup; + let args: usize = system.slot_widths[0].iter().sum(); + let expected_stage2 = 8 * blowup * height * circuit.main_width + + 2 * 32 * blowup * height + + 8 * height * (circuit.num_lookups + args) + + 2 * 8 * 2 * height * circuit.num_lookups + + 8 * height * circuit.stage_2_width; + assert_eq!( + system + .peak_prove_bytes_by( + |index, _| if index == 0 { height } else { 0 }, + 0 + ) + .phase_stage2, + expected_stage2, + "RAM model must use extension degree 2 regardless of lookup packing" + ); + } + let input = [G::from_u8(0xd3), G::from_u8(0x69)]; + let (claim, proof) = compact.prove(0, &input, &mut empty_io_buffer()); + compact.verify(&claim, &proof).unwrap(); + let bytes = crate::vk_codec::aiur_system_to_bytes(&compact).unwrap(); + let vk = crate::vk_codec::AiurVerifyingKey::from_bytes(&bytes).unwrap(); + assert_eq!(vk.to_bytes(), bytes); + let baseline_bytes = + crate::vk_codec::aiur_system_to_bytes(&baseline).unwrap(); + // The first 14 bytes encode ALL seven commitment/FRI parameters. + assert_eq!(&bytes[..14], &baseline_bytes[..14]); + assert_ne!(bytes, baseline_bytes); + vk.verify(&claim, &proof).unwrap(); + assert!(baseline.verify(&claim, &proof).is_err()); + assert!( + vk.proof_to_per_query_advice_bytes(&claim, &proof).unwrap().len() > 100 + ); + let mut wrong_claim = claim; + *wrong_claim.last_mut().unwrap() += G::ONE; + assert!(vk.verify(&wrong_claim, &proof).is_err()); + } + #[test] fn prove_verify_xor_splits() { let (cp, fp) = test_parameters(); @@ -708,6 +844,31 @@ mod tests { ] ); system.verify(&claim, &proof).expect("xor split outputs must verify"); + + // The terminal prover receives only the serialized verifier key, not the + // prover-side `AiurSystem`. Exercise that exact path against a real proof + // so codec round trips alone cannot mask a transcript/config mismatch. + let vk_bytes = crate::vk_codec::aiur_system_to_bytes(&system) + .expect("encode verifier key"); + let vk = crate::vk_codec::AiurVerifyingKey::from_bytes(&vk_bytes) + .expect("decode verifier key"); + assert_eq!(vk.to_bytes(), vk_bytes, "verifier key is canonical"); + vk.verify(&claim, &proof).expect("decoded verifier key must verify"); + let advice = vk + .proof_to_per_query_advice_bytes(&claim, &proof) + .expect("decoded verifier key must serialize valid proof advice"); + assert!(!advice.is_empty(), "serialized verifier advice must not be empty"); + + let mut tampered_claim = claim.clone(); + tampered_claim[2] += G::ONE; + assert!( + vk.verify(&tampered_claim, &proof).is_err(), + "decoded verifier key must bind the outer claim" + ); + assert!( + vk.proof_to_per_query_advice_bytes(&tampered_claim, &proof).is_err(), + "advice serialization must verify and bind the outer claim" + ); } /// Hand-build a toplevel exercising the two migrated integration paths that diff --git a/crates/aiur/src/vk_codec.rs b/crates/aiur/src/vk_codec.rs index 9fa9e00e..f9ca01df 100644 --- a/crates/aiur/src/vk_codec.rs +++ b/crates/aiur/src/vk_codec.rs @@ -28,12 +28,11 @@ //! log_final_poly_len, max_log_arity, num_queries, //! commit_proof_of_work_bits, query_proof_of_work_bits) //! u16 circuit count -//! PER-CIRCUIT RECORDS (circuit count times; each is `u32 LE len` + `len` bytes -//! so a record is a contiguous byte range) +//! PER-CIRCUIT RECORDS (circuit count times; no record-length prefix) //! u16 main_width, u16 preprocessed_width, u32 preprocessed_height, //! u16 max_constraint_degree (combined user + logUp), //! u8 lookup_group_size (k: lookups per chained accumulator step) -//! node_count nodes, each a u8 tag then payload: +//! u16 node_count, then nodes, each a u8 tag then payload: //! 0 ConstSmall: u16 LE canonical value //! 1 ConstBig: u64 LE canonical value //! 2 Public: u8 index @@ -42,8 +41,8 @@ //! 9 Neg: u16 LE child node id //! 10..=15 Var (tag = 10 + 2*source + offset; source 0 Preprocessed //! 1 Main 2 Stage2, offset 0 current 1 next): u16 LE column -//! u32 zero_count, then zero_count x u16 LE constraint-root node ids -//! u32 lookup_count, then per lookup: +//! u16 zero_count, then zero_count x u16 LE constraint-root node ids +//! u16 lookup_count, then per lookup: //! u16 LE multiplicity node id //! u16 LE arg count, then arg_count x u16 LE arg node ids //! TRAILER @@ -56,7 +55,7 @@ //! IsTransition = 0, Var/IsFirstRow/IsLastRow = 1, Add/Sub = max of children, //! Mul = sum, Neg = child) and recomputed on decode in node order (children //! precede parents in the compiled vector). Goldilocks constants are written -//! canonically and reduced on read. +//! canonically; non-canonical values are rejected on read. // The codec is exercised by tests and wired to the FFI / Aiur port. #![allow(dead_code)] @@ -65,9 +64,11 @@ use multi_stark::{ expr::{ColRef, RowOffset, Source}, graph::{ConstraintGraph, Node, NodeId}, lookup::Lookup, - p3_field::{PrimeCharacteristicRing, PrimeField64}, + p3_field::{PrimeCharacteristicRing, PrimeField64, TwoAdicField}, system::{Circuit, System}, - types::{Commitment, CommitmentParameters, FriParameters, Val}, + types::{ + Commitment, CommitmentParameters, ExtVal, FriParameters, PcsError, Val, + }, }; use crate::synthesis::{AiurConfig, AiurSystem}; @@ -239,6 +240,20 @@ pub(crate) fn to_bytes( buf } +/// Serialize a verifier key for a custom [`AiurConfig`] circuit system. +/// +/// Most callers should use [`aiur_system_to_bytes`]. This lower-level entry +/// point exists for custom frontends which build the same concrete Aiur STARK +/// configuration without going through [`AiurSystem`]. The supplied protocol +/// parameters must be the ones used to construct `system.config`. +pub fn aiur_config_system_to_bytes( + system: &System, + commitment_parameters: CommitmentParameters, + fri_parameters: FriParameters, +) -> Vec { + to_bytes(system, commitment_parameters, fri_parameters) +} + /// Convenience: serialize the verifying key of a built [`AiurSystem`]. pub fn aiur_system_to_bytes(sys: &AiurSystem) -> Result, String> { Ok(to_bytes(&sys.system, sys.commitment_parameters, sys.fri_parameters)) @@ -294,7 +309,14 @@ impl<'a> Seg<'a> { fn decode_node(seg: &mut Seg<'_>) -> Result, String> { Ok(match seg.u8()? { 0 => Node::Const(Val::from_u16(seg.u16()?)), - 1 => Node::Const(Val::from_u64(seg.u64()?)), + 1 => { + let word = seg.u64()?; + let value = Val::from_u64(word); + if value.as_canonical_u64() != word { + return Err("non-canonical Goldilocks constant in vk graph".into()); + } + Node::Const(value) + }, 2 => Node::Public(u32::from(seg.u8()?)), 3 => Node::IsFirstRow, 4 => Node::IsLastRow, @@ -321,21 +343,26 @@ fn decode_node(seg: &mut Seg<'_>) -> Result, String> { /// Recompute per-node degree multiples in node order (children precede parents /// in the compiled vector). -fn recompute_degrees(nodes: &[Node]) -> Vec { +fn recompute_degrees(nodes: &[Node]) -> Result, String> { let mut degrees: Vec = Vec::with_capacity(nodes.len()); - for node in nodes { + for (index, node) in nodes.iter().enumerate() { + let degree = |id: NodeId| { + degrees.get(id.index()).copied().ok_or_else(|| { + format!("vk graph node {index} references non-preceding node {}", id.0) + }) + }; let d = match *node { Node::Const(_) | Node::Public(_) | Node::IsTransition => 0, Node::Var(_) | Node::IsFirstRow | Node::IsLastRow => 1, - Node::Add(a, b) | Node::Sub(a, b) => { - degrees[a.0 as usize].max(degrees[b.0 as usize]) - }, - Node::Mul(a, b) => degrees[a.0 as usize] + degrees[b.0 as usize], - Node::Neg(a) => degrees[a.0 as usize], + Node::Add(a, b) | Node::Sub(a, b) => degree(a)?.max(degree(b)?), + Node::Mul(a, b) => degree(a)? + .checked_add(degree(b)?) + .ok_or_else(|| format!("vk graph node {index} degree exceeds u32"))?, + Node::Neg(a) => degree(a)?, }; degrees.push(d); } - degrees + Ok(degrees) } fn decode_circuit(seg: &mut Seg<'_>) -> Result, String> { @@ -369,14 +396,49 @@ fn decode_circuit(seg: &mut Seg<'_>) -> Result, String> { lookups.push(Lookup { multiplicity, args }); } - let degrees = recompute_degrees(&nodes); + let degrees = recompute_degrees(&nodes)?; + for id in zeros.iter().copied().chain(lookups.iter().flat_map(|lookup| { + std::iter::once(lookup.multiplicity).chain(lookup.args.iter().copied()) + })) { + if id.index() >= nodes.len() { + return Err(format!( + "vk constraint/lookup references missing node {}", + id.0 + )); + } + } + let ext_degree = + >::DIMENSION; + let stage_2_width = multi_stark::lookup::stage2_width( + lookups.len(), + lookup_group_size, + ext_degree, + ); + let num_publics = multi_stark::lookup::num_publics(ext_degree); + for node in &nodes { + let (index, width, label) = match node { + Node::Var(column) => ( + column.index as usize, + match column.source { + Source::Main => main_width, + Source::Preprocessed => preprocessed_width, + Source::Stage2 => stage_2_width, + }, + "column", + ), + Node::Public(index) => (*index as usize, num_publics, "public"), + _ => continue, + }; + if index >= width { + return Err(format!( + "vk graph {label} index {index} exceeds width {width}" + )); + } + } // The graph's own max degree covers only the user roots (the serialized // `max_constraint_degree` is the combined user + analytic-logUp value). - let user_max_degree = zeros - .iter() - .map(|z| degrees[usize::try_from(z.0).expect("node id")]) - .max() - .unwrap_or(0); + let user_max_degree = + zeros.iter().map(|z| degrees[z.index()]).max().unwrap_or(0); // The lookup prefix is exactly the nodes interned while compiling the // lookup expressions, all of which are reachable from (and bounded by) // the lookup roots — children always precede parents. @@ -394,11 +456,37 @@ fn decode_circuit(seg: &mut Seg<'_>) -> Result, String> { lookup_prefix_len, max_constraint_degree: user_max_degree, }; + // Compute the analytic logUp degree in u64: a malformed graph can have + // individually valid u32 degrees whose grouped product overflows u32. + let mut logup_degree = 1u64; + for group in graph.lookups.chunks(lookup_group_size) { + let message_degrees: Vec<_> = group + .iter() + .map(|lookup| { + lookup + .args + .iter() + .map(|id| u64::from(graph.degrees[id.index()])) + .max() + .unwrap_or(0) + }) + .collect(); + let sum: u64 = message_degrees.iter().sum(); + logup_degree = logup_degree.max(sum + 1); + for (lookup, message_degree) in group.iter().zip(message_degrees) { + logup_degree = logup_degree.max( + u64::from(graph.degrees[lookup.multiplicity.index()]) + sum + - message_degree, + ); + } + } + let observed_degree = u64::from(user_max_degree).max(logup_degree); + if observed_degree != max_constraint_degree as u64 { + return Err(format!( + "vk constraint degree is {max_constraint_degree}; graph requires {observed_degree}" + )); + } let num_lookups = graph.lookups.len(); - let ext_degree = - >::DIMENSION; Ok(Circuit { graph, main_width, @@ -406,12 +494,8 @@ fn decode_circuit(seg: &mut Seg<'_>) -> Result, String> { preprocessed_width, preprocessed_height, num_lookups, - stage_2_width: multi_stark::lookup::stage2_width( - num_lookups, - lookup_group_size, - ext_degree, - ), - num_publics: multi_stark::lookup::num_publics(ext_degree), + stage_2_width, + num_publics, lookup_group_size, constraint_count: zeros_plus_logup( zero_count, @@ -453,6 +537,24 @@ pub(crate) fn from_bytes( commit_proof_of_work_bits: r.u16()? as usize, query_proof_of_work_bits: r.u16()? as usize, }; + if commitment_parameters.log_blowup > Val::TWO_ADICITY + || commitment_parameters.cap_height > Val::TWO_ADICITY + || fri_parameters.log_final_poly_len > Val::TWO_ADICITY + || fri_parameters.max_log_arity == 0 + || fri_parameters.max_log_arity > Val::TWO_ADICITY + || fri_parameters.num_queries == 0 + || fri_parameters.commit_proof_of_work_bits >= 64 + || fri_parameters.query_proof_of_work_bits >= 64 + { + return Err( + "vk commitment/FRI parameters exceed the Goldilocks domain".into(), + ); + } + #[cfg(feature = "cuda")] + if commitment_parameters.cap_height != 0 || fri_parameters.max_log_arity != 1 + { + return Err("vk parameters are unsupported by the CUDA backend".into()); + } let n_circuits = r.u16()? as usize; let mut circuits = Vec::with_capacity(n_circuits); for _ in 0..n_circuits { @@ -462,6 +564,11 @@ pub(crate) fn from_bytes( 0 => None, 1 => { let n = r.u16()? as usize; + if !n.is_power_of_two() { + return Err( + "vk preprocessed cap must have a nonzero power-of-two length".into(), + ); + } let mut caps = Vec::with_capacity(n.min(1 << 16)); for _ in 0..n { let mut d = [0u8; 32]; @@ -482,6 +589,35 @@ pub(crate) fn from_bytes( }); } r.done("vk")?; + let mut preprocessed_count = 0usize; + for (circuit, &index) in circuits.iter().zip(&preprocessed_indices) { + if circuit.quotient_degree() as u64 + > 1u64 << commitment_parameters.log_blowup + { + return Err("vk constraint degree exceeds the PCS blowup".into()); + } + if circuit.preprocessed_width == 0 { + if circuit.preprocessed_height != 0 || index.is_some() { + return Err("vk has preprocessed metadata for an empty matrix".into()); + } + } else { + if !circuit.preprocessed_height.is_power_of_two() + || circuit.preprocessed_height.ilog2() as usize + > Val::TWO_ADICITY - commitment_parameters.log_blowup + || index != Some(preprocessed_count) + { + return Err( + "vk has invalid preprocessed height or matrix index".into(), + ); + } + preprocessed_count += 1; + } + } + if (preprocessed_count != 0) != preprocessed_commit.is_some() { + return Err( + "vk preprocessed commitment disagrees with matrix metadata".into(), + ); + } let system = System { config: AiurConfig::new(commitment_parameters, fri_parameters), circuits, @@ -491,12 +627,309 @@ pub(crate) fn from_bytes( Ok((system, commitment_parameters, fri_parameters)) } +/// A verifier-only Aiur key decoded from [`aiur_system_to_bytes`]. +/// +/// This is the narrow surface used by zkVM guests: unlike [`AiurSystem`], it +/// carries neither bytecode nor a prover key, but it can verify a serialized +/// proof under the exact commitment and FRI parameters embedded in the key. +pub struct AiurVerifyingKey { + system: System, + commitment_parameters: CommitmentParameters, + fri_parameters: FriParameters, +} + +/// Verifier-known matrix geometry needed to specialise the terminal PCS +/// relation without carrying the full constraint graph into that relation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AiurPcsCircuitMetadata { + pub main_width: usize, + pub stage_2_width: usize, + pub quotient_width: usize, + pub preprocessed_width: usize, + pub preprocessed_height: usize, + pub preprocessed_slot: Option, +} + +/// Constraint program and geometry needed to specialise the terminal AIR +/// evaluation relation for one circuit. +/// +/// This is deliberately a clone of the verifier-owned compiled graph. Stage 3 +/// treats the graph as fixed circuit data, never as proof witness data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AiurAirCircuitMetadata { + pub graph: ConstraintGraph, + pub main_width: usize, + pub stage_2_width: usize, + pub quotient_degree: usize, + pub preprocessed_width: usize, + pub preprocessed_slot: Option, + pub lookup_group_size: usize, +} + +impl AiurVerifyingKey { + /// Decode a verifying key and require full input consumption. + pub fn from_bytes(bytes: &[u8]) -> Result { + from_bytes(bytes).map(|(system, commitment_parameters, fri_parameters)| { + Self { system, commitment_parameters, fri_parameters } + }) + } + + /// Re-encode to the canonical Aiur verifying-key wire format. + pub fn to_bytes(&self) -> Vec { + to_bytes(&self.system, self.commitment_parameters, self.fri_parameters) + } + + pub const fn commitment_parameters(&self) -> CommitmentParameters { + self.commitment_parameters + } + + pub const fn fri_parameters(&self) -> FriParameters { + self.fri_parameters + } + + pub fn num_circuits(&self) -> usize { + self.system.circuits.len() + } + + /// Canonical PCS matrix widths and preprocessed slots in circuit order. + pub fn pcs_circuit_metadata(&self) -> Vec { + let extension_degree = + >::DIMENSION; + self + .system + .circuits + .iter() + .zip(&self.system.preprocessed_indices) + .map(|(circuit, &preprocessed_slot)| AiurPcsCircuitMetadata { + main_width: circuit.main_width, + stage_2_width: circuit.stage_2_width, + quotient_width: circuit.quotient_degree() * extension_degree, + preprocessed_width: circuit.preprocessed_width, + preprocessed_height: circuit.preprocessed_height, + preprocessed_slot, + }) + .collect() + } + + /// Fixed compiled AIR programs in canonical circuit order. + pub fn air_circuit_metadata(&self) -> Vec { + self + .system + .circuits + .iter() + .zip(&self.system.preprocessed_indices) + .map(|(circuit, &preprocessed_slot)| AiurAirCircuitMetadata { + graph: circuit.graph.clone(), + main_width: circuit.main_width, + stage_2_width: circuit.stage_2_width, + quotient_degree: circuit.quotient_degree(), + preprocessed_width: circuit.preprocessed_width, + preprocessed_slot, + lookup_group_size: circuit.lookup_group_size, + }) + .collect() + } + + /// Exact challenger seed followed by the `System::observe_shape` words, + /// serialized as the Goldilocks byte challenger observes them. + /// + /// Terminal verifier circuits use this instead of duplicating the shape + /// derivation from the compact verifying-key codec. + pub fn transcript_seed_and_shape_bytes(&self) -> Vec { + let mut bytes = b"multi-stark/v0".to_vec(); + for parameter in [ + self.commitment_parameters.log_blowup, + self.commitment_parameters.cap_height, + self.fri_parameters.log_final_poly_len, + self.fri_parameters.max_log_arity, + self.fri_parameters.num_queries, + self.fri_parameters.commit_proof_of_work_bits, + self.fri_parameters.query_proof_of_work_bits, + ] { + bytes.extend_from_slice( + &u64::try_from(parameter) + .expect("protocol parameter fits u64") + .to_le_bytes(), + ); + } + let mut observe = |value: usize| { + bytes.extend_from_slice( + &u64::try_from(value) + .expect("system shape value fits u64") + .to_le_bytes(), + ); + }; + observe(self.system.circuits.len()); + for circuit in &self.system.circuits { + observe(circuit.constraint_count()); + observe(circuit.max_constraint_degree()); + observe(circuit.preprocessed_height); + observe(circuit.preprocessed_width); + observe(circuit.main_width); + observe(circuit.stage_2_width); + observe(circuit.lookup_group_size); + } + bytes + } + + /// Roots observed for the optional preprocessed commitment, in cap order. + pub fn preprocessed_commitment_roots(&self) -> Option> { + self + .system + .preprocessed_commit + .as_ref() + .map(|commitment| commitment.roots().to_vec()) + } + + pub fn verify( + &self, + claim: &[Val], + proof: &crate::synthesis::AiurProof, + ) -> Result<(), multi_stark::verifier::VerificationError> { + self.system.verify(claim, proof) + } + + /// Verify and expand the native pruned multiproof into per-query advice. + pub fn proof_to_per_query_advice_bytes( + &self, + claim: &[Val], + proof: &crate::synthesis::AiurProof, + ) -> Result, String> { + multi_stark::advice::proof_to_advice_bytes( + &self.system, + self.commitment_parameters, + self.fri_parameters, + &[claim], + proof, + ) + .map_err(|error| format!("{error:?}")) + } +} #[cfg(test)] mod tests { use super::*; use crate::gadgets::{AiurGadget, bytes1::Bytes1, bytes2::Bytes2}; use multi_stark::system::CircuitInputs; + fn graph_key( + nodes: &[Node], + zeros: &[NodeId], + lookups: &[Lookup], + ) -> Vec { + let mut bytes = Vec::new(); + for word in [1, 0, 0, 1, 2, 0, 0, 1, 1, 0] { + push_u16(&mut bytes, word); + } + push_u32(&mut bytes, 0); // Preprocessed height. + push_u16(&mut bytes, 1); // Combined constraint degree. + bytes.push(1); // Lookup group size. + push_u16(&mut bytes, nodes.len()); + for node in nodes { + push_node(&mut bytes, node); + } + push_u16(&mut bytes, zeros.len()); + for &root in zeros { + push_node_id(&mut bytes, root); + } + push_u16(&mut bytes, lookups.len()); + for lookup in lookups { + push_node_id(&mut bytes, lookup.multiplicity); + push_u16(&mut bytes, lookup.args.len()); + for &argument in &lookup.args { + push_node_id(&mut bytes, argument); + } + } + bytes.push(0); + bytes.extend_from_slice(&NO_PREP_INDEX.to_le_bytes()); + bytes + } + + #[test] + fn malformed_graph_references_and_degrees_return_errors() { + let leaf = Node::Var(ColRef { + source: Source::Main, + offset: RowOffset::Current, + index: 0, + }); + let valid = graph_key(&[leaf], &[NodeId(0)], &[]); + AiurVerifyingKey::from_bytes(&valid).expect("valid small key"); + for node in [ + Node::Neg(NodeId(0)), + Node::Add(NodeId(0), NodeId(1)), + Node::Mul(NodeId(u32::from(u16::MAX)), NodeId(0)), + Node::Public(8), + Node::Var(ColRef { + source: Source::Main, + offset: RowOffset::Current, + index: 1, + }), + Node::Var(ColRef { + source: Source::Preprocessed, + offset: RowOffset::Current, + index: 0, + }), + Node::Var(ColRef { + source: Source::Stage2, + offset: RowOffset::Current, + index: 2, + }), + ] { + assert!( + AiurVerifyingKey::from_bytes(&graph_key(&[node], &[], &[])).is_err() + ); + } + assert!( + AiurVerifyingKey::from_bytes(&graph_key(&[], &[NodeId(0)], &[])).is_err() + ); + for lookup in [ + Lookup { multiplicity: NodeId(1), args: vec![NodeId(0)] }, + Lookup { multiplicity: NodeId(0), args: vec![NodeId(1)] }, + ] { + assert!( + AiurVerifyingKey::from_bytes(&graph_key(&[leaf], &[], &[lookup])) + .is_err() + ); + } + let mut nodes = vec![leaf]; + for index in 0..32 { + nodes.push(Node::Mul(NodeId(index), NodeId(index))); + } + assert!( + AiurVerifyingKey::from_bytes(&graph_key(&nodes, &[], &[])).is_err() + ); + + for offset in (0..14).step_by(2) { + let mut malformed = valid.clone(); + let invalid = if offset == 8 { 0u16 } else { u16::MAX }; + malformed[offset..offset + 2].copy_from_slice(&invalid.to_le_bytes()); + assert!( + AiurVerifyingKey::from_bytes(&malformed).is_err(), + "header offset {offset}" + ); + } + let mut wrong_degree = valid.clone(); + wrong_degree[24..26].copy_from_slice(&0u16.to_le_bytes()); + assert!(AiurVerifyingKey::from_bytes(&wrong_degree).is_err()); + } + + #[test] + fn key_decode_mutation_smoke_and_noncanonical_constant() { + let valid = graph_key(&[Node::Const(Val::from_u64(65_536))], &[], &[]); + AiurVerifyingKey::from_bytes(&valid).unwrap(); + for index in 0..valid.len() { + for bit in 0..8 { + let mut mutated = valid.clone(); + mutated[index] ^= 1 << bit; + // A mutation may remain valid, but decoding must never panic. + let _ = AiurVerifyingKey::from_bytes(&mutated); + } + } + let mut noncanonical = valid; + assert_eq!(noncanonical[29], 1); // Big constant tag after node count. + noncanonical[30..38].copy_from_slice(&u64::MAX.to_le_bytes()); + assert!(AiurVerifyingKey::from_bytes(&noncanonical).is_err()); + } + fn test_parameters() -> (CommitmentParameters, FriParameters) { let cp = CommitmentParameters { log_blowup: 1, cap_height: 0 }; let fp = FriParameters { @@ -563,6 +996,47 @@ mod tests { } } + #[test] + fn transcript_seed_and_shape_bytes_match_observe_shape_order() { + let (system, cp, fp) = test_system(); + let key = AiurVerifyingKey { + system, + commitment_parameters: cp, + fri_parameters: fp, + }; + let mut expected = b"multi-stark/v0".to_vec(); + for value in [ + cp.log_blowup, + cp.cap_height, + fp.log_final_poly_len, + fp.max_log_arity, + fp.num_queries, + fp.commit_proof_of_work_bits, + fp.query_proof_of_work_bits, + key.system.circuits.len(), + ] { + expected.extend_from_slice(&(value as u64).to_le_bytes()); + } + for circuit in &key.system.circuits { + for value in [ + circuit.constraint_count(), + circuit.max_constraint_degree(), + circuit.preprocessed_height, + circuit.preprocessed_width, + circuit.main_width, + circuit.stage_2_width, + circuit.lookup_group_size, + ] { + expected.extend_from_slice(&(value as u64).to_le_bytes()); + } + } + assert_eq!(key.transcript_seed_and_shape_bytes(), expected); + assert_eq!( + key.preprocessed_commitment_roots().is_some(), + key.system.preprocessed_commit.is_some() + ); + } + #[test] fn rejects_trailing_bytes() { let (system, cp, fp) = test_system(); diff --git a/crates/ffi/Cargo.toml b/crates/ffi/Cargo.toml index 7a0bc789..7da259aa 100644 --- a/crates/ffi/Cargo.toml +++ b/crates/ffi/Cargo.toml @@ -35,6 +35,10 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-texray = { workspace = true } +# Optional no-RISC-V Flock Stage 3 connector. This stays out of the normal +# host build because the pinned prover stack is intentionally isolated. +flock-stage3-host = { path = "../../flock-stage3/host", optional = true } + # Iroh dependencies bytes = { version = "1.10.1", optional = true } tokio = { version = "1.44.1", optional = true } @@ -45,12 +49,32 @@ getrandom = { version = "0.3", optional = true } bincode = { version = "2.0.1", optional = true } serde = { version = "1.0.219", features = ["derive"], optional = true } +# iroh 0.97 pins ed25519-dalek 3.0.0-pre.1, whose permissive prerelease +# requirements otherwise resolve to incompatible final releases. Keep the +# known-good companion API versions explicit until iroh updates dalek. +ed25519-compat-pin = { package = "ed25519", version = "=3.0.0-rc.4", optional = true } +pkcs8-compat-pin = { package = "pkcs8", version = "=0.11.0-rc.11", optional = true } +signature-compat-pin = { package = "signature", version = "=3.0.0-rc.10", optional = true } + [features] default = [] parallel = ["aiur/parallel"] cuda = ["aiur/cuda"] test-ffi = [] -net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"] +net = [ + "bytes", + "tokio", + "iroh", + "iroh-base", + "n0-error", + "getrandom", + "bincode", + "serde", + "dep:ed25519-compat-pin", + "dep:pkcs8-compat-pin", + "dep:signature-compat-pin", +] +flock = ["dep:flock-stage3-host"] [lints] workspace = true diff --git a/crates/ffi/src/aiur/protocol.rs b/crates/ffi/src/aiur/protocol.rs index 5dac21c9..08a03e28 100644 --- a/crates/ffi/src/aiur/protocol.rs +++ b/crates/ffi/src/aiur/protocol.rs @@ -109,6 +109,21 @@ extern "C" fn rs_aiur_system_build( LeanExternal::alloc(&AIUR_SYSTEM_CLASS, system) } +/// Explicit experimental key profile; default construction is unchanged. +#[unsafe(no_mangle)] +extern "C" fn rs_aiur_system_build_min_opening_width( + toplevel: LeanAiurToplevel>, + commitment_parameters: LeanAiurCommitmentParameters>, + fri_parameters: LeanAiurFriParameters>, +) -> LeanExternal { + let system = AiurSystem::build_min_opening_width( + decode_toplevel(&toplevel), + decode_commitment_parameters(&commitment_parameters), + decode_fri_parameters(&fri_parameters), + ); + LeanExternal::alloc(&AIUR_SYSTEM_CLASS, system) +} + /// Helper: encode `CircuitShape`s as a Lean `Array CircuitShape`. Field /// order must match `Aiur.CircuitShape` in `Ix/Aiur/Protocol.lean`. fn build_circuit_shapes_array(shapes: &[CircuitShape]) -> LeanArray { @@ -1677,3 +1692,136 @@ fn decode_io_buffer_map( } map } + +// ============================================================================= +// Flock aggregate-root Stage 3 (feature flock) +// ============================================================================= + +/// Compile/evaluate, prove, or independently verify the complete no-RISC-V +/// Flock relation for one canonical ix_aggr root. Default builds retain a +/// checked feature-disabled stub so the Lean CLI remains linkable. The IO +/// payload is `Except String String`: Lean constructs any `IO.Error`, avoiding +/// the pinned lean-ffi helper's toolchain-dependent IO.Error constructor tag. +#[unsafe(no_mangle)] +extern "C" fn rs_flock_stage3_aggregate_root( + vk_bytes: LeanByteArray>, + claim_bytes: LeanByteArray>, + proof_bytes: LeanByteArray>, + fri_parameters: LeanAiurFriParameters>, + mode: LeanString>, + artifact_path: LeanString>, + output: LeanString>, + limits_json: LeanString>, +) -> lean_ffi::object::LeanIOResult { + #[cfg(feature = "flock")] + { + let fri = decode_fri_parameters(&fri_parameters); + let backend = flock_stage3_host::FlockStage3Backend; + let result = (|| -> anyhow::Result { + use flock_stage3_host::{ + Stage3ArtifactV1, Stage3ArtifactWriterV1, Stage3ResourceLimitsV1, + }; + use serde_json::json; + let elapsed_us = |start: std::time::Instant| { + u64::try_from(start.elapsed().as_micros()).unwrap_or(u64::MAX) + }; + let limits: Stage3ResourceLimitsV1 = + serde_json::from_str(limits_json.as_str())?; + let started = std::time::Instant::now(); + let mode = mode.as_str(); + match mode { + "preflight" => { + if !artifact_path.as_str().is_empty() || !output.as_str().is_empty() { + anyhow::bail!( + "--artifact and --output are not valid with --mode preflight" + ); + } + }, + "prove" => { + if !artifact_path.as_str().is_empty() || output.as_str().is_empty() { + anyhow::bail!( + "Flock proving requires --output and forbids --artifact" + ); + } + }, + "verify" => { + if artifact_path.as_str().is_empty() || !output.as_str().is_empty() { + anyhow::bail!( + "Flock verification requires --artifact and forbids --output" + ); + } + }, + other => anyhow::bail!( + "unknown Flock mode '{other}' (expected preflight|prove|verify)" + ), + } + // Check file operations before native validation or circuit compilation. + let writer = if mode == "prove" { + Some(Stage3ArtifactWriterV1::reserve(output.as_str())?) + } else { + None + }; + let artifact = if mode == "verify" { + Some(Stage3ArtifactV1::read_from_path(artifact_path.as_str())?) + } else { + None + }; + let prepared = backend.prepare_stage2_with_limits( + vk_bytes.as_bytes(), + claim_bytes.as_bytes(), + proof_bytes.as_bytes(), + &fri, + limits, + )?; + // Keep stdout exclusively for the Lean caller's text/JSONL records, and + // expose a successful preflight even if subsequent proving fails. + eprintln!("{}", prepared.report()); + let mut result = json!({"preflight": prepared.report().to_json_value()}); + if let Some(writer) = writer { + eprintln!("Flock Stage 3 preflight passed; starting prover"); + let (artifact, timings) = prepared.prove_with_timings()?; + let write_started = std::time::Instant::now(); + writer.write(&artifact)?; + result["artifact_bytes"] = json!(artifact.encoded_len()); + result["artifact_path"] = json!(output.as_str()); + result["proof_timings"] = json!(timings); + result["artifact_write_us"] = json!(elapsed_us(write_started)); + } + if let Some(artifact) = artifact { + let verify_started = std::time::Instant::now(); + prepared.verify(&artifact)?; + result["verify_us"] = json!(elapsed_us(verify_started)); + result["artifact_bytes"] = json!(artifact.encoded_len()); + result["artifact_path"] = json!(artifact_path.as_str()); + } + result["operation_total_us"] = json!(elapsed_us(started)); + result["process_peak_rss_bytes"] = + json!(flock_stage3_host::stage3_process_peak_rss_bytes()); + Ok(result.to_string()) + })(); + match result { + Ok(report) => lean_ffi::object::LeanIOResult::ok(LeanExcept::ok( + LeanString::new(&report), + )), + Err(error) => lean_ffi::object::LeanIOResult::ok( + LeanExcept::error_string(&format!("{error:#}")), + ), + } + } + #[cfg(not(feature = "flock"))] + { + let _ = ( + &vk_bytes, + &claim_bytes, + &proof_bytes, + &fri_parameters, + &mode, + &artifact_path, + &output, + &limits_json, + ); + lean_ffi::object::LeanIOResult::ok(LeanExcept::error_string( + "ix was built without Flock Stage 3; rebuild with IX_FLOCK=1", + )) + } +} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml new file mode 100644 index 00000000..afdfa654 --- /dev/null +++ b/crates/terminal/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ix-terminal" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +aiur = { workspace = true, features = ["parallel"] } +anyhow = { workspace = true } +bincode = { workspace = true } +blake3 = { workspace = true } +multi-stark = { workspace = true, features = ["parallel"] } + +[lints] +workspace = true diff --git a/crates/terminal/src/lib.rs b/crates/terminal/src/lib.rs new file mode 100644 index 00000000..640c16a1 --- /dev/null +++ b/crates/terminal/src/lib.rs @@ -0,0 +1,682 @@ +//! Canonical boundary between the recursive Aiur aggregate and terminal +//! compression backends. +//! +//! SP1 and Flock must validate and bind exactly the same Stage 2 statement. +//! This crate owns that byte-level contract so terminal backends cannot drift. + +use aiur::{G, synthesis::AiurProof, vk_codec::AiurVerifyingKey}; +use anyhow::{Result, bail}; +use bincode::{config, serde::decode_from_slice}; +use multi_stark::{ + advice::AdviceProof, + p3_field::{PrimeCharacteristicRing, PrimeField64}, + types::FriParameters, +}; + +/// Domain of the canonical Stage 2 aggregate-root statement. +/// +/// This value is already used by the SP1 compressor and must not change +/// without introducing a new statement version. +pub const STAGE2_ROOT_DOMAIN: &[u8; 8] = b"IXROOT01"; +/// Backwards-compatible name used by the SP1 public-values API. +pub const PUBLIC_VALUES_DOMAIN: &[u8; 8] = STAGE2_ROOT_DOMAIN; +pub const OUTER_CLAIM_ELEMENTS: usize = 18; +pub const FRI_PARAMETER_ELEMENTS: usize = 5; +pub const FRI_PARAMETERS_BYTES: usize = FRI_PARAMETER_ELEMENTS * 8; +pub const OUTER_CLAIM_BYTES: usize = OUTER_CLAIM_ELEMENTS * 8; +pub const STAGE2_CLAIMS_BYTES: usize = 8 + 8 + OUTER_CLAIM_BYTES; +pub const STAGE2_ROOT_STATEMENT_BYTES: usize = + STAGE2_ROOT_DOMAIN.len() + 32 + FRI_PARAMETERS_BYTES + OUTER_CLAIM_BYTES; + +const ADVICE_PROFILE_DOMAIN: &[u8; 8] = b"IXADVP01"; + +/// Versioned, canonical public statement asserted by a closed Stage 2 root. +/// +/// Wire format: +/// `IXROOT01 || blake3(aiur_vk) || five FRI u64s LE || 18 Goldilocks u64s LE`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2RootStatementV1 { + verifying_key_digest: [u8; 32], + fri_parameters: [u64; FRI_PARAMETER_ELEMENTS], + outer_claim: [u64; OUTER_CLAIM_ELEMENTS], +} + +/// Shape census of the verified, per-query proof transport consumed by the +/// Flock verifier. This is diagnostic input to the Flock capacity model; +/// it is not itself a proof or a substitute for in-relation shape checks. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2AdviceProfileV1 { + pub advice_bytes: u64, + pub total_circuits: u64, + pub active_circuits: u64, + pub queries: u64, + pub fri_rounds: u64, + pub input_rounds_per_query: u64, + pub commitment_cap_digests: u64, + pub input_merkle_siblings: u64, + pub fri_merkle_siblings: u64, + pub opened_base_values: u64, + pub fri_sibling_extension_values: u64, + pub other_extension_values: u64, +} + +impl Stage2AdviceProfileV1 { + /// Parse the canonical Flock-verifier advice and census its fixed and + /// capacity-driving dimensions. The parser requires exact byte consumption. + pub fn from_advice_bytes(bytes: &[u8], fri: &FriParameters) -> Result { + profile_advice(bytes, fri) + } + + pub fn to_bytes(&self) -> Vec { + let words = [ + self.advice_bytes, + self.total_circuits, + self.active_circuits, + self.queries, + self.fri_rounds, + self.input_rounds_per_query, + self.commitment_cap_digests, + self.input_merkle_siblings, + self.fri_merkle_siblings, + self.opened_base_values, + self.fri_sibling_extension_values, + self.other_extension_values, + ]; + let mut bytes = + Vec::with_capacity(ADVICE_PROFILE_DOMAIN.len() + words.len() * 8); + bytes.extend_from_slice(ADVICE_PROFILE_DOMAIN); + for word in words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes + } + + pub fn digest(&self) -> [u8; 32] { + *blake3::hash(&self.to_bytes()).as_bytes() + } +} + +/// A compact proof that has been verified and expanded to the per-query advice +/// layout used by Flock Stage 3. The verifying key is retained for compiling a +/// specialised typed verifier witness; the claims remain the private words +/// bound by the Stage 2 statement inside that relation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ValidatedStage2RootV1 { + statement: Stage2RootStatementV1, + verifying_key_bytes: Vec, + claims_bytes: Vec, + advice_bytes: Vec, + advice_profile: Stage2AdviceProfileV1, +} + +impl ValidatedStage2RootV1 { + pub fn statement(&self) -> &Stage2RootStatementV1 { + &self.statement + } + + pub fn verifying_key_bytes(&self) -> &[u8] { + &self.verifying_key_bytes + } + + pub fn claims_bytes(&self) -> &[u8] { + &self.claims_bytes + } + + pub fn advice_bytes(&self) -> &[u8] { + &self.advice_bytes + } + + pub fn advice_profile(&self) -> &Stage2AdviceProfileV1 { + &self.advice_profile + } +} + +impl Stage2RootStatementV1 { + /// Construct the statement while enforcing the exact claim shape and + /// canonical Goldilocks encoding. + pub fn new( + vk_bytes: &[u8], + claim_bytes: &[u8], + fri: &FriParameters, + ) -> Result { + Ok(Self { + verifying_key_digest: *blake3::hash(vk_bytes).as_bytes(), + fri_parameters: fri_parameter_words(fri), + outer_claim: decode_claim_words(claim_bytes)?, + }) + } + + /// Parse the canonical format. Exact length, domain, and field encodings are + /// checked; trailing bytes are rejected. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != STAGE2_ROOT_STATEMENT_BYTES { + bail!( + "Stage 2 root statement is {} bytes; expected {STAGE2_ROOT_STATEMENT_BYTES}", + bytes.len() + ); + } + if &bytes[..STAGE2_ROOT_DOMAIN.len()] != STAGE2_ROOT_DOMAIN { + bail!("invalid Stage 2 root statement domain"); + } + + let mut verifying_key_digest = [0u8; 32]; + verifying_key_digest.copy_from_slice(&bytes[8..40]); + + let mut fri_parameters = [0u64; FRI_PARAMETER_ELEMENTS]; + for (word, chunk) in + fri_parameters.iter_mut().zip(bytes[40..80].as_chunks::<8>().0) + { + *word = u64::from_le_bytes(*chunk); + } + + Ok(Self { + verifying_key_digest, + fri_parameters, + outer_claim: decode_claim_words(&bytes[80..])?, + }) + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(STAGE2_ROOT_STATEMENT_BYTES); + bytes.extend_from_slice(STAGE2_ROOT_DOMAIN); + bytes.extend_from_slice(&self.verifying_key_digest); + for word in self.fri_parameters { + bytes.extend_from_slice(&word.to_le_bytes()); + } + for word in self.outer_claim { + bytes.extend_from_slice(&word.to_le_bytes()); + } + debug_assert_eq!(bytes.len(), STAGE2_ROOT_STATEMENT_BYTES); + bytes + } + + /// BLAKE3 digest of the complete, domain-separated canonical statement. + pub fn digest(&self) -> [u8; 32] { + *blake3::hash(&self.to_bytes()).as_bytes() + } + + pub fn verifying_key_digest(&self) -> &[u8; 32] { + &self.verifying_key_digest + } + + pub fn fri_parameter_words(&self) -> &[u64; FRI_PARAMETER_ELEMENTS] { + &self.fri_parameters + } + + pub fn outer_claim_words(&self) -> &[u64; OUTER_CLAIM_ELEMENTS] { + &self.outer_claim + } +} + +pub fn fri_parameter_words( + fri: &FriParameters, +) -> [u64; FRI_PARAMETER_ELEMENTS] { + [ + fri.log_final_poly_len as u64, + fri.max_log_arity as u64, + fri.num_queries as u64, + fri.commit_proof_of_work_bits as u64, + fri.query_proof_of_work_bits as u64, + ] +} + +pub fn fri_parameters_to_bytes(fri: &FriParameters) -> Vec { + fri_parameter_words(fri) + .iter() + .flat_map(|value| value.to_le_bytes()) + .collect() +} + +/// Decode the exact 18-word outer claim and reject non-canonical Goldilocks +/// representatives. +pub fn decode_claim_words( + claim_bytes: &[u8], +) -> Result<[u64; OUTER_CLAIM_ELEMENTS]> { + if claim_bytes.len() != OUTER_CLAIM_BYTES { + bail!( + "ix_aggr outer claim is {} bytes; expected {OUTER_CLAIM_BYTES} (18 Goldilocks words)", + claim_bytes.len() + ); + } + + let mut words = [0u64; OUTER_CLAIM_ELEMENTS]; + for (index, (word_out, chunk)) in + words.iter_mut().zip(claim_bytes.as_chunks::<8>().0).enumerate() + { + let word = u64::from_le_bytes(*chunk); + let value = G::from_u64(word); + if value.as_canonical_u64() != word { + bail!("outer claim word {index} is not canonical Goldilocks"); + } + *word_out = word; + } + Ok(words) +} + +/// Canonical `&[&[Goldilocks]]` encoding consumed by the existing recursive +/// verifier: one claim, its 18-word length, then its little-endian words. +pub fn stage2_claims_bytes(claim_bytes: &[u8]) -> Result> { + let words = decode_claim_words(claim_bytes)?; + let mut bytes = Vec::with_capacity(STAGE2_CLAIMS_BYTES); + bytes.extend_from_slice(&1u64.to_le_bytes()); + bytes.extend_from_slice(&(OUTER_CLAIM_ELEMENTS as u64).to_le_bytes()); + for word in words { + bytes.extend_from_slice(&word.to_le_bytes()); + } + Ok(bytes) +} + +fn fri_matches(actual: &FriParameters, expected: &FriParameters) -> bool { + fri_parameter_words(actual) == fri_parameter_words(expected) +} + +struct DecodedRootInputs { + statement: Stage2RootStatementV1, + claim: Vec, + verifying_key: AiurVerifyingKey, + proof: AiurProof, +} + +fn decode_root_inputs( + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, +) -> Result { + let statement = Stage2RootStatementV1::new(vk_bytes, claim_bytes, fri)?; + let claim: Vec = + statement.outer_claim.iter().copied().map(G::from_u64).collect(); + let verifying_key = AiurVerifyingKey::from_bytes(vk_bytes) + .map_err(|error| anyhow::anyhow!("invalid Aiur verifying key: {error}"))?; + if verifying_key.to_bytes() != vk_bytes { + bail!("Aiur verifying key is not canonically encoded"); + } + if !fri_matches(&verifying_key.fri_parameters(), fri) { + bail!("requested recursion FRI parameters do not match the Aiur vk"); + } + let proof = AiurProof::from_bytes(proof_bytes) + .map_err(|error| anyhow::anyhow!("invalid Aiur proof: {error}"))?; + let canonical_proof = proof + .to_bytes() + .map_err(|error| anyhow::anyhow!("re-encode Aiur proof: {error}"))?; + if canonical_proof != proof_bytes { + bail!("Aiur proof is non-canonical or contains trailing bytes"); + } + Ok(DecodedRootInputs { statement, claim, verifying_key, proof }) +} + +/// Validate a persisted aggregate root natively and return the exact public +/// statement terminal backends must prove. A backend circuit must repeat all +/// verification checks; this native pass is a cost and ergonomics guard. +pub fn validate_root_inputs( + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, +) -> Result { + let decoded = decode_root_inputs(vk_bytes, claim_bytes, proof_bytes, fri)?; + verify_decoded_root_inputs(&decoded)?; + Ok(decoded.statement) +} + +fn verify_decoded_root_inputs(decoded: &DecodedRootInputs) -> Result<()> { + decoded.verifying_key.verify(&decoded.claim, &decoded.proof).map_err( + |error| anyhow::anyhow!("aggregate root does not verify: {error:?}"), + ) +} + +/// Verify a compact Stage 2 root and expand its pruned Merkle multiproofs into +/// the per-query advice layout consumed by the Flock Stage 3 verifier. +/// No host-derived acceptance bit crosses the boundary: Stage 3 must parse and +/// re-check these retained vk, claim, and advice bytes inside its relation. +pub fn validate_and_expand_root_inputs( + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, +) -> Result { + validate_and_expand_root_inputs_bounded( + vk_bytes, + claim_bytes, + proof_bytes, + fri, + u64::MAX, + ) +} + +/// Bound the expanded transport before native verification/path expansion. +/// This limits serialized advice, not the verifier's total working memory. +pub fn validate_and_expand_root_inputs_bounded( + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + max_advice_bytes: u64, +) -> Result { + let decoded = decode_root_inputs(vk_bytes, claim_bytes, proof_bytes, fri)?; + let upper_bound = + expanded_advice_upper_bound(&decoded.proof, proof_bytes.len(), fri)?; + if upper_bound > max_advice_bytes { + bail!( + "expanded Stage 2 advice upper bound {upper_bound} bytes exceeds admission limit {max_advice_bytes}" + ); + } + // The pinned expansion API verifies the complete proof before expanding + // its paths. Do not repeat that native verification here. + let advice_bytes = decoded + .verifying_key + .proof_to_per_query_advice_bytes(&decoded.claim, &decoded.proof) + .map_err(|error| anyhow::anyhow!("expand verified Aiur proof: {error}"))?; + if advice_bytes.len() as u64 > max_advice_bytes { + bail!("expanded Stage 2 advice exceeds admission limit {max_advice_bytes}"); + } + let advice_profile = + Stage2AdviceProfileV1::from_advice_bytes(&advice_bytes, fri)?; + Ok(ValidatedStage2RootV1 { + statement: decoded.statement, + verifying_key_bytes: vk_bytes.to_vec(), + claims_bytes: stage2_claims_bytes(claim_bytes)?, + advice_bytes, + advice_profile, + }) +} + +fn expanded_advice_upper_bound( + proof: &AiurProof, + compact_bytes: usize, + fri: &FriParameters, +) -> Result { + // The pinned multiproof already carries every query's opened rows and FRI + // siblings. Expansion retains them, adding full binary MMCS paths and + // vector framing. Goldilocks has at most 32 binary tree levels; reserve + // twice that many digests to also cover every possible height injection. + // The compact framing remains in this bound, so no subtraction can make + // the estimate too small when queries or matrices share paths. + const PATH_AND_FRAMING_BYTES: u64 = 64 * 32 + 64; + let batches = proof.opening_proof.input_openings.len() as u64; + let rounds = proof.opening_proof.commit_phase_openings.len() as u64; + batches + .checked_add(rounds) + .and_then(|paths| paths.checked_mul(PATH_AND_FRAMING_BYTES)) + .and_then(|per_query| per_query.checked_add(16)) + .and_then(|per_query| per_query.checked_mul(fri.num_queries as u64)) + .and_then(|extra| extra.checked_add(16)) + .and_then(|extra| extra.checked_add(compact_bytes as u64)) + .ok_or_else(|| anyhow::anyhow!("expanded Stage 2 advice size overflow")) +} + +fn profile_advice( + bytes: &[u8], + fri: &FriParameters, +) -> Result { + let proof = decode_stage2_advice(bytes, fri)?; + + let input_rounds_per_query = proof + .opening_proof + .query_proofs + .first() + .map_or(0, |query| query.input_proof.len()); + let commitment_cap_digests = proof.commitments.stage_1_trace.roots().len() + + proof.commitments.stage_2_trace.roots().len() + + proof.commitments.quotient_chunks.roots().len() + + proof + .opening_proof + .commit_phase_commits + .iter() + .map(|commitment| commitment.roots().len()) + .sum::(); + let input_merkle_siblings = proof + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.input_proof) + .map(|opening| opening.opening_proof.len()) + .sum(); + let fri_merkle_siblings = proof + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.commit_phase_openings) + .map(|opening| opening.opening_proof.len()) + .sum(); + let opened_base_values = proof + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.input_proof) + .flat_map(|opening| &opening.opened_values) + .map(Vec::len) + .sum(); + let fri_sibling_extension_values = proof + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.commit_phase_openings) + .map(|opening| opening.sibling_values.len()) + .sum(); + let other_extension_values = proof.intermediate_accumulators.len() + + count_opened_values(&proof.quotient_opened_values) + + proof + .preprocessed_opened_values + .as_ref() + .map_or(0, |values| count_opened_values(values)) + + count_opened_values(&proof.stage_1_opened_values) + + count_opened_values(&proof.stage_2_opened_values) + + proof.opening_proof.final_poly.len(); + + Ok(Stage2AdviceProfileV1 { + advice_bytes: to_u64(bytes.len(), "advice bytes")?, + total_circuits: to_u64(proof.active.len(), "circuit count")?, + active_circuits: to_u64( + proof.active.iter().filter(|&&active| active).count(), + "active circuit count", + )?, + queries: to_u64(proof.opening_proof.query_proofs.len(), "query count")?, + fri_rounds: to_u64( + proof.opening_proof.commit_phase_commits.len(), + "FRI round count", + )?, + input_rounds_per_query: to_u64( + input_rounds_per_query, + "input rounds per query", + )?, + commitment_cap_digests: to_u64( + commitment_cap_digests, + "commitment cap digests", + )?, + input_merkle_siblings: to_u64( + input_merkle_siblings, + "input Merkle siblings", + )?, + fri_merkle_siblings: to_u64(fri_merkle_siblings, "FRI Merkle siblings")?, + opened_base_values: to_u64(opened_base_values, "opened base values")?, + fri_sibling_extension_values: to_u64( + fri_sibling_extension_values, + "FRI sibling extension values", + )?, + other_extension_values: to_u64( + other_extension_values, + "other extension values", + )?, + }) +} + +/// Decode the canonical per-query Stage 2 proof transport into semantic proof +/// fields. The persisted bincode representation ends here: Flock backends +/// should lower this typed value, not reproduce byte parsing in their +/// relation. +pub fn decode_stage2_advice( + bytes: &[u8], + fri: &FriParameters, +) -> Result { + let codec = config::standard().with_little_endian().with_fixed_int_encoding(); + let (proof, consumed): (AdviceProof, usize) = decode_from_slice(bytes, codec) + .map_err(|error| { + anyhow::anyhow!("decode canonical Stage 2 advice: {error}") + })?; + if consumed != bytes.len() { + bail!("Stage 2 advice contains trailing bytes"); + } + if proof.opening_proof.query_proofs.len() != fri.num_queries { + bail!( + "Stage 2 advice has {} queries; expected {}", + proof.opening_proof.query_proofs.len(), + fri.num_queries + ); + } + + let input_rounds_per_query = proof + .opening_proof + .query_proofs + .first() + .map_or(0, |query| query.input_proof.len()); + if proof.opening_proof.query_proofs.iter().any(|query| { + query.input_proof.len() != input_rounds_per_query + || query.commit_phase_openings.len() + != proof.opening_proof.commit_phase_commits.len() + }) { + bail!("Stage 2 advice has non-uniform per-query round counts"); + } + Ok(proof) +} + +fn count_opened_values(values: &[Vec>]) -> usize { + values.iter().flat_map(|matrix| matrix.iter()).map(Vec::len).sum() +} + +fn to_u64(value: usize, label: &str) -> Result { + u64::try_from(value) + .map_err(|error| anyhow::anyhow!("{label} exceeds u64: {error}")) +} + +/// Backwards-compatible SP1 public-values constructor. +pub fn expected_public_values( + vk_bytes: &[u8], + claim_bytes: &[u8], + fri: &FriParameters, +) -> Result> { + Ok(Stage2RootStatementV1::new(vk_bytes, claim_bytes, fri)?.to_bytes()) +} + +#[cfg(test)] +mod tests { + use super::*; + use multi_stark::{ + advice::proof_to_advice_bytes, + p3_matrix::dense::RowMajorMatrix, + system::{CircuitInputs, System, SystemWitness}, + types::{CommitmentParameters, GoldilocksBlake3Config}, + }; + + fn test_fri() -> FriParameters { + FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + } + } + + fn canonical_claim() -> Vec { + (0..OUTER_CLAIM_ELEMENTS as u64).flat_map(u64::to_le_bytes).collect() + } + + #[test] + fn statement_round_trip_is_exact() { + let statement = + Stage2RootStatementV1::new(b"vk", &canonical_claim(), &test_fri()) + .expect("statement"); + let bytes = statement.to_bytes(); + assert_eq!(bytes.len(), STAGE2_ROOT_STATEMENT_BYTES); + assert_eq!(&bytes[..8], STAGE2_ROOT_DOMAIN); + assert_eq!(&bytes[8..40], blake3::hash(b"vk").as_bytes()); + assert_eq!(&bytes[40..80], fri_parameters_to_bytes(&test_fri())); + assert_eq!(&bytes[80..], canonical_claim()); + assert_eq!(Stage2RootStatementV1::from_bytes(&bytes).unwrap(), statement); + assert_eq!( + blake3::Hash::from_bytes(statement.digest()).to_hex().as_str(), + "f1e778aa3d903008a6e755daee2e4f36f1a7a168277cbb6625984602c12dbe4f" + ); + } + + #[test] + fn parser_rejects_domain_length_and_noncanonical_claim() { + let mut bytes = + Stage2RootStatementV1::new(b"vk", &canonical_claim(), &test_fri()) + .unwrap() + .to_bytes(); + bytes[0] ^= 1; + assert!(Stage2RootStatementV1::from_bytes(&bytes).is_err()); + + let mut bytes = + Stage2RootStatementV1::new(b"vk", &canonical_claim(), &test_fri()) + .unwrap() + .to_bytes(); + bytes.extend_from_slice(&[0]); + assert!(Stage2RootStatementV1::from_bytes(&bytes).is_err()); + + let mut claim = canonical_claim(); + claim[..8].copy_from_slice(&u64::MAX.to_le_bytes()); + assert!(Stage2RootStatementV1::new(b"vk", &claim, &test_fri()).is_err()); + } + + #[test] + fn claims_transport_is_the_recursive_verifier_wire_format() { + let claim = canonical_claim(); + let bytes = stage2_claims_bytes(&claim).unwrap(); + assert_eq!(bytes.len(), STAGE2_CLAIMS_BYTES); + assert_eq!(&bytes[..8], &1u64.to_le_bytes()); + assert_eq!(&bytes[8..16], &(OUTER_CLAIM_ELEMENTS as u64).to_le_bytes()); + assert_eq!(&bytes[16..], claim); + } + + #[test] + fn advice_profile_parses_a_real_proof_and_rejects_extensions() { + let commitment = CommitmentParameters { log_blowup: 1, cap_height: 0 }; + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 2, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 0, + }; + let (system, key) = System::new( + GoldilocksBlake3Config::new(commitment, fri), + [ + CircuitInputs { main_width: 2, ..Default::default() }, + CircuitInputs { main_width: 3, ..Default::default() }, + ], + ); + let trace_1 = + RowMajorMatrix::new((0..16u32).map(G::from_u32).collect::>(), 2); + let trace_2 = RowMajorMatrix::new( + (0..12u32).map(|value| G::from_u32(7 * value + 3)).collect(), + 3, + ); + let witness = SystemWitness::from_stage_1(vec![trace_1, trace_2], &system); + let proof = system.prove_multiple_claims(&key, &[], witness); + let advice = proof_to_advice_bytes(&system, commitment, fri, &[], &proof) + .expect("expand proof advice"); + let bound = expanded_advice_upper_bound( + &proof, + proof.to_bytes().unwrap().len(), + &fri, + ) + .unwrap(); + assert!(advice.len() as u64 <= bound); + + let profile = Stage2AdviceProfileV1::from_advice_bytes(&advice, &fri) + .expect("profile canonical advice"); + assert_eq!(profile.advice_bytes, advice.len() as u64); + assert_eq!(profile.total_circuits, 2); + assert_eq!(profile.active_circuits, 2); + assert_eq!(profile.queries, 2); + assert!(profile.input_rounds_per_query > 0); + assert!(profile.input_merkle_siblings > 0); + + let mut extended = advice; + extended.push(0); + assert!(Stage2AdviceProfileV1::from_advice_bytes(&extended, &fri).is_err()); + } +} diff --git a/flake.lock b/flake.lock index 6a0edb59..4a8f9a87 100644 --- a/flake.lock +++ b/flake.lock @@ -284,6 +284,23 @@ "type": "github" } }, + "lspec": { + "flake": false, + "locked": { + "lastModified": 1787413499, + "narHash": "sha256-EPGav84gN1Ki96SzVnH+TGjMAI1IGk4qsQVIaYsr4VM=", + "owner": "argumentcomputer", + "repo": "LSpec", + "rev": "ab4d5eb461941837f48eb891be755c8c73e89fdd", + "type": "github" + }, + "original": { + "owner": "argumentcomputer", + "repo": "LSpec", + "rev": "ab4d5eb461941837f48eb891be755c8c73e89fdd", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1765779637, @@ -399,6 +416,7 @@ "fenix": "fenix_2", "flake-parts": "flake-parts_2", "lean4-nix": "lean4-nix", + "lspec": "lspec", "nixpkgs": [ "lean4-nix", "nixpkgs" diff --git a/flake.nix b/flake.nix index 4e725b2c..996a4b08 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,13 @@ inputs.lean4-nix.follows = "lean4-nix"; }; + # Fetch LSpec through the flake input machinery instead of lake2nix's + # unauthenticated builtins.fetchGit evaluation path. + lspec = { + url = "github:argumentcomputer/LSpec/ab4d5eb461941837f48eb891be755c8c73e89fdd"; + flake = false; + }; + # Zisk dev shell (cargo-zisk, ziskemu, RISC-V toolchain) for `zisk-guest`. zisk.url = "github:argumentcomputer/zisk.nix/blake3-precompile"; @@ -57,6 +64,7 @@ fenix, crane, blake3-lean, + lspec, zisk, sp1, ... @@ -189,6 +197,8 @@ ./Cargo.toml ./Cargo.lock (pkgs.lib.fileset.fileFilter (f: f.hasExt "rs" || f.hasExt "toml") ./crates) + ./flock-stage3/Cargo.lock + (pkgs.lib.fileset.fileFilter (f: f.hasExt "rs" || f.hasExt "toml") ./flock-stage3) (pkgs.lib.fileset.fileFilter (f: f.hasExt "lean") ./.) ]; }; @@ -212,6 +222,15 @@ }; depOverrideDeriv = { Blake3 = blake3-lean.packages.${system}.rust; + # Keep the root manifest authoritative for LSpec's inherited + # plausible dependency while sourcing LSpec from its locked, + # content-addressed flake input. + LSpec = lake2nix.mkLakeDerivation { + name = "LSpec"; + src = lspec; + deps = { inherit (lakeDeps) plausible; }; + buildLibrary = true; + }; }; }; # Shared Lake build args: patches out the Cargo build (Crane handles it) diff --git a/flock-stage3/Cargo.lock b/flock-stage3/Cargo.lock new file mode 100644 index 00000000..55337df9 --- /dev/null +++ b/flock-stage3/Cargo.lock @@ -0,0 +1,1278 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aiur" +version = "0.1.0" +dependencies = [ + "hashbrown 0.15.5", + "indexmap", + "libc", + "multi-stark", + "num-bigint 0.4.8", + "rayon", + "rustc-hash", + "tracing", + "tracing-texray", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flock-core" +version = "0.1.0" +source = "git+https://github.com/succinctlabs/flock?rev=b310f35f35f68095537150a1c8c0a43caca9a29e#b310f35f35f68095537150a1c8c0a43caca9a29e" +dependencies = [ + "bincode 1.3.3", + "blake3", + "rand_core 0.9.5", + "rayon", + "serde", + "sha2", + "toml", +] + +[[package]] +name = "flock-prover" +version = "0.1.0" +source = "git+https://github.com/succinctlabs/flock?rev=b310f35f35f68095537150a1c8c0a43caca9a29e#b310f35f35f68095537150a1c8c0a43caca9a29e" +dependencies = [ + "bincode 1.3.3", + "blake3", + "flock-core", + "rayon", + "serde", + "sha2", +] + +[[package]] +name = "flock-stage3-host" +version = "0.1.0" +dependencies = [ + "aiur", + "anyhow", + "bincode 1.3.3", + "blake3", + "flock-prover", + "ix-terminal", + "multi-stark", + "rayon", + "serde", + "serde_json", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ix-terminal" +version = "0.1.0" +dependencies = [ + "aiur", + "anyhow", + "bincode 2.0.1", + "blake3", + "multi-stark", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "multi-stark" +version = "0.1.0" +source = "git+https://github.com/argumentcomputer/multi-stark.git?rev=6ad074c1f2983ecdd7a56984d333441d6b38186a#6ad074c1f2983ecdd7a56984d333441d6b38186a" +dependencies = [ + "bincode 2.0.1", + "p3-air", + "p3-blake3", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-fri", + "p3-goldilocks", + "p3-keccak", + "p3-matrix", + "p3-maybe-rayon", + "p3-merkle-tree", + "p3-symmetric", + "p3-util", + "serde", + "tracing", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p3-air" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-field", + "p3-matrix", + "serde", + "tracing", +] + +[[package]] +name = "p3-blake3" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "blake3", + "p3-symmetric", + "p3-util", +] + +[[package]] +name = "p3-challenger" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-commit" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-multilinear-util", + "p3-util", + "serde", +] + +[[package]] +name = "p3-dft" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand", + "serde", + "tracing", +] + +[[package]] +name = "p3-fri" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-security", + "p3-util", + "rand", + "serde", + "spin", + "thiserror", + "tracing", +] + +[[package]] +name = "p3-goldilocks" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand", + "serde", + "spin", +] + +[[package]] +name = "p3-keccak" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-symmetric", + "p3-util", + "tiny-keccak", +] + +[[package]] +name = "p3-matrix" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "rayon", +] + +[[package]] +name = "p3-mds" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand", +] + +[[package]] +name = "p3-merkle-tree" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-commit", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand", + "serde", + "spin", + "thiserror", + "tracing", +] + +[[package]] +name = "p3-multilinear-util" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "rand", + "serde", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand", +] + +[[package]] +name = "p3-security" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "libm", + "p3-air", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "itertools", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.0" +source = "git+https://github.com/Plonky3/Plonky3?rev=3152b14a89067c83775a8076cc262ffc48a1fd7c#3152b14a89067c83775a8076cc262ffc48a1fd7c" +dependencies = [ + "p3-maybe-rayon", + "serde", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", + "sha2-asm", +] + +[[package]] +name = "sha2-asm" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" +dependencies = [ + "cc", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-texray" +version = "0.2.0" +source = "git+https://github.com/argumentcomputer/tracing-texray?rev=465bbca0bea4721e58419c11cabd8cce21757822#465bbca0bea4721e58419c11cabd8cce21757822" +dependencies = [ + "loom", + "parking_lot", + "terminal_size", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/flock-stage3/Cargo.toml b/flock-stage3/Cargo.toml new file mode 100644 index 00000000..b5d84a89 --- /dev/null +++ b/flock-stage3/Cargo.toml @@ -0,0 +1,32 @@ +[workspace] +members = ["host"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[workspace.dependencies] +anyhow = "1" +aiur = { path = "../crates/aiur" } +bincode = "1.3" +blake3 = "1.8.4" +flock-prover = { git = "https://github.com/succinctlabs/flock", rev = "b310f35f35f68095537150a1c8c0a43caca9a29e" } +ix-terminal = { path = "../crates/terminal" } +multi-stark = { git = "https://github.com/argumentcomputer/multi-stark.git", rev = "6ad074c1f2983ecdd7a56984d333441d6b38186a" } +rayon = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[workspace.lints.rust] +invalid_reference_casting = "warn" +nonstandard_style = "warn" +rust_2018_idioms = { level = "warn", priority = -1 } +unreachable_pub = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +dbg_macro = "warn" diff --git a/flock-stage3/README.md b/flock-stage3/README.md new file mode 100644 index 00000000..846b5900 --- /dev/null +++ b/flock-stage3/README.md @@ -0,0 +1,581 @@ +# Ix Flock Stage 3 + +This workspace implements the no-RISC-V Stage 3 compressor: + +```text +Stage 2 Aiur recursive-FRI root + -> Flock proof of statement + AIR/logUp + PCS + FRI verification + -> Stage 4 terminal SNARK +``` + +The backend uses Flock `Fast128` over `F128`, BLAKE3 Merkle commitments, and +chained-BLAKE3 Fiat-Shamir. The upstream revision is pinned to +`b310f35f35f68095537150a1c8c0a43caca9a29e`; changing it is a protocol change. + +## Current status + +`FlockStage3Backend::prove_stage2` now generates a complete Stage 3 proof. +The production path: + +1. validates canonical Aiur verifier-key, claim, and proof encodings; +2. expands the compact multiproof into the typed verifier witness; +3. admits its table/buffer geometry, compiles and evaluates the specialised + fixed-shape relation, and validates the concrete PCS configuration; +4. proves it under the production Stage 3 transcript domain; +5. binds the compiled circuit, Flock configuration, Stage 2 key, witness + layout, exact witness shape, and completed phase mask in + `Stage3RelationManifestV1`; +6. verifies the generated Flock bundle against that same relation; and +7. returns a strict, versioned `Stage3ArtifactV1`. + +`FlockStage3Backend::verify_stage2` requires an externally expected +`Stage3StatementV1`. It reconstructs the relation from the canonical Stage 2 +transport, checks the expected root and relation-manifest digest, and verifies +the Flock bundle. The relation digest therefore has to be pinned by the +deployment; accepting a relation digest supplied only by the prover would not +specialise the verifier key. + +`verify_stage2_for_root` is the operational verification path: it derives the +expected statement from an external persisted aggregate root, requires the +artifact's embedded compact transport to match it, and reuses that one +validation/relation build for cryptographic verification. + +`prepare_stage2[_with_limits]` returns an explicitly owned +`Stage3PreparedRootV1`. Inspect `report()` before calling `prove_with_timings()`; +`verify()` can reuse that same admitted relation. Dropping the handle releases +the root transport and relation. Production calls no longer retain an +exact-witness relation in a global cache. Invariant R1CS/lincheck tables remain +shared; the older standalone manifest/conformance helpers still have their +own exact-witness cache. + +Capacity admission runs the same constraint emitter with a lightweight row +counter before constructing R1CS tables or wiring. It chooses the smallest +power-of-two capacity fitting the busiest table, subject to the existing +minimum. The finished circuit must match every counted table's rows and I/O +arity. This replaces the sum of independently rounded regional budgets. + +Arithmetic data zero is kept separate from assertion outputs. Bounded groups +of at most 256 residuals are anchored by constrained `canonical(0,0)` outputs. +This avoids quadratic wiring compilation and a pinned-builder hazard where +later inputs appended to an already merged zero wire could become detached +from its fixed-public class. Regression tests inspect the actual wiring +classes as well as native evaluation and cryptographic proofs. + +These capacity/wiring changes alter compiled circuit and relation-manifest +digests. Regenerate Stage 3 artifacts and deliberately update deployment pins +when adopting this compiler; existing Stage 2 roots are unchanged. The Flock +dependency and configuration pins have not changed. + +The single relation constrains all eleven registered verifier phases: + +- typed witness shape, sparse activation, and active trace heights; +- specialised Aiur verifying-key/AIR metadata; +- all 18 canonical Goldilocks claim words and the 224-byte Stage 2 statement; +- lookup-message inversion, intermediate logUp accumulators, and final balance; +- exact chained-BLAKE3 transcript replay; +- Goldilocks and degree-two extension arithmetic; +- first/last/transition selectors and compiled AIR DAG evaluation; +- alpha-folded OOD composition and quotient recombination; +- every multi-matrix, multi-height PCS opening and BLAKE3 MMCS path; +- every binary FRI beta, grinding draw, query index, fold, roll-in, and final + polynomial check; and +- one published BLAKE3 Stage 2 root shared by the statement and proof checks. + +PCS leaves use the full BLAKE3 tree hasher, including rows wider than one block +and messages beyond one 1,024-byte chunk. Transcript field sampling follows +Plonky3 rejection sampling across the current digest plus one constrained +chained refill. The bounded circuit fails closed only if fewer than two values +are canonical among eight candidates, or among seven after a raw commit-PoW +draw. The latter probability is below roughly `2^-189`. + +The manifest is deliberately exact-shape: the current compiler does not pad a +smaller proof into a reusable capacity. All transport/profile words and the +full nested typed-witness layout, activation values and active trace heights +must match. Changing claim values with unchanged specialization preserves the +relation identity. A capacity-based deployment would +require explicit in-relation padding and a new manifest version. + +Before witness generation, the production circuit zero-initializes every +recycled Flock slot buffer. This preserves deterministic dummy rows and padding +across heterogeneous relation shapes instead of making proof validity depend +on allocator contents. Deterministically poisoned-buffer tests cover all eleven +production generators across growing, shrinking and empty row sets. The serial +real-proof CI suite exercises all 13 +cryptographic vectors in one process specifically to retain this regression. + +## Measured integration proof + +The integration regression fixture is a real canonical multi-STARK proof with +an inactive leading circuit, active circuits at heights 8 and 4, an active +preprocessed matrix, an 18-word claim lookup, nontrivial first-row/transition +constraints, and two FRI queries. + +The release regression produces a **343,867-byte artifact** with a +**343,741-byte production payload**. Exact sizing first reduced its capacity +from `nu=16` to `nu=13`; query-point/denominator reuse now lowers it to `nu=12` +and **192 MiB** padded z/a/b. A standalone local 2026-09-05 run measured +1.46 s of preparation, 1.12 s of proving (including initial lincheck setup and +row evaluation), and 0.012 s of self-verification. Canonicality packing keeps +the same capacity here: 2,173 addition rows now determine it. Before query sharing it was +368,163 bytes / 384 MiB padded. Across compiler versions, fewer rows do not +guarantee smaller proofs when PCS geometry changes. These are diagnostic +timings, not performance guarantees. + +Verification costs depend critically on what is reused: + +| Verification path | Local elapsed time | +| --- | ---: | +| Explicit prepared-root handle, no rebuilding | 0.011 s | +| Fresh relation in the same process, invariant tables warm | 1.21 s | +| Separate process, no prior Flock preparation/proving | 2.49 s | + +The old ~13 ms external-root figure was warm exact-witness cache reuse, **not +standalone verification**. The regression now persists the artifact and +separate external root inputs, then verifies them in a fresh child process. +It also tests the reused handle and fresh-relation paths independently. This +artifact is the off-chain Stage 3 proof, not the sub-kilobyte Ethereum proof. + +Run the exact regression with: + +```sh +cargo test --release --locked --manifest-path flock-stage3/Cargo.toml \ + -p flock-stage3-host \ + real_stage2_integration_artifact_round_trip -- --ignored --nocapture +``` + +The ordinary suite exercises relation construction and native/circuit +differential checks without paying the full proving cost: + +```sh +cargo test --release --locked --manifest-path flock-stage3/Cargo.toml \ + --workspace --lib +cargo test --release --locked --manifest-path flock-stage3/Cargo.toml \ + -p flock-stage3-host --lib -- --ignored --test-threads=1 +cargo clippy --release --locked --manifest-path flock-stage3/Cargo.toml \ + --workspace --all-targets -- -D warnings +``` + +Print the selected Flock configuration and digest with: + +```sh +cargo run --release --locked --manifest-path flock-stage3/Cargo.toml \ + -p flock-stage3-host --bin flock-stage3-config +``` + +## Production aggregate preflight + +Build the optional root connector and compile/evaluate the complete Stage 3 +relation for a persisted `ix_aggr` root without starting the Flock prover: + +```sh +IX_FLOCK=1 nix develop --command lake exe ix flock-root ROOT_ADDRESS \ + --mode preflight +``` + +Preflight applies bounded host admission first, natively verifies and expands +the compact Stage 2 proof, constructs the typed AIR/PCS/FRI witness, evaluates +every Flock gate, and prints the Stage 2 advice geometry, `nu`, table capacity, +relation/public sizes, per-gate row counts, union and PCS buffer geometry, +phase timings, process lifetime peak RSS, effective admission limits, and +content-addressed relation/statement digests. The expansion bound is checked +before native proof expansion; table capacity and padded z/a/b bytes are +checked before wiring compilation. These are not total process RSS limits: +compiler, lincheck, PCS and allocator scratch are additional. + +Preflight is the mandatory gate before a production-sized +proof. The ordinary suite generates and natively verifies a canonical transport +with the deployed blowup-two, 100-query, 20-bit query-PoW parameters and lowers +its complete typed AIR/PCS/FRI witness. It does not replace a full +production-root Flock proof vector. + +Collect a corpus as versioned JSONL (one result per requested root): + +```sh +.lake/build/bin/ix flock-root ROOT_A ROOT_B --mode preflight --jsonl +.lake/build/bin/ix flock-root --roots-file roots.txt --jsonl > roots.jsonl +.lake/build/bin/ix flock-root --root-file root.ixon-proof --jsonl +``` + +`roots.txt` accepts one address per line, blank lines, and whole-line `#` +comments. Batches continue after individual failures and exit nonzero if any +root fails. `prove` and `verify` require one root. Address-mode reads rehash the +wrapper and never create store directories; file mode derives its address +from the bounded input. The historical Mathlib wrapper in `Tests/Fixtures` +is intentionally rejected by the current protocol and is not a usable +production corpus. + +Stdout JSONL uses `ix.flock-stage3.root` version 1, with `status`, `source`, +`mode`, and either `error` or `result`. A successful result contains +`details.preflight` (`ix.flock-stage3.preflight` version 1). Reports include +key/proof hashes, activation/heights/layout, protocol provenance, and timing +units in field names. Progress and the successful pre-prove report go to +stderr. RSS is a process lifetime high-water mark, so use separate processes +for individual-root memory comparisons. The operation-level RSS record also +includes proving/verification after preflight. + +Flock backend failures cross the Rust/Lean boundary as an `Except` payload +inside `IO`; Lean constructs the actual `IO.Error`. This avoids the pinned +`lean-ffi` helper's incompatible `IO.Error.userError` constructor tag, which +previously produced bogus OS error codes. Exact error regressions cover this +path. Other users of that helper in the kernel/catalog/compiler FFI remain a +separate compatibility-cleanup task; this change does not repair them. + +Resource overrides are `--max-advice-mib` (default 256), +`--max-witness-mib` (default 32768, only the padded z/a/b buffers), and +`--max-table-capacity` (default 4194304). Raising a limit changes admission, +not the cryptographic configuration or proof shape, and does not guarantee +that a proof will fit in memory. + +Set `IX_FLOCK_TIMING=1` to also retain the count-only diagnostic on stderr +(`ix.flock-stage3.shape-count`, version 1). It reports exact per-gate rows, +capacity, padded-buffer bytes, count time, and process peak RSS **before** +admission. Thus rejected corpus roots can be measured without wiring +compilation. `compiled: false` is intentional: this is not a successful +preflight, evaluated relation, or deployment-owned circuit digest. + +Once preflight succeeds, retain the expensive verified artifact explicitly: + +```sh +IX_FLOCK=1 nix develop --command lake exe ix flock-root ROOT_ADDRESS \ + --mode prove --output root.stage3.flock +``` + +The prove command performs preflight, proving, and self-verification through a +single validated witness/relation path. It prints preflight before starting +the prover and does not repeat native Stage 2 verification between phases. + +The output is durably installed through an exclusive temporary file and is +never allowed to overwrite an existing artifact. The temporary destination, +directory writability and hard-link support are checked before native +validation/compilation. An abandoned reservation is removed; installation +still fails safely if another writer claims the final name. Artifact framing +is streamed once instead of allocating an extra full encoded copy. +Verify it later against the +persisted aggregate root, which independently derives the expected Stage 3 +statement and relation digest: + +```sh +IX_FLOCK=1 nix develop --command lake exe ix flock-root ROOT_ADDRESS \ + --mode verify --artifact root.stage3.flock +``` + +The binary-FRI lowering supports the full height-derived schedule: the prior +eight-round implementation ceiling is gone, with evaluated regressions at 9, +16, and the current 30-round maximum. The isolated Stage 3 workspace's fast +relation/differential suite and serial real cryptographic proof vectors are +both required pull-request CI checks. + +## First current-protocol persisted-root measurement + +The retained [singleton aggregate fixture](../Tests/Fixtures/Aggregate/singleton-2026-09-05/PROVENANCE.md) +uses the real IxVM and `ix_aggr` circuits, with the production 100-query, +20-bit grinding parameters. It certifies one well-formed axiom declaration, +has 175 active aggregate circuits, and passes fresh-process native +verification. Its 8.6 MB wrapper took 75.8 s to prove at 25.3 GiB sampled RSS. +The fixture generator never accesses the personal store or cache. + +After sharing PCS constraints, weighted quotients, query points and denominators, +then packing two canonicality requests per row, +its Flock cost is still much larger than the toy integration fixture: + +| Exact Flock count | Toy fixture | Persisted singleton `ix_aggr` | +| --- | ---: | ---: | +| Largest table rows | 33,661 (addition) | 3,162,519 (canonicality) | +| Canonicality rows | 32,857 | 3,162,519 | +| Uniform capacity | `nu=16` | `nu=22` | +| Padded z/a/b | 3 GiB | **192 GiB** | +| Flock proof completed | Yes | No: refused before wiring compilation | + +The real-root count took 0.155 s with a 350 MiB process peak RSS. Its capacity +now fits the default `2^22` table limit, but the unchanged 32 GiB padded-witness +guard still rejects it. A `2^21` table override exercises the capacity guard. No Flock +prover was started and no admission defaults were relaxed. See the +[current paired-profile measurement](measurements/packed-canonicality-2026-09-05.json). +The original [3 TiB / 62.9-million-row measurement](measurements/persisted-singleton-2026-09-05.json) +is retained unchanged: this is a 16x padded-buffer reduction on exactly the same +native root, not a smaller or weaker proof fixture. + +Recheck the persisted native certificate and collect its count safely: + +```sh +IX_FLOCK=1 lake build ix bench-flock-root-fixture +.lake/build/bin/bench-flock-root-fixture \ + --verify Tests/Fixtures/Aggregate/singleton-2026-09-05 +( + ulimit -v 16777216 + IX_FLOCK_TIMING=1 RAYON_NUM_THREADS=8 .lake/build/bin/ix flock-root \ + --root-file Tests/Fixtures/Aggregate/singleton-2026-09-05/root.ixon-proof \ + --jsonl +) +``` + +The last command intentionally exits 1. CI checks both admission failures +and the stderr/stdout distinction, alongside fresh native fixture verification. +The harness also supports `--output NEW_DIRECTORY [--prove]`; generation +requires a finite Linux address-space cap of at most 64 GiB and refuses +existing directories. See its provenance for reproduction details. + +**Next priority:** measure real-root compilation/proving on a suitably bounded +high-memory host, while preserving all constraints and interleaving soundness +review. The 192 GiB estimate covers only padded z/a/b, not peak RAM; a 512 GiB +machine has substantially more headroom now but remains untested. Do not treat +the toy's 3 GiB result as evidence that production aggregates fit. More activation/height +shapes remain necessary before selecting a capacity policy or freezing Stage 4. + +## Stage 2 lookup-packing experiment + +An explicit `AiurSystem.buildMinOpeningWidth` profile reduces the number of +Stage 2 columns opened by Stage 3. It searches the pinned protocol's existing +circuit-local lookup groups (`k=1..8`), minimizing accumulator **plus quotient** +width within the existing PCS blowup. This accounts for the extra quotient +columns that larger groups can require. Only strict improvements are used; +ties retain the original key. User constraints, lookup order, preprocessed +commitments, FRI queries and grinding parameters do not change. + +This changes the aggregate verifying key, allowed-key digest, outer claim and +root. It is experimental and opt-in: default system construction and the +production CLI still use the original keys. The +[new retained fixture](../Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/PROVENANCE.md) +contains the **same byte-identical IxVM child**, environment and CheckEnv claim +as the original singleton. Its 175 active circuit heights are unchanged. +The following paired measurement predates query-point/denominator sharing: + +| Singleton measurement | Default Stage 2 packing | Minimum-opening-width profile | +| --- | ---: | ---: | +| Sum of active committed column widths | 9,025 | 8,367 (−7.29%) | +| Native root wrapper bytes | 8,565,030 | 8,002,662 (−6.57%) | +| Native aggregate proving, sampled peak RSS | 25.26 GiB | 22.61 GiB | +| Native aggregate proving | 75.813 s | 75.386 s | +| Native aggregate verification | 0.121 s | 0.228 s | +| Stage 3 canonicality rows | 12,792,346 | 12,580,880 (−1.65%) | +| Stage 3 padded z/a/b | 768 GiB (`nu=24`) | 768 GiB (`nu=24`) | + +These are single local runs, not throughput guarantees. Smaller native proofs +do not imply proportionally smaller Stage 3 relations: both counts remain in +the same capacity bucket, and native verification was slower in this run. +Neither real aggregate was compiled, evaluated or proven with Flock. The +[measurement](measurements/stage2-lookup-packing-2026-09-05.json) retains both +profiles, all per-gate counts and changed circuit widths. The original fixture +and measurement JSON remain unchanged. The subsequent query-sharing compiler +reduced these same roots to 6,283,484 / 6,072,018 canonicality rows and 384 GiB +padded z/a/b each. Canonicality packing now counts 3,162,519 / 3,056,107 rows +and 192 GiB each; the Stage 2 profile is still explicit opt-in. + +Recheck and count the experimental fixture with explicit profile selection: + +```sh +.lake/build/bin/bench-flock-root-fixture --min-opening-width \ + --verify Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05 +( + ulimit -v 16777216 + IX_FLOCK_TIMING=1 RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture \ + --min-opening-width --count Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05 +) +``` + +The count mode deliberately sets a **one-byte** witness limit and reports +success only after the expected admission refusal. It writes the shape count +to stderr and one `ix.flock-stage3.fixture-count` JSON record to stdout; it +cannot compile wiring or start a Flock prover. The same mode supports the +original fixture without the profile flag. Generation supports +`--min-opening-width --output NEW_DIRECTORY --prove` under the existing 64 GiB +process cap; it never replaces an existing directory. + +Tests cover native proof/key round trips, old-key and wrong-claim rejection, +four-message grouped lookups in the recursive verifier (interpreter and +generated code), and a compiled/evaluated Stage 3 relation. CLI regressions +reject implicit experimental-key selection. The native RAM model also now +uses the field's actual extension degree, not an inference from packed lookup +widths; its historical calibration is still approximate and does not replace +process memory caps. An independent soundness review remains outstanding. + +## Shared PCS constraints and exact weighted quotients + +The compiler now constrains alpha powers, transcript-bound OOD weighted sums, +opening points and commitment bindings once, sharing those wires across +queries. It preserves the PCS's independent exponent counter per matrix +height, including batch, matrix, point and column order. Sharing uses explicit +metadata indices, never `Wire` identity: the count-only emitter intentionally +returns one placeholder wire for every expression. + +For each matrix/query, base-field row values use constrained duplicated lanes +for both hashing and scalar multiplication. One weighted row sum feeds all +opening points. For each point, the relation constrains `D + x = z`, +`D * inverse = 1`, and `D * Q + row_sum = opened_sum`, then accumulates +`alpha^offset * Q` into the height bucket. Since `D` is nonzero, these equations +uniquely determine the same weighted quotient as the original columnwise +division. This is exact elimination of auxiliary quotients, using the existing +PCS challenge; it does not replace checks with a new probabilistic combination. +Every source field word remains canonical and bound to its original +authentication/transcript constraints. The Boolean table schemas and arithmetic +output-canonicality checks are unchanged. + +Differential tests compare grouped and columnwise reductions across interleaved +heights, repeated matrices, empty/odd/wide column sets, and alpha edge cases. +Compiled-gate tests bypass native prevalidation and public-vector comparison +to reject altered/noncanonical denominators, inverses and quotients, including +a coordinated zero-denominator attack that satisfies both other equations. +The 75 fast tests and 13 serial cryptographic vectors pass; these are not an +independent soundness audit. Circuit, public-layout and relation digests change, +so deployment adoption requires regenerated artifacts and explicit pin updates. + +## Shared query points and denominators + +Within one PCS query, every matrix at the same LDE height uses the same query +point. The compiler now declares its fixed bit-reversed subgroup factors once +per height and computes one point per distinct height/query, using the correct +suffix of the transcript-derived index bits. Those operands are constrained +base-field embeddings `[x, 0]`, so lane-wise multiplication replaces the +extension multiplication expansion. Arithmetic residuals and canonical outputs +remain constrained; the Merkle authentication and PCS alpha ordering do not +change. + +The denominator `D = point - x` and its inverse are shared only within one +query and one `(height, opening-point-kind)` key. The key distinguishes `zeta` +from `zeta * g`, including the generator's log degree. Each distinct denominator +is still canonical, bound by `D + x = point`, and nonzero via `D * inverse = 1`, +including empty column sets. Each matrix/point retains its own weighted quotient +and reconstruction equation. The caches are keyed by explicit metadata, never +wire identities or coincident witness values. Point/denominator caches reset +for each query; only the fixed factors and other query-independent wires are shared. + +On the unchanged default aggregate, query-point sharing alone removes 5.50 +million canonicality rows; denominator sharing removes another 1.01 million. +The combined count falls from 12,792,346 to 6,283,484 rows, halving padded z/a/b +from 768 to **384 GiB**. The experimental Stage 2 profile also halves to 384 GiB. +At this query-sharing step both remained count-only admission failures, not +evaluated or proven aggregate relations. See the +[paired census and toy proof](measurements/pcs-query-sharing-2026-09-05.json). + +Compiled tests use an independent exponentiation oracle for interleaved heights +through 32, repeated heights, distinct query indices and both bit-order extremes. +They reject changed index bits, non-Boolean selectors and wrong coordinate +values/upper lanes without native PCS validation or public-vector comparison. +The same emission is checked with the all-placeholder counting builder. Existing +denominator/inverse/quotient and zero-denominator adversarial tests remain in +place. That query-sharing change retained Boolean table schemas, Flock +configuration and Stage 2 keys, while changing circuit and relation identities +and requiring deliberate artifact/pin regeneration before deployment adoption. + +## Packed canonicality checks + +The busiest table now packs two `F128` requests (four Goldilocks limbs) into +one row. Its layout uses 256 input bits, 128 independent violation bits and +124 high-bit AND-chain intermediates: **508 columns**, still within the old +512-column table. Every limb retains the exact `value < 2^64 - 2^32 + 1` +check. No probabilistic combination or omitted range constraint is involved. + +Requests are paired by emission order, never wire identity or witness value. +Both the count pass and compiled emission explicitly flush an odd final request +against fixed data zero. Zero-residual anchors remain separate output-only +classes with at most 256 assertions. Transcript sampling and the small statement +conformance relation use the same table, with a zero or duplicate second word. + +On the unchanged default aggregate, canonicality rows fall from 6,283,484 to +3,162,519. The experimental Stage 2 root falls from 6,072,018 to 3,056,107. +All other table row counts are unchanged, and both roots now fit `nu=22`, +halving padded z/a/b from **384 to 192 GiB**. These remain count-only results, +not real-root compilation, evaluation or Flock proofs. See the +[paired census and standalone toy proofs](measurements/packed-canonicality-2026-09-05.json). + +Tests reject noncanonical values in all four limbs, independently recompute +all 128 violation bits in Boolean R1CS, and mutate every requested word across +empty/even/odd batches and assertion-group boundaries. Count/compiled arities, +actual zero-wiring classes and poisoned recycled buffers are checked. All 75 +fast tests, 13 serial cryptographic vectors and the separate 100-query full +proof pass. These tests are not an independent soundness audit. + +The canonicality table schema, compiled circuit and relation identities change. +Deployment adoption requires regenerated Stage 3 artifacts and deliberate pin +updates. Native roots, Stage 2 keys, FRI parameters, Flock configuration and +admission defaults are unchanged. + +## Toy production-parameter sizing and proof + +The small integration fixture at the deployed blowup-two, 100-query, +20-bit query-PoW parameters now compiles, proves, persists, and verifies in a +fresh process under the default admission limits. It is still a small native +multi-STARK fixture, **not a persisted `ix_aggr` corpus root**. + +| Compiler | Capacity | Largest table rows | Padded z/a/b | Census, no prover | +| --- | ---: | ---: | ---: | ---: | +| Original | `nu=21` | 248,886 | 96 GiB | 653.60 s | +| Exact counting + bounded assertions | `nu=18` | 251,484 | 12 GiB | 2.62 s | +| Shared PCS + weighted quotients | `nu=17` | 103,160 | 6 GiB | 2.01 s | +| Shared query points + denominators | `nu=16` | 65,268 | 3 GiB | 1.84 s | +| Packed canonicality checks | `nu=16` | 33,661 (addition) | 3 GiB | 1.80 s | + +The current count/admission pass took 2.69 ms. Slot declaration took 788 ms, +query emission 24 ms, and builder finalization 873 ms in the standalone +census. Useful dense witness is approximately 94 MiB and the PCS codeword +188 MiB: reducing virtual capacity does not eliminate all other memory costs. +Canonicality rows fell to 32,857, but 33,661 addition rows still require +`nu=16`; the toy therefore does not share the real root's latest halving. + +The separate full proof produced a **478,483-byte artifact**, with 1.68 s of +preparation, 2.26 s of proving including initial lincheck setup, 0.018 s of +self-verification, and 2.61 s of fresh-process external-root verification. +The proving process reached **5.1 GiB peak RSS**, greater than the 3 GiB +padded-buffer estimate. Explicit verifier reuse took 0.018 s; fresh relation +verification with warm invariant tables took 1.44 s. The prior PCS compiler +measured 3.18 s proving and 8.4 GiB peak RSS. Its artifact was 461,635 bytes: +the current artifact is still larger despite fewer rows, reflecting changed +PCS geometry (47 lanes instead of 35). Timings are local diagnostics, not +guarantees. See the retained +[baseline](measurements/production-parameters-2026-09-05.json) and +[exact-count measurement](measurements/production-parameters-exact-2026-09-05.json), +the [PCS measurement](measurements/production-parameters-pcs-2026-09-05.json), +the [query-sharing census/proof](measurements/pcs-query-sharing-2026-09-05.json), +and the [current packed-canonicality census/proof](measurements/packed-canonicality-2026-09-05.json). + +The census is opt-in and never invokes a prover. It also checks acceptance +at exactly 3 GiB / `2^16` rows and rejection one byte/row below those limits: + +```sh +cargo test --release --locked --manifest-path flock-stage3/Cargo.toml \ + -p flock-stage3-host --features production-measurements \ + production_parameter_relation_census -- --ignored --nocapture +``` + +Run the high-memory proof separately, with sufficient headroom for compiler, +PCS, lincheck and allocator scratch beyond the padded-buffer estimate: + +```sh +cargo test --release --locked --manifest-path flock-stage3/Cargo.toml \ + -p flock-stage3-host --features production-measurements \ + production_parameter_artifact_round_trip -- --ignored --nocapture +``` + +Both proof fixtures persist an artifact and separate trusted root transport, +verify in a fresh child process, and reject wrong relation statements and +corrupted proof bundles. The default serial proof suite remains 13 vectors; +the 100-query proof requires explicit opt-in. + +## Scope and remaining work + +The current relation is deliberately specialised to the configuration used by +Ix: 18 claim words, binary FRI, cap height zero, and an exact activation/height +shape. Host deserialization is witness generation rather than trusted +acceptance; every lowered value reaches a verifier constraint. Native +prevalidation remains an ergonomics and cost guard. + +Before freezing a production deployment, Stage 3 still needs: + +- a practical memory representation for real aggregate relations: the first + persisted singleton still requires 192 GiB padded z/a/b after PCS and + canonicality-packing optimizations (down from 3 TiB), with real proving + peak RSS unmeasured; +- exact-shape measurements over the intended aggregate-proof corpus rather + than one small fixture, followed by a decision to freeze one shape or add + explicitly constrained padding; +- full proof vectors for persisted, production-sized Aiur roots at 100 queries + and 20-bit query grinding; the small fixture now covers these parameters; +- an independent review of the local Boolean R1CS tables and pinned Flock + soundness profile; and +- a canonical export of the fixed Flock verifier inputs for Stage 4 witness + generation. + +Design the [Stage 4 verifier boundary](STAGE4-BOUNDARY.md) alongside these +measurements, but do not freeze its relation or exporter until the capacity +policy and production vectors are established. The formalization workspace is +not incorporated into this work. diff --git a/flock-stage3/STAGE4-BOUNDARY.md b/flock-stage3/STAGE4-BOUNDARY.md new file mode 100644 index 00000000..7405c18e --- /dev/null +++ b/flock-stage3/STAGE4-BOUNDARY.md @@ -0,0 +1,109 @@ +# Stage 4 verifier boundary — design, not a frozen encoding + +Stage 4 should verify a Flock proof against a deployment-owned relation and +expose the same 104-byte `Stage3StatementV1`: domain, Stage 2 root digest, +relation-manifest digest, and Flock configuration digest. This document defines +the trust boundary to preserve while Stage 3 capacity sizing and corpus +measurements are still changing. It does not add a Stage 4 backend or import +the separate formalization work. + +## Ownership + +| Input | Authority / required check | +| --- | --- | +| Flock revision, profile, transcript domain, PCS security configuration | Deployment-owned protocol pin | +| Relation manifest, compiled circuit, table schemas, wiring, fixed publics | Deployment-owned specialization; never selected solely by the prover | +| Stage 2 key, FRI parameters, activation, trace heights, nested witness layout | Must match that specialization exactly | +| Stage 2 root / 18 claim words | Application-owned expected statement, bound by the relation | +| Flock proof, remaining verifier public words | Untrusted witness; checked by the fixed verifier | +| JSONL report, native validation result, process cache, timings and RSS | Diagnostics / host safeguards only; no acceptance authority | + +The Flock verifier's public vector is currently larger than the 104-byte outer +statement: it contains verifier transport values as well as fixed constants +and constrained derived values. An exporter must preserve the compiled public +ordering, enforce all fixed-public checks, and connect the published Stage 2 +root to the outer statement. It must not replace those checks with a native +"accepted" bit or assume the outer statement is Flock's entire public vector. + +## Export requirements + +The current bincode production payload is a versioned host transport, not a +canonical circuit-language witness ABI. A future exporter must specify and +test its own domain/version, exact lengths and order, F128 limb/byte order, +proof-bundle sections, transcript initialization, public-word mapping, and +reject trailing or noncanonical encodings. It must independently reproduce +the configuration and relation digests; a self-consistent prover-supplied +manifest is not a trust anchor. + +The explicit `Stage3PreparedRootV1` is the host integration point: one admitted +root owns one compiled relation, report and expected statement. Export should +consume this validated context, with separate exported verifier inputs tested +against the pinned native Flock verifier in a fresh process. Native Stage 2 +prevalidation remains a cost guard, not a substitute for verification inside +Stage 3 or Stage 4. + +## Order before freezing + +The checked count-based compiler, bounded arithmetic assertion groups, +shared PCS/weighted quotients, query-point/denominator reuse and packed +canonicality checks are implemented: +the 100-query toy fixture fits in 3 GiB padded z/a/b, compiles and evaluates in +about 1.8 s, and has passed a full proof with fresh-process verification. Its measured proving +peak was 5.1 GiB RSS (previously 6 GiB padded / 8.4 GiB RSS). Canonicality +packing retains the toy's 3 GiB capacity because additions now set its row +bound. Its current artifact is 478,483 bytes; proof size is not monotonic in +table capacity. +The PCS rewrite constrains nonzero denominators and preserves columnwise reduction exactly; +compiled-gate adversarial tests do not substitute for an independent review. +Circuit, table-schema and relation digests changed; adopting this compiler +requires regenerated artifacts and explicit deployment-pin updates, not +acceptance of prover-selected pins. + +The first genuine persisted `ix_aggr` fixture now passes native verification, +but its Stage 3 count still requires **192 GiB padded z/a/b** (`nu=22`, 3.16 +million packed canonicality rows), down from 3 TiB / 62.9 million rows on the identical +root. It fits the default table limit but not the default padded-witness limit; +admission rejects it before wiring compilation. Actual real-root peak RAM, +including compiler/PCS/prover scratch, remains unmeasured. The toy's successful +proof is therefore not evidence of practical aggregate proving. The +[baseline](measurements/persisted-singleton-2026-09-05.json), +[PCS measurement](measurements/persisted-singleton-pcs-2026-09-05.json) and +[query-sharing measurement](measurements/pcs-query-sharing-2026-09-05.json) and +[current canonicality-packing measurement](measurements/packed-canonicality-2026-09-05.json) +are retained. + +The opt-in Stage 2 `min-opening-width-v1` experiment reduces the same +singleton's active committed column widths by 7.29%. Initially, Stage 3 rows +fell only 1.65% to 12,580,880, with padded z/a/b unchanged at 768 GiB. The +query-sharing compiler counted 6,072,018 rows and 384 GiB; packed canonicality +now counts 3,056,107 rows and **192 GiB** for this +root, which is also rejected before wiring compilation. The experiment changes the +aggregate key, outer claim and root while retaining the same IxVM child and +CheckEnv statement; it does not change the deployment default or authorize +acceptance of a profile selected by the prover. Native verification was slower +in the single measured run despite smaller proof bytes and sampled proving +RSS. See the [paired measurement](measurements/stage2-lookup-packing-2026-09-05.json). +Adoption would require explicit Stage 2 key and Stage 3 relation pin changes. + +1. Measure real-root compilation/proving with explicit process bounds on a + suitable high-memory host; 192 GiB is not a total peak-RAM estimate. Further + arithmetic/layout optimizations must preserve every constraint and be + accompanied by independent soundness review. Retain the singleton's native + proof and count-only admission failures as regressions. +2. Collect current-protocol persisted roots with different activation/height + patterns using JSONL; retain failures as well as successful measurements. + Historical protocol-incompatible fixtures cannot stand in for this corpus. +3. Choose a fixed shape, a bounded family, or explicitly constrained padding. + Capacity padding needs a new manifest version and in-relation constraints; + host bounds alone cannot authorize reuse across activations or heights. +4. Extend the 100-query full-proof coverage to persisted ix_aggr roots, with + artifact persistence and fresh-process verification. Interleave independent + soundness review and adversarial table/wiring vectors with corpus work. +5. Freeze relation identities, canonical export bytes and golden vectors. + Only then specialize the terminal SNARK and measure its costs. + +The required negative vectors include changed claims, activation, heights, +key/configuration, Merkle paths, fold evaluations, grinding/query draws, +noncanonical field words, truncated/extended proof encodings, mismatched +external roots and poisoned recycled buffers. The existing tests cover a +useful subset; they are not an independent security audit. diff --git a/flock-stage3/host/Cargo.toml b/flock-stage3/host/Cargo.toml new file mode 100644 index 00000000..563cd4d1 --- /dev/null +++ b/flock-stage3/host/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "flock-stage3-host" +version.workspace = true +edition.workspace = true +license.workspace = true + +[features] +# Production-parameter census/proof experiments are opt-in, separate from +# both the fast suite and the routine serial cryptographic vectors. Run the +# high-memory proof separately; the census itself never invokes a prover. +production-measurements = [] + +[dependencies] +anyhow = { workspace = true } +aiur = { workspace = true } +bincode = { workspace = true } +blake3 = { workspace = true } +flock-prover = { workspace = true } +ix-terminal = { workspace = true } +multi-stark = { workspace = true } +rayon = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/flock-stage3/host/src/air.rs b/flock-stage3/host/src/air.rs new file mode 100644 index 00000000..c2d7ff22 --- /dev/null +++ b/flock-stage3/host/src/air.rs @@ -0,0 +1,997 @@ +//! Compiled Aiur AIR evaluation inside the Stage 3 Flock relation. +//! +//! The verifier evaluates every compiled base-polynomial node in the degree-2 +//! challenge field. LogUp is then evaluated in coordinates: one logical +//! accumulator consists of two such challenge-field values. This mirrors +//! `multi_stark::verifier` rather than trusting a host-computed composition. + +use aiur::vk_codec::{AiurAirCircuitMetadata, AiurVerifyingKey}; +use anyhow::{Result, bail}; +use flock_prover::{ + circuit::builder::{SlotId, Wire}, + field::F128, +}; +use ix_terminal::{ + STAGE2_ROOT_STATEMENT_BYTES, ValidatedStage2RootV1, fri_parameter_words, +}; +use multi_stark::{ + expr::{RowOffset, Source}, + graph::Node, + lookup::Lookup, + p3_field::{BasedVectorSpace, Field, PrimeCharacteristicRing, PrimeField64}, + types::{ExtVal, FriParameters, Val}, +}; + +use crate::{ + Stage2PcsInstanceV1, Stage2TranscriptByteBindingV1, Stage2TranscriptReplayV1, + Stage2TranscriptSegmentV1, Stage3TypedProofWitnessV1, + binding::pack_bytes, + extension::GoldilocksCircuitSlots, + fri::{ + assert_f128_equal, bound_transcript_extension, bound_transcript_window, + record_fixed, + }, + goldilocks::GOLDILOCKS_MODULUS, + sizing::CircuitEmitter, + transcript::TranscriptConstraintRegion, + transcript::{constrain_hash, hash_trace}, +}; + +const EXTENSION_DEGREE: usize = 2; +const STAGE2_STATEMENT_PREFIX_BYTES: usize = 80; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2ActiveAirCircuitV1 { + pub circuit_index: usize, + pub log_degree: u8, + pub metadata: AiurAirCircuitMetadata, + pub log_degree_binding: Stage2TranscriptByteBindingV1, + pub accumulator_binding: Stage2TranscriptByteBindingV1, +} + +/// Fixed compiled programs and transcript locations for one Stage 2 proof +/// shape. Claim values remain dynamic public transcript words. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2AirProgramV1 { + pub active: Vec, + pub activation_bindings: Vec, + pub active_circuits: Vec, + pub claim_bindings: Vec, + pub statement_prefix: [u8; STAGE2_STATEMENT_PREFIX_BYTES], + pub statement_digest: [u8; 32], +} + +impl Stage2AirProgramV1 { + pub fn from_prepared( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + pcs: &Stage2PcsInstanceV1, + ) -> Result { + let typed = Stage3TypedProofWitnessV1::from_prepared(prepared, fri)?; + Self::from_prepared_and_typed(prepared, fri, pcs, &typed) + } + + pub fn from_prepared_and_typed( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + pcs: &Stage2PcsInstanceV1, + typed: &Stage3TypedProofWitnessV1, + ) -> Result { + typed.ensure_profile(prepared.advice_profile())?; + let key = AiurVerifyingKey::from_bytes(prepared.verifying_key_bytes()) + .map_err(|error| anyhow::anyhow!("decode Aiur AIR key: {error}"))?; + if key.to_bytes() != prepared.verifying_key_bytes() { + bail!("Aiur AIR key is not canonically encoded"); + } + if fri_parameter_words(&key.fri_parameters()) != fri_parameter_words(fri) { + bail!("Stage 3 AIR lowering uses different FRI parameters"); + } + if key.commitment_parameters().cap_height != 0 { + bail!("Stage 3 AIR lowering currently requires cap height zero"); + } + + let metadata = key.air_circuit_metadata(); + if metadata.len() != typed.active.len() { + bail!("Aiur AIR metadata and activation lengths disagree"); + } + let active_indices: Vec<_> = typed + .active + .iter() + .enumerate() + .filter_map(|(index, &active)| active.then_some(index)) + .collect(); + if active_indices.len() != typed.log_degrees.len() + || active_indices.len() != typed.intermediate_accumulators.len() + { + bail!("Aiur AIR active-circuit vectors disagree"); + } + if typed.intermediate_accumulators.last() != Some(&[0, 0]) { + bail!("Aiur AIR lookup accumulator is not balanced"); + } + + validate_pcs_geometry(pcs, &metadata, &active_indices, typed)?; + + let seed_bytes = key.transcript_seed_and_shape_bytes().len(); + let activation_base = seed_bytes; + let preprocessed_bytes = key + .preprocessed_commitment_roots() + .as_ref() + .map_or(0, |roots| roots.len() * 32); + let stage_1_bytes = typed.commitments.stage_1_trace.len() * 32; + let log_degree_base = activation_base + .checked_add(typed.active.len() * 8) + .and_then(|offset| offset.checked_add(preprocessed_bytes)) + .and_then(|offset| offset.checked_add(stage_1_bytes)) + .ok_or_else(|| anyhow::anyhow!("AIR transcript offset overflow"))?; + let claims_base = log_degree_base + .checked_add(typed.log_degrees.len() * 8) + .ok_or_else(|| anyhow::anyhow!("AIR claim offset overflow"))?; + + let claim_words = prepared.statement().outer_claim_words().to_vec(); + if prepared.claims_bytes().len() != 16 + claim_words.len() * 8 { + bail!("Stage 2 recursive claim transport has the wrong length"); + } + let claim_bindings = (0..claim_words.len()) + .map(|word| { + Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Initial, + claims_base + 16 + word * 8, + ) + }) + .collect(); + let activation_bindings = (0..typed.active.len()) + .map(|circuit| { + Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Initial, + activation_base + circuit * 8, + ) + }) + .collect(); + + let active_circuits = active_indices + .iter() + .enumerate() + .map(|(position, &circuit_index)| Stage2ActiveAirCircuitV1 { + circuit_index, + log_degree: typed.log_degrees[position], + metadata: metadata[circuit_index].clone(), + log_degree_binding: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Initial, + log_degree_base + position * 8, + ), + accumulator_binding: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Stage2AndAccumulator, + typed.commitments.stage_2_trace.len() * 32 + position * 16, + ), + }) + .collect::>(); + + let statement_bytes = prepared.statement().to_bytes(); + if statement_bytes.len() != STAGE2_ROOT_STATEMENT_BYTES { + bail!("Stage 2 statement has the wrong length"); + } + let mut statement_prefix = [0u8; STAGE2_STATEMENT_PREFIX_BYTES]; + statement_prefix + .copy_from_slice(&statement_bytes[..STAGE2_STATEMENT_PREFIX_BYTES]); + + Ok(Self { + active: typed.active.clone(), + activation_bindings, + active_circuits, + claim_bindings, + statement_prefix, + statement_digest: prepared.statement().digest(), + }) + } +} + +fn validate_pcs_geometry( + pcs: &Stage2PcsInstanceV1, + metadata: &[AiurAirCircuitMetadata], + active_indices: &[usize], + typed: &Stage3TypedProofWitnessV1, +) -> Result<()> { + let expected_batches = + 3 + usize::from(typed.preprocessed_opened_values.is_some()); + if pcs.batches.len() != expected_batches { + bail!("AIR PCS batch count disagrees with the typed proof"); + } + for batch in &pcs.batches[..3] { + if batch.matrices.len() != active_indices.len() { + bail!("AIR PCS active-matrix count disagrees with the typed proof"); + } + } + for (position, &circuit_index) in active_indices.iter().enumerate() { + let circuit = &metadata[circuit_index]; + let expected = [ + (circuit.main_width, 2usize), + (circuit.stage_2_width, 2), + (circuit.quotient_degree * EXTENSION_DEGREE, 1), + ]; + for (batch, (width, points)) in expected.into_iter().enumerate() { + let matrix = &pcs.batches[batch].matrices[position]; + if matrix.width != width || matrix.opening_points.len() != points { + bail!("AIR PCS matrix geometry disagrees with the verifier key"); + } + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn constrain_stage2_air( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + blake3: SlotId, + equality: SlotId, + equality_zero: Wire, + window: SlotId, + data_zero: Wire, + one: Wire, + iv: [Wire; 2], + inputs: &mut Vec, + public: &mut Vec, + prefix_region: &TranscriptConstraintRegion, + prefix: &Stage2TranscriptReplayV1, + pcs: &Stage2PcsInstanceV1, + program: &Stage2AirProgramV1, +) -> Result<()> { + let challenges = prefix.challenges()?; + let lookup = prefix_region.challenges.lookup; + let fingerprint = prefix_region.challenges.fingerprint; + let alpha = prefix_region.challenges.constraint; + let zeta = prefix_region.challenges.zeta; + for wire in [lookup, fingerprint, alpha, zeta] { + arithmetic.assert_canonical(builder, wire); + } + + // Bind the specialised activation pattern and active trace heights to the + // exact words already consumed by Fiat--Shamir. + for (&active, &binding) in + program.active.iter().zip(&program.activation_bindings) + { + let observed = bound_low_word( + builder, + arithmetic, + window, + data_zero, + inputs, + public, + prefix_region, + binding, + ); + let expected = + record_fixed(builder, inputs, public, F128::new(u64::from(active), 0)); + assert_f128_equal(builder, equality, equality_zero, observed, expected); + } + for circuit in &program.active_circuits { + let observed = bound_low_word( + builder, + arithmetic, + window, + data_zero, + inputs, + public, + prefix_region, + circuit.log_degree_binding, + ); + let expected = record_fixed( + builder, + inputs, + public, + F128::new(u64::from(circuit.log_degree), 0), + ); + assert_f128_equal(builder, equality, equality_zero, observed, expected); + } + + let claim_wires: Vec<_> = program + .claim_bindings + .iter() + .map(|&binding| { + let wire = bound_low_word( + builder, + arithmetic, + window, + data_zero, + inputs, + public, + prefix_region, + binding, + ); + arithmetic.assert_canonical(builder, wire); + wire + }) + .collect(); + constrain_stage2_statement( + builder, + arithmetic, + blake3, + data_zero, + iv, + inputs, + public, + &claim_wires, + program, + )?; + + let native_lookup = native_extension(challenges.lookup); + let native_fingerprint = native_extension(challenges.fingerprint); + let mut native_message = ExtVal::ZERO; + let native_claim_words = program + .claim_bindings + .iter() + .map(|&binding| read_bound_u64(prefix, binding)) + .collect::>>()?; + for &word in native_claim_words.iter().rev() { + native_message = native_message * native_fingerprint + Val::from_u64(word); + } + native_message += native_lookup; + let native_inverse = native_message + .try_inverse() + .ok_or_else(|| anyhow::anyhow!("Stage 2 claim lookup message is zero"))?; + + let mut claim_fingerprint = data_zero; + for &word in claim_wires.iter().rev() { + let scaled = arithmetic.ext2_mul(builder, claim_fingerprint, fingerprint); + claim_fingerprint = arithmetic.add(builder, scaled, word); + } + let message = arithmetic.add(builder, lookup, claim_fingerprint); + let inverse = record_private( + builder, + inputs, + pack_extension(extension_words(native_inverse)), + ); + arithmetic.assert_canonical(builder, inverse); + let inverse_check = arithmetic.ext2_mul(builder, message, inverse); + assert_f128_equal(builder, equality, equality_zero, inverse_check, one); + let mut accumulator = inverse; + + let neg_one = + record_fixed(builder, inputs, public, F128::new(GOLDILOCKS_MODULUS - 1, 0)); + let seven = record_fixed(builder, inputs, public, F128::new(7, 0)); + let basis_u = record_fixed(builder, inputs, public, F128::new(0, 1)); + + for (position, circuit) in program.active_circuits.iter().enumerate() { + let next_accumulator = bound_transcript_extension( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + circuit.accumulator_binding, + 0, + ); + arithmetic.assert_canonical(builder, next_accumulator); + if position + 1 == program.active_circuits.len() { + assert_f128_equal( + builder, + equality, + equality_zero, + next_accumulator, + data_zero, + ); + } + + let lookup_coords = arithmetic.ext2_coordinates(builder, lookup); + let fingerprint_coords = arithmetic.ext2_coordinates(builder, fingerprint); + let accumulator_coords = arithmetic.ext2_coordinates(builder, accumulator); + let next_accumulator_coords = + arithmetic.ext2_coordinates(builder, next_accumulator); + let publics = [ + lookup_coords[0], + lookup_coords[1], + fingerprint_coords[0], + fingerprint_coords[1], + accumulator_coords[0], + accumulator_coords[1], + next_accumulator_coords[0], + next_accumulator_coords[1], + ]; + + let openings = bind_air_openings( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + pcs, + circuit, + position, + )?; + let selector_values = + native_selectors(challenges.zeta, circuit.log_degree)?; + let is_first = + record_private(builder, inputs, pack_extension(selector_values.is_first)); + let is_last = + record_private(builder, inputs, pack_extension(selector_values.is_last)); + let inv_vanishing = record_private( + builder, + inputs, + pack_extension(selector_values.inv_vanishing), + ); + for selector in [is_first, is_last, inv_vanishing] { + arithmetic.assert_canonical(builder, selector); + } + + let mut zeta_pow_n = zeta; + for _ in 0..circuit.log_degree { + zeta_pow_n = arithmetic.ext2_mul(builder, zeta_pow_n, zeta_pow_n); + } + let z_h = ext_sub(builder, arithmetic, neg_one, zeta_pow_n, one); + let inv_check = arithmetic.ext2_mul(builder, inv_vanishing, z_h); + assert_f128_equal(builder, equality, equality_zero, inv_check, one); + let zeta_minus_one = ext_sub(builder, arithmetic, neg_one, zeta, one); + let first_check = arithmetic.ext2_mul(builder, is_first, zeta_minus_one); + assert_f128_equal(builder, equality, equality_zero, first_check, z_h); + + let generator = Val::TWO_ADIC_GENERATORS[usize::from(circuit.log_degree)]; + let generator_inverse = generator.inverse(); + let generator_inverse_wire = record_fixed( + builder, + inputs, + public, + F128::new(generator_inverse.as_canonical_u64(), 0), + ); + let is_transition = + ext_sub(builder, arithmetic, neg_one, zeta, generator_inverse_wire); + let last_check = arithmetic.ext2_mul(builder, is_last, is_transition); + assert_f128_equal(builder, equality, equality_zero, last_check, z_h); + + let n = Val::from_u64(1u64 << circuit.log_degree); + let injection_scale = (n * generator).inverse(); + let injection_scale = record_fixed( + builder, + inputs, + public, + F128::new(injection_scale.as_canonical_u64(), 0), + ); + let delta_scaled = std::array::from_fn(|coordinate| { + let delta = ext_sub( + builder, + arithmetic, + neg_one, + next_accumulator_coords[coordinate], + accumulator_coords[coordinate], + ); + arithmetic.ext2_mul(builder, delta, injection_scale) + }); + + let node_values = constrain_graph( + builder, + arithmetic, + neg_one, + inputs, + public, + circuit, + &openings, + &publics, + is_first, + is_last, + is_transition, + )?; + let mut constraints: Vec<_> = circuit + .metadata + .graph + .zeros + .iter() + .map(|root| node_values[root.index()]) + .collect(); + constraints.extend(constrain_logup( + builder, + arithmetic, + neg_one, + seven, + data_zero, + one, + &circuit.metadata.graph.lookups, + circuit.metadata.lookup_group_size, + &node_values, + &openings.stage2[0], + &openings.stage2[1], + &publics, + &delta_scaled, + is_last, + inputs, + public, + )); + + let mut composition = data_zero; + for constraint in constraints { + let scaled = arithmetic.ext2_mul(builder, composition, alpha); + composition = arithmetic.add(builder, scaled, constraint); + } + + let mut quotient = data_zero; + let mut power = one; + for chunk in openings.quotient.as_chunks::().0 { + let high = arithmetic.ext2_mul(builder, chunk[1], basis_u); + let coefficient = arithmetic.add(builder, chunk[0], high); + let term = arithmetic.ext2_mul(builder, power, coefficient); + quotient = arithmetic.add(builder, quotient, term); + power = arithmetic.ext2_mul(builder, power, zeta_pow_n); + } + let ood = arithmetic.ext2_mul(builder, composition, inv_vanishing); + assert_f128_equal(builder, equality, equality_zero, ood, quotient); + accumulator = next_accumulator; + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn constrain_stage2_statement( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + blake3: SlotId, + data_zero: Wire, + iv: [Wire; 2], + inputs: &mut Vec, + public: &mut Vec, + claims: &[Wire], + program: &Stage2AirProgramV1, +) -> Result<()> { + if claims.len() != 18 { + bail!("Stage 2 statement binding requires 18 claim words"); + } + let mut message = program + .statement_prefix + .as_chunks::<16>() + .0 + .iter() + .map(|word| record_fixed(builder, inputs, public, pack_bytes(word))) + .collect::>(); + for pair in claims.as_chunks::<2>().0 { + let high_lanes = builder.gate(arithmetic.repack, &[pair[1], data_zero]); + let packed = builder.gate(arithmetic.repack, &[pair[0], high_lanes[0]])[3]; + message.push(packed); + } + let trace = hash_trace(STAGE2_ROOT_STATEMENT_BYTES); + let parameters = trace + .rows + .iter() + .map(|&(_cv, _message, counter, block_len, flags)| { + record_fixed( + builder, + inputs, + public, + crate::binding::pack_params(counter, block_len, flags), + ) + }) + .collect::>(); + let root = constrain_hash( + builder, + blake3, + &trace, + ¶meters, + iv, + data_zero, + &message, + )?; + builder.publish(root[0]); + builder.publish(root[1]); + public.extend_from_slice(&[ + pack_bytes(&program.statement_digest[..16]), + pack_bytes(&program.statement_digest[16..]), + ]); + Ok(()) +} + +struct BoundAirOpenings { + preprocessed: [Vec; 2], + main: [Vec; 2], + stage2: [Vec; 2], + quotient: Vec, +} + +#[allow(clippy::too_many_arguments)] +fn bind_air_openings( + builder: &mut impl CircuitEmitter, + window: SlotId, + data_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + prefix_region: &TranscriptConstraintRegion, + pcs: &Stage2PcsInstanceV1, + circuit: &Stage2ActiveAirCircuitV1, + position: usize, +) -> Result { + let main = bind_matrix( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + pcs, + 0, + position, + )?; + let stage2 = bind_matrix( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + pcs, + 1, + position, + )?; + let quotient = bind_matrix( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + pcs, + 2, + position, + )?; + if main.len() != 2 || stage2.len() != 2 || quotient.len() != 1 { + bail!("AIR opening-point geometry is invalid"); + } + let preprocessed = if let Some(slot) = circuit.metadata.preprocessed_slot { + let values = bind_matrix( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + pcs, + 3, + slot, + )?; + if values.len() != 2 { + bail!("active AIR preprocessed matrix has the wrong opening count"); + } + [values[0].clone(), values[1].clone()] + } else { + [Vec::new(), Vec::new()] + }; + Ok(BoundAirOpenings { + preprocessed, + main: [main[0].clone(), main[1].clone()], + stage2: [stage2[0].clone(), stage2[1].clone()], + quotient: quotient[0].clone(), + }) +} + +#[allow(clippy::too_many_arguments)] +fn bind_matrix( + builder: &mut impl CircuitEmitter, + window: SlotId, + data_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + prefix_region: &TranscriptConstraintRegion, + pcs: &Stage2PcsInstanceV1, + batch: usize, + matrix: usize, +) -> Result>> { + let matrix = pcs + .batches + .get(batch) + .and_then(|batch| batch.matrices.get(matrix)) + .ok_or_else(|| anyhow::anyhow!("AIR PCS matrix is missing"))?; + Ok( + (0..matrix.opening_points.len()) + .map(|point| { + (0..matrix.width) + .map(|column| { + bound_transcript_extension( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + matrix.opened_values, + point * matrix.width + column, + ) + }) + .collect() + }) + .collect(), + ) +} + +#[allow(clippy::too_many_arguments)] +fn constrain_graph( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + neg_one: Wire, + inputs: &mut Vec, + public: &mut Vec, + circuit: &Stage2ActiveAirCircuitV1, + openings: &BoundAirOpenings, + publics: &[Wire; 8], + is_first: Wire, + is_last: Wire, + is_transition: Wire, +) -> Result> { + let mut values = Vec::with_capacity(circuit.metadata.graph.nodes.len()); + for node in &circuit.metadata.graph.nodes { + let value = match *node { + Node::Const(value) => record_fixed( + builder, + inputs, + public, + F128::new(value.as_canonical_u64(), 0), + ), + Node::Var(column) => { + let rows = match column.source { + Source::Preprocessed => &openings.preprocessed, + Source::Main => &openings.main, + Source::Stage2 => &openings.stage2, + }; + let row = match column.offset { + RowOffset::Current => 0, + RowOffset::Next => 1, + }; + *rows[row] + .get(usize::try_from(column.index).unwrap()) + .ok_or_else(|| anyhow::anyhow!("AIR graph column is out of range"))? + }, + Node::Public(index) => *publics + .get(usize::try_from(index).unwrap()) + .ok_or_else(|| anyhow::anyhow!("AIR graph public is out of range"))?, + Node::IsFirstRow => is_first, + Node::IsLastRow => is_last, + Node::IsTransition => is_transition, + Node::Add(left, right) => { + arithmetic.add(builder, values[left.index()], values[right.index()]) + }, + Node::Sub(left, right) => ext_sub( + builder, + arithmetic, + neg_one, + values[left.index()], + values[right.index()], + ), + Node::Mul(left, right) => arithmetic.ext2_mul( + builder, + values[left.index()], + values[right.index()], + ), + Node::Neg(value) => { + arithmetic.ext2_mul(builder, values[value.index()], neg_one) + }, + }; + values.push(value); + } + Ok(values) +} + +#[allow(clippy::too_many_arguments)] +fn constrain_logup( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + neg_one: Wire, + seven: Wire, + zero: Wire, + one: Wire, + lookups: &[Lookup], + group_size: usize, + node_values: &[Wire], + stage2: &[Wire], + stage2_next: &[Wire], + publics: &[Wire; 8], + delta_scaled: &[Wire; 2], + is_last: Wire, + inputs: &mut Vec, + public_inputs: &mut Vec, +) -> Vec { + let beta = [publics[0], publics[1]]; + let gamma = [publics[2], publics[3]]; + let injection = [ + arithmetic.ext2_mul(builder, is_last, delta_scaled[0]), + arithmetic.ext2_mul(builder, is_last, delta_scaled[1]), + ]; + if lookups.is_empty() { + return (0..EXTENSION_DEGREE) + .map(|coordinate| { + let difference = ext_sub( + builder, + arithmetic, + neg_one, + stage2_next[coordinate], + stage2[coordinate], + ); + arithmetic.add(builder, difference, injection[coordinate]) + }) + .collect(); + } + + let group_size = group_size.max(1); + let last_group = lookups.len().div_ceil(group_size) - 1; + let mut constraints = Vec::new(); + for (group, chunk) in lookups.chunks(group_size).enumerate() { + let source = [stage2[2 * group], stage2[2 * group + 1]]; + let target = if group < last_group { + [stage2[2 * group + 2], stage2[2 * group + 3]] + } else { + [ + arithmetic.add(builder, stage2_next[0], injection[0]), + arithmetic.add(builder, stage2_next[1], injection[1]), + ] + }; + let difference = [ + ext_sub(builder, arithmetic, neg_one, target[0], source[0]), + ext_sub(builder, arithmetic, neg_one, target[1], source[1]), + ]; + let messages: Vec<_> = chunk + .iter() + .map(|lookup| { + let seed = + record_fixed(builder, inputs, public_inputs, F128::new(0, 0)); + let mut fingerprint = [seed, zero]; + for &argument in lookup.args.iter().rev() { + fingerprint = + coord_mul(builder, arithmetic, seven, fingerprint, gamma); + fingerprint[0] = arithmetic.add( + builder, + fingerprint[0], + node_values[argument.index()], + ); + } + [ + arithmetic.add(builder, fingerprint[0], beta[0]), + arithmetic.add(builder, fingerprint[1], beta[1]), + ] + }) + .collect(); + let mut product = [one, zero]; + for &message in &messages { + product = coord_mul(builder, arithmetic, seven, product, message); + } + let lhs = coord_mul(builder, arithmetic, seven, product, difference); + let mut rhs = [zero, zero]; + for (excluded, lookup) in chunk.iter().enumerate() { + let mut others = [one, zero]; + for (index, &message) in messages.iter().enumerate() { + if index != excluded { + others = coord_mul(builder, arithmetic, seven, others, message); + } + } + for coordinate in 0..EXTENSION_DEGREE { + let term = arithmetic.ext2_mul( + builder, + others[coordinate], + node_values[lookup.multiplicity.index()], + ); + rhs[coordinate] = arithmetic.add(builder, rhs[coordinate], term); + } + } + constraints.extend((0..EXTENSION_DEGREE).map(|coordinate| { + ext_sub(builder, arithmetic, neg_one, lhs[coordinate], rhs[coordinate]) + })); + } + constraints +} + +fn coord_mul( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + seven: Wire, + left: [Wire; 2], + right: [Wire; 2], +) -> [Wire; 2] { + let low = arithmetic.ext2_mul(builder, left[0], right[0]); + let high_product = arithmetic.ext2_mul(builder, left[1], right[1]); + let reduced_high = arithmetic.ext2_mul(builder, high_product, seven); + let cross_0 = arithmetic.ext2_mul(builder, left[0], right[1]); + let cross_1 = arithmetic.ext2_mul(builder, left[1], right[0]); + [ + arithmetic.add(builder, low, reduced_high), + arithmetic.add(builder, cross_0, cross_1), + ] +} + +fn ext_sub( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + neg_one: Wire, + left: Wire, + right: Wire, +) -> Wire { + let negated = arithmetic.ext2_mul(builder, right, neg_one); + arithmetic.add(builder, left, negated) +} + +#[allow(clippy::too_many_arguments)] +fn bound_low_word( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + window: SlotId, + data_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + prefix_region: &TranscriptConstraintRegion, + binding: Stage2TranscriptByteBindingV1, +) -> Wire { + let word = bound_transcript_window( + builder, + window, + data_zero, + inputs, + public, + prefix_region, + binding, + 0, + ); + arithmetic.embed_low_lane(builder, word) +} + +fn record_private( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + value: F128, +) -> Wire { + inputs.push(value); + builder.input() +} + +struct NativeSelectors { + is_first: [u64; 2], + is_last: [u64; 2], + inv_vanishing: [u64; 2], +} + +fn native_selectors(zeta: [u64; 2], log_degree: u8) -> Result { + let zeta = native_extension(zeta); + let z_h = zeta.exp_power_of_2(usize::from(log_degree)) - ExtVal::ONE; + let generator = Val::TWO_ADIC_GENERATORS[usize::from(log_degree)]; + let generator_inverse = ExtVal::from(generator.inverse()); + let is_first = z_h + * (zeta - ExtVal::ONE) + .try_inverse() + .ok_or_else(|| anyhow::anyhow!("OOD point is the first trace point"))?; + let is_last = z_h + * (zeta - generator_inverse) + .try_inverse() + .ok_or_else(|| anyhow::anyhow!("OOD point is the last trace point"))?; + let inv_vanishing = z_h + .try_inverse() + .ok_or_else(|| anyhow::anyhow!("OOD point is inside the trace domain"))?; + Ok(NativeSelectors { + is_first: extension_words(is_first), + is_last: extension_words(is_last), + inv_vanishing: extension_words(inv_vanishing), + }) +} + +fn native_extension(value: [u64; 2]) -> ExtVal { + ExtVal::new([Val::from_u64(value[0]), Val::from_u64(value[1])]) +} + +fn extension_words(value: ExtVal) -> [u64; 2] { + let values: &[Val] = value.as_basis_coefficients_slice(); + [values[0].as_canonical_u64(), values[1].as_canonical_u64()] +} + +fn pack_extension(value: [u64; 2]) -> F128 { + F128::new(value[0], value[1]) +} + +fn read_bound_u64( + prefix: &Stage2TranscriptReplayV1, + binding: Stage2TranscriptByteBindingV1, +) -> Result { + let segment = match binding.segment { + Stage2TranscriptSegmentV1::Initial => &prefix.initial_observations, + Stage2TranscriptSegmentV1::Stage2AndAccumulator => { + &prefix.stage2_and_accumulator_observations + }, + Stage2TranscriptSegmentV1::QuotientCommitment => { + &prefix.quotient_commitment_observations + }, + Stage2TranscriptSegmentV1::PcsOpening => &prefix.pcs_opening_observations, + }; + let bytes = segment + .get(binding.byte_offset..binding.byte_offset + 8) + .ok_or_else(|| anyhow::anyhow!("AIR transcript word is out of range"))?; + Ok(u64::from_le_bytes(bytes.try_into().unwrap())) +} diff --git a/flock-stage3/host/src/arithmetic.rs b/flock-stage3/host/src/arithmetic.rs new file mode 100644 index 00000000..36a6502a --- /dev/null +++ b/flock-stage3/host/src/arithmetic.rs @@ -0,0 +1,677 @@ +//! Real Flock circuit proof for the custom Goldilocks arithmetic tables. +//! +//! This remains a labelled conformance artifact, not a Stage 3 proof. It +//! exists to ensure new non-native field tables survive the complete union, +//! wiring, PCS, Fiat-Shamir, serialization, and verifier path before verifier +//! phases depend on them. + +use anyhow::{Context, Result, bail}; +use bincode::Options; +use flock_prover::{ + challenger::FsChallenger, + circuit::builder::{CircuitShape, ShapeBuilder, SlotId}, + field::F128, + pcs::Commitment, + proof::R1csProofCircuitMerged, + prover::{self, UnionSlotProverInput}, + union::UnionInstance, + verifier, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + ARITHMETIC_CONFORMANCE_TRANSCRIPT_DOMAIN, FlockConfigV1, + binding::pcs_params, + extension::{ + GoldilocksCircuitSlots, GoldilocksLaneRepackGate, build_lane_repack_r1cs, + generate_lane_repack_witness, goldilocks_ext2_mul, + }, + goldilocks::{ + CanonicalGoldilocksQuadGate, GOLDILOCKS_MODULUS, GoldilocksAddPairGate, + build_canonical_quad_r1cs, build_goldilocks_add_r1cs, + generate_canonical_quad_witness, generate_goldilocks_add_witness, + }, + multiplication::{ + GoldilocksMulPairGate, build_goldilocks_mul_r1cs, + generate_goldilocks_mul_witness, + }, +}; + +pub const ARITHMETIC_CONFORMANCE_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLKGA1"; +const ARTIFACT_VERSION: u16 = 1; +const FIXED_PREFIX_BYTES: usize = 8 + 2 + 32 + 2 + 2 + 2; +const FIXED_SUFFIX_BYTES: usize = 32 + 8; +const OPERAND_BYTES: usize = 4 * 8; +const MAX_ADDITIONS: usize = 64; +const MAX_MULTIPLICATIONS: usize = 64; +const MAX_EXTENSION_MULTIPLICATIONS: usize = 16; +const MAX_BUNDLE_BYTES: usize = 64 * 1024 * 1024; +// The shared row domain leaves ample virtual address space for the largest +// custom table and always reaches Flock's audited Fast128 geometries. +// Declared row counts remain exact; this only supplies zero padding. +const MIN_SECURE_NU: usize = 10; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GoldilocksAddPairV1 { + pub left: [u64; 2], + pub right: [u64; 2], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GoldilocksMulPairV1 { + pub left: [u64; 2], + pub right: [u64; 2], +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GoldilocksExt2MulV1 { + pub left: [u64; 2], + pub right: [u64; 2], +} + +impl GoldilocksExt2MulV1 { + pub fn result(self) -> [u64; 2] { + let result = goldilocks_ext2_mul( + F128::new(self.left[0], self.left[1]), + F128::new(self.right[0], self.right[1]), + ); + [result.lo, result.hi] + } +} + +/// A real Flock proof of public lane-wise Goldilocks arithmetic. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArithmeticConformanceArtifactV1 { + additions: Vec, + multiplications: Vec, + extension_multiplications: Vec, + circuit_digest: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl ArithmeticConformanceArtifactV1 { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity( + FIXED_PREFIX_BYTES + + (self.additions.len() + + self.multiplications.len() + + self.extension_multiplications.len()) + * OPERAND_BYTES + + FIXED_SUFFIX_BYTES + + self.proof_bundle_bytes.len(), + ); + bytes.extend_from_slice(ARITHMETIC_CONFORMANCE_ARTIFACT_MAGIC); + bytes.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.extend_from_slice( + &u16::try_from(self.additions.len()) + .expect("addition count") + .to_le_bytes(), + ); + bytes.extend_from_slice( + &u16::try_from(self.multiplications.len()) + .expect("multiplication count") + .to_le_bytes(), + ); + bytes.extend_from_slice( + &u16::try_from(self.extension_multiplications.len()) + .expect("extension multiplication count") + .to_le_bytes(), + ); + for addition in &self.additions { + encode_operands(&mut bytes, addition.left, addition.right); + } + for multiplication in &self.multiplications { + encode_operands(&mut bytes, multiplication.left, multiplication.right); + } + for multiplication in &self.extension_multiplications { + encode_operands(&mut bytes, multiplication.left, multiplication.right); + } + bytes.extend_from_slice(&self.circuit_digest); + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < FIXED_PREFIX_BYTES + FIXED_SUFFIX_BYTES { + bail!("truncated Flock arithmetic conformance artifact"); + } + if &bytes[..8] != ARITHMETIC_CONFORMANCE_ARTIFACT_MAGIC { + bail!("invalid Flock arithmetic conformance artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != ARTIFACT_VERSION { + bail!("unsupported Flock arithmetic artifact version {version}"); + } + if bytes[10..42] != FlockConfigV1.digest() { + bail!("Flock arithmetic artifact configuration mismatch"); + } + let addition_count = + usize::from(u16::from_le_bytes(bytes[42..44].try_into().unwrap())); + let multiplication_count = + usize::from(u16::from_le_bytes(bytes[44..46].try_into().unwrap())); + let extension_multiplication_count = + usize::from(u16::from_le_bytes(bytes[46..48].try_into().unwrap())); + validate_counts( + addition_count, + multiplication_count, + extension_multiplication_count, + )?; + let operation_count = + addition_count + multiplication_count + extension_multiplication_count; + let operands_end = FIXED_PREFIX_BYTES + .checked_add(operation_count * OPERAND_BYTES) + .ok_or_else(|| anyhow::anyhow!("arithmetic operand length overflow"))?; + let suffix_end = operands_end + .checked_add(FIXED_SUFFIX_BYTES) + .ok_or_else(|| anyhow::anyhow!("arithmetic artifact length overflow"))?; + if bytes.len() < suffix_end { + bail!("truncated Flock arithmetic operands or proof header"); + } + let mut additions = Vec::with_capacity(addition_count); + let mut multiplications = Vec::with_capacity(multiplication_count); + let mut extension_multiplications = + Vec::with_capacity(extension_multiplication_count); + let (encoded_operations, remainder) = + bytes[FIXED_PREFIX_BYTES..operands_end].as_chunks::(); + debug_assert!(remainder.is_empty()); + for encoded in &encoded_operations[..addition_count] { + let (left, right) = decode_operands(encoded); + let addition = GoldilocksAddPairV1 { left, right }; + validate_operands(addition.left, addition.right)?; + additions.push(addition); + } + let multiplication_end = addition_count + multiplication_count; + for encoded in &encoded_operations[addition_count..multiplication_end] { + let (left, right) = decode_operands(encoded); + let multiplication = GoldilocksMulPairV1 { left, right }; + validate_operands(multiplication.left, multiplication.right)?; + multiplications.push(multiplication); + } + for encoded in &encoded_operations[multiplication_end..] { + let (left, right) = decode_operands(encoded); + let multiplication = GoldilocksExt2MulV1 { left, right }; + validate_operands(multiplication.left, multiplication.right)?; + extension_multiplications.push(multiplication); + } + let mut circuit_digest = [0u8; 32]; + circuit_digest.copy_from_slice(&bytes[operands_end..operands_end + 32]); + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[operands_end + 32..suffix_end].try_into().unwrap(), + )) + .map_err(|error| { + anyhow::anyhow!("proof bundle length does not fit usize: {error}") + })?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock arithmetic proof bundle length {bundle_len}"); + } + let expected_len = suffix_end + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("arithmetic proof length overflow"))?; + if bytes.len() != expected_len { + bail!( + "Flock arithmetic artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let proof_bundle_bytes = bytes[suffix_end..].to_vec(); + decode_bundle(&proof_bundle_bytes) + .context("decode Flock arithmetic conformance proof bundle")?; + Ok(Self { + additions, + multiplications, + extension_multiplications, + circuit_digest, + proof_bundle_bytes, + }) + } + + pub fn additions(&self) -> &[GoldilocksAddPairV1] { + &self.additions + } + + pub fn multiplications(&self) -> &[GoldilocksMulPairV1] { + &self.multiplications + } + + pub fn extension_multiplications(&self) -> &[GoldilocksExt2MulV1] { + &self.extension_multiplications + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +#[derive(Serialize, Deserialize)] +struct ArithmeticProofBundle { + commitment: Commitment, + proof: R1csProofCircuitMerged, +} + +pub fn prove_arithmetic_conformance( + additions: &[GoldilocksAddPairV1], + multiplications: &[GoldilocksMulPairV1], + extension_multiplications: &[GoldilocksExt2MulV1], +) -> Result { + validate_operations(additions, multiplications, extension_multiplications)?; + let relation = ArithmeticRelation::build( + additions.len(), + multiplications.len(), + extension_multiplications.len(), + )?; + let inputs = + relation_inputs(additions, multiplications, extension_multiplications); + let witness = relation.shape.run(&inputs, &[]); + relation.ensure_registry_order()?; + + let add_rows = witness.rows::(relation.add_slot); + let mul_rows = witness.rows::(relation.mul_slot); + let repack_rows = + witness.rows::(relation.repack_slot); + let canonical_rows = + witness.rows::(relation.canonical_slot); + let add_r1cs = build_goldilocks_add_r1cs(relation.nu); + let add_lincheck = add_r1cs.csc_lincheck_circuit(); + let mul_r1cs = build_goldilocks_mul_r1cs(relation.nu); + let mul_lincheck = mul_r1cs.csc_lincheck_circuit(); + let repack_r1cs = build_lane_repack_r1cs(relation.nu); + let repack_lincheck = repack_r1cs.csc_lincheck_circuit(); + let canonical_r1cs = build_canonical_quad_r1cs(relation.nu); + let canonical_lincheck = canonical_r1cs.csc_lincheck_circuit(); + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = + FsChallenger::with_chained_blake3(ARITHMETIC_CONFORMANCE_TRANSCRIPT_DOMAIN); + let (proof, commitment, _) = prover::prove_fast_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &witness.public, + ¶ms, + vec![ + UnionSlotProverInput::new( + generate_goldilocks_mul_witness(mul_rows, relation.nu), + mul_lincheck, + ), + UnionSlotProverInput::new( + generate_goldilocks_add_witness(add_rows, relation.nu), + add_lincheck, + ), + UnionSlotProverInput::new( + generate_lane_repack_witness(repack_rows, relation.nu), + repack_lincheck, + ), + UnionSlotProverInput::new( + generate_canonical_quad_witness(canonical_rows, relation.nu), + canonical_lincheck, + ), + ], + Vec::new(), + &mut challenger, + ); + let proof_bundle_bytes = + encode_bundle(&ArithmeticProofBundle { commitment, proof })?; + if proof_bundle_bytes.len() > MAX_BUNDLE_BYTES { + bail!("Flock arithmetic proof bundle exceeds {MAX_BUNDLE_BYTES} bytes"); + } + Ok(ArithmeticConformanceArtifactV1 { + additions: additions.to_vec(), + multiplications: multiplications.to_vec(), + extension_multiplications: extension_multiplications.to_vec(), + circuit_digest: relation.shape.circuit.digest(), + proof_bundle_bytes, + }) +} + +pub fn verify_arithmetic_conformance( + artifact: &ArithmeticConformanceArtifactV1, +) -> Result<()> { + validate_operations( + &artifact.additions, + &artifact.multiplications, + &artifact.extension_multiplications, + )?; + let relation = ArithmeticRelation::build( + artifact.additions.len(), + artifact.multiplications.len(), + artifact.extension_multiplications.len(), + )?; + relation.ensure_registry_order()?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Flock arithmetic conformance circuit digest mismatch"); + } + let witness = relation.shape.run( + &relation_inputs( + &artifact.additions, + &artifact.multiplications, + &artifact.extension_multiplications, + ), + &[], + ); + let bundle = decode_bundle(&artifact.proof_bundle_bytes) + .context("decode Flock arithmetic conformance proof bundle")?; + let add_r1cs = build_goldilocks_add_r1cs(relation.nu); + let add_lincheck = add_r1cs.csc_lincheck_circuit(); + let mul_r1cs = build_goldilocks_mul_r1cs(relation.nu); + let mul_lincheck = mul_r1cs.csc_lincheck_circuit(); + let repack_r1cs = build_lane_repack_r1cs(relation.nu); + let repack_lincheck = repack_r1cs.csc_lincheck_circuit(); + let canonical_r1cs = build_canonical_quad_r1cs(relation.nu); + let canonical_lincheck = canonical_r1cs.csc_lincheck_circuit(); + let linchecks: [&dyn flock_prover::lincheck::LincheckCircuit; 4] = + [mul_lincheck, add_lincheck, repack_lincheck, canonical_lincheck]; + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = + FsChallenger::with_chained_blake3(ARITHMETIC_CONFORMANCE_TRANSCRIPT_DOMAIN); + verifier::verify_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &witness.public, + &linchecks, + &bundle.commitment, + &bundle.proof, + ¶ms, + &mut challenger, + ) + .map_err(|error| { + anyhow::anyhow!("Flock arithmetic conformance proof rejected: {error:?}") + })?; + Ok(()) +} + +struct ArithmeticRelation { + shape: CircuitShape, + add_slot: SlotId, + mul_slot: SlotId, + canonical_slot: SlotId, + repack_slot: SlotId, + nu: usize, +} + +impl ArithmeticRelation { + fn build( + addition_count: usize, + multiplication_count: usize, + extension_multiplication_count: usize, + ) -> Result { + validate_counts( + addition_count, + multiplication_count, + extension_multiplication_count, + )?; + let row_bound = [ + addition_count + 5 * extension_multiplication_count, + multiplication_count + 2 * extension_multiplication_count, + 3 * addition_count + + 3 * multiplication_count + + 9 * extension_multiplication_count, + 3 * extension_multiplication_count, + ] + .into_iter() + .max() + .unwrap(); + let nu = usize::try_from(row_bound.next_power_of_two().ilog2()) + .unwrap() + .max(MIN_SECURE_NU); + let mut builder = ShapeBuilder::new(nu); + let slots = GoldilocksCircuitSlots::declare(&mut builder, nu); + for _ in 0..addition_count { + let left = builder.public_input(); + let right = builder.public_input(); + for value in [left, right] { + slots.assert_canonical(&mut builder, value); + } + let result = slots.add(&mut builder, left, right); + builder.publish(result); + } + for _ in 0..multiplication_count { + let left = builder.public_input(); + let right = builder.public_input(); + for value in [left, right] { + slots.assert_canonical(&mut builder, value); + } + let result = slots.mul(&mut builder, left, right); + builder.publish(result); + } + for _ in 0..extension_multiplication_count { + let left = builder.public_input(); + let right = builder.public_input(); + let result = slots.ext2_mul(&mut builder, left, right); + builder.publish(result); + } + slots.finish_canonical(&mut builder); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock arithmetic conformance circuit: {error:?}") + })?; + Ok(Self { + shape, + add_slot: slots.add, + mul_slot: slots.mul, + canonical_slot: slots.canonical, + repack_slot: slots.repack, + nu, + }) + } + + fn ensure_registry_order(&self) -> Result<()> { + // Registry::new sorts Boolean tables by descending k_log. + if self.shape.registry_slot(self.mul_slot) != 0 + || self.shape.registry_slot(self.add_slot) != 1 + || self.shape.registry_slot(self.repack_slot) != 2 + || self.shape.registry_slot(self.canonical_slot) != 3 + { + bail!("unexpected Flock arithmetic table registry order"); + } + Ok(()) + } +} + +fn relation_inputs( + additions: &[GoldilocksAddPairV1], + multiplications: &[GoldilocksMulPairV1], + extension_multiplications: &[GoldilocksExt2MulV1], +) -> Vec { + let mut inputs = Vec::with_capacity( + 1 + 2 + * (additions.len() + + multiplications.len() + + extension_multiplications.len()), + ); + inputs.push(F128::ZERO); + for addition in additions { + inputs.push(F128::new(addition.left[0], addition.left[1])); + inputs.push(F128::new(addition.right[0], addition.right[1])); + } + for multiplication in multiplications { + inputs.push(F128::new(multiplication.left[0], multiplication.left[1])); + inputs.push(F128::new(multiplication.right[0], multiplication.right[1])); + } + for multiplication in extension_multiplications { + inputs.push(F128::new(multiplication.left[0], multiplication.left[1])); + inputs.push(F128::new(multiplication.right[0], multiplication.right[1])); + } + inputs +} + +fn validate_operations( + additions: &[GoldilocksAddPairV1], + multiplications: &[GoldilocksMulPairV1], + extension_multiplications: &[GoldilocksExt2MulV1], +) -> Result<()> { + validate_counts( + additions.len(), + multiplications.len(), + extension_multiplications.len(), + )?; + for addition in additions { + validate_operands(addition.left, addition.right)?; + } + for multiplication in multiplications { + validate_operands(multiplication.left, multiplication.right)?; + } + for multiplication in extension_multiplications { + validate_operands(multiplication.left, multiplication.right)?; + } + Ok(()) +} + +fn validate_counts( + addition_count: usize, + multiplication_count: usize, + extension_multiplication_count: usize, +) -> Result<()> { + if addition_count > MAX_ADDITIONS { + bail!( + "Flock arithmetic conformance has {addition_count} additions; maximum is {MAX_ADDITIONS}" + ); + } + if multiplication_count > MAX_MULTIPLICATIONS { + bail!( + "Flock arithmetic conformance has {multiplication_count} multiplications; maximum is {MAX_MULTIPLICATIONS}" + ); + } + if extension_multiplication_count > MAX_EXTENSION_MULTIPLICATIONS { + bail!( + "Flock arithmetic conformance has {extension_multiplication_count} extension multiplications; maximum is {MAX_EXTENSION_MULTIPLICATIONS}" + ); + } + if addition_count + multiplication_count + extension_multiplication_count == 0 + { + bail!("Flock arithmetic conformance requires at least one operation"); + } + Ok(()) +} + +fn validate_operands(left: [u64; 2], right: [u64; 2]) -> Result<()> { + if left.iter().chain(&right).any(|&word| word >= GOLDILOCKS_MODULUS) { + bail!("Flock arithmetic operand is not canonical Goldilocks"); + } + Ok(()) +} + +fn encode_operands(bytes: &mut Vec, left: [u64; 2], right: [u64; 2]) { + for word in [left[0], left[1], right[0], right[1]] { + bytes.extend_from_slice(&word.to_le_bytes()); + } +} + +fn decode_operands(encoded: &[u8; OPERAND_BYTES]) -> ([u64; 2], [u64; 2]) { + let words: [u64; 4] = std::array::from_fn(|index| { + let offset = index * 8; + u64::from_le_bytes(encoded[offset..offset + 8].try_into().unwrap()) + }); + ([words[0], words[1]], [words[2], words[3]]) +} + +fn encode_bundle(bundle: &ArithmeticProofBundle) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .serialize(bundle) + .context("encode Flock arithmetic conformance proof bundle") +} + +fn decode_bundle(bytes: &[u8]) -> Result { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(MAX_BUNDLE_BYTES as u64) + .reject_trailing_bytes() + .deserialize(bytes) + .context("invalid Flock arithmetic conformance proof bundle") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn addition_fixture() -> Vec { + (0..8u64) + .map(|index| GoldilocksAddPairV1 { + left: [index, GOLDILOCKS_MODULUS - 1 - index], + right: [GOLDILOCKS_MODULUS - 1 - index, index + 1], + }) + .collect() + } + + fn multiplication_fixture() -> Vec { + (0..4u64) + .map(|index| GoldilocksMulPairV1 { + left: [index + 1, GOLDILOCKS_MODULUS - 1 - index], + right: [GOLDILOCKS_MODULUS - 2 - index, index + 3], + }) + .collect() + } + + fn extension_multiplication_fixture() -> Vec { + vec![ + GoldilocksExt2MulV1 { left: [3, 5], right: [7, 11] }, + GoldilocksExt2MulV1 { + left: [GOLDILOCKS_MODULUS - 1, 17], + right: [23, GOLDILOCKS_MODULUS - 2], + }, + ] + } + + #[test] + fn artifact_parser_is_strict_before_crypto() { + let artifact = ArithmeticConformanceArtifactV1 { + additions: addition_fixture(), + multiplications: multiplication_fixture(), + extension_multiplications: extension_multiplication_fixture(), + circuit_digest: [7; 32], + proof_bundle_bytes: vec![1, 2, 3], + }; + let mut bytes = artifact.to_bytes(); + assert!(ArithmeticConformanceArtifactV1::from_bytes(&bytes).is_err()); + bytes[0] ^= 1; + assert!(ArithmeticConformanceArtifactV1::from_bytes(&bytes).is_err()); + } + + #[test] + #[ignore = "real Flock arithmetic circuit proof; run explicitly"] + fn real_goldilocks_arithmetic_round_trip_and_mutations() { + let artifact = prove_arithmetic_conformance( + &addition_fixture(), + &multiplication_fixture(), + &extension_multiplication_fixture(), + ) + .expect("prove arithmetic"); + eprintln!( + "Flock Goldilocks-arithmetic conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_arithmetic_conformance(&artifact).expect("verify arithmetic"); + + let bytes = artifact.to_bytes(); + let decoded = ArithmeticConformanceArtifactV1::from_bytes(&bytes).unwrap(); + verify_arithmetic_conformance(&decoded).expect("verify decoded arithmetic"); + + let mut wrong_operand = decoded.clone(); + wrong_operand.additions[0].left[0] ^= 1; + assert!(verify_arithmetic_conformance(&wrong_operand).is_err()); + + let mut wrong_multiplication = decoded.clone(); + wrong_multiplication.multiplications[0].right[1] ^= 1; + assert!(verify_arithmetic_conformance(&wrong_multiplication).is_err()); + + let mut wrong_extension = decoded.clone(); + wrong_extension.extension_multiplications[0].left[1] ^= 1; + assert!(verify_arithmetic_conformance(&wrong_extension).is_err()); + + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_arithmetic_conformance(&wrong_proof).is_err()); + } +} diff --git a/flock-stage3/host/src/artifact.rs b/flock-stage3/host/src/artifact.rs new file mode 100644 index 00000000..33127bd9 --- /dev/null +++ b/flock-stage3/host/src/artifact.rs @@ -0,0 +1,608 @@ +use anyhow::{Context, Result, bail}; +use bincode::Options; +use ix_terminal::Stage2RootStatementV1; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::OsString, + fs::{self, File, OpenOptions}, + io::{ErrorKind, Read, Write}, + path::{Path, PathBuf}, + sync::atomic::{AtomicU64, Ordering}, +}; + +use crate::config::FlockConfigV1; + +pub const STAGE3_STATEMENT_DOMAIN: &[u8; 8] = b"IXFLK301"; +pub const STAGE3_STATEMENT_BYTES: usize = 8 + 32 + 32 + 32; +const ARTIFACT_MAGIC: &[u8; 8] = b"IXFLOCK3"; +const ARTIFACT_VERSION: u16 = 1; +const ARTIFACT_HEADER_BYTES: usize = 8 + 2 + 4 + 8; +pub const MAX_STAGE3_PROOF_BYTES: usize = 64 * 1024 * 1024; +pub const MAX_STAGE3_ARTIFACT_BYTES: usize = + ARTIFACT_HEADER_BYTES + STAGE3_STATEMENT_BYTES + MAX_STAGE3_PROOF_BYTES; +const PRODUCTION_PAYLOAD_MAGIC: [u8; 8] = *b"IXFLK3P1"; +const PRODUCTION_PAYLOAD_VERSION: u16 = 1; +static NEXT_TEMP_FILE: AtomicU64 = AtomicU64::new(0); + +/// Public input to the complete Flock Stage 3 relation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3StatementV1 { + stage2_root_digest: [u8; 32], + relation_digest: [u8; 32], + config_digest: [u8; 32], +} + +impl Stage3StatementV1 { + pub fn new( + stage2_root: &Stage2RootStatementV1, + relation_digest: [u8; 32], + ) -> Self { + Self { + stage2_root_digest: stage2_root.digest(), + relation_digest, + config_digest: FlockConfigV1.digest(), + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != STAGE3_STATEMENT_BYTES { + bail!( + "Stage 3 statement is {} bytes; expected {STAGE3_STATEMENT_BYTES}", + bytes.len() + ); + } + if &bytes[..8] != STAGE3_STATEMENT_DOMAIN { + bail!("invalid Stage 3 statement domain"); + } + let mut stage2_root_digest = [0u8; 32]; + stage2_root_digest.copy_from_slice(&bytes[8..40]); + let mut relation_digest = [0u8; 32]; + relation_digest.copy_from_slice(&bytes[40..72]); + let mut config_digest = [0u8; 32]; + config_digest.copy_from_slice(&bytes[72..104]); + if config_digest != FlockConfigV1.digest() { + bail!("Stage 3 statement uses a different Flock configuration"); + } + Ok(Self { stage2_root_digest, relation_digest, config_digest }) + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(STAGE3_STATEMENT_BYTES); + bytes.extend_from_slice(STAGE3_STATEMENT_DOMAIN); + bytes.extend_from_slice(&self.stage2_root_digest); + bytes.extend_from_slice(&self.relation_digest); + bytes.extend_from_slice(&self.config_digest); + bytes + } + + pub fn digest(&self) -> [u8; 32] { + *blake3::hash(&self.to_bytes()).as_bytes() + } + + pub fn stage2_root_digest(&self) -> &[u8; 32] { + &self.stage2_root_digest + } + + pub fn relation_digest(&self) -> &[u8; 32] { + &self.relation_digest + } + + pub fn config_digest(&self) -> &[u8; 32] { + &self.config_digest + } +} + +/// Strict transport framing for a complete Stage 3 proof. +/// +/// Parsing establishes canonical framing only; cryptographic acceptance also +/// requires `FlockStage3Backend::verify_stage2` with an expected statement. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3ArtifactV1 { + statement: Stage3StatementV1, + proof: Vec, +} + +impl Stage3ArtifactV1 { + pub(crate) fn new( + statement: Stage3StatementV1, + proof: Vec, + ) -> Result { + if proof.is_empty() { + bail!("Stage 3 proof is empty"); + } + if proof.len() > MAX_STAGE3_PROOF_BYTES { + bail!("Stage 3 proof exceeds {MAX_STAGE3_PROOF_BYTES} bytes"); + } + Ok(Self { statement, proof }) + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.encoded_len()); + self.write_encoded(&mut bytes).expect("write artifact to Vec"); + bytes + } + + pub fn encoded_len(&self) -> usize { + ARTIFACT_HEADER_BYTES + STAGE3_STATEMENT_BYTES + self.proof.len() + } + + fn write_encoded(&self, writer: &mut impl Write) -> std::io::Result<()> { + writer.write_all(ARTIFACT_MAGIC)?; + writer.write_all(&ARTIFACT_VERSION.to_le_bytes())?; + writer.write_all(&(STAGE3_STATEMENT_BYTES as u32).to_le_bytes())?; + writer.write_all(&(self.proof.len() as u64).to_le_bytes())?; + writer.write_all(&self.statement.to_bytes())?; + writer.write_all(&self.proof) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < ARTIFACT_HEADER_BYTES { + bail!("truncated Stage 3 artifact header"); + } + if &bytes[..8] != ARTIFACT_MAGIC { + bail!("invalid Stage 3 artifact magic"); + } + let version = read_u16(&bytes[8..10]); + if version != ARTIFACT_VERSION { + bail!("unsupported Stage 3 artifact version {version}"); + } + let statement_len = + usize::try_from(read_u32(&bytes[10..14])).expect("u32 fits in usize"); + if statement_len != STAGE3_STATEMENT_BYTES { + bail!("invalid Stage 3 statement length {statement_len}"); + } + let proof_len = + usize::try_from(read_u64(&bytes[14..22])).map_err(|_| { + anyhow::anyhow!("Stage 3 proof length does not fit usize") + })?; + if proof_len == 0 { + bail!("Stage 3 proof is empty"); + } + if proof_len > MAX_STAGE3_PROOF_BYTES { + bail!("Stage 3 proof exceeds {MAX_STAGE3_PROOF_BYTES} bytes"); + } + let expected_len = ARTIFACT_HEADER_BYTES + .checked_add(statement_len) + .and_then(|len| len.checked_add(proof_len)) + .ok_or_else(|| anyhow::anyhow!("Stage 3 artifact length overflow"))?; + if bytes.len() != expected_len { + bail!( + "Stage 3 artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let statement_end = ARTIFACT_HEADER_BYTES + statement_len; + let statement = Stage3StatementV1::from_bytes( + &bytes[ARTIFACT_HEADER_BYTES..statement_end], + )?; + Self::new(statement, bytes[statement_end..].to_vec()) + } + + pub fn ensure_statement(&self, expected: &Stage3StatementV1) -> Result<()> { + if &self.statement != expected { + bail!("Stage 3 artifact statement does not match the expected root"); + } + Ok(()) + } + + pub fn statement(&self) -> &Stage3StatementV1 { + &self.statement + } + + pub fn proof_bytes(&self) -> &[u8] { + &self.proof + } + + /// Read a strictly bounded artifact without first allocating according to + /// an untrusted file size. + pub fn read_from_path(path: impl AsRef) -> Result { + let path = path.as_ref(); + let file = File::open(path) + .with_context(|| format!("open Stage 3 artifact {}", path.display()))?; + let declared_len = file + .metadata() + .with_context(|| format!("stat Stage 3 artifact {}", path.display()))? + .len(); + let maximum = u64::try_from(MAX_STAGE3_ARTIFACT_BYTES) + .expect("Stage 3 artifact limit fits u64"); + if declared_len > maximum { + bail!( + "Stage 3 artifact {} is {declared_len} bytes; maximum is {maximum}", + path.display() + ); + } + + let mut bytes = Vec::with_capacity( + usize::try_from(declared_len).context("Stage 3 artifact size")?, + ); + file + .take(maximum + 1) + .read_to_end(&mut bytes) + .with_context(|| format!("read Stage 3 artifact {}", path.display()))?; + if bytes.len() > MAX_STAGE3_ARTIFACT_BYTES { + bail!( + "Stage 3 artifact {} grew beyond {MAX_STAGE3_ARTIFACT_BYTES} bytes while being read", + path.display() + ); + } + Self::from_bytes(&bytes) + .with_context(|| format!("decode Stage 3 artifact {}", path.display())) + } + + /// Durably install an artifact in the destination directory. Temporary + /// files are created exclusively, and an existing destination is never + /// overwritten. + pub fn write_atomic(&self, path: impl AsRef) -> Result<()> { + Stage3ArtifactWriterV1::reserve(path)?.write(self) + } +} + +/// Reserve an exclusive temporary file before doing expensive proving work. +/// This checks destination existence, directory writability and hard-link +/// support, but does not promise free disk space or lock the final name. +/// Installation still atomically refuses a concurrent writer's destination. +pub struct Stage3ArtifactWriterV1 { + destination: PathBuf, + temporary: PathBuf, + file: File, +} + +impl Stage3ArtifactWriterV1 { + pub fn reserve(path: impl AsRef) -> Result { + let path = path.as_ref(); + if path.file_name().is_none() { + bail!("Stage 3 artifact path has no file name: {}", path.display()); + } + match fs::symlink_metadata(path) { + Ok(_) => { + bail!("refusing to overwrite Stage 3 artifact {}", path.display()) + }, + Err(error) if error.kind() == ErrorKind::NotFound => {}, + Err(error) => { + return Err(error).with_context(|| { + format!("stat artifact destination {}", path.display()) + }); + }, + } + let parent = artifact_parent(path); + let (temporary, file) = create_temporary(parent)?; + let reservation = Self { destination: path.to_owned(), temporary, file }; + // A predictable but exclusively created scratch name is safe here: a + // competing entry fails closed and is never removed by this reservation. + let probe = reservation.temporary.with_extension("link-probe"); + fs::hard_link(&reservation.temporary, &probe).with_context(|| { + format!("check atomic artifact installation in {}", parent.display()) + })?; + fs::remove_file(&probe).with_context(|| { + format!("remove artifact link probe {}", probe.display()) + })?; + Ok(reservation) + } + + pub fn write(mut self, artifact: &Stage3ArtifactV1) -> Result<()> { + artifact.write_encoded(&mut self.file).with_context(|| { + format!("write temporary artifact {}", self.temporary.display()) + })?; + self.file.sync_all().with_context(|| { + format!("sync temporary artifact {}", self.temporary.display()) + })?; + + fs::hard_link(&self.temporary, &self.destination).with_context(|| { + if fs::symlink_metadata(&self.destination).is_ok() { + format!( + "refusing to overwrite Stage 3 artifact {}", + self.destination.display() + ) + } else { + format!("install Stage 3 artifact {}", self.destination.display()) + } + })?; + fs::remove_file(&self.temporary).with_context(|| { + format!("remove temporary artifact {}", self.temporary.display()) + })?; + let parent = artifact_parent(&self.destination); + File::open(parent) + .and_then(|directory| directory.sync_all()) + .with_context(|| { + format!( + "artifact installed at {} but directory sync failed", + self.destination.display() + ) + })?; + Ok(()) + } +} + +impl Drop for Stage3ArtifactWriterV1 { + fn drop(&mut self) { + let _ = fs::remove_file(&self.temporary); + } +} + +fn artifact_parent(path: &Path) -> &Path { + path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")) +} + +fn create_temporary(parent: &Path) -> Result<(PathBuf, File)> { + for _ in 0..1_024 { + let nonce = NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed); + let temporary_name = OsString::from(format!( + ".ix-flock-stage3-{}-{nonce}.tmp", + std::process::id() + )); + let temporary = parent.join(temporary_name); + match OpenOptions::new().write(true).create_new(true).open(&temporary) { + Ok(file) => return Ok((temporary, file)), + Err(error) if error.kind() == ErrorKind::AlreadyExists => {}, + Err(error) => { + return Err(error).with_context(|| { + format!("create temporary artifact {}", temporary.display()) + }); + }, + } + } + bail!( + "could not reserve a unique temporary Stage 3 artifact in {}", + parent.display() + ) +} + +/// Canonical host transport needed to reconstruct the Flock public input. +/// +/// The compact Stage 2 inputs are not trusted by verification: they are +/// decoded again, lowered into the fixed relation, and checked against the +/// proof. Keeping them here avoids making Rust's serializer part of the Flock +/// circuit while giving Stage 4 a deterministic source for the verifier +/// witness. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Stage3ProductionPayloadV1 { + magic: [u8; 8], + version: u16, + config_digest: [u8; 32], + vk_bytes: Vec, + claim_bytes: Vec, + stage2_proof_bytes: Vec, + circuit_digest: [u8; 32], + flock_proof_bundle_bytes: Vec, +} + +impl Stage3ProductionPayloadV1 { + pub(crate) fn new( + vk_bytes: &[u8], + claim_bytes: &[u8], + stage2_proof_bytes: &[u8], + circuit_digest: [u8; 32], + flock_proof_bundle_bytes: &[u8], + ) -> Result { + let payload = Self { + magic: PRODUCTION_PAYLOAD_MAGIC, + version: PRODUCTION_PAYLOAD_VERSION, + config_digest: FlockConfigV1.digest(), + vk_bytes: vk_bytes.to_vec(), + claim_bytes: claim_bytes.to_vec(), + stage2_proof_bytes: stage2_proof_bytes.to_vec(), + circuit_digest, + flock_proof_bundle_bytes: flock_proof_bundle_bytes.to_vec(), + }; + payload.validate()?; + Ok(payload) + } + + pub(crate) fn encode(&self) -> Result> { + self.validate()?; + let bytes = bincode::DefaultOptions::new() + .with_fixint_encoding() + .serialize(self) + .context("encode Stage 3 production payload")?; + if bytes.len() > MAX_STAGE3_PROOF_BYTES { + bail!( + "Stage 3 production payload exceeds {MAX_STAGE3_PROOF_BYTES} bytes" + ); + } + Ok(bytes) + } + + pub(crate) fn decode(bytes: &[u8]) -> Result { + let payload: Self = bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(MAX_STAGE3_PROOF_BYTES as u64) + .reject_trailing_bytes() + .deserialize(bytes) + .context("invalid Stage 3 production payload")?; + payload.validate()?; + Ok(payload) + } + + fn validate(&self) -> Result<()> { + if self.magic != PRODUCTION_PAYLOAD_MAGIC { + bail!("invalid Stage 3 production payload magic"); + } + if self.version != PRODUCTION_PAYLOAD_VERSION { + bail!("unsupported Stage 3 production payload version {}", self.version); + } + if self.config_digest != FlockConfigV1.digest() { + bail!("Stage 3 production payload configuration mismatch"); + } + for (bytes, label) in [ + (self.vk_bytes.as_slice(), "verifying key"), + (self.claim_bytes.as_slice(), "claim"), + (self.stage2_proof_bytes.as_slice(), "Stage 2 proof"), + (self.flock_proof_bundle_bytes.as_slice(), "Flock proof bundle"), + ] { + if bytes.is_empty() { + bail!("Stage 3 production payload has an empty {label}"); + } + } + Ok(()) + } + + pub(crate) fn vk_bytes(&self) -> &[u8] { + &self.vk_bytes + } + + pub(crate) fn claim_bytes(&self) -> &[u8] { + &self.claim_bytes + } + + pub(crate) fn stage2_proof_bytes(&self) -> &[u8] { + &self.stage2_proof_bytes + } + + pub(crate) const fn circuit_digest(&self) -> [u8; 32] { + self.circuit_digest + } + + pub(crate) fn flock_proof_bundle_bytes(&self) -> &[u8] { + &self.flock_proof_bundle_bytes + } +} + +fn read_u16(bytes: &[u8]) -> u16 { + u16::from_le_bytes(bytes.try_into().expect("fixed u16")) +} + +fn read_u32(bytes: &[u8]) -> u32 { + u32::from_le_bytes(bytes.try_into().expect("fixed u32")) +} + +fn read_u64(bytes: &[u8]) -> u64 { + u64::from_le_bytes(bytes.try_into().expect("fixed u64")) +} + +#[cfg(test)] +mod tests { + use super::*; + use multi_stark::types::FriParameters; + + fn statement() -> Stage3StatementV1 { + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + }; + let claim: Vec = (0..18u64).flat_map(u64::to_le_bytes).collect(); + let root = Stage2RootStatementV1::new(b"vk", &claim, &fri).unwrap(); + Stage3StatementV1::new(&root, [7; 32]) + } + + #[test] + fn artifact_round_trip_rejects_extensions_and_mutations() { + let artifact = Stage3ArtifactV1::new(statement(), vec![1, 2, 3]).unwrap(); + assert_eq!( + blake3::Hash::from_bytes(artifact.statement().digest()).to_hex().as_str(), + "9f8062ce1801b29ed755cfb394fe888d5d82af77fe1ba2e5f539567e14e8b00d" + ); + let bytes = artifact.to_bytes(); + assert_eq!(artifact.encoded_len(), bytes.len()); + assert_eq!(Stage3ArtifactV1::from_bytes(&bytes).unwrap(), artifact); + + let mut extended = bytes.clone(); + extended.push(0); + assert!(Stage3ArtifactV1::from_bytes(&extended).is_err()); + + let mut wrong_domain = bytes.clone(); + wrong_domain[ARTIFACT_HEADER_BYTES] ^= 1; + assert!(Stage3ArtifactV1::from_bytes(&wrong_domain).is_err()); + + let mut wrong_config = bytes; + wrong_config[ARTIFACT_HEADER_BYTES + 72] ^= 1; + assert!(Stage3ArtifactV1::from_bytes(&wrong_config).is_err()); + } + + #[test] + fn expected_statement_is_checked_before_crypto() { + let artifact = Stage3ArtifactV1::new(statement(), vec![1]).unwrap(); + assert!(artifact.ensure_statement(&statement()).is_ok()); + let mut other = statement(); + other.relation_digest[0] ^= 1; + assert!(artifact.ensure_statement(&other).is_err()); + } + + #[test] + fn production_payload_is_strict_and_configuration_bound() { + let payload = Stage3ProductionPayloadV1::new( + b"vk", + b"claim", + b"stage2 proof", + [9; 32], + b"flock proof", + ) + .unwrap(); + let bytes = payload.encode().unwrap(); + assert_eq!(Stage3ProductionPayloadV1::decode(&bytes).unwrap(), payload); + + let mut extended = bytes.clone(); + extended.push(0); + assert!(Stage3ProductionPayloadV1::decode(&extended).is_err()); + + let mut wrong_magic = bytes; + wrong_magic[0] ^= 1; + assert!(Stage3ProductionPayloadV1::decode(&wrong_magic).is_err()); + + let mut wrong_config = payload; + wrong_config.config_digest[0] ^= 1; + assert!(wrong_config.encode().is_err()); + } + + #[test] + fn artifact_file_io_is_bounded_atomic_and_no_clobber() { + let nonce = NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed); + let directory = std::env::temp_dir().join(format!( + "ix-flock-stage3-artifact-test-{}-{nonce}", + std::process::id() + )); + fs::create_dir(&directory).unwrap(); + let path = directory.join("root.stage3.flock"); + + let artifact = Stage3ArtifactV1::new(statement(), vec![1, 2, 3]).unwrap(); + // An abandoned reservation removes only its own scratch file and never + // creates the final destination, including after a failed preflight. + let reservation = Stage3ArtifactWriterV1::reserve(&path).unwrap(); + assert!(!path.exists()); + assert_eq!(fs::read_dir(&directory).unwrap().count(), 1); + drop(reservation); + assert_eq!(fs::read_dir(&directory).unwrap().count(), 0); + assert!( + Stage3ArtifactWriterV1::reserve(directory.join("missing/root.flock")) + .is_err() + ); + + artifact.write_atomic(&path).unwrap(); + assert_eq!(Stage3ArtifactV1::read_from_path(&path).unwrap(), artifact); + + let replacement = Stage3ArtifactV1::new(statement(), vec![4, 5]).unwrap(); + let error = replacement.write_atomic(&path).unwrap_err().to_string(); + assert!(error.contains("refusing to overwrite")); + assert_eq!(Stage3ArtifactV1::read_from_path(&path).unwrap(), artifact); + assert_eq!(fs::read_dir(&directory).unwrap().count(), 1); + + fs::remove_file(&path).unwrap(); + let concurrent_path = + std::sync::Arc::new(directory.join("concurrent.flock")); + let contenders = [artifact.clone(), replacement.clone()].map(|artifact| { + let path = std::sync::Arc::clone(&concurrent_path); + std::thread::spawn(move || artifact.write_atomic(path.as_path())) + }); + let outcomes = contenders.map(|thread| thread.join().unwrap()); + assert_eq!(outcomes.iter().filter(|outcome| outcome.is_ok()).count(), 1); + let installed = + Stage3ArtifactV1::read_from_path(concurrent_path.as_path()).unwrap(); + assert!(installed == artifact || installed == replacement); + assert_eq!(fs::read_dir(&directory).unwrap().count(), 1); + fs::remove_file(concurrent_path.as_path()).unwrap(); + + let oversized = directory.join("oversized.stage3.flock"); + File::create(&oversized) + .unwrap() + .set_len(u64::try_from(MAX_STAGE3_ARTIFACT_BYTES).unwrap() + 1) + .unwrap(); + assert!(Stage3ArtifactV1::read_from_path(&oversized).is_err()); + fs::remove_file(oversized).unwrap(); + fs::remove_dir(directory).unwrap(); + } +} diff --git a/flock-stage3/host/src/bin/flock-stage3-config.rs b/flock-stage3/host/src/bin/flock-stage3-config.rs new file mode 100644 index 00000000..d389b79c --- /dev/null +++ b/flock-stage3/host/src/bin/flock-stage3-config.rs @@ -0,0 +1,19 @@ +use flock_stage3_host::{ + FLOCK_UPSTREAM_REVISION, FlockConfigV1, STAGE3_TRANSCRIPT_DOMAIN, +}; + +fn main() { + println!("flock_revision={FLOCK_UPSTREAM_REVISION}"); + println!("field=f128"); + println!("profile=fast128"); + println!("merkle_hash=blake3"); + println!("transcript=chained-blake3"); + println!( + "transcript_domain={}", + String::from_utf8_lossy(STAGE3_TRANSCRIPT_DOMAIN) + ); + println!( + "config_digest={}", + blake3::Hash::from_bytes(FlockConfigV1.digest()).to_hex() + ); +} diff --git a/flock-stage3/host/src/binding.rs b/flock-stage3/host/src/binding.rs new file mode 100644 index 00000000..57f8a936 --- /dev/null +++ b/flock-stage3/host/src/binding.rs @@ -0,0 +1,673 @@ +//! First production-shaped Flock circuit slice: hash the canonical Stage 2 +//! root statement and expose only its BLAKE3 digest. The 80-byte domain/vk/FRI +//! prefix and BLAKE3 padding are fixed by the circuit; the 144 claim bytes are +//! private inputs whose 18 u64 limbs are constrained to be canonical +//! Goldilocks representatives. +//! +//! This proves real Boolean R1CS plus inter-row wiring with statement-bound +//! public I/O. It is deliberately not called a Stage 3 proof: it does not yet +//! parse the statement or verify the Aiur proof whose root it commits to. + +use anyhow::{Context, Result, bail}; +use bincode::Options; +use flock_prover::{ + challenger::FsChallenger, + circuit::builder::{ + CircuitShape, GateType, ShapeBuilder, SlotId, SlotWitness, Wire, + }, + field::F128, + pcs::{Commitment, PcsParams, ligerito::embedded_initial_k_or_default}, + proof::R1csProofCircuitMerged, + prover::{self, UnionSlotProverInput}, + r1cs_hashes::blake3, + schedule::TableType, + union::UnionInstance, + verifier, +}; +use ix_terminal::{STAGE2_ROOT_STATEMENT_BYTES, Stage2RootStatementV1}; +use serde::{Deserialize, Serialize}; + +use crate::{ + FlockConfigV1, STAGE3_TRANSCRIPT_DOMAIN, + goldilocks::{ + CanonicalGoldilocksQuadGate, build_canonical_quad_r1cs, + generate_canonical_quad_witness, + }, +}; + +pub const STAGE3_BINDING_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLK3B1"; +const STAGE3_BINDING_ARTIFACT_VERSION: u16 = 1; +const STAGE2_FIXED_PREFIX_BYTES: usize = 80; +const CONFIG_OFFSET: usize = 10; +const PREFIX_OFFSET: usize = CONFIG_OFFSET + 32; +const CIRCUIT_DIGEST_OFFSET: usize = PREFIX_OFFSET + STAGE2_FIXED_PREFIX_BYTES; +const ROOT_DIGEST_OFFSET: usize = CIRCUIT_DIGEST_OFFSET + 32; +const BUNDLE_LENGTH_OFFSET: usize = ROOT_DIGEST_OFFSET + 32; +const ARTIFACT_HEADER_BYTES: usize = BUNDLE_LENGTH_OFFSET + 8; +const MAX_PROOF_BUNDLE_BYTES: usize = 64 * 1024 * 1024; + +const BLAKE3_CAPACITY_LOG: usize = 8; +const BLOCK_BYTES: usize = 64; +const WORD_BYTES: usize = 16; +const MESSAGE_BLOCKS: usize = STAGE2_ROOT_STATEMENT_BYTES.div_ceil(BLOCK_BYTES); +const FIRST_CLAIM_WORD: usize = STAGE2_FIXED_PREFIX_BYTES / WORD_BYTES; +const CLAIM_WORDS: usize = + (STAGE2_ROOT_STATEMENT_BYTES - STAGE2_FIXED_PREFIX_BYTES) / WORD_BYTES; +pub(crate) const CHUNK_START: u32 = 1 << 0; +pub(crate) const CHUNK_END: u32 = 1 << 1; +pub(crate) const ROOT: u32 = 1 << 3; +pub(crate) const IV: [u32; 8] = [ + 0x6A09_E667, + 0xBB67_AE85, + 0x3C6E_F372, + 0xA54F_F53A, + 0x510E_527F, + 0x9B05_688C, + 0x1F83_D9AB, + 0x5BE0_CD19, +]; + +/// A genuine Flock circuit proof of the statement-hash subrelation. +/// +/// This artifact must never be accepted as [`crate::Stage3ArtifactV1`]. It +/// establishes only +/// `BLAKE3(fixed_domain_vk_fri_prefix || private_claim) = stage2_root_digest`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3BindingArtifactV1 { + fixed_statement_prefix: [u8; STAGE2_FIXED_PREFIX_BYTES], + circuit_digest: [u8; 32], + stage2_root_digest: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl Stage3BindingArtifactV1 { + pub fn to_bytes(&self) -> Vec { + let mut bytes = + Vec::with_capacity(ARTIFACT_HEADER_BYTES + self.proof_bundle_bytes.len()); + bytes.extend_from_slice(STAGE3_BINDING_ARTIFACT_MAGIC); + bytes.extend_from_slice(&STAGE3_BINDING_ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.extend_from_slice(&self.fixed_statement_prefix); + bytes.extend_from_slice(&self.circuit_digest); + bytes.extend_from_slice(&self.stage2_root_digest); + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < ARTIFACT_HEADER_BYTES { + bail!("truncated Flock statement-binding artifact"); + } + if &bytes[..8] != STAGE3_BINDING_ARTIFACT_MAGIC { + bail!("invalid Flock statement-binding artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != STAGE3_BINDING_ARTIFACT_VERSION { + bail!("unsupported Flock statement-binding artifact version {version}"); + } + if bytes[CONFIG_OFFSET..PREFIX_OFFSET] != FlockConfigV1.digest() { + bail!("Flock statement-binding artifact configuration mismatch"); + } + let mut fixed_statement_prefix = [0u8; STAGE2_FIXED_PREFIX_BYTES]; + fixed_statement_prefix + .copy_from_slice(&bytes[PREFIX_OFFSET..CIRCUIT_DIGEST_OFFSET]); + let mut circuit_digest = [0u8; 32]; + circuit_digest + .copy_from_slice(&bytes[CIRCUIT_DIGEST_OFFSET..ROOT_DIGEST_OFFSET]); + let mut stage2_root_digest = [0u8; 32]; + stage2_root_digest + .copy_from_slice(&bytes[ROOT_DIGEST_OFFSET..BUNDLE_LENGTH_OFFSET]); + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[BUNDLE_LENGTH_OFFSET..ARTIFACT_HEADER_BYTES].try_into().unwrap(), + )) + .map_err(|error| { + anyhow::anyhow!("Flock proof bundle length does not fit usize: {error}") + })?; + if bundle_len == 0 || bundle_len > MAX_PROOF_BUNDLE_BYTES { + bail!("invalid Flock proof bundle length {bundle_len}"); + } + let expected_len = + ARTIFACT_HEADER_BYTES.checked_add(bundle_len).ok_or_else(|| { + anyhow::anyhow!("Flock binding artifact length overflow") + })?; + if bytes.len() != expected_len { + bail!( + "Flock statement-binding artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let proof_bundle_bytes = bytes[ARTIFACT_HEADER_BYTES..].to_vec(); + decode_proof_bundle(&proof_bundle_bytes) + .context("decode Flock statement-binding proof bundle")?; + Ok(Self { + fixed_statement_prefix, + circuit_digest, + stage2_root_digest, + proof_bundle_bytes, + }) + } + + pub fn fixed_statement_prefix(&self) -> &[u8; STAGE2_FIXED_PREFIX_BYTES] { + &self.fixed_statement_prefix + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn stage2_root_digest(&self) -> &[u8; 32] { + &self.stage2_root_digest + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +#[derive(Serialize, Deserialize)] +struct CircuitProofBundle { + commitment: Commitment, + proof: R1csProofCircuitMerged, +} + +/// Produce a real Flock circuit proof that checks the private claim's +/// Goldilocks encodings and hashes the canonical 224-byte Stage 2 statement +/// to its public root digest. +pub fn prove_stage3_statement_binding( + statement: &Stage2RootStatementV1, +) -> Result { + let statement_bytes = statement.to_bytes(); + let fixed_statement_prefix = statement_prefix(&statement_bytes); + let relation = StatementHashRelation::build(&fixed_statement_prefix)?; + let inputs = relation_inputs(&statement_bytes); + let witness = relation.shape.run(&inputs, &[]); + let stage2_root_digest = statement.digest(); + let expected_public = + relation_public(&fixed_statement_prefix, &stage2_root_digest); + if witness.public != expected_public { + bail!("Flock BLAKE3 gate output disagrees with native Stage 2 digest"); + } + + let rows = witness.rows::(relation.blake3_slot); + let canonical_rows = witness + .rows::(relation.canonical_goldilocks_slot); + relation.ensure_registry_order()?; + let blake3_r1cs = blake3::build_block_r1cs(BLAKE3_CAPACITY_LOG); + let blake3_lincheck = blake3_r1cs.csc_lincheck_circuit(); + let canonical_r1cs = build_canonical_quad_r1cs(BLAKE3_CAPACITY_LOG); + let canonical_lincheck = canonical_r1cs.csc_lincheck_circuit(); + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let pcs_params = pcs_params(&union); + let mut challenger = + FsChallenger::with_chained_blake3(STAGE3_TRANSCRIPT_DOMAIN); + let (proof, commitment, _) = prover::prove_fast_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &witness.public, + &pcs_params, + vec![ + UnionSlotProverInput::new( + blake3::generate_witness_batch_major_partial(rows, BLAKE3_CAPACITY_LOG), + blake3_lincheck, + ), + UnionSlotProverInput::new( + generate_canonical_quad_witness(canonical_rows, BLAKE3_CAPACITY_LOG), + canonical_lincheck, + ), + ], + Vec::new(), + &mut challenger, + ); + let proof_bundle_bytes = + encode_proof_bundle(&CircuitProofBundle { commitment, proof })?; + if proof_bundle_bytes.len() > MAX_PROOF_BUNDLE_BYTES { + bail!("Flock proof bundle exceeds {MAX_PROOF_BUNDLE_BYTES} bytes"); + } + Ok(Stage3BindingArtifactV1 { + fixed_statement_prefix, + circuit_digest: relation.shape.circuit.digest(), + stage2_root_digest, + proof_bundle_bytes, + }) +} + +/// Verify the statement-hash circuit proof against the digest carried by its +/// strict artifact. Callers that expect a particular Stage 2 root must also +/// use [`verify_stage3_statement_binding_for`]. +pub fn verify_stage3_statement_binding( + artifact: &Stage3BindingArtifactV1, +) -> Result<()> { + let relation = + StatementHashRelation::build(&artifact.fixed_statement_prefix)?; + relation.ensure_registry_order()?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Flock statement-binding circuit digest mismatch"); + } + let bundle = decode_proof_bundle(&artifact.proof_bundle_bytes) + .context("decode Flock statement-binding proof bundle")?; + let public = relation_public( + &artifact.fixed_statement_prefix, + &artifact.stage2_root_digest, + ); + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let pcs_params = pcs_params(&union); + let blake3_r1cs = blake3::build_block_r1cs(BLAKE3_CAPACITY_LOG); + let blake3_lincheck = blake3_r1cs.csc_lincheck_circuit(); + let canonical_r1cs = build_canonical_quad_r1cs(BLAKE3_CAPACITY_LOG); + let canonical_lincheck = canonical_r1cs.csc_lincheck_circuit(); + let linchecks: [&dyn flock_prover::lincheck::LincheckCircuit; 2] = + [blake3_lincheck, canonical_lincheck]; + let mut challenger = + FsChallenger::with_chained_blake3(STAGE3_TRANSCRIPT_DOMAIN); + verifier::verify_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &public, + &linchecks, + &bundle.commitment, + &bundle.proof, + &pcs_params, + &mut challenger, + ) + .map_err(|error| { + anyhow::anyhow!("Flock statement-binding proof rejected: {error:?}") + })?; + Ok(()) +} + +/// Verify and bind the proof to an expected canonical Stage 2 statement. +pub fn verify_stage3_statement_binding_for( + artifact: &Stage3BindingArtifactV1, + expected: &Stage2RootStatementV1, +) -> Result<()> { + if artifact.fixed_statement_prefix != statement_prefix(&expected.to_bytes()) { + bail!("Flock binding proof uses a different Stage 2 vk or FRI prefix"); + } + if artifact.stage2_root_digest != expected.digest() { + bail!("Flock binding proof targets a different Stage 2 root"); + } + verify_stage3_statement_binding(artifact) +} + +/// Content digest of this partial circuit. It is useful for diagnostics and +/// reproducibility, but is not the complete Stage 3 relation digest. +pub fn stage3_statement_binding_circuit_digest( + statement: &Stage2RootStatementV1, +) -> Result<[u8; 32]> { + let prefix = statement_prefix(&statement.to_bytes()); + Ok(StatementHashRelation::build(&prefix)?.shape.circuit.digest()) +} + +struct StatementHashRelation { + shape: CircuitShape, + blake3_slot: SlotId, + canonical_goldilocks_slot: SlotId, +} + +impl StatementHashRelation { + fn build(prefix: &[u8; STAGE2_FIXED_PREFIX_BYTES]) -> Result { + let mut builder = ShapeBuilder::new(BLAKE3_CAPACITY_LOG); + let blake3_slot = builder.slot(Blake3Gate { nu: BLAKE3_CAPACITY_LOG }); + let canonical_goldilocks_slot = + builder.slot(CanonicalGoldilocksQuadGate { nu: BLAKE3_CAPACITY_LOG }); + let packed_iv = pack8(&IV); + let initial_cv = [ + builder.fixed_public_input(packed_iv[0]), + builder.fixed_public_input(packed_iv[1]), + ]; + let canonical_zero = builder.fixed_public_input(F128::ZERO); + let mut messages = Vec::<[Wire; 4]>::with_capacity(MESSAGE_BLOCKS); + let mut params = Vec::::with_capacity(MESSAGE_BLOCKS); + for block in 0..MESSAGE_BLOCKS { + let message: [_; 4] = std::array::from_fn(|word| { + let word = block * 4 + word; + match fixed_statement_word(prefix, word) { + Some(value) => builder.fixed_public_input(value), + None => builder.input(), + } + }); + messages.push(message); + params.push(builder.fixed_public_input(block_params(block))); + } + + for word in FIRST_CLAIM_WORD..FIRST_CLAIM_WORD + CLAIM_WORDS { + let message = messages[word / 4][word % 4]; + // This small conformance relation keeps one row per claim word; + // duplicating its input exercises the same four-limb production table. + let violation = + builder.gate(canonical_goldilocks_slot, &[message, message])[0]; + builder.connect(violation, canonical_zero); + } + + let mut cv = initial_cv; + for block in 0..MESSAGE_BLOCKS { + let message = messages[block]; + let outputs = builder.gate( + blake3_slot, + &[ + cv[0], + cv[1], + message[0], + message[1], + message[2], + message[3], + params[block], + ], + ); + cv = [outputs[0], outputs[1]]; + } + builder.publish(cv[0]); + builder.publish(cv[1]); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock binding circuit: {error:?}") + })?; + Ok(Self { shape, blake3_slot, canonical_goldilocks_slot }) + } + + fn ensure_registry_order(&self) -> Result<()> { + if self.shape.registry_slot(self.blake3_slot) != 0 + || self.shape.registry_slot(self.canonical_goldilocks_slot) != 1 + { + bail!("unexpected Flock Boolean table registry order"); + } + Ok(()) + } +} + +pub(crate) struct Blake3Gate { + pub(crate) nu: usize, +} + +impl GateType for Blake3Gate { + type Row = blake3::Compression; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(blake3::build_block_r1cs(self.nu)) + .with_io_schema(blake3::io_schema()) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let cv = unpack8(inputs[0], inputs[1]); + let mut message = [0u32; 16]; + for index in 0..4 { + message[4 * index..4 * index + 4] + .copy_from_slice(&unpack4(inputs[2 + index])); + } + let (counter, block_len, flags) = unpack_params(inputs[6]); + let output = + blake3::blake3_compress(&cv, &message, counter, block_len, flags); + let output_lo: [u32; 8] = output[..8].try_into().unwrap(); + let output_hi: [u32; 8] = output[8..].try_into().unwrap(); + outputs.extend_from_slice(&[ + pack8(&output_lo)[0], + pack8(&output_lo)[1], + pack8(&output_hi)[0], + pack8(&output_hi)[1], + ]); + (cv, message, counter, block_len, flags) + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +fn relation_inputs(statement: &[u8]) -> Vec { + assert_eq!(statement.len(), STAGE2_ROOT_STATEMENT_BYTES); + let mut padded = [0u8; MESSAGE_BLOCKS * BLOCK_BYTES]; + padded[..statement.len()].copy_from_slice(statement); + let packed_iv = pack8(&IV); + let mut inputs = Vec::with_capacity(3 + MESSAGE_BLOCKS * 5); + inputs.extend_from_slice(&packed_iv); + inputs.push(F128::ZERO); + for block in 0..MESSAGE_BLOCKS { + let start = block * BLOCK_BYTES; + for word in 0..4 { + let offset = start + word * WORD_BYTES; + inputs.push(pack_bytes(&padded[offset..offset + WORD_BYTES])); + } + inputs.push(block_params(block)); + } + inputs +} + +fn relation_public( + prefix: &[u8; STAGE2_FIXED_PREFIX_BYTES], + digest: &[u8; 32], +) -> Vec { + let packed_iv = pack8(&IV); + let mut public = Vec::with_capacity(3 + 7 + MESSAGE_BLOCKS + 2); + public.extend_from_slice(&packed_iv); + public.push(F128::ZERO); + for block in 0..MESSAGE_BLOCKS { + for word in 0..4 { + if let Some(value) = fixed_statement_word(prefix, block * 4 + word) { + public.push(value); + } + } + public.push(block_params(block)); + } + public.push(pack_bytes(&digest[..16])); + public.push(pack_bytes(&digest[16..])); + public +} + +fn statement_prefix(statement: &[u8]) -> [u8; STAGE2_FIXED_PREFIX_BYTES] { + statement[..STAGE2_FIXED_PREFIX_BYTES].try_into().unwrap() +} + +fn fixed_statement_word( + prefix: &[u8; STAGE2_FIXED_PREFIX_BYTES], + word: usize, +) -> Option { + if word * WORD_BYTES < STAGE2_FIXED_PREFIX_BYTES { + let offset = word * WORD_BYTES; + Some(pack_bytes(&prefix[offset..offset + WORD_BYTES])) + } else if word * WORD_BYTES >= STAGE2_ROOT_STATEMENT_BYTES { + Some(F128::ZERO) + } else { + None + } +} + +fn block_params(block: usize) -> F128 { + let is_first = block == 0; + let is_last = block + 1 == MESSAGE_BLOCKS; + let mut flags = 0; + if is_first { + flags |= CHUNK_START; + } + if is_last { + flags |= CHUNK_END | ROOT; + } + let consumed = block * BLOCK_BYTES; + let remaining = STAGE2_ROOT_STATEMENT_BYTES - consumed; + let block_len = u32::try_from(remaining.min(BLOCK_BYTES)).unwrap(); + pack_params(0, block_len, flags) +} + +pub(crate) fn pcs_params(union: &UnionInstance<'_>) -> PcsParams { + let profile = FlockConfigV1.profile(); + let m = union.dense_m(); + let log_batch_size = embedded_initial_k_or_default(m, profile); + PcsParams { + m, + log_inv_rate: profile.log_inv_rate(), + log_batch_size, + profile, + num_lanes: union.commit_lanes(log_batch_size), + merkle_hash: FlockConfigV1.merkle_hash(), + } +} + +pub(crate) fn pack_bytes(bytes: &[u8]) -> F128 { + assert_eq!(bytes.len(), WORD_BYTES); + F128::new( + u64::from_le_bytes(bytes[..8].try_into().unwrap()), + u64::from_le_bytes(bytes[8..].try_into().unwrap()), + ) +} + +pub(crate) fn pack4(words: [u32; 4]) -> F128 { + F128::new( + words[0] as u64 | ((words[1] as u64) << 32), + words[2] as u64 | ((words[3] as u64) << 32), + ) +} + +pub(crate) fn unpack4(value: F128) -> [u32; 4] { + [ + value.lo as u32, + (value.lo >> 32) as u32, + value.hi as u32, + (value.hi >> 32) as u32, + ] +} + +pub(crate) fn pack8(words: &[u32; 8]) -> [F128; 2] { + [ + pack4([words[0], words[1], words[2], words[3]]), + pack4([words[4], words[5], words[6], words[7]]), + ] +} + +pub(crate) fn unpack8(first: F128, second: F128) -> [u32; 8] { + let first = unpack4(first); + let second = unpack4(second); + [ + first[0], first[1], first[2], first[3], second[0], second[1], second[2], + second[3], + ] +} + +pub(crate) fn pack_params(counter: u64, block_len: u32, flags: u32) -> F128 { + F128::new(counter, block_len as u64 | ((flags as u64) << 32)) +} + +fn unpack_params(value: F128) -> (u64, u32, u32) { + (value.lo, value.hi as u32, (value.hi >> 32) as u32) +} + +fn encode_proof_bundle(bundle: &CircuitProofBundle) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .serialize(bundle) + .context("encode Flock statement-binding proof bundle") +} + +fn decode_proof_bundle(bytes: &[u8]) -> Result { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(MAX_PROOF_BUNDLE_BYTES as u64) + .reject_trailing_bytes() + .deserialize(bytes) + .context("invalid Flock statement-binding proof bundle") +} + +#[cfg(test)] +mod tests { + use super::*; + use ix_terminal::OUTER_CLAIM_ELEMENTS; + use multi_stark::types::FriParameters; + + fn statement() -> Stage2RootStatementV1 { + let claim: Vec = + (0..OUTER_CLAIM_ELEMENTS as u64).flat_map(u64::to_le_bytes).collect(); + Stage2RootStatementV1::new( + b"binding-test-vk", + &claim, + &FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + }, + ) + .unwrap() + } + + #[test] + fn circuit_hashes_the_exact_stage2_statement() { + let statement = statement(); + let statement_bytes = statement.to_bytes(); + let prefix = statement_prefix(&statement_bytes); + let relation = StatementHashRelation::build(&prefix).unwrap(); + let witness = relation.shape.run(&relation_inputs(&statement_bytes), &[]); + assert_eq!(witness.public, relation_public(&prefix, &statement.digest())); + assert_eq!(relation.shape.counts, vec![MESSAGE_BLOCKS, CLAIM_WORDS]); + assert_eq!(witness.rows::(relation.blake3_slot).len(), 4); + assert_eq!( + witness + .rows::(relation.canonical_goldilocks_slot) + .len(), + CLAIM_WORDS + ); + + let mut changed = statement.to_bytes(); + let last = changed.len() - 1; + changed[last] ^= 1; + let changed = relation.shape.run(&relation_inputs(&changed), &[]); + assert_ne!(changed.public, witness.public); + + let mut other_prefix = prefix; + other_prefix[8] ^= 1; + let other = StatementHashRelation::build(&other_prefix).unwrap(); + assert_ne!(other.shape.circuit.digest(), relation.shape.circuit.digest()); + } + + #[test] + fn artifact_parser_rejects_short_and_wrong_magic() { + assert!(Stage3BindingArtifactV1::from_bytes(&[]).is_err()); + let mut header = vec![0u8; ARTIFACT_HEADER_BYTES]; + header[..8].copy_from_slice(b"NOTFLOCK"); + assert!(Stage3BindingArtifactV1::from_bytes(&header).is_err()); + } + + #[test] + #[ignore = "large upstream Flock circuit proof; run explicitly"] + fn real_statement_binding_proof_round_trip_and_mutations() { + let statement = statement(); + let artifact = + prove_stage3_statement_binding(&statement).expect("prove binding"); + eprintln!( + "Flock statement-binding circuit proof bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_stage3_statement_binding_for(&artifact, &statement) + .expect("verify binding"); + + let encoded = artifact.to_bytes(); + let decoded = Stage3BindingArtifactV1::from_bytes(&encoded).unwrap(); + verify_stage3_statement_binding_for(&decoded, &statement) + .expect("verify decoded binding"); + + let mut wrong_prefix = decoded.clone(); + wrong_prefix.fixed_statement_prefix[8] ^= 1; + assert!( + verify_stage3_statement_binding_for(&wrong_prefix, &statement).is_err() + ); + + let mut wrong_root = decoded.clone(); + wrong_root.stage2_root_digest[0] ^= 1; + assert!(verify_stage3_statement_binding(&wrong_root).is_err()); + + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_stage3_statement_binding(&wrong_proof).is_err()); + } +} diff --git a/flock-stage3/host/src/boolean.rs b/flock-stage3/host/src/boolean.rs new file mode 100644 index 00000000..6a137e0b --- /dev/null +++ b/flock-stage3/host/src/boolean.rs @@ -0,0 +1,628 @@ +//! Small builder for custom block-diagonal Boolean R1CS tables. +//! +//! Flock fixes `C = I`, so every constraint row also names its output +//! variable. This builder allocates derived variables in dependency order, +//! records their `(linear A) * (linear B) = output` operations once, and uses +//! that same record for both the sparse matrices and native witness filling. + +use std::sync::OnceLock; + +use flock_prover::{ + bits::transpose_8_u64s_to_64_bytes, + field::F128, + lincheck::pack_z_lincheck, + r1cs::{BlockR1cs, SparseBinaryMatrix, WitnessLayout}, + schedule::{TableClass, TableType}, + scratch, + union::SlotWitnessDest, +}; +use rayon::prelude::*; + +/// Move a freshly built Boolean R1CS into a union table without cloning its +/// sparse matrices. `TableType::from_block_r1cs` accepts a borrow and must +/// deep-copy all three matrices, which is especially costly for BLAKE3. +pub(crate) fn table_from_block_r1cs(r1cs: BlockR1cs) -> TableType { + TableType { + k_log: r1cs.k_log, + useful_bits: r1cs.useful_bits, + a_0: r1cs.a_0, + b_0: r1cs.b_0, + c_0: r1cs.c_0, + const_pin: r1cs.const_pin, + class: TableClass::Boolean, + io_schema: Vec::new(), + } +} + +#[derive(Clone, Debug)] +struct BooleanOperation { + output: usize, + a: Vec, + b: Vec, +} + +/// A finished Boolean table plan, independent of its outer row capacity. +#[derive(Clone, Debug)] +pub(crate) struct BooleanR1csPlan { + k_log: usize, + useful_bits: usize, + a_rows: Vec>, + b_rows: Vec>, + operations: Vec, + const_pin: Option, +} + +impl BooleanR1csPlan { + pub(crate) fn k_log(&self) -> usize { + self.k_log + } + + pub(crate) fn k(&self) -> usize { + 1usize << self.k_log + } + + #[cfg(test)] + pub(crate) fn useful_bits(&self) -> usize { + self.useful_bits + } + + pub(crate) fn block_r1cs(&self, nu: usize) -> BlockR1cs { + assert!(nu >= 3, "Flock lincheck requires at least eight rows"); + let k = self.k(); + BlockR1cs { + m: self.k_log + nu, + k_log: self.k_log, + k_skip: 6, + useful_bits: self.useful_bits, + a_0: sparse_matrix(k, self.a_rows.clone()), + b_0: sparse_matrix(k, self.b_rows.clone()), + c_0: sparse_matrix(k, (0..k).map(|row| vec![row]).collect()), + layout: WitnessLayout::BatchMajor, + const_pin: self.const_pin, + digest_cache: OnceLock::new(), + csc_cache: OnceLock::new(), + } + } + + /// Fill fixed/free columns, then derive every internal/output column from + /// the same operation list that created the R1CS matrices. + pub(crate) fn fill_row( + &self, + bits: &mut [bool], + fill_free: impl FnOnce(&mut [bool]), + ) { + assert_eq!(bits.len(), self.k()); + bits.fill(false); + if let Some(column) = self.const_pin { + bits[column] = true; + } + fill_free(bits); + for operation in &self.operations { + let a = parity(bits, &operation.a); + let b = parity(bits, &operation.b); + bits[operation.output] = a & b; + } + } +} + +/// Mutable construction half of [`BooleanR1csPlan`]. +pub(crate) struct BooleanR1csBuilder { + k_log: usize, + k: usize, + next_column: usize, + a_rows: Vec>, + b_rows: Vec>, + assigned: Vec, + operations: Vec, + const_pin: Option, +} + +impl BooleanR1csBuilder { + /// Reserve `[0, reserved_columns)` for word-aligned circuit I/O. + pub(crate) fn new(k_log: usize, reserved_columns: usize) -> Self { + assert!(k_log >= 7, "BatchMajor Boolean tables need k_log >= 7"); + let k = 1usize << k_log; + assert!(reserved_columns <= k); + Self { + k_log, + k, + next_column: reserved_columns, + a_rows: vec![Vec::new(); k], + b_rows: vec![Vec::new(); k], + assigned: vec![false; k], + operations: Vec::new(), + const_pin: None, + } + } + + /// Mark a supplied bit as free Boolean advice (`x * x = x`). + pub(crate) fn free_boolean_at(&mut self, column: usize) { + self.set_constraint(column, vec![column], vec![column], false); + } + + /// Require a supplied Boolean advice bit to be zero. + /// + /// With `one = 1`, the constraint `x * (x + one) = x` accepts `x = 0` + /// and rejects `x = 1`. This is useful for word-aligned gates whose logical + /// input occupies only one lane of an `F128` word. + pub(crate) fn assert_zero_at(&mut self, column: usize, one: usize) { + self.set_constraint(column, vec![column], vec![column, one], false); + } + + /// Allocate one supplied Boolean advice bit after the reserved I/O region. + pub(crate) fn alloc_free_boolean(&mut self) -> usize { + let column = self.alloc_column(); + self.free_boolean_at(column); + column + } + + /// Allocate the table's one constant-one column and bind it through + /// Flock's count-aware lincheck pin. + pub(crate) fn alloc_constant_one(&mut self) -> usize { + assert!(self.const_pin.is_none(), "constant-one column already allocated"); + let column = self.alloc_free_boolean(); + self.const_pin = Some(column); + column + } + + pub(crate) fn and(&mut self, lhs: usize, rhs: usize) -> usize { + self.alloc_gate(vec![lhs], vec![rhs]) + } + + /// Multiply two non-empty GF(2) linear forms. + /// + /// This is useful for compact full adders: `z * (x + y)` is one R1CS + /// constraint and does not need an intermediate column for `x + y`. + pub(crate) fn product_of_parities( + &mut self, + lhs: &[usize], + rhs: &[usize], + ) -> usize { + assert!(!lhs.is_empty()); + assert!(!rhs.is_empty()); + self.alloc_gate(lhs.to_vec(), rhs.to_vec()) + } + + /// Derive a pre-reserved output from two GF(2) linear forms. + pub(crate) fn write_product_of_parities( + &mut self, + output: usize, + lhs: &[usize], + rhs: &[usize], + ) { + assert!(!lhs.is_empty()); + assert!(!rhs.is_empty()); + self.set_constraint(output, lhs.to_vec(), rhs.to_vec(), true); + } + + /// XOR a non-empty set of bits using multiplication by the pinned one. + pub(crate) fn xor(&mut self, inputs: &[usize], one: usize) -> usize { + assert!(!inputs.is_empty()); + self.alloc_gate(inputs.to_vec(), vec![one]) + } + + /// Derive a pre-reserved output bit instead of allocating a new column. + pub(crate) fn write_xor( + &mut self, + output: usize, + inputs: &[usize], + one: usize, + ) { + assert!(!inputs.is_empty()); + self.set_constraint(output, inputs.to_vec(), vec![one], true); + } + + pub(crate) fn finish(self) -> BooleanR1csPlan { + BooleanR1csPlan { + k_log: self.k_log, + useful_bits: self.next_column, + a_rows: self.a_rows, + b_rows: self.b_rows, + operations: self.operations, + const_pin: self.const_pin, + } + } + + fn alloc_gate(&mut self, a: Vec, b: Vec) -> usize { + let output = self.alloc_column(); + self.set_constraint(output, a, b, true); + output + } + + fn alloc_column(&mut self) -> usize { + assert!(self.next_column < self.k, "Boolean R1CS table exceeded 2^k_log"); + let column = self.next_column; + self.next_column += 1; + column + } + + fn set_constraint( + &mut self, + output: usize, + a: Vec, + b: Vec, + derive: bool, + ) { + assert!(output < self.k); + assert!(!self.assigned[output], "Boolean column {output} assigned twice"); + assert!(a.iter().chain(&b).all(|&column| column < self.k)); + self.a_rows[output] = a.clone(); + self.b_rows[output] = b.clone(); + self.assigned[output] = true; + if derive { + self.operations.push(BooleanOperation { output, a, b }); + } + } +} + +/// Produce Flock's BatchMajor `(z, A z, B z, lincheck stripe)` tuple from a +/// row filler that supplies only the plan's free columns. +pub(crate) fn generate_boolean_witness( + plan: &BooleanR1csPlan, + rows: &[T], + nu: usize, + fill_free: impl Fn(&T, &mut [bool]), +) -> (Vec, Vec, Vec, Vec) { + let capacity = 1usize << nu; + assert!(rows.len() <= capacity); + let r1cs = plan.block_r1cs(nu); + let k = plan.k(); + let mut z = vec![false; r1cs.n()]; + for (outer, row) in rows.iter().enumerate() { + plan.fill_row(&mut z[outer * k..(outer + 1) * k], |bits| { + fill_free(row, bits) + }); + } + let a = r1cs.apply_a(&z); + let b = r1cs.apply_b(&z); + assert!( + a.iter() + .zip(&b) + .zip(&z) + .all(|((a_bit, b_bit), z_bit)| (*a_bit & *b_bit) == *z_bit), + "custom Boolean witness does not satisfy its R1CS" + ); + let stripe = pack_z_lincheck(&z, r1cs.m, r1cs.k_log); + ( + pack_batch_major(&z, plan.k_log(), nu), + pack_batch_major(&a, plan.k_log(), nu), + pack_batch_major(&b, plan.k_log(), nu), + stripe, + ) +} + +/// Generate a partial-count Boolean witness directly into one union slot. +/// +/// The allocating compatibility driver above constructs three logical +/// `capacity * k` bit-vectors, applies both sparse matrices over every dummy +/// row, repacks all three vectors, and then makes the union copy them again. +/// Stage 3 tables are deliberately sparse, so that work is overwhelmingly +/// padding. This driver evaluates only declared rows, eight at a time (the +/// lincheck stripe's native grouping), and writes their packed words directly +/// into Flock's pooled union buffers. +pub(crate) fn generate_boolean_witness_into( + plan: &BooleanR1csPlan, + rows: &[T], + nu: usize, + dst: SlotWitnessDest<'_>, + fill_free: impl Fn(&T, &mut [bool]) + Send + Sync, +) -> Vec { + generate_boolean_rows_into( + plan.k_log, + plan.useful_bits, + &plan.a_rows, + &plan.b_rows, + rows, + nu, + dst, + |row, bits| plan.fill_row(bits, |bits| fill_free(row, bits)), + ) +} + +/// Low-level form of [`generate_boolean_witness_into`] for Boolean tables +/// whose matrices and row filler predate [`BooleanR1csPlan`]. +#[allow(clippy::too_many_arguments)] +pub(crate) fn generate_boolean_rows_into( + k_log: usize, + useful_bits: usize, + a_rows: &[Vec], + b_rows: &[Vec], + rows: &[T], + nu: usize, + dst: SlotWitnessDest<'_>, + fill_row: impl Fn(&T, &mut [bool]) + Send + Sync, +) -> Vec { + const GROUP_ROWS: usize = 8; + const ZERO_CHUNK_WORDS: usize = 1 << 16; + + assert!(nu >= 3, "Flock lincheck requires at least eight rows"); + assert!(k_log >= 7, "BatchMajor Boolean tables need k_log >= 7"); + let capacity = 1usize << nu; + let k = 1usize << k_log; + assert!(rows.len() <= capacity); + assert!(useful_bits <= k); + assert_eq!(a_rows.len(), k); + assert_eq!(b_rows.len(), k); + + let chunks = k / 128; + let slot_words = capacity * chunks; + let useful_chunks = useful_bits.div_ceil(128); + let useful_words = useful_bits.div_ceil(64); + let stored_words = 2 * useful_chunks; + let SlotWitnessDest { z, a, b, elide_padding_writes } = dst; + for buffer in [&*z, &*a, &*b] { + assert_eq!(buffer.len(), slot_words, "Boolean slot destination length"); + } + + // When padding is observable, initialize it once with parallel contiguous + // stores. The merged union path marks it unread, so production normally + // skips this multi-gigabyte memset entirely. + if !elide_padding_writes { + rayon::join( + || { + z.par_chunks_mut(ZERO_CHUNK_WORDS) + .for_each(|chunk| chunk.fill(F128::ZERO)); + }, + || { + rayon::join( + || { + a.par_chunks_mut(ZERO_CHUNK_WORDS) + .for_each(|chunk| chunk.fill(F128::ZERO)); + }, + || { + b.par_chunks_mut(ZERO_CHUNK_WORDS) + .for_each(|chunk| chunk.fill(F128::ZERO)); + }, + ); + }, + ); + } + + let stripe_len = capacity * k / GROUP_ROWS; + let mut stripe = scratch::take_u8(stripe_len); + if !elide_padding_writes { + stripe.par_chunks_mut(1 << 20).for_each(|chunk| chunk.fill(0)); + } + + let z_ptr = SendPtr(z.as_mut_ptr()); + let a_ptr = SendPtr(a.as_mut_ptr()); + let b_ptr = SendPtr(b.as_mut_ptr()); + let stripe_ptr = SendPtr(stripe.as_mut_ptr()); + let groups = rows.len().div_ceil(GROUP_ROWS); + + (0..groups).into_par_iter().for_each_init( + || BooleanGroupScratch::new(k, stored_words), + |scratch, group| { + scratch.clear_words(); + let first_row = group * GROUP_ROWS; + let live = rows.len().saturating_sub(first_row).min(GROUP_ROWS); + for lane in 0..live { + scratch.z_bits.fill(false); + scratch.a_bits.fill(false); + scratch.b_bits.fill(false); + fill_row(&rows[first_row + lane], &mut scratch.z_bits); + for column in 0..useful_bits { + let a_bit = parity(&scratch.z_bits, &a_rows[column]); + let b_bit = parity(&scratch.z_bits, &b_rows[column]); + scratch.a_bits[column] = a_bit; + scratch.b_bits[column] = b_bit; + assert_eq!( + a_bit & b_bit, + scratch.z_bits[column], + "custom Boolean witness does not satisfy column {column}", + ); + } + for word in 0..useful_words { + let bit = word * 64; + scratch.z_words[word][lane] = pack_bool_word(&scratch.z_bits, bit); + scratch.a_words[word][lane] = pack_bool_word(&scratch.a_bits, bit); + scratch.b_words[word][lane] = pack_bool_word(&scratch.b_bits, bit); + } + } + + // Every group owns disjoint row positions in every chunk-column and a + // disjoint `k`-byte stripe block. The raw pointers avoid materializing + // and then copying a second capacity-sized set of slot buffers. + for chunk in 0..useful_chunks { + for lane in 0..live { + let at = (chunk << nu) + first_row + lane; + let word = 2 * chunk; + unsafe { + z_ptr.get().add(at).write(F128::new( + scratch.z_words[word][lane], + scratch.z_words[word + 1][lane], + )); + a_ptr.get().add(at).write(F128::new( + scratch.a_words[word][lane], + scratch.a_words[word + 1][lane], + )); + b_ptr.get().add(at).write(F128::new( + scratch.b_words[word][lane], + scratch.b_words[word + 1][lane], + )); + } + } + } + + let stripe_base = group * k; + for (word, lanes) in scratch.z_words.iter().take(useful_words).enumerate() + { + let out = unsafe { + std::slice::from_raw_parts_mut( + stripe_ptr.get().add(stripe_base + word * 64), + 64, + ) + }; + transpose_8_u64s_to_64_bytes(lanes, out); + } + if elide_padding_writes { + let tail_start = stripe_base + useful_words * 64; + let tail_len = k - useful_words * 64; + if tail_len != 0 { + unsafe { + std::slice::from_raw_parts_mut( + stripe_ptr.get().add(tail_start), + tail_len, + ) + .fill(0); + } + } + } + }, + ); + + stripe +} + +struct BooleanGroupScratch { + z_bits: Vec, + a_bits: Vec, + b_bits: Vec, + z_words: Vec<[u64; 8]>, + a_words: Vec<[u64; 8]>, + b_words: Vec<[u64; 8]>, +} + +impl BooleanGroupScratch { + fn new(k: usize, stored_words: usize) -> Self { + Self { + z_bits: vec![false; k], + a_bits: vec![false; k], + b_bits: vec![false; k], + z_words: vec![[0; 8]; stored_words], + a_words: vec![[0; 8]; stored_words], + b_words: vec![[0; 8]; stored_words], + } + } + + fn clear_words(&mut self) { + self.z_words.fill([0; 8]); + self.a_words.fill([0; 8]); + self.b_words.fill([0; 8]); + } +} + +#[derive(Clone, Copy)] +struct SendPtr(*mut T); + +unsafe impl Send for SendPtr {} +unsafe impl Sync for SendPtr {} + +impl SendPtr { + fn get(self) -> *mut T { + self.0 + } +} + +fn pack_bool_word(bits: &[bool], start: usize) -> u64 { + bits[start..start + 64] + .iter() + .enumerate() + .fold(0, |word, (bit, value)| word | (u64::from(*value) << bit)) +} + +pub(crate) fn write_f128(bits: &mut [bool], offset: usize, value: F128) { + assert!(offset + 128 <= bits.len()); + for local in 0..64 { + bits[offset + local] = (value.lo >> local) & 1 == 1; + bits[offset + 64 + local] = (value.hi >> local) & 1 == 1; + } +} + +fn parity(bits: &[bool], columns: &[usize]) -> bool { + columns.iter().fold(false, |value, &column| value ^ bits[column]) +} + +fn sparse_matrix(k: usize, rows: Vec>) -> SparseBinaryMatrix { + SparseBinaryMatrix { num_rows: k, num_cols: k, rows } +} + +fn pack_batch_major(bits: &[bool], k_log: usize, nu: usize) -> Vec { + let capacity = 1usize << nu; + let k = 1usize << k_log; + assert_eq!(bits.len(), capacity * k); + let chunks = k / 128; + let mut packed = vec![F128::ZERO; chunks * capacity]; + for chunk in 0..chunks { + for outer in 0..capacity { + let start = outer * k + chunk * 128; + let mut lo = 0u64; + let mut hi = 0u64; + for local in 0..64 { + lo |= u64::from(bits[start + local]) << local; + hi |= u64::from(bits[start + 64 + local]) << local; + } + packed[(chunk << nu) + outer] = F128::new(lo, hi); + } + } + packed +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn one_description_builds_matrices_and_witness() { + let mut builder = BooleanR1csBuilder::new(7, 3); + builder.free_boolean_at(0); + builder.free_boolean_at(1); + let one = builder.alloc_constant_one(); + let product = builder.and(0, 1); + builder.write_xor(2, &[product, 0], one); + let plan = builder.finish(); + let r1cs = plan.block_r1cs(3); + + for (x, y) in [(false, false), (false, true), (true, false), (true, true)] { + let mut row = vec![false; plan.k()]; + plan.fill_row(&mut row, |bits| { + bits[0] = x; + bits[1] = y; + }); + assert_eq!(row[2], (x & y) ^ x); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.k()].copy_from_slice(&row); + assert!(r1cs.satisfies(&witness)); + } + } + + #[test] + fn in_place_partial_driver_matches_allocating_driver() { + let mut builder = BooleanR1csBuilder::new(7, 3); + builder.free_boolean_at(0); + builder.free_boolean_at(1); + let one = builder.alloc_constant_one(); + let product = builder.and(0, 1); + builder.write_xor(2, &[product, 0], one); + let plan = builder.finish(); + let rows = [(false, false), (true, false), (false, true), (true, true)]; + let nu = 4; + let fill = |row: &(bool, bool), bits: &mut [bool]| { + bits[0] = row.0; + bits[1] = row.1; + }; + let expected = generate_boolean_witness(&plan, &rows, nu, fill); + + let words = 1usize << (nu + plan.k_log() - 7); + let mut z = vec![F128::ZERO; words]; + let mut a = vec![F128::ZERO; words]; + let mut b = vec![F128::ZERO; words]; + let stripe = generate_boolean_witness_into( + &plan, + &rows, + nu, + SlotWitnessDest { + z: &mut z, + a: &mut a, + b: &mut b, + elide_padding_writes: false, + }, + fill, + ); + + assert_eq!(z, expected.0); + assert_eq!(a, expected.1); + assert_eq!(b, expected.2); + assert_eq!(stripe, expected.3); + } +} diff --git a/flock-stage3/host/src/config.rs b/flock-stage3/host/src/config.rs new file mode 100644 index 00000000..6c9377c4 --- /dev/null +++ b/flock-stage3/host/src/config.rs @@ -0,0 +1,121 @@ +use flock_prover::{ + hash::HashKind, pcs::ligerito::LigeritoProfile, + r1cs_hashes::blake3::Blake3Setup, +}; + +pub const FLOCK_UPSTREAM_REVISION: &str = + "b310f35f35f68095537150a1c8c0a43caca9a29e"; +pub const STAGE3_TRANSCRIPT_DOMAIN: &[u8] = b"ix:flock-stage3:fri-verifier:v1"; +pub const ENGINE_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:blake3-engine-conformance:v1"; +pub const ARITHMETIC_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:goldilocks-arithmetic-conformance:v1"; +pub const MERKLE_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:blake3-merkle-conformance:v1"; +pub const FRI_FOLD_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:authenticated-fri-fold-conformance:v1"; +pub const FRI_QUERY_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:fri-commit-phase-query-conformance:v1"; +pub const PCS_REDUCTION_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:pcs-reduced-opening-conformance:v1"; +pub const STAGE2_TRANSCRIPT_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:stage2-transcript-conformance:v1"; +pub const TRANSCRIPT_BOUND_PCS_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:transcript-bound-pcs-conformance:v1"; +pub const TRANSCRIPT_BOUND_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:transcript-bound-fri-query-conformance:v1"; +pub const TRANSCRIPT_BOUND_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:transcript-bound-fri-all-queries-conformance:v1"; +pub const TRANSCRIPT_BOUND_PCS_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN: + &[u8] = + b"ix:flock-stage3:transcript-bound-pcs-fri-all-queries-conformance:v1"; +pub const STAGE2_AIR_PCS_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN: &[u8] = + b"ix:flock-stage3:stage2-air-pcs-fri-conformance:v1"; + +const CONFIG_DOMAIN: &[u8; 8] = b"IXFLKCF1"; +const FIELD_F128: u8 = 1; +const PROFILE_FAST128: u8 = 1; +const MERKLE_BLAKE3: u8 = 1; +const TRANSCRIPT_CHAINED_BLAKE3: u8 = 1; + +/// The only Flock protocol configuration accepted by this backend version. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FlockConfigV1; + +impl FlockConfigV1 { + /// Canonical bytes committed by every Stage 3 statement: + /// `IXFLKCF1 || len(rev) u16 LE || rev || field || profile || merkle || + /// transcript || len(domain) u16 LE || domain`. + /// + /// The four one-byte IDs are respectively F128=1, Fast128=1, BLAKE3=1, + /// and chained-BLAKE3=1. New choices require a new configuration version. + pub fn to_bytes(self) -> Vec { + let revision = FLOCK_UPSTREAM_REVISION.as_bytes(); + let domain = STAGE3_TRANSCRIPT_DOMAIN; + let mut bytes = + Vec::with_capacity(8 + 2 + revision.len() + 4 + 2 + domain.len()); + bytes.extend_from_slice(CONFIG_DOMAIN); + bytes.extend_from_slice( + &u16::try_from(revision.len()).expect("revision length").to_le_bytes(), + ); + bytes.extend_from_slice(revision); + bytes.extend_from_slice(&[ + FIELD_F128, + PROFILE_FAST128, + MERKLE_BLAKE3, + TRANSCRIPT_CHAINED_BLAKE3, + ]); + bytes.extend_from_slice( + &u16::try_from(domain.len()).expect("domain length").to_le_bytes(), + ); + bytes.extend_from_slice(domain); + bytes + } + + pub fn digest(self) -> [u8; 32] { + *blake3::hash(&self.to_bytes()).as_bytes() + } + + pub const fn profile(self) -> LigeritoProfile { + LigeritoProfile::Fast128 + } + + pub const fn merkle_hash(self) -> HashKind { + HashKind::Blake3 + } + + /// Construct the pinned Flock BLAKE3 relation used by the engine smoke test. + /// The production Stage 2-verifier relation will reuse these PCS parameters. + pub(crate) fn blake3_setup(self, n_blocks: usize) -> Blake3Setup { + let mut setup = Blake3Setup::with_profile(n_blocks, self.profile()); + setup.pcs_params.merkle_hash = self.merkle_hash(); + setup + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_is_explicit_and_domain_separated() { + let bytes = FlockConfigV1.to_bytes(); + assert_eq!(&bytes[..8], CONFIG_DOMAIN); + assert!( + bytes + .windows(FLOCK_UPSTREAM_REVISION.len()) + .any(|window| window == FLOCK_UPSTREAM_REVISION.as_bytes()) + ); + assert!( + bytes + .windows(STAGE3_TRANSCRIPT_DOMAIN.len()) + .any(|window| window == STAGE3_TRANSCRIPT_DOMAIN) + ); + assert_eq!(FlockConfigV1.profile(), LigeritoProfile::Fast128); + assert_eq!(FlockConfigV1.merkle_hash(), HashKind::Blake3); + assert_eq!( + blake3::Hash::from_bytes(FlockConfigV1.digest()).to_hex().as_str(), + "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc" + ); + } +} diff --git a/flock-stage3/host/src/conformance.rs b/flock-stage3/host/src/conformance.rs new file mode 100644 index 00000000..fc42d89f --- /dev/null +++ b/flock-stage3/host/src/conformance.rs @@ -0,0 +1,186 @@ +//! A real Flock/BLAKE3 round trip used to lock the upstream engine API. +//! +//! This is intentionally not exposed as a Stage 3 proof: the standalone +//! upstream BLAKE3 batch relation has existential (unbound) I/O. + +use anyhow::{Context, Result, bail}; +use flock_prover::{ + challenger::FsChallenger, proof_io::R1csProofBundleLigerito, + r1cs_hashes::blake3::Compression, +}; + +use crate::config::{ENGINE_CONFORMANCE_TRANSCRIPT_DOMAIN, FlockConfigV1}; + +const MAGIC: &[u8; 8] = b"IXFLKB3C"; +const VERSION: u16 = 1; +const HEADER_BYTES: usize = 8 + 2 + 32 + 4 + 8; +const MIN_BLOCKS: usize = 256; +const MAX_BLOCKS: usize = 1 << 20; +const MAX_BUNDLE_BYTES: usize = 64 * 1024 * 1024; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EngineConformanceArtifact { + n_blocks: usize, + bundle_bytes: Vec, +} + +impl EngineConformanceArtifact { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(HEADER_BYTES + self.bundle_bytes.len()); + bytes.extend_from_slice(MAGIC); + bytes.extend_from_slice(&VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.extend_from_slice( + &u32::try_from(self.n_blocks).expect("block count").to_le_bytes(), + ); + bytes.extend_from_slice( + &u64::try_from(self.bundle_bytes.len()) + .expect("bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < HEADER_BYTES { + bail!("truncated Flock conformance artifact"); + } + if &bytes[..8] != MAGIC { + bail!("invalid Flock conformance artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != VERSION { + bail!("unsupported Flock conformance artifact version {version}"); + } + if bytes[10..42] != FlockConfigV1.digest() { + bail!("Flock conformance artifact configuration mismatch"); + } + let n_blocks = + usize::try_from(u32::from_le_bytes(bytes[42..46].try_into().unwrap())) + .expect("u32 fits usize"); + validate_n_blocks(n_blocks)?; + let bundle_len = + usize::try_from(u64::from_le_bytes(bytes[46..54].try_into().unwrap())) + .map_err(|_| { + anyhow::anyhow!("Flock bundle length does not fit usize") + })?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock bundle length {bundle_len}"); + } + let expected_len = HEADER_BYTES + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("Flock artifact length overflow"))?; + if bytes.len() != expected_len { + bail!( + "Flock conformance artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + Ok(Self { n_blocks, bundle_bytes: bytes[HEADER_BYTES..].to_vec() }) + } + + pub fn n_blocks(&self) -> usize { + self.n_blocks + } + + pub fn bundle_bytes(&self) -> &[u8] { + &self.bundle_bytes + } +} + +/// Prove an existential batch of valid BLAKE3 compression rows using the +/// exact hash/profile choices intended for Stage 3. +pub fn prove_engine_conformance( + blocks: &[Compression], +) -> Result { + validate_n_blocks(blocks.len())?; + let setup = FlockConfigV1.blake3_setup(blocks.len()); + let mut challenger = + FsChallenger::with_chained_blake3(ENGINE_CONFORMANCE_TRANSCRIPT_DOMAIN); + let (proof, commitment, _) = setup.prove_fast(blocks, &mut challenger); + let bundle = R1csProofBundleLigerito { commitment, proof }; + let bundle_bytes = bundle.to_bytes(); + if bundle_bytes.len() > MAX_BUNDLE_BYTES { + bail!("Flock proof bundle exceeds {MAX_BUNDLE_BYTES} bytes"); + } + Ok(EngineConformanceArtifact { n_blocks: blocks.len(), bundle_bytes }) +} + +pub fn verify_engine_conformance( + artifact: &EngineConformanceArtifact, +) -> Result<()> { + validate_n_blocks(artifact.n_blocks)?; + let bundle = R1csProofBundleLigerito::from_bytes(&artifact.bundle_bytes) + .context("decode Flock proof bundle")?; + let setup = FlockConfigV1.blake3_setup(artifact.n_blocks); + let mut challenger = + FsChallenger::with_chained_blake3(ENGINE_CONFORMANCE_TRANSCRIPT_DOMAIN); + setup.verify(&bundle.commitment, &bundle.proof, &mut challenger).map_err( + |error| anyhow::anyhow!("Flock engine proof rejected: {error:?}"), + )?; + Ok(()) +} + +fn validate_n_blocks(n_blocks: usize) -> Result<()> { + if !(MIN_BLOCKS..=MAX_BLOCKS).contains(&n_blocks) { + bail!( + "Flock conformance batch has {n_blocks} blocks; expected {MIN_BLOCKS}..={MAX_BLOCKS}" + ); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn envelope_is_strict_and_configuration_bound() { + let artifact = EngineConformanceArtifact { + n_blocks: MIN_BLOCKS, + bundle_bytes: vec![1, 2, 3], + }; + let bytes = artifact.to_bytes(); + assert_eq!( + EngineConformanceArtifact::from_bytes(&bytes).unwrap(), + artifact + ); + + let mut extended = bytes.clone(); + extended.push(0); + assert!(EngineConformanceArtifact::from_bytes(&extended).is_err()); + + let mut wrong_config = bytes; + wrong_config[10] ^= 1; + assert!(EngineConformanceArtifact::from_bytes(&wrong_config).is_err()); + } + + #[test] + #[ignore = "large upstream Flock proof; run explicitly for revision conformance"] + fn real_fast128_blake3_round_trip() { + let blocks: Vec = (0..MIN_BLOCKS) + .map(|index| { + let mut message = [0u32; 16]; + message[0] = u32::try_from(index).unwrap(); + ([0u32; 8], message, index as u64, 64, 0) + }) + .collect(); + let artifact = prove_engine_conformance(&blocks).expect("prove"); + eprintln!( + "Flock Fast128/BLAKE3 conformance bundle: {} bytes", + artifact.bundle_bytes().len() + ); + let encoded = artifact.to_bytes(); + let decoded = EngineConformanceArtifact::from_bytes(&encoded).unwrap(); + verify_engine_conformance(&decoded).expect("verify"); + + let mut mutated = decoded; + let flip_at = mutated.bundle_bytes.len() / 2; + mutated.bundle_bytes[flip_at] ^= 1; + assert!( + verify_engine_conformance(&mutated).is_err(), + "mutated Flock proof must be rejected" + ); + } +} diff --git a/flock-stage3/host/src/equality.rs b/flock-stage3/host/src/equality.rs new file mode 100644 index 00000000..c74292ab --- /dev/null +++ b/flock-stage3/host/src/equality.rs @@ -0,0 +1,122 @@ +//! A directed equality assertion for two `F128` wires. +//! +//! Flock wire connections merge producer classes, so connecting two values +//! that were independently computed can create a cyclic circuit graph. This +//! gate keeps the graph directed: it emits their bitwise XOR, which callers +//! pin to the fixed zero wire. + +use flock_prover::{ + circuit::builder::{GateType, SlotWitness}, + field::F128, + r1cs::BlockR1cs, + schedule::{IoWord, TableType}, + union::SlotWitnessDest, +}; + +use crate::boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_witness_into, + write_f128, +}; + +const K_LOG: usize = 9; +const LEFT_BASE: usize = 0; +const RIGHT_BASE: usize = 128; +const RESIDUAL_BASE: usize = 256; +const COLUMNS: usize = 384; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct F128EqualityGate { + pub(crate) nu: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct F128EqualityRow { + left: F128, + right: F128, +} + +impl GateType for F128EqualityGate { + type Row = F128EqualityRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_f128_equality_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::output(2), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let left = inputs[0]; + let right = inputs[1]; + outputs.push(F128::new(left.lo ^ right.lo, left.hi ^ right.hi)); + F128EqualityRow { left, right } + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_f128_equality_r1cs(nu: usize) -> BlockR1cs { + build_plan().block_r1cs(nu) +} + +pub(crate) fn generate_f128_equality_witness_into( + rows: &[F128EqualityRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + let plan = build_plan(); + generate_boolean_witness_into(&plan, rows, nu, dst, |row, bits| { + write_f128(bits, LEFT_BASE, row.left); + write_f128(bits, RIGHT_BASE, row.right); + }) +} + +fn build_plan() -> BooleanR1csPlan { + let mut builder = BooleanR1csBuilder::new(K_LOG, COLUMNS); + for column in LEFT_BASE..RIGHT_BASE + 128 { + builder.free_boolean_at(column); + } + let one = builder.alloc_constant_one(); + for bit in 0..128 { + builder.write_xor( + RESIDUAL_BASE + bit, + &[LEFT_BASE + bit, RIGHT_BASE + bit], + one, + ); + } + builder.finish() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn equality_r1cs_rejects_nonzero_residual() { + let plan = build_plan(); + let r1cs = plan.block_r1cs(3); + let left = F128::new(0x1234, 0x5678); + let mut logical = vec![false; plan.k()]; + plan.fill_row(&mut logical, |bits| { + write_f128(bits, LEFT_BASE, left); + write_f128(bits, RIGHT_BASE, left); + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.k()].copy_from_slice(&logical); + assert!(r1cs.satisfies(&witness)); + + let mut wrong = witness; + wrong[RIGHT_BASE + 7] ^= true; + assert!(!r1cs.satisfies(&wrong)); + } +} diff --git a/flock-stage3/host/src/extension.rs b/flock-stage3/host/src/extension.rs new file mode 100644 index 00000000..02cc68ff --- /dev/null +++ b/flock-stage3/host/src/extension.rs @@ -0,0 +1,526 @@ +//! Degree-two Goldilocks extension arithmetic lowered to reusable base gates. +//! +//! Extension elements are packed as `F128::new(c0, c1)` and use +//! `X^2 = 7`, matching Plonky3's Goldilocks binomial extension. The lowering +//! deliberately composes the already checked base-field addition and +//! multiplication relations rather than introducing another large monolithic +//! arithmetic table. + +use std::cell::Cell; + +use flock_prover::{ + circuit::builder::{GateType, SlotId, SlotWitness, Wire}, + field::F128, + r1cs::BlockR1cs, + schedule::{IoWord, TableType}, + union::SlotWitnessDest, +}; + +use crate::{ + boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_witness, + generate_boolean_witness_into, write_f128, + }, + goldilocks::{CanonicalGoldilocksQuadGate, GoldilocksAddPairGate}, + multiplication::{GoldilocksMulPairGate, goldilocks_mul}, + sizing::CircuitEmitter, +}; + +const REPACK_K_LOG: usize = 10; +const FIRST_BASE: usize = 0; +const SECOND_BASE: usize = 128; +const DUPLICATE_LOW_BASE: usize = 256; +const DUPLICATE_HIGH_BASE: usize = 384; +const SWAP_BASE: usize = 512; +const SELECT_BASE: usize = 640; +const REPACK_COLUMNS: usize = 768; + +/// Fixed lane transforms used by the degree-two extension lowering. +/// +/// For `first = [a,b]` and `second = [c,d]`, the outputs are +/// `[a,a]`, `[b,b]`, `[b,a]`, and `[a,d]`. +#[derive(Clone, Copy, Debug)] +pub(crate) struct GoldilocksLaneRepackGate { + pub(crate) nu: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct GoldilocksLaneRepackRow { + first: F128, + second: F128, +} + +impl GateType for GoldilocksLaneRepackGate { + type Row = GoldilocksLaneRepackRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_lane_repack_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::output(2), + IoWord::output(3), + IoWord::output(4), + IoWord::output(5), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let first = inputs[0]; + let second = inputs[1]; + outputs.extend_from_slice(&[ + F128::new(first.lo, first.lo), + F128::new(first.hi, first.hi), + F128::new(first.hi, first.lo), + F128::new(first.lo, second.hi), + ]); + GoldilocksLaneRepackRow { first, second } + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_lane_repack_r1cs(nu: usize) -> BlockR1cs { + build_lane_repack_plan().block_r1cs(nu) +} + +pub(crate) fn generate_lane_repack_witness( + rows: &[GoldilocksLaneRepackRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + let plan = build_lane_repack_plan(); + generate_boolean_witness(&plan, rows, nu, |row, bits| { + write_f128(bits, FIRST_BASE, row.first); + write_f128(bits, SECOND_BASE, row.second); + }) +} + +pub(crate) fn generate_lane_repack_witness_into( + rows: &[GoldilocksLaneRepackRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + let plan = build_lane_repack_plan(); + generate_boolean_witness_into(&plan, rows, nu, dst, |row, bits| { + write_f128(bits, FIRST_BASE, row.first); + write_f128(bits, SECOND_BASE, row.second); + }) +} + +fn build_lane_repack_plan() -> BooleanR1csPlan { + let mut builder = BooleanR1csBuilder::new(REPACK_K_LOG, REPACK_COLUMNS); + for column in FIRST_BASE..SECOND_BASE + 128 { + builder.free_boolean_at(column); + } + for bit in 0..64 { + let first_low = FIRST_BASE + bit; + let first_high = FIRST_BASE + 64 + bit; + let second_high = SECOND_BASE + 64 + bit; + for output in [DUPLICATE_LOW_BASE + bit, DUPLICATE_LOW_BASE + 64 + bit] { + builder.write_product_of_parities(output, &[first_low], &[first_low]); + } + for output in [DUPLICATE_HIGH_BASE + bit, DUPLICATE_HIGH_BASE + 64 + bit] { + builder.write_product_of_parities(output, &[first_high], &[first_high]); + } + builder.write_product_of_parities( + SWAP_BASE + bit, + &[first_high], + &[first_high], + ); + builder.write_product_of_parities( + SWAP_BASE + 64 + bit, + &[first_low], + &[first_low], + ); + builder.write_product_of_parities( + SELECT_BASE + bit, + &[first_low], + &[first_low], + ); + builder.write_product_of_parities( + SELECT_BASE + 64 + bit, + &[second_high], + &[second_high], + ); + } + builder.finish() +} + +/// The four table slots and fixed zero wire needed by Goldilocks arithmetic. +/// Call `finish_canonical` before finishing the emission, including a census. +pub(crate) struct GoldilocksCircuitSlots { + pub(crate) add: SlotId, + pub(crate) mul: SlotId, + pub(crate) canonical: SlotId, + pub(crate) repack: SlotId, + zero: Wire, + assertion_group: Cell>, + pending_canonical: Cell>, +} + +impl GoldilocksCircuitSlots { + pub(crate) fn declare(builder: &mut impl CircuitEmitter, nu: usize) -> Self { + let add = builder.slot(GoldilocksAddPairGate { nu }); + let mul = builder.slot(GoldilocksMulPairGate { nu }); + let canonical = builder.slot(CanonicalGoldilocksQuadGate { nu }); + let repack = builder.slot(GoldilocksLaneRepackGate { nu }); + let zero = builder.fixed_public_input(F128::ZERO); + Self { + add, + mul, + canonical, + repack, + zero, + assertion_group: Cell::new(None), + pending_canonical: Cell::new(None), + } + } + + fn assert_zero(&self, builder: &mut impl CircuitEmitter, residual: Wire) { + // Keep data zero input-only. Connecting residual outputs to that same + // class creates producer -> consumer cycles. The pinned builder also + // appends later gate inputs to the original wire, not its union-find + // root, so merging data zero away could silently split its circuit cells. + // + // Each assertion class instead has its own canonical(0) output, which + // the existing table constrains to zero. No member is used as data. + // Bound the class size because the pinned dataflow checker scans every + // class cell for every producer, even when there are no consumers. + const GROUP_SIZE: usize = 256; + let (zero, used) = match self.assertion_group.get() { + Some((zero, used)) if used < GROUP_SIZE => (zero, used), + _ => (builder.gate(self.canonical, &[self.zero, self.zero])[0], 0), + }; + // connect moves the second class into the first; keep this anchor stable. + builder.connect(zero, residual); + self.assertion_group.set(Some((zero, used + 1))); + } + + pub(crate) fn assert_canonical( + &self, + builder: &mut impl CircuitEmitter, + value: Wire, + ) { + // Batch by emission order, never by wire identity: counting wires are + // all the same opaque placeholder. Each requested check is retained. + if let Some(first) = self.pending_canonical.take() { + let violation = builder.gate(self.canonical, &[first, value])[0]; + self.assert_zero(builder, violation); + } else { + self.pending_canonical.set(Some(value)); + } + } + + /// Flush an odd final check before finishing either a census or a circuit. + /// The unused word is the fixed, input-only zero, not an assertion output. + pub(crate) fn finish_canonical(&self, builder: &mut impl CircuitEmitter) { + if let Some(value) = self.pending_canonical.take() { + let violation = builder.gate(self.canonical, &[value, self.zero])[0]; + self.assert_zero(builder, violation); + } + } + + pub(crate) fn add( + &self, + builder: &mut impl CircuitEmitter, + left: Wire, + right: Wire, + ) -> Wire { + let outputs = builder.gate(self.add, &[left, right]); + for &residual in &outputs[1..] { + self.assert_zero(builder, residual); + } + self.assert_canonical(builder, outputs[0]); + outputs[0] + } + + pub(crate) fn mul( + &self, + builder: &mut impl CircuitEmitter, + left: Wire, + right: Wire, + ) -> Wire { + let outputs = builder.gate(self.mul, &[left, right]); + for &residual in &outputs[1..] { + self.assert_zero(builder, residual); + } + self.assert_canonical(builder, outputs[0]); + outputs[0] + } + + /// Multiply two packed extension values in `Goldilocks[X]/(X^2 - 7)`. + pub(crate) fn ext2_mul( + &self, + builder: &mut impl CircuitEmitter, + left: Wire, + right: Wire, + ) -> Wire { + self.assert_canonical(builder, left); + self.assert_canonical(builder, right); + + let left_lanes = builder.gate(self.repack, &[left, self.zero]); + let products_low = self.mul(builder, left_lanes[0], right); + let products_high = self.mul(builder, left_lanes[1], right); + + let high_repacked = builder.gate(self.repack, &[products_high, self.zero]); + let reversed_high = high_repacked[2]; + let twice = self.add(builder, reversed_high, reversed_high); + let four_times = self.add(builder, twice, twice); + let six_times = self.add(builder, four_times, twice); + let seven_times = self.add(builder, six_times, reversed_high); + let selected = builder.gate(self.repack, &[seven_times, reversed_high])[3]; + self.add(builder, products_low, selected) + } + + /// Embed the low `u64` lane as the constant-coordinate element `[lo, 0]`. + pub(crate) fn embed_low_lane( + &self, + builder: &mut impl CircuitEmitter, + value: Wire, + ) -> Wire { + builder.gate(self.repack, &[value, self.zero])[3] + } + + /// Split `[c0, c1]` into the two base-coordinate embeddings `[c0, 0]` + /// and `[c1, 0]` used by the coordinate-expanded AIR constraints. + pub(crate) fn ext2_coordinates( + &self, + builder: &mut impl CircuitEmitter, + value: Wire, + ) -> [Wire; 2] { + let lanes = builder.gate(self.repack, &[value, self.zero]); + let low = lanes[3]; + let high_first = lanes[2]; + let high = builder.gate(self.repack, &[high_first, self.zero])[3]; + [low, high] + } +} + +pub(crate) fn goldilocks_ext2_mul(left: F128, right: F128) -> F128 { + F128::new( + crate::goldilocks::goldilocks_add( + goldilocks_mul(left.lo, right.lo), + goldilocks_mul(7, goldilocks_mul(left.hi, right.hi)), + ), + crate::goldilocks::goldilocks_add( + goldilocks_mul(left.lo, right.hi), + goldilocks_mul(left.hi, right.lo), + ), + ) +} + +#[cfg(test)] +mod tests { + use flock_prover::{ + circuit::{Cell as CircuitCell, CellSlot, builder::ShapeBuilder}, + schedule::IoDirection, + }; + use multi_stark::{ + p3_field::{ + BasedVectorSpace, PrimeCharacteristicRing, PrimeField64, + extension::BinomialExtensionField, + }, + p3_goldilocks::Goldilocks, + }; + + use super::*; + use crate::goldilocks::GOLDILOCKS_MODULUS; + + #[test] + fn packed_canonical_checks_preserve_odd_tails_and_counting_parity() { + use crate::sizing::CountingEmitter; + fn emit(builder: &mut impl CircuitEmitter, nu: usize, checks: usize) { + let slots = GoldilocksCircuitSlots::declare(builder, nu); + for _ in 0..checks { + let value = builder.input(); + slots.assert_canonical(builder, value); + } + slots.finish_canonical(builder); + // Finalization is idempotent; no check is duplicated on a second call. + slots.finish_canonical(builder); + } + for checks in [0usize, 1, 2, 3, 511, 512, 513] { + let mut count = CountingEmitter::new(); + emit(&mut count, CountingEmitter::COUNT_NU, checks); + let packed = checks.div_ceil(2); + assert_eq!( + count + .table_rows() + .find(|&(name, _)| name == "CanonicalGoldilocksQuadGate"), + Some(("CanonicalGoldilocksQuadGate", packed + packed.div_ceil(256))), + ); + let mut builder = ShapeBuilder::new(10); + emit(&mut builder, 10, checks); + let shape = builder.finish().unwrap(); + count.ensure_matches(&shape).unwrap(); + let mut inputs = vec![F128::new(17, GOLDILOCKS_MODULUS - 1); checks + 1]; + inputs[0] = F128::ZERO; + shape.run(&inputs, &[]); + for index in 1..=checks { + for invalid in + [F128::new(GOLDILOCKS_MODULUS, 0), F128::new(0, u64::MAX)] + { + let mut mutated = inputs.clone(); + mutated[index] = invalid; + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + shape.run(&mutated, &[]) + })) + .is_err(), + "check {index} of {checks}" + ); + } + } + } + } + + #[test] + fn assertion_groups_preserve_every_later_data_zero_cell() { + const NU: usize = 10; + const ROWS: usize = 600; + let mut builder = ShapeBuilder::new(NU); + let slots = GoldilocksCircuitSlots::declare(&mut builder, NU); + let value = builder.public_input(); + let mut result = value; + for _ in 0..ROWS { + slots.assert_canonical(&mut builder, value); + // The data-zero input is consumed AFTER residuals have been connected. + result = slots.embed_low_lane(&mut builder, value); + } + builder.publish(result); + slots.finish_canonical(&mut builder); + let shape = builder.finish().unwrap(); + let inputs = [F128::ZERO, F128::new(17, 23)]; + let witness = shape.run(&inputs, &[]); + assert_eq!(witness.public, [inputs[0], inputs[1], F128::new(17, 0)]); + + // Inspect the actual sigma classes, not just the native runner: the + // pinned builder can resolve online inputs correctly while losing cells + // appended to a wire that was previously merged into another root. + let cells = shape.circuit.cells(); + let public_zero = + cells.cell_index(CircuitCell::new(cells.num_gate_slots(), 0)); + let zero_class = shape + .circuit + .wires() + .iter() + .find(|class| class.contains(&public_zero)) + .expect("fixed data zero belongs to a wiring class"); + let repack = shape.registry_slot(slots.repack); + let second_input = cells + .slots() + .iter() + .position(|slot| { + matches!(slot, CellSlot::Gate { ty, word } + if *ty == repack && word.word_col == 1 && word.dir == IoDirection::In) + }) + .unwrap(); + for row in 0..ROWS { + assert!( + zero_class + .contains(&cells.cell_index(CircuitCell::new(second_input, row,))) + ); + } + assert!(zero_class.iter().all(|index| { + !matches!(cells.slots()[index >> NU], CellSlot::Gate { word, .. } + if word.dir == IoDirection::Out) + })); + + let canonical = shape.registry_slot(slots.canonical); + let assertion_classes: Vec<_> = shape + .circuit + .wires() + .iter() + .filter(|class| { + class.iter().any(|index| { + matches!(cells.slots()[index >> NU], CellSlot::Gate { ty, word } + if ty == canonical && word.dir == IoDirection::Out) + }) + }) + .collect(); + assert_eq!(assertion_classes.len(), ROWS.div_ceil(2).div_ceil(256)); + for class in assertion_classes { + assert!(class.len() <= 257); + assert!(class.iter().all(|index| { + matches!(cells.slots()[index >> NU], CellSlot::Gate { word, .. } + if word.dir == IoDirection::Out) + })); + } + for bad in [F128::new(GOLDILOCKS_MODULUS, 0), F128::new(0, u64::MAX)] { + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + shape.run(&[F128::ZERO, bad], &[]) + })) + .is_err() + ); + } + } + + #[test] + fn native_ext2_mul_matches_plonky3() { + let cases = [ + ([0, 0], [0, 0]), + ([1, 0], [0, 1]), + ([GOLDILOCKS_MODULUS - 1, 17], [23, GOLDILOCKS_MODULUS - 2]), + ([0x1234_5678_9abc_def0, 0xfedc_ba98_7654_3210], [7, 11]), + ]; + for (left, right) in cases { + let reference = BinomialExtensionField::::new([ + Goldilocks::from_u64(left[0]), + Goldilocks::from_u64(left[1]), + ]) * BinomialExtensionField::::new([ + Goldilocks::from_u64(right[0]), + Goldilocks::from_u64(right[1]), + ]); + let reference: &[Goldilocks] = reference.as_basis_coefficients_slice(); + let actual = goldilocks_ext2_mul( + F128::new(left[0], left[1]), + F128::new(right[0], right[1]), + ); + assert_eq!(actual.lo, reference[0].as_canonical_u64()); + assert_eq!(actual.hi, reference[1].as_canonical_u64()); + } + } + + #[test] + fn lane_repack_r1cs_matches_gate_semantics() { + let row = GoldilocksLaneRepackRow { + first: F128::new(0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210), + second: F128::new(9, 0x55aa_aa55_1234_5678), + }; + let plan = build_lane_repack_plan(); + let r1cs = plan.block_r1cs(3); + let mut logical = vec![false; plan.k()]; + plan.fill_row(&mut logical, |bits| { + write_f128(bits, FIRST_BASE, row.first); + write_f128(bits, SECOND_BASE, row.second); + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.k()].copy_from_slice(&logical); + assert!(r1cs.satisfies(&witness)); + + let outputs = [ + F128::new(row.first.lo, row.first.lo), + F128::new(row.first.hi, row.first.hi), + F128::new(row.first.hi, row.first.lo), + F128::new(row.first.lo, row.second.hi), + ]; + for (index, output) in outputs.into_iter().enumerate() { + let mut encoded = vec![false; 128]; + write_f128(&mut encoded, 0, output); + assert_eq!( + &logical[DUPLICATE_LOW_BASE + index * 128 + ..DUPLICATE_LOW_BASE + (index + 1) * 128], + encoded + ); + } + } +} diff --git a/flock-stage3/host/src/fri.rs b/flock-stage3/host/src/fri.rs new file mode 100644 index 00000000..5459f11a --- /dev/null +++ b/flock-stage3/host/src/fri.rs @@ -0,0 +1,8404 @@ +//! One authenticated binary FRI fold lowered into the Flock relation. +//! +//! This is the first conformance artifact that composes proof semantics rather +//! than testing an isolated primitive. It reconstructs the ordered evaluation +//! pair from the query-index bit, hashes the four Goldilocks coordinates with +//! Plonky3's serialized BLAKE3 leaf convention, authenticates the leaf, derives +//! the bit-reversed subgroup point, and constrains the binary fold. +//! +//! Division is deliberately absent from the circuit. The usual equation +//! +//! `f = (e0 + e1)/2 + beta * (e0 - e1)/(2s)` +//! +//! is constrained in the equivalent denominator-free form +//! +//! `2s*f + beta*e1 = s*(e0 + e1) + beta*e0`. + +use ::blake3 as native_blake3; +use aiur::vk_codec::AiurVerifyingKey; +use anyhow::{Context, Result, bail}; +use bincode::Options; +use flock_prover::{ + challenger::FsChallenger, + circuit::builder::{CircuitShape, ShapeBuilder, SlotId, Wire}, + field::F128, + lincheck::{CscCircuit, LincheckCircuit}, + pcs::Commitment, + proof::R1csProofCircuitMerged, + prover::{self, UnionSlotProverInput}, + r1cs::BlockR1cs, + r1cs_hashes::blake3 as flock_blake3, + union::{SlotWitnessDest, UnionInstance}, + verifier, +}; +use ix_terminal::{ + Stage2RootStatementV1, ValidatedStage2RootV1, fri_parameter_words, +}; +use multi_stark::{ + p3_field::{BasedVectorSpace, Field, PrimeCharacteristicRing, PrimeField64}, + types::{ExtVal, FriParameters, Val}, +}; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::sizing::{CircuitEmitter, CountingEmitter}; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex, OnceLock}, +}; + +use crate::{ + FRI_FOLD_CONFORMANCE_TRANSCRIPT_DOMAIN, + FRI_QUERY_CONFORMANCE_TRANSCRIPT_DOMAIN, FlockConfigV1, + PCS_REDUCTION_CONFORMANCE_TRANSCRIPT_DOMAIN, + STAGE2_AIR_PCS_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, Stage3TypedProofWitnessV1, + TRANSCRIPT_BOUND_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_PCS_CONFORMANCE_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_PCS_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + air::{Stage2AirProgramV1, constrain_stage2_air}, + binding::{ + Blake3Gate, CHUNK_END, CHUNK_START, IV, ROOT, pack_bytes, pack_params, + pack8, pcs_params, + }, + equality::{ + F128EqualityGate, build_f128_equality_r1cs, + generate_f128_equality_witness_into, + }, + extension::{ + GoldilocksCircuitSlots, GoldilocksLaneRepackGate, build_lane_repack_r1cs, + generate_lane_repack_witness_into, + }, + goldilocks::{ + CanonicalGoldilocksQuadGate, GOLDILOCKS_MODULUS, GoldilocksAddPairGate, + build_canonical_quad_r1cs, build_goldilocks_add_r1cs, + generate_canonical_quad_witness_into, generate_goldilocks_add_witness_into, + }, + merkle::{ + DigestOrderGate, build_digest_order_r1cs, + generate_digest_order_witness_into, + }, + multiplication::{ + GoldilocksMulPairGate, build_goldilocks_mul_r1cs, + generate_goldilocks_mul_witness_into, goldilocks_mul, + }, + transcript::{ + FriTranscriptCircuitSlots, GoldilocksSampleGate, HashSampleGate, + Stage2FriTranscriptChallengesV1, Stage2FriTranscriptReplayV1, + Stage2TranscriptByteBindingV1, Stage2TranscriptReplayV1, + Stage2TranscriptSegmentV1, TranscriptCircuitSlots, U64SplitGate, + build_goldilocks_sample_r1cs, build_hash_sample_r1cs, build_u64_split_r1cs, + constrain_hash, constrain_stage2_fri_transcript, + constrain_stage2_transcript, generate_goldilocks_sample_witness_into, + generate_hash_sample_witness_into, generate_u64_split_witness_into, + hash_trace, transcript_challenge_words, transcript_nu, + }, + window::{ + ByteWindowGate, build_byte_window_r1cs, generate_byte_window_witness_into, + }, +}; + +pub const FRI_FOLD_CONFORMANCE_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLKFR1"; +pub const FRI_COMMIT_PHASE_CONFORMANCE_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLFQ01"; +pub const PCS_REDUCTION_CONFORMANCE_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLPR01"; +const ARTIFACT_VERSION: u16 = 1; +const CONFIG_OFFSET: usize = 10; +const LOG_HEIGHT_OFFSET: usize = CONFIG_OFFSET + 32; +const QUERY_INDEX_OFFSET: usize = LOG_HEIGHT_OFFSET + 1; +const FOLDED_OFFSET: usize = QUERY_INDEX_OFFSET + 4; +const SIBLING_OFFSET: usize = FOLDED_OFFSET + 16; +const BETA_OFFSET: usize = SIBLING_OFFSET + 16; +const RESULT_OFFSET: usize = BETA_OFFSET + 16; +const PATH_OFFSET: usize = RESULT_OFFSET + 16; +const FIXED_SUFFIX_BYTES: usize = 32 + 32 + 8; +const MAX_BUNDLE_BYTES: usize = 64 * 1024 * 1024; +const MIN_LOG_HEIGHT: u8 = 1; +const MAX_LOG_HEIGHT: u8 = 31; +const MAX_REDUCED_OPENING_WIDTH: usize = 1 << 16; +// The arithmetic slots need enough rows for the bit-reversed exponentiation +// at the maximum supported height. This also keeps every table in a Flock +// Fast128 geometry exercised by the existing conformance proofs. +const NU: usize = 10; + +/// The witness values consumed by one binary FRI commit-phase opening. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FriFoldQueryV1 { + /// Height of the folded row (the original query has one additional bit). + pub log_height: u8, + /// Original FRI query index. Bit zero chooses the evaluation within the + /// pair; bits `1..=log_height` authenticate the pair and derive `s`. + pub query_index: u32, + pub folded: [u64; 2], + pub sibling: [u64; 2], + pub beta: [u64; 2], + /// Cap-height-zero authentication path for the row `[e0, e1]`. + pub opening_proof: Vec<[u8; 32]>, +} + +impl FriFoldQueryV1 { + pub fn folded_result(&self) -> Result<[u64; 2]> { + validate_query(self)?; + Ok(native_fold(self)) + } + + pub fn commitment_root(&self) -> Result<[u8; 32]> { + validate_query(self)?; + Ok(native_root(self)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FriCommitPhaseRoundV1 { + pub sibling: [u64; 2], + pub beta: [u64; 2], + /// Reduced opening at this folded height, if one is scheduled. It is rolled + /// in as `beta^2 * reduced_opening` after the binary fold. + pub reduced_opening: Option<[u64; 2]>, + pub opening_proof: Vec<[u8; 32]>, +} + +/// A complete binary FRI commit-phase fold chain for one sampled query. +/// +/// This intentionally excludes reduced-opening roll-ins and transcript replay; +/// those are separate semantic slices. Each round consumes the next low query +/// bit, authenticates its extension pair, and feeds its constrained result +/// directly into the following round. The last result must equal the constant +/// final polynomial. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FriCommitPhaseQueryV1 { + /// Folded height of the first round. Round `r` has height + /// `initial_log_height - r`. + pub initial_log_height: u8, + pub query_index: u32, + pub initial_folded: [u64; 2], + pub rounds: Vec, + pub final_polynomial: [u64; 2], +} + +impl FriCommitPhaseQueryV1 { + pub fn commitment_roots(&self) -> Result> { + let computation = compute_commit_phase(self)?; + ensure_final_polynomial(self, &computation)?; + Ok(computation.roots) + } + + pub fn folded_results(&self) -> Result> { + let computation = compute_commit_phase(self)?; + ensure_final_polynomial(self, &computation)?; + Ok(computation.results) + } +} + +/// One authenticated PCS row reduced into the FRI accumulator. +/// +/// The conformance circuit supports one BLAKE3 leaf block (at most eight +/// Goldilocks values). Wider rows will use the same arithmetic but require the +/// multi-block/tree leaf hasher before the production relation is complete. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PcsReducedOpeningV1 { + pub log_height: u8, + pub query_index: u32, + pub opened_values: Vec, + pub opened_at_z: Vec<[u64; 2]>, + pub zeta: [u64; 2], + pub alpha: [u64; 2], + pub initial_alpha_power: [u64; 2], + pub initial_accumulator: [u64; 2], + pub opening_proof: Vec<[u8; 32]>, +} + +impl PcsReducedOpeningV1 { + pub fn reduced_accumulator(&self) -> Result<[u64; 2]> { + Ok(compute_pcs_reduction(self)?.accumulator) + } + + pub fn next_alpha_power(&self) -> Result<[u64; 2]> { + Ok(compute_pcs_reduction(self)?.alpha_power) + } + + pub fn commitment_root(&self) -> Result<[u8; 32]> { + Ok(compute_pcs_reduction(self)?.root) + } +} + +/// An opening point used by the specialised Stage 2 PCS verifier. +/// +/// Stage 1, Stage 2, and preprocessed matrices are opened at both `zeta` and +/// `zeta * g`, while quotient matrices are opened only at `zeta`. Keeping the +/// point derivation in the relation prevents the prover from supplying a +/// second, unbound point. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Stage2PcsOpeningPointV1 { + Zeta, + ZetaNext { log_degree: u8 }, +} + +/// Verifier-known metadata for one matrix in a Stage 2 input commitment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2PcsMatrixV1 { + /// Log2 of the LDE matrix height, including the FRI blowup. + pub log_height: u8, + /// Number of base-field columns in the authenticated row. + pub width: usize, + /// Opening points in the exact PCS batching order. + pub opening_points: Vec, + /// First `u64` lane of this matrix's contiguous extension-valued OOD + /// openings in the transcript's PCS-opening observation segment. + pub opened_values: Stage2TranscriptByteBindingV1, +} + +/// One multi-matrix MMCS commitment used as a PCS input batch. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2PcsBatchV1 { + /// First `u64` lane of the 32-byte cap-height-zero commitment root in the + /// constrained transcript prefix. + pub commitment: Stage2TranscriptByteBindingV1, + pub matrices: Vec, +} + +/// Shared, verifier-known PCS instance for all sampled FRI queries. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2PcsInstanceV1 { + pub log_global_height: u8, + pub log_blowup: u8, + pub batches: Vec, +} + +/// Per-query rows and a legacy full Merkle path for one input batch. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2PcsBatchOpeningV1 { + pub opened_rows: Vec>, + pub opening_proof: Vec<[u8; 32]>, +} + +/// Every input-batch opening belonging to one transcript-derived query. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2PcsQueryV1 { + pub batch_openings: Vec, +} + +/// One query's authenticated PCS input followed by its FRI commit-phase +/// opening chain. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TranscriptBoundPcsFriQueryV1 { + pub pcs: Stage2PcsQueryV1, + pub fri: FriCommitPhaseQueryV1, +} + +/// Exact typed Stage 2 PCS/FRI witness prepared for the combined Flock +/// relation. Commitment and OOD bindings point into `prefix`; query indices +/// and betas come from `fri_transcript`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2PcsFriWitnessV1 { + pub prefix: Stage2TranscriptReplayV1, + pub fri_transcript: Stage2FriTranscriptReplayV1, + pub pcs_instance: Stage2PcsInstanceV1, + pub queries: Vec, +} + +impl Stage2PcsFriWitnessV1 { + pub fn from_prepared( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + ) -> Result { + let typed = Stage3TypedProofWitnessV1::from_prepared(prepared, fri)?; + Self::from_prepared_and_typed(prepared, fri, &typed) + } + + pub fn from_prepared_and_typed( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + typed: &Stage3TypedProofWitnessV1, + ) -> Result { + build_stage2_pcs_fri_witness(prepared, fri, typed) + } +} + +/// All currently lowered Stage 2 verifier semantics: compiled AIR/logUp OOD +/// evaluation plus transcript-bound PCS and every FRI query. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2AirPcsFriWitnessV1 { + pub pcs_fri: Stage2PcsFriWitnessV1, + pub air: Stage2AirProgramV1, +} + +impl Stage2AirPcsFriWitnessV1 { + pub fn from_prepared( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + ) -> Result { + let typed = Stage3TypedProofWitnessV1::from_prepared(prepared, fri)?; + Self::from_prepared_and_typed(prepared, fri, &typed) + } + + pub(crate) fn from_prepared_and_typed( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + typed: &Stage3TypedProofWitnessV1, + ) -> Result { + let pcs_fri = + Stage2PcsFriWitnessV1::from_prepared_and_typed(prepared, fri, typed)?; + let air = Stage2AirProgramV1::from_prepared_and_typed( + prepared, + fri, + &pcs_fri.pcs_instance, + typed, + )?; + Ok(Self { pcs_fri, air }) + } +} + +/// Exact circuit census produced by the no-prove Stage 3 preflight. +/// +/// Counts are witness rows before Flock pads each table to `2^nu`. Keeping +/// them named makes production-root growth visible without exposing Flock's +/// internal slot identifiers as part of the Ix API. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Stage3RelationCensusV1 { + #[serde(serialize_with = "crate::report::serialize_digest")] + pub circuit_digest: [u8; 32], + pub nu: u64, + pub table_capacity: u64, + pub relation_inputs: u64, + pub public_values: u64, + pub blake3_rows: u64, + pub digest_order_rows: u64, + pub goldilocks_add_rows: u64, + pub goldilocks_mul_rows: u64, + pub lane_repack_rows: u64, + pub canonical_goldilocks_rows: u64, + pub equality_rows: u64, + pub hash_sample_rows: u64, + pub field_sample_rows: u64, + pub u64_split_rows: u64, + pub byte_window_rows: u64, +} + +impl Stage3RelationCensusV1 { + pub fn total_rows(&self) -> u64 { + self + .blake3_rows + .saturating_add(self.digest_order_rows) + .saturating_add(self.goldilocks_add_rows) + .saturating_add(self.goldilocks_mul_rows) + .saturating_add(self.lane_repack_rows) + .saturating_add(self.canonical_goldilocks_rows) + .saturating_add(self.equality_rows) + .saturating_add(self.hash_sample_rows) + .saturating_add(self.field_sample_rows) + .saturating_add(self.u64_split_rows) + .saturating_add(self.byte_window_rows) + } +} + +/// A real Flock proof of an authenticated Plonky3-compatible binary FRI fold. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FriFoldConformanceArtifactV1 { + query: FriFoldQueryV1, + folded_result: [u64; 2], + circuit_digest: [u8; 32], + commitment_root: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl FriFoldConformanceArtifactV1 { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity( + PATH_OFFSET + + 32 * self.query.opening_proof.len() + + FIXED_SUFFIX_BYTES + + self.proof_bundle_bytes.len(), + ); + bytes.extend_from_slice(FRI_FOLD_CONFORMANCE_ARTIFACT_MAGIC); + bytes.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.push(self.query.log_height); + bytes.extend_from_slice(&self.query.query_index.to_le_bytes()); + encode_extension(&mut bytes, self.query.folded); + encode_extension(&mut bytes, self.query.sibling); + encode_extension(&mut bytes, self.query.beta); + encode_extension(&mut bytes, self.folded_result); + for sibling in &self.query.opening_proof { + bytes.extend_from_slice(sibling); + } + bytes.extend_from_slice(&self.circuit_digest); + bytes.extend_from_slice(&self.commitment_root); + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < PATH_OFFSET + FIXED_SUFFIX_BYTES { + bail!("truncated Flock FRI-fold conformance artifact"); + } + if &bytes[..8] != FRI_FOLD_CONFORMANCE_ARTIFACT_MAGIC { + bail!("invalid Flock FRI-fold conformance artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != ARTIFACT_VERSION { + bail!("unsupported Flock FRI-fold artifact version {version}"); + } + if bytes[CONFIG_OFFSET..LOG_HEIGHT_OFFSET] != FlockConfigV1.digest() { + bail!("Flock FRI-fold artifact configuration mismatch"); + } + let log_height = bytes[LOG_HEIGHT_OFFSET]; + validate_log_height(log_height)?; + let query_index = u32::from_le_bytes( + bytes[QUERY_INDEX_OFFSET..FOLDED_OFFSET].try_into().unwrap(), + ); + let folded = decode_extension(&bytes[FOLDED_OFFSET..SIBLING_OFFSET]); + let sibling = decode_extension(&bytes[SIBLING_OFFSET..BETA_OFFSET]); + let beta = decode_extension(&bytes[BETA_OFFSET..RESULT_OFFSET]); + let folded_result = decode_extension(&bytes[RESULT_OFFSET..PATH_OFFSET]); + let path_end = PATH_OFFSET + .checked_add(usize::from(log_height) * 32) + .ok_or_else(|| anyhow::anyhow!("FRI-fold path length overflow"))?; + let suffix_end = path_end + .checked_add(FIXED_SUFFIX_BYTES) + .ok_or_else(|| anyhow::anyhow!("FRI-fold artifact length overflow"))?; + if bytes.len() < suffix_end { + bail!("truncated Flock FRI-fold path or proof header"); + } + let opening_proof = + bytes[PATH_OFFSET..path_end].as_chunks::<32>().0.to_vec(); + let query = FriFoldQueryV1 { + log_height, + query_index, + folded, + sibling, + beta, + opening_proof, + }; + validate_query(&query)?; + validate_extension(folded_result, "folded result")?; + let mut circuit_digest = [0u8; 32]; + circuit_digest.copy_from_slice(&bytes[path_end..path_end + 32]); + let mut commitment_root = [0u8; 32]; + commitment_root.copy_from_slice(&bytes[path_end + 32..path_end + 64]); + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[path_end + 64..suffix_end].try_into().unwrap(), + )) + .map_err(|error| { + anyhow::anyhow!("proof bundle length does not fit usize: {error}") + })?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock FRI-fold proof bundle length {bundle_len}"); + } + let expected_len = suffix_end + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("FRI-fold proof length overflow"))?; + if bytes.len() != expected_len { + bail!( + "Flock FRI-fold artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let proof_bundle_bytes = bytes[suffix_end..].to_vec(); + decode_bundle(&proof_bundle_bytes) + .context("decode Flock FRI-fold conformance proof bundle")?; + Ok(Self { + query, + folded_result, + circuit_digest, + commitment_root, + proof_bundle_bytes, + }) + } + + pub fn query(&self) -> &FriFoldQueryV1 { + &self.query + } + + pub fn folded_result(&self) -> [u64; 2] { + self.folded_result + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn commitment_root(&self) -> &[u8; 32] { + &self.commitment_root + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +const COMMIT_PHASE_ROUND_COUNT_OFFSET: usize = LOG_HEIGHT_OFFSET + 1; +const COMMIT_PHASE_QUERY_INDEX_OFFSET: usize = + COMMIT_PHASE_ROUND_COUNT_OFFSET + 1; +const COMMIT_PHASE_INITIAL_OFFSET: usize = COMMIT_PHASE_QUERY_INDEX_OFFSET + 4; +const COMMIT_PHASE_FINAL_OFFSET: usize = COMMIT_PHASE_INITIAL_OFFSET + 16; +const COMMIT_PHASE_ROUNDS_OFFSET: usize = COMMIT_PHASE_FINAL_OFFSET + 16; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FriCommitPhaseConformanceArtifactV1 { + query: FriCommitPhaseQueryV1, + circuit_digest: [u8; 32], + commitment_roots: Vec<[u8; 32]>, + proof_bundle_bytes: Vec, +} + +impl FriCommitPhaseConformanceArtifactV1 { + pub fn to_bytes(&self) -> Vec { + let round_bytes = self + .query + .rounds + .iter() + .map(|round| 49 + round.opening_proof.len() * 32) + .sum::(); + let mut bytes = Vec::with_capacity( + COMMIT_PHASE_ROUNDS_OFFSET + + round_bytes + + 32 + + self.commitment_roots.len() * 32 + + 8 + + self.proof_bundle_bytes.len(), + ); + bytes.extend_from_slice(FRI_COMMIT_PHASE_CONFORMANCE_ARTIFACT_MAGIC); + bytes.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.push(self.query.initial_log_height); + bytes.push(u8::try_from(self.query.rounds.len()).expect("FRI round count")); + bytes.extend_from_slice(&self.query.query_index.to_le_bytes()); + encode_extension(&mut bytes, self.query.initial_folded); + encode_extension(&mut bytes, self.query.final_polynomial); + for round in &self.query.rounds { + encode_extension(&mut bytes, round.sibling); + encode_extension(&mut bytes, round.beta); + bytes.push(u8::from(round.reduced_opening.is_some())); + encode_extension(&mut bytes, round.reduced_opening.unwrap_or([0, 0])); + for sibling in &round.opening_proof { + bytes.extend_from_slice(sibling); + } + } + bytes.extend_from_slice(&self.circuit_digest); + for root in &self.commitment_roots { + bytes.extend_from_slice(root); + } + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < COMMIT_PHASE_ROUNDS_OFFSET + 32 + 32 + 8 { + bail!("truncated Flock FRI commit-phase conformance artifact"); + } + if &bytes[..8] != FRI_COMMIT_PHASE_CONFORMANCE_ARTIFACT_MAGIC { + bail!("invalid Flock FRI commit-phase artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != ARTIFACT_VERSION { + bail!("unsupported Flock FRI commit-phase artifact version {version}"); + } + if bytes[CONFIG_OFFSET..LOG_HEIGHT_OFFSET] != FlockConfigV1.digest() { + bail!("Flock FRI commit-phase artifact configuration mismatch"); + } + let initial_log_height = bytes[LOG_HEIGHT_OFFSET]; + validate_log_height(initial_log_height)?; + let round_count = usize::from(bytes[COMMIT_PHASE_ROUND_COUNT_OFFSET]); + validate_commit_phase_round_count(initial_log_height, round_count)?; + let query_index = u32::from_le_bytes( + bytes[COMMIT_PHASE_QUERY_INDEX_OFFSET..COMMIT_PHASE_INITIAL_OFFSET] + .try_into() + .unwrap(), + ); + let initial_folded = decode_extension( + &bytes[COMMIT_PHASE_INITIAL_OFFSET..COMMIT_PHASE_FINAL_OFFSET], + ); + let final_polynomial = decode_extension( + &bytes[COMMIT_PHASE_FINAL_OFFSET..COMMIT_PHASE_ROUNDS_OFFSET], + ); + let rounds_bytes = + commit_phase_rounds_bytes(initial_log_height, round_count)?; + let rounds_end = COMMIT_PHASE_ROUNDS_OFFSET + .checked_add(rounds_bytes) + .ok_or_else(|| anyhow::anyhow!("FRI commit-phase rounds overflow"))?; + let suffix_len = 32usize + .checked_add(round_count * 32) + .and_then(|length| length.checked_add(8)) + .ok_or_else(|| anyhow::anyhow!("FRI commit-phase suffix overflow"))?; + let suffix_end = rounds_end + .checked_add(suffix_len) + .ok_or_else(|| anyhow::anyhow!("FRI commit-phase artifact overflow"))?; + if bytes.len() < suffix_end { + bail!("truncated Flock FRI commit-phase rounds or proof header"); + } + + let mut cursor = COMMIT_PHASE_ROUNDS_OFFSET; + let mut rounds = Vec::with_capacity(round_count); + for round_index in 0..round_count { + let log_height = usize::from(initial_log_height) - round_index; + let sibling = decode_extension(&bytes[cursor..cursor + 16]); + cursor += 16; + let beta = decode_extension(&bytes[cursor..cursor + 16]); + cursor += 16; + let has_reduced_opening = bytes[cursor]; + cursor += 1; + if has_reduced_opening > 1 { + bail!( + "FRI commit-phase round {round_index} has invalid roll-in flag {has_reduced_opening}" + ); + } + let encoded_reduced_opening = + decode_extension(&bytes[cursor..cursor + 16]); + cursor += 16; + let reduced_opening = if has_reduced_opening == 1 { + Some(encoded_reduced_opening) + } else { + if encoded_reduced_opening != [0, 0] { + bail!("absent FRI reduced opening has nonzero encoding"); + } + None + }; + let path_end = cursor + log_height * 32; + let opening_proof = bytes[cursor..path_end].as_chunks::<32>().0.to_vec(); + cursor = path_end; + rounds.push(FriCommitPhaseRoundV1 { + sibling, + beta, + reduced_opening, + opening_proof, + }); + } + debug_assert_eq!(cursor, rounds_end); + let query = FriCommitPhaseQueryV1 { + initial_log_height, + query_index, + initial_folded, + rounds, + final_polynomial, + }; + let computation = compute_commit_phase(&query)?; + ensure_final_polynomial(&query, &computation)?; + + let mut circuit_digest = [0u8; 32]; + circuit_digest.copy_from_slice(&bytes[rounds_end..rounds_end + 32]); + let roots_end = rounds_end + 32 + round_count * 32; + let commitment_roots = + bytes[rounds_end + 32..roots_end].as_chunks::<32>().0.to_vec(); + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[roots_end..suffix_end].try_into().unwrap(), + )) + .map_err(|error| { + anyhow::anyhow!("proof bundle length does not fit usize: {error}") + })?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock FRI commit-phase proof length {bundle_len}"); + } + let expected_len = suffix_end + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("FRI commit-phase proof overflow"))?; + if bytes.len() != expected_len { + bail!( + "Flock FRI commit-phase artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let proof_bundle_bytes = bytes[suffix_end..].to_vec(); + decode_bundle(&proof_bundle_bytes) + .context("decode Flock FRI commit-phase proof bundle")?; + Ok(Self { query, circuit_digest, commitment_roots, proof_bundle_bytes }) + } + + pub fn query(&self) -> &FriCommitPhaseQueryV1 { + &self.query + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn commitment_roots(&self) -> &[[u8; 32]] { + &self.commitment_roots + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +const PCS_WIDTH_OFFSET: usize = LOG_HEIGHT_OFFSET + 1; +const PCS_QUERY_INDEX_OFFSET: usize = PCS_WIDTH_OFFSET + 1; +const PCS_ZETA_OFFSET: usize = PCS_QUERY_INDEX_OFFSET + 4; +const PCS_ALPHA_OFFSET: usize = PCS_ZETA_OFFSET + 16; +const PCS_INITIAL_ALPHA_POWER_OFFSET: usize = PCS_ALPHA_OFFSET + 16; +const PCS_INITIAL_ACCUMULATOR_OFFSET: usize = + PCS_INITIAL_ALPHA_POWER_OFFSET + 16; +const PCS_REDUCED_ACCUMULATOR_OFFSET: usize = + PCS_INITIAL_ACCUMULATOR_OFFSET + 16; +const PCS_NEXT_ALPHA_POWER_OFFSET: usize = PCS_REDUCED_ACCUMULATOR_OFFSET + 16; +const PCS_DYNAMIC_OFFSET: usize = PCS_NEXT_ALPHA_POWER_OFFSET + 16; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PcsReductionConformanceArtifactV1 { + opening: PcsReducedOpeningV1, + reduced_accumulator: [u64; 2], + next_alpha_power: [u64; 2], + circuit_digest: [u8; 32], + commitment_root: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl PcsReductionConformanceArtifactV1 { + pub fn to_bytes(&self) -> Vec { + let width = self.opening.opened_values.len(); + let dynamic_bytes = + width * 8 + width * 16 + self.opening.opening_proof.len() * 32; + let mut bytes = Vec::with_capacity( + PCS_DYNAMIC_OFFSET + + dynamic_bytes + + FIXED_SUFFIX_BYTES + + self.proof_bundle_bytes.len(), + ); + bytes.extend_from_slice(PCS_REDUCTION_CONFORMANCE_ARTIFACT_MAGIC); + bytes.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.push(self.opening.log_height); + bytes.push(u8::try_from(width).expect("PCS row width")); + bytes.extend_from_slice(&self.opening.query_index.to_le_bytes()); + encode_extension(&mut bytes, self.opening.zeta); + encode_extension(&mut bytes, self.opening.alpha); + encode_extension(&mut bytes, self.opening.initial_alpha_power); + encode_extension(&mut bytes, self.opening.initial_accumulator); + encode_extension(&mut bytes, self.reduced_accumulator); + encode_extension(&mut bytes, self.next_alpha_power); + for value in &self.opening.opened_values { + bytes.extend_from_slice(&value.to_le_bytes()); + } + for value in &self.opening.opened_at_z { + encode_extension(&mut bytes, *value); + } + for sibling in &self.opening.opening_proof { + bytes.extend_from_slice(sibling); + } + bytes.extend_from_slice(&self.circuit_digest); + bytes.extend_from_slice(&self.commitment_root); + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < PCS_DYNAMIC_OFFSET + FIXED_SUFFIX_BYTES { + bail!("truncated Flock PCS-reduction conformance artifact"); + } + if &bytes[..8] != PCS_REDUCTION_CONFORMANCE_ARTIFACT_MAGIC { + bail!("invalid Flock PCS-reduction artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != ARTIFACT_VERSION { + bail!("unsupported Flock PCS-reduction artifact version {version}"); + } + if bytes[CONFIG_OFFSET..LOG_HEIGHT_OFFSET] != FlockConfigV1.digest() { + bail!("Flock PCS-reduction artifact configuration mismatch"); + } + let log_height = bytes[LOG_HEIGHT_OFFSET]; + validate_log_height(log_height)?; + let width = usize::from(bytes[PCS_WIDTH_OFFSET]); + validate_reduced_opening_width(width)?; + let query_index = u32::from_le_bytes( + bytes[PCS_QUERY_INDEX_OFFSET..PCS_ZETA_OFFSET].try_into().unwrap(), + ); + let zeta = decode_extension(&bytes[PCS_ZETA_OFFSET..PCS_ALPHA_OFFSET]); + let alpha = decode_extension( + &bytes[PCS_ALPHA_OFFSET..PCS_INITIAL_ALPHA_POWER_OFFSET], + ); + let initial_alpha_power = decode_extension( + &bytes[PCS_INITIAL_ALPHA_POWER_OFFSET..PCS_INITIAL_ACCUMULATOR_OFFSET], + ); + let initial_accumulator = decode_extension( + &bytes[PCS_INITIAL_ACCUMULATOR_OFFSET..PCS_REDUCED_ACCUMULATOR_OFFSET], + ); + let reduced_accumulator = decode_extension( + &bytes[PCS_REDUCED_ACCUMULATOR_OFFSET..PCS_NEXT_ALPHA_POWER_OFFSET], + ); + let next_alpha_power = + decode_extension(&bytes[PCS_NEXT_ALPHA_POWER_OFFSET..PCS_DYNAMIC_OFFSET]); + let opened_values_end = PCS_DYNAMIC_OFFSET + .checked_add(width * 8) + .ok_or_else(|| anyhow::anyhow!("PCS opened-values length overflow"))?; + let opened_at_z_end = opened_values_end + .checked_add(width * 16) + .ok_or_else(|| anyhow::anyhow!("PCS OOD-values length overflow"))?; + let path_end = opened_at_z_end + .checked_add(usize::from(log_height) * 32) + .ok_or_else(|| anyhow::anyhow!("PCS Merkle path length overflow"))?; + let suffix_end = path_end + .checked_add(FIXED_SUFFIX_BYTES) + .ok_or_else(|| anyhow::anyhow!("PCS artifact length overflow"))?; + if bytes.len() < suffix_end { + bail!("truncated Flock PCS values, path, or proof header"); + } + let opened_values = bytes[PCS_DYNAMIC_OFFSET..opened_values_end] + .as_chunks::<8>() + .0 + .iter() + .map(|word| u64::from_le_bytes(*word)) + .collect(); + let opened_at_z = bytes[opened_values_end..opened_at_z_end] + .as_chunks::<16>() + .0 + .iter() + .map(|value| decode_extension(value)) + .collect(); + let opening_proof = + bytes[opened_at_z_end..path_end].as_chunks::<32>().0.to_vec(); + let opening = PcsReducedOpeningV1 { + log_height, + query_index, + opened_values, + opened_at_z, + zeta, + alpha, + initial_alpha_power, + initial_accumulator, + opening_proof, + }; + let computation = compute_pcs_reduction(&opening)?; + validate_extension(reduced_accumulator, "reduced accumulator")?; + validate_extension(next_alpha_power, "next alpha power")?; + + let mut circuit_digest = [0u8; 32]; + circuit_digest.copy_from_slice(&bytes[path_end..path_end + 32]); + let mut commitment_root = [0u8; 32]; + commitment_root.copy_from_slice(&bytes[path_end + 32..path_end + 64]); + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[path_end + 64..suffix_end].try_into().unwrap(), + )) + .map_err(|error| { + anyhow::anyhow!("proof bundle length does not fit usize: {error}") + })?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock PCS-reduction proof length {bundle_len}"); + } + let expected_len = suffix_end + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("PCS-reduction proof overflow"))?; + if bytes.len() != expected_len { + bail!( + "Flock PCS-reduction artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let proof_bundle_bytes = bytes[suffix_end..].to_vec(); + decode_bundle(&proof_bundle_bytes) + .context("decode Flock PCS-reduction proof bundle")?; + if reduced_accumulator != computation.accumulator + || next_alpha_power != computation.alpha_power + || commitment_root != computation.root + { + bail!("Flock PCS-reduction artifact carries inconsistent native outputs"); + } + Ok(Self { + opening, + reduced_accumulator, + next_alpha_power, + circuit_digest, + commitment_root, + proof_bundle_bytes, + }) + } + + pub fn opening(&self) -> &PcsReducedOpeningV1 { + &self.opening + } + + pub fn reduced_accumulator(&self) -> [u64; 2] { + self.reduced_accumulator + } + + pub fn next_alpha_power(&self) -> [u64; 2] { + self.next_alpha_power + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn commitment_root(&self) -> &[u8; 32] { + &self.commitment_root + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +/// One Flock proof in which the exact Stage 2 BLAKE3 transcript directly +/// supplies zeta and the PCS opening-batch challenge to an authenticated +/// reduced-opening check. +/// +/// This is the first composed semantic slice: changing any transcript byte +/// changes the wires used by the PCS arithmetic inside the same circuit. It +/// remains a conformance artifact, not the complete Stage 3 proof. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TranscriptBoundPcsReductionArtifactV1 { + replay: Stage2TranscriptReplayV1, + opening: PcsReducedOpeningV1, + reduced_accumulator: [u64; 2], + next_alpha_power: [u64; 2], + circuit_digest: [u8; 32], + commitment_root: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl TranscriptBoundPcsReductionArtifactV1 { + pub fn replay(&self) -> &Stage2TranscriptReplayV1 { + &self.replay + } + + pub fn opening(&self) -> &PcsReducedOpeningV1 { + &self.opening + } + + pub fn reduced_accumulator(&self) -> [u64; 2] { + self.reduced_accumulator + } + + pub fn next_alpha_power(&self) -> [u64; 2] { + self.next_alpha_power + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn commitment_root(&self) -> &[u8; 32] { + &self.commitment_root + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +/// One Flock proof that continues the exact Stage 2 transcript through FRI, +/// then uses a transcript-derived query index and folding challenges to check +/// one complete authenticated binary commit-phase chain. +/// +/// Cap roots and the final polynomial are consumed from the same transcript +/// wires used by the fold relation. This is still a conformance slice: a full +/// Stage 3 proof must check every sampled query and all PCS reduced openings. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TranscriptBoundFriCommitPhaseArtifactV1 { + prefix: Stage2TranscriptReplayV1, + fri_transcript: Stage2FriTranscriptReplayV1, + query_number: usize, + query: FriCommitPhaseQueryV1, + circuit_digest: [u8; 32], + commitment_roots: Vec<[u8; 32]>, + proof_bundle_bytes: Vec, +} + +impl TranscriptBoundFriCommitPhaseArtifactV1 { + pub fn prefix(&self) -> &Stage2TranscriptReplayV1 { + &self.prefix + } + + pub fn fri_transcript(&self) -> &Stage2FriTranscriptReplayV1 { + &self.fri_transcript + } + + pub const fn query_number(&self) -> usize { + self.query_number + } + + pub fn query(&self) -> &FriCommitPhaseQueryV1 { + &self.query + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn commitment_roots(&self) -> &[[u8; 32]] { + &self.commitment_roots + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +/// One Flock proof of every transcript-derived FRI query. All queries share +/// one constrained transcript, beta vector, cap set, and final polynomial. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TranscriptBoundFriQueriesArtifactV1 { + prefix: Stage2TranscriptReplayV1, + fri_transcript: Stage2FriTranscriptReplayV1, + queries: Vec, + circuit_digest: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl TranscriptBoundFriQueriesArtifactV1 { + pub fn prefix(&self) -> &Stage2TranscriptReplayV1 { + &self.prefix + } + + pub fn fri_transcript(&self) -> &Stage2FriTranscriptReplayV1 { + &self.fri_transcript + } + + pub fn queries(&self) -> &[FriCommitPhaseQueryV1] { + &self.queries + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +/// One Flock proof that authenticates every Stage 2 PCS input row, computes +/// all per-height reduced openings from transcript-bound OOD values, and feeds +/// those accumulators into every transcript-derived FRI query. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TranscriptBoundPcsFriQueriesArtifactV1 { + prefix: Stage2TranscriptReplayV1, + fri_transcript: Stage2FriTranscriptReplayV1, + pcs_instance: Stage2PcsInstanceV1, + queries: Vec, + circuit_digest: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl TranscriptBoundPcsFriQueriesArtifactV1 { + pub fn prefix(&self) -> &Stage2TranscriptReplayV1 { + &self.prefix + } + + pub fn fri_transcript(&self) -> &Stage2FriTranscriptReplayV1 { + &self.fri_transcript + } + + pub fn pcs_instance(&self) -> &Stage2PcsInstanceV1 { + &self.pcs_instance + } + + pub fn queries(&self) -> &[TranscriptBoundPcsFriQueryV1] { + &self.queries + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +/// A real Flock proof of statement binding and compiled AIR/logUp OOD checks +/// composed with the exact PCS-to-FRI relation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2AirPcsFriArtifactV1 { + witness: Stage2AirPcsFriWitnessV1, + circuit_digest: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl Stage2AirPcsFriArtifactV1 { + pub(crate) fn from_parts( + witness: Stage2AirPcsFriWitnessV1, + circuit_digest: [u8; 32], + proof_bundle_bytes: Vec, + ) -> Result { + if proof_bundle_bytes.is_empty() { + bail!("Stage 2 AIR/PCS/FRI proof bundle is empty"); + } + if proof_bundle_bytes.len() > MAX_BUNDLE_BYTES { + bail!( + "Stage 2 AIR/PCS/FRI proof bundle exceeds {MAX_BUNDLE_BYTES} bytes" + ); + } + Ok(Self { witness, circuit_digest, proof_bundle_bytes }) + } + + pub fn witness(&self) -> &Stage2AirPcsFriWitnessV1 { + &self.witness + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } + + pub fn stage2_root_digest(&self) -> &[u8; 32] { + &self.witness.air.statement_digest + } +} + +#[derive(Serialize, Deserialize)] +struct FriFoldProofBundle { + commitment: Commitment, + proof: R1csProofCircuitMerged, +} + +pub fn prove_fri_fold_conformance( + query: &FriFoldQueryV1, +) -> Result { + validate_query(query)?; + let folded_result = native_fold(query); + let commitment_root = native_root(query); + let relation = FriFoldRelation::build(query.log_height)?; + let inputs = relation_inputs(query, folded_result); + let expected_public = relation_public(query, folded_result, &commitment_root); + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.table_slots(), + None, + None, + None, + NU, + &inputs, + &expected_public, + FRI_FOLD_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(FriFoldConformanceArtifactV1 { + query: query.clone(), + folded_result, + circuit_digest: relation.shape.circuit.digest(), + commitment_root, + proof_bundle_bytes, + }) +} + +pub fn verify_fri_fold_conformance( + artifact: &FriFoldConformanceArtifactV1, +) -> Result<()> { + validate_query(&artifact.query)?; + if artifact.folded_result != native_fold(&artifact.query) { + bail!("Flock FRI-fold artifact carries the wrong folded result"); + } + if artifact.commitment_root != native_root(&artifact.query) { + bail!("Flock FRI-fold artifact carries the wrong commitment root"); + } + let relation = FriFoldRelation::build(artifact.query.log_height)?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Flock FRI-fold conformance circuit digest mismatch"); + } + let public = relation_public( + &artifact.query, + artifact.folded_result, + &artifact.commitment_root, + ); + verify_fri_circuit( + &relation.shape, + relation.table_slots(), + None, + None, + None, + NU, + &public, + &artifact.proof_bundle_bytes, + FRI_FOLD_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_fri_commit_phase_conformance( + query: &FriCommitPhaseQueryV1, +) -> Result { + let computation = compute_commit_phase(query)?; + ensure_final_polynomial(query, &computation)?; + let relation = FriCommitPhaseRelation::build(query)?; + let inputs = commit_phase_relation_inputs(query, &computation); + let public = commit_phase_relation_public(query, &computation); + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + None, + None, + None, + relation.nu, + &inputs, + &public, + FRI_QUERY_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(FriCommitPhaseConformanceArtifactV1 { + query: query.clone(), + circuit_digest: relation.shape.circuit.digest(), + commitment_roots: computation.roots, + proof_bundle_bytes, + }) +} + +pub fn verify_fri_commit_phase_conformance( + artifact: &FriCommitPhaseConformanceArtifactV1, +) -> Result<()> { + let computation = compute_commit_phase(&artifact.query)?; + ensure_final_polynomial(&artifact.query, &computation)?; + if artifact.commitment_roots != computation.roots { + bail!("Flock FRI commit-phase artifact carries the wrong round roots"); + } + let relation = FriCommitPhaseRelation::build(&artifact.query)?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Flock FRI commit-phase circuit digest mismatch"); + } + let public = commit_phase_relation_public(&artifact.query, &computation); + verify_fri_circuit( + &relation.shape, + relation.slots, + None, + None, + None, + relation.nu, + &public, + &artifact.proof_bundle_bytes, + FRI_QUERY_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_pcs_reduction_conformance( + opening: &PcsReducedOpeningV1, +) -> Result { + let computation = compute_pcs_reduction(opening)?; + let relation = PcsReductionRelation::build(opening)?; + let inputs = pcs_reduction_relation_inputs(opening, &computation); + let public = pcs_reduction_relation_public(opening, &computation); + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + None, + None, + None, + relation.nu, + &inputs, + &public, + PCS_REDUCTION_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(PcsReductionConformanceArtifactV1 { + opening: opening.clone(), + reduced_accumulator: computation.accumulator, + next_alpha_power: computation.alpha_power, + circuit_digest: relation.shape.circuit.digest(), + commitment_root: computation.root, + proof_bundle_bytes, + }) +} + +pub fn verify_pcs_reduction_conformance( + artifact: &PcsReductionConformanceArtifactV1, +) -> Result<()> { + let computation = compute_pcs_reduction(&artifact.opening)?; + if artifact.reduced_accumulator != computation.accumulator + || artifact.next_alpha_power != computation.alpha_power + || artifact.commitment_root != computation.root + { + bail!("Flock PCS-reduction artifact carries the wrong native outputs"); + } + let relation = PcsReductionRelation::build(&artifact.opening)?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Flock PCS-reduction circuit digest mismatch"); + } + let public = pcs_reduction_relation_public(&artifact.opening, &computation); + verify_fri_circuit( + &relation.shape, + relation.slots, + None, + None, + None, + relation.nu, + &public, + &artifact.proof_bundle_bytes, + PCS_REDUCTION_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_transcript_bound_pcs_reduction_conformance( + replay: &Stage2TranscriptReplayV1, + opening: &PcsReducedOpeningV1, +) -> Result { + let challenges = replay.challenges()?; + ensure_transcript_binds_opening(challenges, opening)?; + let computation = compute_pcs_reduction(opening)?; + let relation = TranscriptBoundPcsReductionRelation::build( + replay, + opening, + &computation, + challenges, + )?; + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + None, + None, + None, + relation.nu, + &relation.inputs, + &relation.public, + TRANSCRIPT_BOUND_PCS_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(TranscriptBoundPcsReductionArtifactV1 { + replay: replay.clone(), + opening: opening.clone(), + reduced_accumulator: computation.accumulator, + next_alpha_power: computation.alpha_power, + circuit_digest: relation.shape.circuit.digest(), + commitment_root: computation.root, + proof_bundle_bytes, + }) +} + +pub fn verify_transcript_bound_pcs_reduction_conformance( + artifact: &TranscriptBoundPcsReductionArtifactV1, +) -> Result<()> { + let challenges = artifact.replay.challenges()?; + ensure_transcript_binds_opening(challenges, &artifact.opening)?; + let computation = compute_pcs_reduction(&artifact.opening)?; + if artifact.reduced_accumulator != computation.accumulator + || artifact.next_alpha_power != computation.alpha_power + || artifact.commitment_root != computation.root + { + bail!("transcript-bound PCS artifact carries the wrong native outputs"); + } + let relation = TranscriptBoundPcsReductionRelation::build( + &artifact.replay, + &artifact.opening, + &computation, + challenges, + )?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("transcript-bound PCS circuit digest mismatch"); + } + verify_fri_circuit( + &relation.shape, + relation.slots, + None, + None, + None, + relation.nu, + &relation.public, + &artifact.proof_bundle_bytes, + TRANSCRIPT_BOUND_PCS_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_transcript_bound_fri_commit_phase_conformance( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + query_number: usize, + query: &FriCommitPhaseQueryV1, +) -> Result { + let challenges = fri_transcript.challenges(prefix)?; + ensure_transcript_binds_fri_query( + fri_transcript, + &challenges, + query_number, + query, + )?; + let computation = compute_commit_phase(query)?; + ensure_final_polynomial(query, &computation)?; + let relation = TranscriptBoundFriCommitPhaseRelation::build( + prefix, + fri_transcript, + &challenges, + query_number, + query, + &computation, + )?; + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.inputs, + &relation.public, + TRANSCRIPT_BOUND_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(TranscriptBoundFriCommitPhaseArtifactV1 { + prefix: prefix.clone(), + fri_transcript: fri_transcript.clone(), + query_number, + query: query.clone(), + circuit_digest: relation.shape.circuit.digest(), + commitment_roots: computation.roots, + proof_bundle_bytes, + }) +} + +pub fn verify_transcript_bound_fri_commit_phase_conformance( + artifact: &TranscriptBoundFriCommitPhaseArtifactV1, +) -> Result<()> { + let challenges = artifact.fri_transcript.challenges(&artifact.prefix)?; + ensure_transcript_binds_fri_query( + &artifact.fri_transcript, + &challenges, + artifact.query_number, + &artifact.query, + )?; + let computation = compute_commit_phase(&artifact.query)?; + ensure_final_polynomial(&artifact.query, &computation)?; + if artifact.commitment_roots != computation.roots { + bail!("transcript-bound FRI artifact carries the wrong native roots"); + } + let relation = TranscriptBoundFriCommitPhaseRelation::build( + &artifact.prefix, + &artifact.fri_transcript, + &challenges, + artifact.query_number, + &artifact.query, + &computation, + )?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("transcript-bound FRI circuit digest mismatch"); + } + verify_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.public, + &artifact.proof_bundle_bytes, + TRANSCRIPT_BOUND_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_transcript_bound_fri_queries_conformance( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + queries: &[FriCommitPhaseQueryV1], +) -> Result { + let challenges = fri_transcript.challenges(prefix)?; + let computations = validate_all_transcript_bound_fri_queries( + fri_transcript, + &challenges, + queries, + )?; + let relation = TranscriptBoundFriCommitPhaseRelation::build_all( + prefix, + fri_transcript, + &challenges, + queries, + &computations, + )?; + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.inputs, + &relation.public, + TRANSCRIPT_BOUND_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(TranscriptBoundFriQueriesArtifactV1 { + prefix: prefix.clone(), + fri_transcript: fri_transcript.clone(), + queries: queries.to_vec(), + circuit_digest: relation.shape.circuit.digest(), + proof_bundle_bytes, + }) +} + +pub fn verify_transcript_bound_fri_queries_conformance( + artifact: &TranscriptBoundFriQueriesArtifactV1, +) -> Result<()> { + let challenges = artifact.fri_transcript.challenges(&artifact.prefix)?; + let computations = validate_all_transcript_bound_fri_queries( + &artifact.fri_transcript, + &challenges, + &artifact.queries, + )?; + let relation = TranscriptBoundFriCommitPhaseRelation::build_all( + &artifact.prefix, + &artifact.fri_transcript, + &challenges, + &artifact.queries, + &computations, + )?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("all-query transcript-bound FRI circuit digest mismatch"); + } + verify_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.public, + &artifact.proof_bundle_bytes, + TRANSCRIPT_BOUND_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_transcript_bound_pcs_fri_queries_conformance( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + pcs_instance: &Stage2PcsInstanceV1, + queries: &[TranscriptBoundPcsFriQueryV1], +) -> Result { + let prefix_challenges = prefix.challenges()?; + let fri_challenges = fri_transcript.challenges(prefix)?; + let (fri_computations, pcs_computations) = + validate_all_transcript_bound_pcs_fri_queries( + prefix, + fri_transcript, + &fri_challenges, + prefix_challenges, + pcs_instance, + queries, + )?; + let relation = TranscriptBoundFriCommitPhaseRelation::build_all_with_pcs( + prefix, + fri_transcript, + &fri_challenges, + pcs_instance, + queries, + &fri_computations, + &pcs_computations, + )?; + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.inputs, + &relation.public, + TRANSCRIPT_BOUND_PCS_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + )?; + Ok(TranscriptBoundPcsFriQueriesArtifactV1 { + prefix: prefix.clone(), + fri_transcript: fri_transcript.clone(), + pcs_instance: pcs_instance.clone(), + queries: queries.to_vec(), + circuit_digest: relation.shape.circuit.digest(), + proof_bundle_bytes, + }) +} + +pub fn verify_transcript_bound_pcs_fri_queries_conformance( + artifact: &TranscriptBoundPcsFriQueriesArtifactV1, +) -> Result<()> { + let prefix_challenges = artifact.prefix.challenges()?; + let fri_challenges = artifact.fri_transcript.challenges(&artifact.prefix)?; + let (fri_computations, pcs_computations) = + validate_all_transcript_bound_pcs_fri_queries( + &artifact.prefix, + &artifact.fri_transcript, + &fri_challenges, + prefix_challenges, + &artifact.pcs_instance, + &artifact.queries, + )?; + let relation = TranscriptBoundFriCommitPhaseRelation::build_all_with_pcs( + &artifact.prefix, + &artifact.fri_transcript, + &fri_challenges, + &artifact.pcs_instance, + &artifact.queries, + &fri_computations, + &pcs_computations, + )?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("transcript-bound PCS/FRI all-query circuit digest mismatch"); + } + verify_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.public, + &artifact.proof_bundle_bytes, + TRANSCRIPT_BOUND_PCS_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +pub fn prove_stage2_air_pcs_fri_conformance( + witness: &Stage2AirPcsFriWitnessV1, +) -> Result { + prove_stage2_air_pcs_fri_with_domain( + witness, + STAGE2_AIR_PCS_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +fn prove_stage2_air_pcs_fri_with_domain( + witness: &Stage2AirPcsFriWitnessV1, + transcript_domain: &[u8], +) -> Result { + let trace = std::env::var_os("IX_FLOCK_TIMING").is_some(); + let total_started = std::time::Instant::now(); + let phase_started = std::time::Instant::now(); + let (relation, _) = rayon::join( + || cached_stage2_air_pcs_fri_relation(witness), + || { + stage3_linchecks(); + }, + ); + let relation = relation?; + if trace { + eprintln!( + " [stage3] relation build: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let proof_bundle_bytes = prove_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.inputs, + &relation.public, + transcript_domain, + )?; + if trace { + eprintln!( + " [stage3] circuit evaluate + prove: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let artifact = Stage2AirPcsFriArtifactV1::from_parts( + witness.clone(), + relation.shape.circuit.digest(), + proof_bundle_bytes, + )?; + if trace { + eprintln!( + " [stage3] relation-to-artifact total: {:.2} ms", + total_started.elapsed().as_secs_f64() * 1e3, + ); + } + Ok(artifact) +} + +fn build_stage2_air_pcs_fri_relation( + witness: &Stage2AirPcsFriWitnessV1, +) -> Result { + build_stage2_air_pcs_fri_relation_with_limits(witness, None) +} + +fn build_stage2_air_pcs_fri_relation_with_limits( + witness: &Stage2AirPcsFriWitnessV1, + limits: Option, +) -> Result { + let trace = std::env::var_os("IX_FLOCK_TIMING").is_some(); + let total_started = std::time::Instant::now(); + let pcs_fri = &witness.pcs_fri; + let phase_started = std::time::Instant::now(); + let prefix_challenges = pcs_fri.prefix.challenges()?; + let fri_challenges = pcs_fri.fri_transcript.challenges(&pcs_fri.prefix)?; + if trace { + eprintln!( + " [stage3-relation] replay transcript: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let (fri_computations, pcs_computations) = + validate_all_transcript_bound_pcs_fri_queries( + &pcs_fri.prefix, + &pcs_fri.fri_transcript, + &fri_challenges, + prefix_challenges, + &pcs_fri.pcs_instance, + &pcs_fri.queries, + )?; + if trace { + eprintln!( + " [stage3-relation] native PCS/FRI validation: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let relation = + TranscriptBoundFriCommitPhaseRelation::build_all_with_pcs_and_air( + &pcs_fri.prefix, + &pcs_fri.fri_transcript, + &fri_challenges, + &pcs_fri.pcs_instance, + &witness.air, + &pcs_fri.queries, + &fri_computations, + &pcs_computations, + limits, + )?; + if trace { + eprintln!( + " [stage3-relation] build circuit shape: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + eprintln!( + " [stage3-relation] total: {:.2} ms", + total_started.elapsed().as_secs_f64() * 1e3, + ); + } + Ok(relation) +} + +/// Retain only the most recently used complete relation. The circuit shape is +/// immutable and value-independent once built, while the input/public vectors +/// in this relation are specific to one witness. Comparing the full witness +/// (rather than a digest) makes an exact hit safe for the conformance and +/// standalone manifest helpers. Production operations use explicit ownership +/// instead, so batch processing never leaves a root in this cache. +fn cached_stage2_air_pcs_fri_relation( + witness: &Stage2AirPcsFriWitnessV1, +) -> Result> { + type Entry = + (Stage2AirPcsFriWitnessV1, Arc); + static CACHE: OnceLock>> = OnceLock::new(); + + let cache = CACHE.get_or_init(|| Mutex::new(None)); + let mut cached = + cache.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some((cached_witness, relation)) = cached.as_ref() + && cached_witness == witness + { + if std::env::var_os("IX_FLOCK_TIMING").is_some() { + eprintln!(" [stage3-relation] exact-witness cache: hit"); + } + return Ok(Arc::clone(relation)); + } + + if std::env::var_os("IX_FLOCK_TIMING").is_some() { + eprintln!(" [stage3-relation] exact-witness cache: miss"); + } + let relation = Arc::new(build_stage2_air_pcs_fri_relation(witness)?); + *cached = Some((witness.clone(), Arc::clone(&relation))); + Ok(relation) +} + +pub(crate) fn stage2_air_pcs_fri_circuit_digest( + witness: &Stage2AirPcsFriWitnessV1, +) -> Result<[u8; 32]> { + Ok(cached_stage2_air_pcs_fri_relation(witness)?.shape.circuit.digest()) +} + +/// A production relation with explicit ownership. It never enters the +/// conformance helpers' process-global exact-witness cache. +pub(crate) struct CompiledStage3Relation { + relation: TranscriptBoundFriCommitPhaseRelation, +} + +impl CompiledStage3Relation { + pub(crate) fn build( + witness: &Stage2AirPcsFriWitnessV1, + limits: crate::Stage3ResourceLimitsV1, + ) -> Result { + let relation = + build_stage2_air_pcs_fri_relation_with_limits(witness, Some(limits))?; + Ok(Self { relation }) + } + + pub(crate) fn census(&self) -> Result { + relation_census(&self.relation) + } + + pub(crate) fn resources(&self) -> Result { + let relation = &self.relation; + let union = UnionInstance::new( + &relation.shape.registry, + relation.shape.counts.clone(), + ); + let params = pcs_params(&union); + params.ligerito_prover_config().map_err(|error| { + anyhow::anyhow!( + "Stage 3 PCS prover configuration is unsupported: {error}" + ) + })?; + params.ligerito_verifier_config().map_err(|error| { + anyhow::anyhow!( + "Stage 3 PCS verifier configuration is unsupported: {error}" + ) + })?; + let bytes = |words: usize, copies: u64| -> Result { + u64::try_from(words)? + .checked_mul(16) + .and_then(|n| n.checked_mul(copies)) + .ok_or_else(|| anyhow::anyhow!("Stage 3 buffer size overflow")) + }; + if bytes(union.packed_len(), 3)? + != production_padded_witness_bytes(relation.nu)? + { + bail!( + "Stage 3 table schemas changed: update the pre-compilation resource bound" + ); + } + let tables = relation + .shape + .registry + .types() + .iter() + .enumerate() + .map(|(slot, table)| { + Ok(crate::Stage3TableReportV1 { + registry_slot: slot as u64, + rows: relation.shape.counts[slot] as u64, + boolean_columns: 1u64 << table.k_log, + useful_boolean_columns: table.useful_bits as u64, + padded_witness_bytes: bytes( + 1usize << (relation.nu + table.k_log - 7), + 3, + )?, + }) + }) + .collect::>>()?; + Ok(crate::Stage3ResourceReportV1 { + virtual_union_log: union.m_total() as u64, + committed_union_log: union.dense_m() as u64, + dense_witness_bytes: bytes(union.dense_words(), 1)?, + padded_union_witness_bytes: bytes(union.packed_len(), 3)?, + pcs_message_bytes: bytes(params.msg_len_f128(), 1)?, + pcs_codeword_bytes: bytes(params.codeword_len_f128(), 1)?, + pcs_log_batch_size: params.log_batch_size as u64, + pcs_lanes: params.num_ntts() as u64, + pcs_log_inverse_rate: params.log_inv_rate as u64, + tables, + }) + } + + pub(crate) fn evaluate(&self) -> Result<()> { + let relation = &self.relation; + if relation.shape.run(&relation.inputs, &[]).public != relation.public { + bail!("Flock Stage 3 preflight disagrees with native verifier semantics"); + } + Ok(()) + } + + pub(crate) fn prove(&self) -> Result> { + let r = &self.relation; + prove_fri_circuit( + &r.shape, + r.slots, + Some(r.sample_slot), + Some(r.split_slot), + r.window_slot, + r.nu, + &r.inputs, + &r.public, + crate::STAGE3_TRANSCRIPT_DOMAIN, + ) + } + + pub(crate) fn verify(&self, digest: [u8; 32], proof: &[u8]) -> Result<()> { + let r = &self.relation; + if digest != r.shape.circuit.digest() { + bail!("Stage 2 AIR/PCS/FRI circuit digest mismatch"); + } + verify_fri_circuit( + &r.shape, + r.slots, + Some(r.sample_slot), + Some(r.split_slot), + r.window_slot, + r.nu, + &r.public, + proof, + crate::STAGE3_TRANSCRIPT_DOMAIN, + ) + } +} + +fn relation_census( + relation: &TranscriptBoundFriCommitPhaseRelation, +) -> Result { + let count = |value: usize, label: &str| { + u64::try_from(value) + .map_err(|error| anyhow::anyhow!("{label} exceeds u64: {error}")) + }; + let nu = count(relation.nu, "Flock table logarithm")?; + let shift = u32::try_from(nu).map_err(|error| { + anyhow::anyhow!("Flock table logarithm exceeds u32: {error}") + })?; + let table_capacity = 1u64.checked_shl(shift).ok_or_else(|| { + anyhow::anyhow!("Flock table logarithm {nu} exceeds the preflight report") + })?; + let field_sample_rows = relation.slots.field_sample.map_or(0, |slot| { + relation.shape.counts[relation.shape.registry_slot(slot)] + }); + let byte_window_rows = relation.window_slot.map_or(0, |slot| { + relation.shape.counts[relation.shape.registry_slot(slot)] + }); + + let rows = |slot| relation.shape.counts[relation.shape.registry_slot(slot)]; + + Ok(Stage3RelationCensusV1 { + circuit_digest: relation.shape.circuit.digest(), + nu, + table_capacity, + relation_inputs: count(relation.inputs.len(), "relation input count")?, + public_values: count(relation.public.len(), "public-value count")?, + blake3_rows: count(rows(relation.slots.blake3), "BLAKE3 row count")?, + digest_order_rows: count( + rows(relation.slots.order), + "digest-order row count", + )?, + goldilocks_add_rows: count( + rows(relation.slots.add), + "Goldilocks-add row count", + )?, + goldilocks_mul_rows: count( + rows(relation.slots.mul), + "Goldilocks-mul row count", + )?, + lane_repack_rows: count( + rows(relation.slots.repack), + "lane-repack row count", + )?, + canonical_goldilocks_rows: count( + rows(relation.slots.canonical), + "canonical-Goldilocks row count", + )?, + equality_rows: count(rows(relation.slots.equality), "equality row count")?, + hash_sample_rows: count( + rows(relation.sample_slot), + "hash-sample row count", + )?, + field_sample_rows: count(field_sample_rows, "field-sample row count")?, + u64_split_rows: count(rows(relation.split_slot), "u64-split row count")?, + byte_window_rows: count(byte_window_rows, "byte-window row count")?, + }) +} + +pub fn verify_stage2_air_pcs_fri_conformance( + artifact: &Stage2AirPcsFriArtifactV1, +) -> Result<()> { + verify_stage2_air_pcs_fri_with_domain( + artifact, + STAGE2_AIR_PCS_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + ) +} + +fn verify_stage2_air_pcs_fri_with_domain( + artifact: &Stage2AirPcsFriArtifactV1, + transcript_domain: &[u8], +) -> Result<()> { + let witness = &artifact.witness; + let (relation, _) = rayon::join( + || cached_stage2_air_pcs_fri_relation(witness), + || { + stage3_linchecks(); + }, + ); + let relation = relation?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Stage 2 AIR/PCS/FRI circuit digest mismatch"); + } + verify_fri_circuit( + &relation.shape, + relation.slots, + Some(relation.sample_slot), + Some(relation.split_slot), + relation.window_slot, + relation.nu, + &relation.public, + &artifact.proof_bundle_bytes, + transcript_domain, + ) +} + +pub fn verify_stage2_air_pcs_fri_conformance_for( + artifact: &Stage2AirPcsFriArtifactV1, + expected: &Stage2RootStatementV1, +) -> Result<()> { + let expected_bytes = expected.to_bytes(); + if artifact.witness.air.statement_prefix != expected_bytes[..80] { + bail!("Stage 2 AIR/PCS/FRI proof uses a different vk or FRI prefix"); + } + if artifact.witness.air.statement_digest != expected.digest() { + bail!("Stage 2 AIR/PCS/FRI proof targets a different Stage 2 root"); + } + verify_stage2_air_pcs_fri_conformance(artifact) +} + +fn validate_all_transcript_bound_pcs_fri_queries( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + fri_challenges: &Stage2FriTranscriptChallengesV1, + prefix_challenges: crate::Stage2TranscriptChallengesV1, + pcs_instance: &Stage2PcsInstanceV1, + queries: &[TranscriptBoundPcsFriQueryV1], +) -> Result<(Vec, Vec)> { + validate_stage2_pcs_instance(prefix, pcs_instance)?; + if queries.len() != fri_transcript.num_queries + || queries.len() != fri_challenges.query_indices.len() + { + bail!( + "all-query PCS/FRI relation has {} queries; transcript requires {}", + queries.len(), + fri_transcript.num_queries + ); + } + let mut fri_computations = Vec::with_capacity(queries.len()); + let mut pcs_computations = Vec::with_capacity(queries.len()); + for (query_number, query) in queries.iter().enumerate() { + ensure_transcript_binds_fri_query( + fri_transcript, + fri_challenges, + query_number, + &query.fri, + )?; + let pcs_computation = + compute_stage2_pcs_query(prefix, pcs_instance, query, prefix_challenges)?; + ensure_stage2_pcs_feeds_fri(pcs_instance, query, &pcs_computation)?; + let fri_computation = compute_commit_phase(&query.fri)?; + ensure_final_polynomial(&query.fri, &fri_computation)?; + fri_computations.push(fri_computation); + pcs_computations.push(pcs_computation); + } + Ok((fri_computations, pcs_computations)) +} + +fn validate_all_transcript_bound_fri_queries( + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + queries: &[FriCommitPhaseQueryV1], +) -> Result> { + if queries.len() != fri_transcript.num_queries + || queries.len() != challenges.query_indices.len() + { + bail!( + "all-query FRI relation has {} queries; transcript requires {}", + queries.len(), + fri_transcript.num_queries + ); + } + queries + .iter() + .enumerate() + .map(|(query_number, query)| { + ensure_transcript_binds_fri_query( + fri_transcript, + challenges, + query_number, + query, + )?; + let computation = compute_commit_phase(query)?; + ensure_final_polynomial(query, &computation)?; + Ok(computation) + }) + .collect() +} + +fn ensure_transcript_binds_fri_query( + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + query_number: usize, + query: &FriCommitPhaseQueryV1, +) -> Result<()> { + if query_number >= challenges.query_indices.len() { + bail!("FRI query number {query_number} is out of range"); + } + if fri_transcript.log_arities.iter().any(|&arity| arity != 1) { + bail!("binary FRI composition requires every log arity to equal one"); + } + if query.rounds.len() != challenges.betas.len() + || query.rounds.len() != fri_transcript.commit_phase_commitments.len() + { + bail!("transcript and FRI query round counts disagree"); + } + for (round, (query_round, &beta)) in + query.rounds.iter().zip(&challenges.betas).enumerate() + { + if query_round.beta != beta { + bail!("FRI round {round} beta does not equal the transcript challenge"); + } + } + let query_index = challenges.query_indices[query_number]; + if u64::from(query.query_index) != query_index { + bail!("FRI query index does not equal the transcript-derived index"); + } + if usize::from(fri_transcript.query_index_bits) + != usize::from(query.initial_log_height) + 1 + { + bail!("FRI query height does not equal the transcript sampling width"); + } + if fri_transcript.final_polynomial.as_slice() != [query.final_polynomial] { + bail!("FRI final polynomial does not equal the transcript observation"); + } + if fri_transcript.commit_phase_commitments.iter().any(|cap| cap.len() != 1) { + bail!("current transcript-bound FRI composition requires cap height zero"); + } + let roots = query.commitment_roots()?; + for (round, (cap, root)) in + fri_transcript.commit_phase_commitments.iter().zip(&roots).enumerate() + { + if cap[0] != *root { + bail!( + "FRI round {round} opening does not authenticate to its transcript cap" + ); + } + } + Ok(()) +} + +fn ensure_transcript_binds_opening( + challenges: crate::Stage2TranscriptChallengesV1, + opening: &PcsReducedOpeningV1, +) -> Result<()> { + if opening.zeta != challenges.zeta { + bail!("PCS zeta does not equal the constrained Stage 2 transcript zeta"); + } + if opening.alpha != challenges.pcs_alpha { + bail!( + "PCS batching challenge does not equal the constrained Stage 2 transcript challenge" + ); + } + Ok(()) +} + +struct FriFoldRelation { + shape: CircuitShape, + blake3_slot: SlotId, + order_slot: SlotId, + add_slot: SlotId, + mul_slot: SlotId, + repack_slot: SlotId, + canonical_slot: SlotId, + equality_slot: SlotId, +} + +#[derive(Clone, Copy)] +struct FriTableSlots { + blake3: SlotId, + order: SlotId, + add: SlotId, + mul: SlotId, + repack: SlotId, + canonical: SlotId, + equality: SlotId, + field_sample: Option, +} + +impl FriFoldRelation { + fn build(log_height: u8) -> Result { + validate_log_height(log_height)?; + let mut builder = ShapeBuilder::new(NU); + let arithmetic = GoldilocksCircuitSlots::declare(&mut builder, NU); + let blake3_slot = builder.slot(Blake3Gate { nu: NU }); + let order_slot = builder.slot(DigestOrderGate { nu: NU }); + let equality_slot = builder.slot(F128EqualityGate { nu: NU }); + let data_zero = builder.fixed_public_input(F128::ZERO); + let equality_zero = builder.fixed_public_input(F128::ZERO); + + let packed_iv = pack8(&IV); + let iv = [ + builder.fixed_public_input(packed_iv[0]), + builder.fixed_public_input(packed_iv[1]), + ]; + let leaf_params = builder.fixed_public_input(pack_params( + 0, + 32, + CHUNK_START | CHUNK_END | ROOT, + )); + let node_params = builder.fixed_public_input(pack_params( + 0, + 64, + CHUNK_START | CHUNK_END | ROOT, + )); + let one = builder.fixed_public_input(F128::new(1, 0)); + let factor_wires: Vec<_> = twiddle_factors(log_height) + .into_iter() + .map(|factor| builder.fixed_public_input(F128::new(factor, 0))) + .collect(); + + let folded = builder.public_input(); + let sibling = builder.public_input(); + let beta = builder.public_input(); + let index_bits: Vec<_> = + (0..=log_height).map(|_| builder.public_input()).collect(); + let path: Vec<_> = (0..log_height) + .map(|_| [builder.public_input(), builder.public_input()]) + .collect(); + let folded_result = builder.public_input(); + + let root = constrain_authenticated_fold( + &mut builder, + &arithmetic, + blake3_slot, + order_slot, + equality_slot, + data_zero, + equality_zero, + iv, + leaf_params, + node_params, + one, + &factor_wires, + folded, + sibling, + beta, + &index_bits, + &path, + folded_result, + ); + + builder.publish(root[0]); + builder.publish(root[1]); + arithmetic.finish_canonical(&mut builder); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock authenticated FRI-fold circuit: {error:?}") + })?; + Ok(Self { + shape, + blake3_slot, + order_slot, + add_slot: arithmetic.add, + mul_slot: arithmetic.mul, + repack_slot: arithmetic.repack, + canonical_slot: arithmetic.canonical, + equality_slot, + }) + } + + fn table_slots(&self) -> FriTableSlots { + FriTableSlots { + blake3: self.blake3_slot, + order: self.order_slot, + add: self.add_slot, + mul: self.mul_slot, + repack: self.repack_slot, + canonical: self.canonical_slot, + equality: self.equality_slot, + field_sample: None, + } + } +} + +struct FriCommitPhaseRelation { + shape: CircuitShape, + slots: FriTableSlots, + nu: usize, +} + +struct FriCommitPhaseRoundWires { + sibling: Wire, + beta: Wire, + reduced_opening: Option, + path: Vec<[Wire; 2]>, + result: Wire, +} + +impl FriCommitPhaseRelation { + fn build(query: &FriCommitPhaseQueryV1) -> Result { + validate_commit_phase_structure(query)?; + let nu = commit_phase_nu(query); + let mut builder = ShapeBuilder::new(nu); + let arithmetic = GoldilocksCircuitSlots::declare(&mut builder, nu); + let blake3 = builder.slot(Blake3Gate { nu }); + let order = builder.slot(DigestOrderGate { nu }); + let equality = builder.slot(F128EqualityGate { nu }); + let slots = FriTableSlots { + blake3, + order, + add: arithmetic.add, + mul: arithmetic.mul, + repack: arithmetic.repack, + canonical: arithmetic.canonical, + equality, + field_sample: None, + }; + let data_zero = builder.fixed_public_input(F128::ZERO); + let equality_zero = builder.fixed_public_input(F128::ZERO); + let packed_iv = pack8(&IV); + let iv = [ + builder.fixed_public_input(packed_iv[0]), + builder.fixed_public_input(packed_iv[1]), + ]; + let leaf_params = builder.fixed_public_input(pack_params( + 0, + 32, + CHUNK_START | CHUNK_END | ROOT, + )); + let node_params = builder.fixed_public_input(pack_params( + 0, + 64, + CHUNK_START | CHUNK_END | ROOT, + )); + let one = builder.fixed_public_input(F128::new(1, 0)); + let factor_wires: Vec> = (0..query.rounds.len()) + .map(|round| { + let log_height = query.initial_log_height - round as u8; + twiddle_factors(log_height) + .into_iter() + .map(|factor| builder.fixed_public_input(F128::new(factor, 0))) + .collect() + }) + .collect(); + + // Declare all free values before publishing computed roots, keeping the + // public layout equal to `inputs || roots`. + let initial_folded = builder.public_input(); + let index_bits: Vec<_> = + (0..=query.initial_log_height).map(|_| builder.public_input()).collect(); + let round_wires: Vec<_> = (0..query.rounds.len()) + .map(|round| { + let log_height = usize::from(query.initial_log_height) - round; + FriCommitPhaseRoundWires { + sibling: builder.public_input(), + beta: builder.public_input(), + reduced_opening: query.rounds[round] + .reduced_opening + .map(|_| builder.public_input()), + path: (0..log_height) + .map(|_| [builder.public_input(), builder.public_input()]) + .collect(), + result: builder.public_input(), + } + }) + .collect(); + let final_polynomial = builder.public_input(); + + let mut folded = initial_folded; + for (round, wires) in round_wires.iter().enumerate() { + let root = constrain_authenticated_fold( + &mut builder, + &arithmetic, + blake3, + order, + equality, + data_zero, + equality_zero, + iv, + leaf_params, + node_params, + one, + &factor_wires[round], + folded, + wires.sibling, + wires.beta, + &index_bits[round..], + &wires.path, + wires.result, + ); + builder.publish(root[0]); + builder.publish(root[1]); + folded = if let Some(reduced_opening) = wires.reduced_opening { + let beta_squared = + arithmetic.ext2_mul(&mut builder, wires.beta, wires.beta); + let rollin = + arithmetic.ext2_mul(&mut builder, beta_squared, reduced_opening); + arithmetic.add(&mut builder, wires.result, rollin) + } else { + wires.result + }; + } + let final_residual = builder.gate(equality, &[folded, final_polynomial])[0]; + builder.connect(equality_zero, final_residual); + + arithmetic.finish_canonical(&mut builder); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock FRI commit-phase circuit: {error:?}") + })?; + Ok(Self { shape, slots, nu }) + } +} + +struct PcsReductionRelation { + shape: CircuitShape, + slots: FriTableSlots, + nu: usize, +} + +impl PcsReductionRelation { + fn build(opening: &PcsReducedOpeningV1) -> Result { + validate_pcs_reduction(opening)?; + let nu = pcs_reduction_nu(opening); + let mut builder = ShapeBuilder::new(nu); + let arithmetic = GoldilocksCircuitSlots::declare(&mut builder, nu); + let blake3 = builder.slot(Blake3Gate { nu }); + let order = builder.slot(DigestOrderGate { nu }); + let equality = builder.slot(F128EqualityGate { nu }); + let slots = FriTableSlots { + blake3, + order, + add: arithmetic.add, + mul: arithmetic.mul, + repack: arithmetic.repack, + canonical: arithmetic.canonical, + equality, + field_sample: None, + }; + let data_zero = builder.fixed_public_input(F128::ZERO); + let equality_zero = builder.fixed_public_input(F128::ZERO); + let packed_iv = pack8(&IV); + let iv = [ + builder.fixed_public_input(packed_iv[0]), + builder.fixed_public_input(packed_iv[1]), + ]; + let leaf_trace = hash_trace(opening.opened_values.len() * 8); + let leaf_params: Vec<_> = leaf_trace + .rows + .iter() + .map(|&(_cv, _message, counter, block_len, flags)| { + builder.fixed_public_input(pack_params(counter, block_len, flags)) + }) + .collect(); + let node_params = builder.fixed_public_input(pack_params( + 0, + 64, + CHUNK_START | CHUNK_END | ROOT, + )); + let one = builder.fixed_public_input(F128::new(1, 0)); + let coset_shift = builder.fixed_public_input(F128::new(7, 0)); + let factor_wires: Vec<_> = pcs_x_factors(opening.log_height) + .into_iter() + .map(|factor| builder.fixed_public_input(F128::new(factor, 0))) + .collect(); + + let packed_values: Vec<_> = + opening.opened_values.chunks(2).map(|_| builder.public_input()).collect(); + let opened_at_z: Vec<_> = + opening.opened_at_z.iter().map(|_| builder.public_input()).collect(); + let zeta = builder.public_input(); + let alpha = builder.public_input(); + let initial_alpha_power = builder.public_input(); + let initial_accumulator = builder.public_input(); + let index_bits: Vec<_> = + (0..opening.log_height).map(|_| builder.public_input()).collect(); + let path: Vec<_> = (0..opening.log_height) + .map(|_| [builder.public_input(), builder.public_input()]) + .collect(); + let denominator = builder.public_input(); + let quotients: Vec<_> = + opening.opened_values.iter().map(|_| builder.public_input()).collect(); + let reduced_accumulator = builder.public_input(); + let next_alpha_power = builder.public_input(); + + for value in [zeta, alpha, initial_alpha_power, initial_accumulator] { + arithmetic.assert_canonical(&mut builder, value); + } + for &value in &opened_at_z { + arithmetic.assert_canonical(&mut builder, value); + } + arithmetic.assert_canonical(&mut builder, denominator); + for "ient in "ients { + arithmetic.assert_canonical(&mut builder, quotient); + } + + let mut px_values = Vec::with_capacity(opening.opened_values.len()); + for (packed_index, packed) in packed_values.iter().enumerate() { + arithmetic.assert_canonical(&mut builder, *packed); + let lanes = builder.gate(arithmetic.repack, &[*packed, data_zero]); + px_values.push(lanes[3]); + if 2 * packed_index + 1 < opening.opened_values.len() { + let high = builder.gate(arithmetic.repack, &[lanes[1], data_zero])[3]; + px_values.push(high); + } else { + let high = builder.gate(arithmetic.repack, &[lanes[1], data_zero])[3]; + let padding_residual = builder.gate(equality, &[high, data_zero])[0]; + builder.connect(equality_zero, padding_residual); + } + } + + let mut x = coset_shift; + for (bit, factor) in index_bits.iter().zip(&factor_wires) { + let selected = + builder.gate(order, &[*bit, one, data_zero, *factor, data_zero])[0]; + x = arithmetic.ext2_mul(&mut builder, x, selected); + } + let denominator_check = arithmetic.add(&mut builder, denominator, x); + let denominator_residual = + builder.gate(equality, &[denominator_check, zeta])[0]; + builder.connect(equality_zero, denominator_residual); + + let mut accumulator = initial_accumulator; + let mut alpha_power = initial_alpha_power; + for ((px, pz), quotient) in + px_values.iter().zip(&opened_at_z).zip("ients) + { + let quotient_product = + arithmetic.ext2_mul(&mut builder, denominator, *quotient); + let reconstructed = arithmetic.add(&mut builder, quotient_product, *px); + let quotient_residual = builder.gate(equality, &[reconstructed, *pz])[0]; + builder.connect(equality_zero, quotient_residual); + let term = arithmetic.ext2_mul(&mut builder, alpha_power, *quotient); + accumulator = arithmetic.add(&mut builder, accumulator, term); + alpha_power = arithmetic.ext2_mul(&mut builder, alpha_power, alpha); + } + let accumulator_residual = + builder.gate(equality, &[accumulator, reduced_accumulator])[0]; + builder.connect(equality_zero, accumulator_residual); + let alpha_power_residual = + builder.gate(equality, &[alpha_power, next_alpha_power])[0]; + builder.connect(equality_zero, alpha_power_residual); + + let mut current = constrain_hash( + &mut builder, + blake3, + &leaf_trace, + &leaf_params, + iv, + data_zero, + &packed_values, + )?; + for (level, sibling) in path.iter().enumerate() { + let ordered = builder.gate( + order, + &[index_bits[level], current[0], current[1], sibling[0], sibling[1]], + ); + let parent = builder.gate( + blake3, + &[ + iv[0], + iv[1], + ordered[0], + ordered[1], + ordered[2], + ordered[3], + node_params, + ], + ); + current = [parent[0], parent[1]]; + } + builder.publish(current[0]); + builder.publish(current[1]); + arithmetic.finish_canonical(&mut builder); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock PCS-reduction circuit: {error:?}") + })?; + Ok(Self { shape, slots, nu }) + } +} + +struct TranscriptBoundPcsReductionRelation { + shape: CircuitShape, + slots: FriTableSlots, + nu: usize, + inputs: Vec, + public: Vec, +} + +impl TranscriptBoundPcsReductionRelation { + fn build( + replay: &Stage2TranscriptReplayV1, + opening: &PcsReducedOpeningV1, + computation: &PcsReductionComputation, + challenges: crate::Stage2TranscriptChallengesV1, + ) -> Result { + validate_pcs_reduction(opening)?; + ensure_transcript_binds_opening(challenges, opening)?; + let transcript_capacity = 1usize << transcript_nu(replay)?; + let pcs_capacity = 1usize << pcs_reduction_nu(opening); + let nu = usize::try_from( + transcript_capacity + .checked_add(pcs_capacity) + .ok_or_else(|| anyhow::anyhow!("transcript-bound PCS row overflow"))? + .next_power_of_two() + .ilog2(), + ) + .expect("PCS row logarithm fits usize") + .max(NU); + let mut builder = ShapeBuilder::new(nu); + let arithmetic = GoldilocksCircuitSlots::declare(&mut builder, nu); + let blake3 = builder.slot(Blake3Gate { nu }); + let order = builder.slot(DigestOrderGate { nu }); + let equality = builder.slot(F128EqualityGate { nu }); + let sample_slot = builder.slot(GoldilocksSampleGate { nu }); + let slots = FriTableSlots { + blake3, + order, + add: arithmetic.add, + mul: arithmetic.mul, + repack: arithmetic.repack, + canonical: arithmetic.canonical, + equality, + field_sample: Some(sample_slot), + }; + + // `GoldilocksCircuitSlots::declare` creates its fixed canonical zero first. + let mut inputs = vec![F128::ZERO]; + let transcript = constrain_stage2_transcript( + &mut builder, + TranscriptCircuitSlots { + blake3, + sample: sample_slot, + canonical: arithmetic.canonical, + }, + replay, + nu, + )?; + inputs.extend_from_slice(&transcript.inputs); + for challenge in transcript.challenges.all() { + builder.publish(challenge); + } + let mut public = inputs.clone(); + public.extend(transcript_challenge_words(challenges)); + + let data_zero = + record_fixed(&mut builder, &mut inputs, &mut public, F128::ZERO); + let equality_zero = + record_fixed(&mut builder, &mut inputs, &mut public, F128::ZERO); + let packed_iv = pack8(&IV); + let iv = [ + record_fixed(&mut builder, &mut inputs, &mut public, packed_iv[0]), + record_fixed(&mut builder, &mut inputs, &mut public, packed_iv[1]), + ]; + let leaf_trace = hash_trace(opening.opened_values.len() * 8); + let leaf_params: Vec<_> = leaf_trace + .rows + .iter() + .map(|&(_cv, _message, counter, block_len, flags)| { + record_fixed( + &mut builder, + &mut inputs, + &mut public, + pack_params(counter, block_len, flags), + ) + }) + .collect(); + let node_params = record_fixed( + &mut builder, + &mut inputs, + &mut public, + pack_params(0, 64, CHUNK_START | CHUNK_END | ROOT), + ); + let one = + record_fixed(&mut builder, &mut inputs, &mut public, F128::new(1, 0)); + let coset_shift = + record_fixed(&mut builder, &mut inputs, &mut public, F128::new(7, 0)); + let factor_wires: Vec<_> = pcs_x_factors(opening.log_height) + .into_iter() + .map(|factor| { + record_fixed( + &mut builder, + &mut inputs, + &mut public, + F128::new(factor, 0), + ) + }) + .collect(); + + let packed_values: Vec<_> = opening + .opened_values + .chunks(2) + .map(|pair| { + record_public( + &mut builder, + &mut inputs, + &mut public, + F128::new(pair[0], pair.get(1).copied().unwrap_or(0)), + ) + }) + .collect(); + let opened_at_z: Vec<_> = opening + .opened_at_z + .iter() + .copied() + .map(|value| { + record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(value), + ) + }) + .collect(); + let initial_alpha_power = record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(opening.initial_alpha_power), + ); + let initial_accumulator = record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(opening.initial_accumulator), + ); + let index_bits: Vec<_> = (0..opening.log_height) + .map(|bit| { + record_public( + &mut builder, + &mut inputs, + &mut public, + F128::new(u64::from((opening.query_index >> bit) & 1), 0), + ) + }) + .collect(); + let path: Vec<_> = opening + .opening_proof + .iter() + .map(|sibling| { + let digest = pack_digest(sibling); + [ + record_public(&mut builder, &mut inputs, &mut public, digest[0]), + record_public(&mut builder, &mut inputs, &mut public, digest[1]), + ] + }) + .collect(); + let denominator = record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(computation.denominator), + ); + let quotients: Vec<_> = computation + .quotients + .iter() + .copied() + .map(|quotient| { + record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(quotient), + ) + }) + .collect(); + let reduced_accumulator = record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(computation.accumulator), + ); + let next_alpha_power = record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(computation.alpha_power), + ); + + let zeta = transcript.challenges.zeta; + let alpha = transcript.challenges.pcs_alpha; + for value in [zeta, alpha, initial_alpha_power, initial_accumulator] { + arithmetic.assert_canonical(&mut builder, value); + } + for &value in &opened_at_z { + arithmetic.assert_canonical(&mut builder, value); + } + arithmetic.assert_canonical(&mut builder, denominator); + for "ient in "ients { + arithmetic.assert_canonical(&mut builder, quotient); + } + + let mut px_values = Vec::with_capacity(opening.opened_values.len()); + for (packed_index, packed) in packed_values.iter().enumerate() { + arithmetic.assert_canonical(&mut builder, *packed); + let lanes = builder.gate(arithmetic.repack, &[*packed, data_zero]); + px_values.push(lanes[3]); + if 2 * packed_index + 1 < opening.opened_values.len() { + let high = builder.gate(arithmetic.repack, &[lanes[1], data_zero])[3]; + px_values.push(high); + } else { + let high = builder.gate(arithmetic.repack, &[lanes[1], data_zero])[3]; + let padding_residual = builder.gate(equality, &[high, data_zero])[0]; + builder.connect(equality_zero, padding_residual); + } + } + + let mut x = coset_shift; + for (bit, factor) in index_bits.iter().zip(&factor_wires) { + let selected = + builder.gate(order, &[*bit, one, data_zero, *factor, data_zero])[0]; + x = arithmetic.ext2_mul(&mut builder, x, selected); + } + let denominator_check = arithmetic.add(&mut builder, denominator, x); + let denominator_residual = + builder.gate(equality, &[denominator_check, zeta])[0]; + builder.connect(equality_zero, denominator_residual); + + let mut accumulator = initial_accumulator; + let mut alpha_power = initial_alpha_power; + for ((px, pz), quotient) in + px_values.iter().zip(&opened_at_z).zip("ients) + { + let quotient_product = + arithmetic.ext2_mul(&mut builder, denominator, *quotient); + let reconstructed = arithmetic.add(&mut builder, quotient_product, *px); + let quotient_residual = builder.gate(equality, &[reconstructed, *pz])[0]; + builder.connect(equality_zero, quotient_residual); + let term = arithmetic.ext2_mul(&mut builder, alpha_power, *quotient); + accumulator = arithmetic.add(&mut builder, accumulator, term); + alpha_power = arithmetic.ext2_mul(&mut builder, alpha_power, alpha); + } + let accumulator_residual = + builder.gate(equality, &[accumulator, reduced_accumulator])[0]; + builder.connect(equality_zero, accumulator_residual); + let alpha_power_residual = + builder.gate(equality, &[alpha_power, next_alpha_power])[0]; + builder.connect(equality_zero, alpha_power_residual); + + let mut current = constrain_hash( + &mut builder, + blake3, + &leaf_trace, + &leaf_params, + iv, + data_zero, + &packed_values, + )?; + for (level, sibling) in path.iter().enumerate() { + let ordered = builder.gate( + order, + &[index_bits[level], current[0], current[1], sibling[0], sibling[1]], + ); + let parent = builder.gate( + blake3, + &[ + iv[0], + iv[1], + ordered[0], + ordered[1], + ordered[2], + ordered[3], + node_params, + ], + ); + current = [parent[0], parent[1]]; + } + builder.publish(current[0]); + builder.publish(current[1]); + public.extend_from_slice(&pack_digest(&computation.root)); + + arithmetic.finish_canonical(&mut builder); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build transcript-bound PCS circuit: {error:?}") + })?; + Ok(Self { shape, slots, nu, inputs, public }) + } +} + +struct TranscriptBoundFriCommitPhaseRelation { + shape: CircuitShape, + slots: FriTableSlots, + sample_slot: SlotId, + split_slot: SlotId, + window_slot: Option, + nu: usize, + inputs: Vec, + public: Vec, +} + +struct EmittedFriRelation { + slots: FriTableSlots, + sample_slot: SlotId, + split_slot: SlotId, + window_slot: Option, + inputs: Vec, + public: Vec, +} + +#[derive(Clone, Copy)] +struct SelectedFriQuery<'a> { + query_number: usize, + query: &'a FriCommitPhaseQueryV1, + computation: &'a FriCommitPhaseComputation, + pcs_query: Option<&'a Stage2PcsQueryV1>, + pcs_computation: Option<&'a Stage2PcsQueryComputation>, +} + +impl TranscriptBoundFriCommitPhaseRelation { + #[allow(clippy::too_many_arguments)] + fn build( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + query_number: usize, + query: &FriCommitPhaseQueryV1, + computation: &FriCommitPhaseComputation, + ) -> Result { + Self::build_selected( + prefix, + fri_transcript, + challenges, + &[SelectedFriQuery { + query_number, + query, + computation, + pcs_query: None, + pcs_computation: None, + }], + None, + None, + None, + ) + } + + fn build_all( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + queries: &[FriCommitPhaseQueryV1], + computations: &[FriCommitPhaseComputation], + ) -> Result { + if queries.len() != computations.len() { + bail!("FRI query/computation vector lengths disagree"); + } + let selected: Vec<_> = queries + .iter() + .zip(computations) + .enumerate() + .map(|(query_number, (query, computation))| SelectedFriQuery { + query_number, + query, + computation, + pcs_query: None, + pcs_computation: None, + }) + .collect(); + Self::build_selected( + prefix, + fri_transcript, + challenges, + &selected, + None, + None, + None, + ) + } + + fn build_all_with_pcs( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + pcs_instance: &Stage2PcsInstanceV1, + queries: &[TranscriptBoundPcsFriQueryV1], + fri_computations: &[FriCommitPhaseComputation], + pcs_computations: &[Stage2PcsQueryComputation], + ) -> Result { + if queries.len() != fri_computations.len() + || queries.len() != pcs_computations.len() + { + bail!("PCS/FRI query and computation vector lengths disagree"); + } + let selected: Vec<_> = queries + .iter() + .zip(fri_computations) + .zip(pcs_computations) + .enumerate() + .map(|(query_number, ((query, computation), pcs_computation))| { + SelectedFriQuery { + query_number, + query: &query.fri, + computation, + pcs_query: Some(&query.pcs), + pcs_computation: Some(pcs_computation), + } + }) + .collect(); + Self::build_selected( + prefix, + fri_transcript, + challenges, + &selected, + Some(pcs_instance), + None, + None, + ) + } + + #[allow(clippy::too_many_arguments)] + fn build_all_with_pcs_and_air( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + pcs_instance: &Stage2PcsInstanceV1, + air: &Stage2AirProgramV1, + queries: &[TranscriptBoundPcsFriQueryV1], + fri_computations: &[FriCommitPhaseComputation], + pcs_computations: &[Stage2PcsQueryComputation], + limits: Option, + ) -> Result { + if queries.len() != fri_computations.len() + || queries.len() != pcs_computations.len() + { + bail!("PCS/FRI query and computation vector lengths disagree"); + } + let selected: Vec<_> = queries + .iter() + .zip(fri_computations) + .zip(pcs_computations) + .enumerate() + .map(|(query_number, ((query, computation), pcs_computation))| { + SelectedFriQuery { + query_number, + query: &query.fri, + computation, + pcs_query: Some(&query.pcs), + pcs_computation: Some(pcs_computation), + } + }) + .collect(); + Self::build_selected( + prefix, + fri_transcript, + challenges, + &selected, + Some(pcs_instance), + Some(air), + limits, + ) + } + + fn build_selected( + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + selected: &[SelectedFriQuery<'_>], + pcs_instance: Option<&Stage2PcsInstanceV1>, + air: Option<&Stage2AirProgramV1>, + limits: Option, + ) -> Result { + let trace = std::env::var_os("IX_FLOCK_TIMING").is_some(); + let total_started = std::time::Instant::now(); + let phase_started = std::time::Instant::now(); + if selected.is_empty() { + bail!("transcript-bound FRI relation has no selected queries"); + } + for item in selected { + ensure_transcript_binds_fri_query( + fri_transcript, + challenges, + item.query_number, + item.query, + )?; + ensure_final_polynomial(item.query, item.computation)?; + if item.pcs_query.is_some() != pcs_instance.is_some() + || item.pcs_computation.is_some() != pcs_instance.is_some() + { + bail!("transcript-bound FRI relation has inconsistent PCS inputs"); + } + } + if air.is_some() && pcs_instance.is_none() { + bail!("AIR evaluation requires the transcript-bound PCS instance"); + } + if trace { + eprintln!( + " [stage3-shape] validate structure: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let mut counted = CountingEmitter::new(); + Self::emit_selected( + &mut counted, + prefix, + fri_transcript, + challenges, + selected, + pcs_instance, + air, + CountingEmitter::COUNT_NU, + false, + )?; + let nu = counted.required_nu(NU)?; + if trace { + // Admission failures are useful corpus measurements too. Emit only + // the count-only result, before either limit can reject the root; + // this is not a compiled/evaluated relation or a proof-ready report. + eprintln!( + "{}", + serde_json::json!({ + "schema": "ix.flock-stage3.shape-count", "version": 1, + "compiled": false, "nu": nu, + "table_capacity": 1u64.checked_shl(u32::try_from(nu)?), + "padded_union_witness_bytes": production_padded_witness_bytes(nu)?, + "tables": counted.table_rows().map(|(gate, rows)| { + serde_json::json!({"gate": gate, "rows": rows}) + }).collect::>(), + "count_us": crate::report::elapsed_us(phase_started), + "process_peak_rss_bytes": crate::report::process_peak_rss_bytes(), + }), + ); + } + if let Some(limits) = limits { + limits.ensure_table_capacity(nu)?; + limits.ensure_union_witness(production_padded_witness_bytes(nu)?)?; + } + if trace { + eprintln!( + " [stage3-shape] count + admit (nu={nu}): {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let mut builder = ShapeBuilder::new(nu); + let emitted = Self::emit_selected( + &mut builder, + prefix, + fri_transcript, + challenges, + selected, + pcs_instance, + air, + nu, + trace, + )?; + let phase_started = std::time::Instant::now(); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build transcript-bound FRI circuit: {error:?}") + })?; + counted.ensure_matches(&shape)?; + if trace { + eprintln!( + " [stage3-shape] finish builder: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + eprintln!( + " [stage3-shape] total: {:.2} ms", + total_started.elapsed().as_secs_f64() * 1e3, + ); + } + Ok(Self { + shape, + slots: emitted.slots, + sample_slot: emitted.sample_slot, + split_slot: emitted.split_slot, + window_slot: emitted.window_slot, + nu, + inputs: emitted.inputs, + public: emitted.public, + }) + } + + #[allow(clippy::too_many_arguments)] + fn emit_selected( + builder: &mut impl CircuitEmitter, + prefix: &Stage2TranscriptReplayV1, + fri_transcript: &Stage2FriTranscriptReplayV1, + challenges: &Stage2FriTranscriptChallengesV1, + selected: &[SelectedFriQuery<'_>], + pcs_instance: Option<&Stage2PcsInstanceV1>, + air: Option<&Stage2AirProgramV1>, + nu: usize, + trace: bool, + ) -> Result { + let phase_started = std::time::Instant::now(); + let arithmetic = GoldilocksCircuitSlots::declare(builder, nu); + let blake3 = builder.slot(Blake3Gate { nu }); + let order = builder.slot(DigestOrderGate { nu }); + let equality = builder.slot(F128EqualityGate { nu }); + let sample_slot = builder.slot(HashSampleGate { nu }); + let field_sample_slot = builder.slot(GoldilocksSampleGate { nu }); + let split_slot = builder.slot(U64SplitGate { nu }); + let window_slot = pcs_instance.map(|_| builder.slot(ByteWindowGate { nu })); + let slots = FriTableSlots { + blake3, + order, + add: arithmetic.add, + mul: arithmetic.mul, + repack: arithmetic.repack, + canonical: arithmetic.canonical, + equality, + field_sample: Some(field_sample_slot), + }; + if trace { + eprintln!( + " [stage3-shape] declare slots: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + + // GoldilocksCircuitSlots declares its canonical-zero fixed input first. + let mut inputs = vec![F128::ZERO]; + let mut public = vec![F128::ZERO]; + let phase_started = std::time::Instant::now(); + let prefix_region = constrain_stage2_transcript( + builder, + TranscriptCircuitSlots { + blake3, + sample: field_sample_slot, + canonical: arithmetic.canonical, + }, + prefix, + nu, + )?; + inputs.extend_from_slice(&prefix_region.inputs); + public.extend_from_slice(&prefix_region.inputs); + for challenge in prefix_region.challenges.all() { + builder.publish(challenge); + } + public.extend(transcript_challenge_words(prefix.challenges()?)); + if trace { + eprintln!( + " [stage3-shape] prefix transcript: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + + let phase_started = std::time::Instant::now(); + let fri_region = constrain_stage2_fri_transcript( + builder, + FriTranscriptCircuitSlots { + blake3, + sample: sample_slot, + field_sample: field_sample_slot, + canonical: arithmetic.canonical, + repack: arithmetic.repack, + split: split_slot, + }, + fri_transcript, + prefix_region.state_digest, + nu, + )?; + inputs.extend_from_slice(&fri_region.inputs); + public.extend_from_slice(&fri_region.inputs); + for &beta in &fri_region.betas { + builder.publish(beta); + } + public.extend(challenges.betas.iter().copied().map(pack_extension)); + for bits in &fri_region.query_index_bits { + for &bit in bits { + builder.publish(bit); + } + } + for &index in &challenges.query_indices { + public.extend( + (0..fri_transcript.query_index_bits) + .map(|bit| F128::new((index >> bit) & 1, 0)), + ); + } + if trace { + eprintln!( + " [stage3-shape] FRI transcript: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + + let phase_started = std::time::Instant::now(); + let data_zero = record_fixed(builder, &mut inputs, &mut public, F128::ZERO); + let equality_zero = + record_fixed(builder, &mut inputs, &mut public, F128::ZERO); + let packed_iv = pack8(&IV); + let iv = [ + record_fixed(builder, &mut inputs, &mut public, packed_iv[0]), + record_fixed(builder, &mut inputs, &mut public, packed_iv[1]), + ]; + let leaf_params = record_fixed( + builder, + &mut inputs, + &mut public, + pack_params(0, 32, CHUNK_START | CHUNK_END | ROOT), + ); + let node_params = record_fixed( + builder, + &mut inputs, + &mut public, + pack_params(0, 64, CHUNK_START | CHUNK_END | ROOT), + ); + let one = record_fixed(builder, &mut inputs, &mut public, F128::new(1, 0)); + let fixed = TranscriptBoundFriFixedWires { + blake3, + order, + equality, + data_zero, + equality_zero, + iv, + leaf_params, + node_params, + one, + }; + if trace { + eprintln!( + " [stage3-shape] fixed wires: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + if let Some(air) = air { + constrain_stage2_air( + builder, + &arithmetic, + blake3, + equality, + equality_zero, + window_slot.expect("AIR byte-window slot declared above"), + data_zero, + one, + iv, + &mut inputs, + &mut public, + &prefix_region, + prefix, + pcs_instance.expect("AIR PCS instance checked above"), + air, + )?; + } + if trace { + eprintln!( + " [stage3-shape] AIR constraints: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let shared_pcs = pcs_instance + .map(|instance| { + constrain_stage2_pcs_shared( + builder, + &arithmetic, + fixed, + &mut inputs, + &mut public, + &prefix_region, + prefix, + window_slot.expect("PCS byte-window slot declared above"), + instance, + ) + }) + .transpose()?; + if trace { + eprintln!( + " [stage3-shape] shared PCS constraints: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let mut pcs_elapsed = std::time::Duration::ZERO; + let mut fri_elapsed = std::time::Duration::ZERO; + for item in selected { + let phase_started = std::time::Instant::now(); + let reduced_openings = if let Some(instance) = pcs_instance { + Some(constrain_stage2_pcs_query( + builder, + &arithmetic, + fixed, + &mut inputs, + &mut public, + &fri_region, + instance, + shared_pcs.as_ref().expect("shared PCS wires declared above"), + item.query_number, + item.pcs_query.expect("PCS query presence checked above"), + item.pcs_computation.expect("PCS computation presence checked above"), + )?) + } else { + None + }; + pcs_elapsed += phase_started.elapsed(); + let phase_started = std::time::Instant::now(); + constrain_transcript_bound_fri_query( + builder, + &arithmetic, + fixed, + &mut inputs, + &mut public, + &fri_region, + item.query_number, + item.query, + item.computation, + reduced_openings.as_ref(), + ); + fri_elapsed += phase_started.elapsed(); + } + if trace { + eprintln!( + " [stage3-shape] PCS query constraints: {:.2} ms", + pcs_elapsed.as_secs_f64() * 1e3, + ); + eprintln!( + " [stage3-shape] FRI query constraints: {:.2} ms", + fri_elapsed.as_secs_f64() * 1e3, + ); + } + + arithmetic.finish_canonical(builder); + Ok(EmittedFriRelation { + slots, + sample_slot, + split_slot, + window_slot, + inputs, + public, + }) + } +} + +/// The eleven production Boolean tables occupy 2^17 padded column bits in +/// the pinned registry. This is independent of row counts and available before +/// expensive wiring compilation. `resources()` checks it against the actual +/// finished registry, so changing a table schema cannot silently stale it. +fn production_padded_witness_bytes(nu: usize) -> Result { + const UNION_COLUMN_LOG: u32 = 17; + let log_words = + u32::try_from(nu)?.checked_add(UNION_COLUMN_LOG - 7).ok_or_else(|| { + anyhow::anyhow!("Stage 3 padded witness logarithm overflow") + })?; + 1u64 + .checked_shl(log_words) + .and_then(|words| words.checked_mul(3 * 16)) + .ok_or_else(|| anyhow::anyhow!("Stage 3 padded witness size overflow")) +} +#[derive(Clone, Copy)] +struct TranscriptBoundFriFixedWires { + blake3: SlotId, + order: SlotId, + equality: SlotId, + data_zero: Wire, + equality_zero: Wire, + iv: [Wire; 2], + leaf_params: Wire, + node_params: Wire, + one: Wire, +} + +#[allow(clippy::too_many_arguments)] +fn constrain_transcript_bound_fri_query( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + fixed: TranscriptBoundFriFixedWires, + inputs: &mut Vec, + public: &mut Vec, + fri_region: &crate::transcript::FriTranscriptConstraintRegion, + query_number: usize, + query: &FriCommitPhaseQueryV1, + computation: &FriCommitPhaseComputation, + authenticated_reduced_openings: Option<&BTreeMap>, +) { + let factor_wires: Vec> = computation + .round_queries + .iter() + .map(|round| { + twiddle_factors(round.log_height) + .into_iter() + .map(|factor| { + record_fixed(builder, inputs, public, F128::new(factor, 0)) + }) + .collect() + }) + .collect(); + let initial_folded = authenticated_reduced_openings.map_or_else( + || { + record_public( + builder, + inputs, + public, + pack_extension(query.initial_folded), + ) + }, + |openings| openings[&(query.initial_log_height + 1)], + ); + let round_wires: Vec<_> = query + .rounds + .iter() + .zip(&computation.fold_results) + .enumerate() + .map(|(round, (source, &fold_result))| { + let depth = usize::from(query.initial_log_height) - round; + FriCommitPhaseRoundWires { + sibling: record_public( + builder, + inputs, + public, + pack_extension(source.sibling), + ), + // The beta is the transcript wire, not a duplicated query input. + beta: fri_region.betas[round], + reduced_opening: if let Some(openings) = authenticated_reduced_openings + { + let height = query.initial_log_height + - u8::try_from(round).expect("bounded FRI round index"); + openings.get(&height).copied() + } else { + source.reduced_opening.map(|value| { + record_public(builder, inputs, public, pack_extension(value)) + }) + }, + path: source + .opening_proof + .iter() + .take(depth) + .map(|sibling| { + let digest = pack_digest(sibling); + [ + record_public(builder, inputs, public, digest[0]), + record_public(builder, inputs, public, digest[1]), + ] + }) + .collect(), + result: record_public( + builder, + inputs, + public, + pack_extension(fold_result), + ), + } + }) + .collect(); + + let index_bits = &fri_region.query_index_bits[query_number]; + let mut folded = initial_folded; + for (round, wires) in round_wires.iter().enumerate() { + let root = constrain_authenticated_fold( + builder, + arithmetic, + fixed.blake3, + fixed.order, + fixed.equality, + fixed.data_zero, + fixed.equality_zero, + fixed.iv, + fixed.leaf_params, + fixed.node_params, + fixed.one, + &factor_wires[round], + folded, + wires.sibling, + wires.beta, + &index_bits[round..], + &wires.path, + wires.result, + ); + let cap_root = fri_region.commitment_roots[round][0]; + for lane in 0..2 { + let residual = + builder.gate(fixed.equality, &[root[lane], cap_root[lane]])[0]; + builder.connect(fixed.equality_zero, residual); + } + folded = if let Some(reduced_opening) = wires.reduced_opening { + let beta_squared = arithmetic.ext2_mul(builder, wires.beta, wires.beta); + let rollin = arithmetic.ext2_mul(builder, beta_squared, reduced_opening); + arithmetic.add(builder, wires.result, rollin) + } else { + wires.result + }; + } + let final_residual = + builder.gate(fixed.equality, &[folded, fri_region.final_polynomial[0]])[0]; + builder.connect(fixed.equality_zero, final_residual); +} + +struct Stage2PcsRowWires { + lanes: Vec, +} + +struct Stage2PcsPointWires { + point: Wire, + alpha_offset: Wire, + opened_sum: Wire, +} + +struct Stage2PcsBatchWires { + commitment: [Wire; 2], + matrices: Vec>, +} + +/// Query-independent expressions, emitted once and then shared by explicit +/// matrix/point indices. Never deduplicate by Wire identity: the count-only +/// emitter deliberately uses one placeholder for every wire. +struct Stage2PcsSharedWires { + alpha: ExtVal, + alpha_powers: Vec, + query_point_basis: PcsQueryPointBasis, + batches: Vec, +} + +/// Fixed base-field factors for each matrix height used by an opening. +/// The cache key is protocol metadata, never a witness value or Wire ID. +struct PcsQueryPointBasis { + coset_shift: Wire, + factors: BTreeMap>, +} + +impl PcsQueryPointBasis { + fn declare( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + public: &mut Vec, + heights: impl IntoIterator, + ) -> Self { + let coset_shift = record_fixed(builder, inputs, public, F128::new(7, 0)); + let mut factors = BTreeMap::new(); + for height in heights { + factors.entry(height).or_insert_with(|| { + pcs_x_factors(height) + .into_iter() + .map(|factor| { + record_fixed(builder, inputs, public, F128::new(factor, 0)) + }) + .collect() + }); + } + Self { coset_shift, factors } + } + + #[allow(clippy::too_many_arguments)] + fn constrain_query( + &self, + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + order: SlotId, + one: Wire, + zero: Wire, + log_global_height: u8, + index_bits: &[Wire], + ) -> Result> { + if index_bits.len() != usize::from(log_global_height) { + bail!("PCS query-point bit width differs from the global height"); + } + let mut points = BTreeMap::new(); + for (&height, factors) in &self.factors { + let bit_offset = + log_global_height.checked_sub(height).ok_or_else(|| { + anyhow::anyhow!("PCS query-point height exceeds the global height") + })?; + let mut x = self.coset_shift; + for (&bit, &factor) in + index_bits[usize::from(bit_offset)..].iter().zip(factors) + { + let selected = builder.gate(order, &[bit, one, zero, factor, zero])[0]; + // Both operands are [base, 0]: the fixed factors and the selector + // constrain the upper lane to zero, as does each multiplication. + // A lane-wise multiply is exactly their extension-field product. + // Keep the arithmetic residual and output-canonicality constraints. + x = arithmetic.mul(builder, x, selected); + } + points.insert(height, x); + } + Ok(points) + } +} + +#[allow(clippy::too_many_arguments)] +fn constrain_stage2_pcs_shared( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + fixed: TranscriptBoundFriFixedWires, + inputs: &mut Vec, + public: &mut Vec, + prefix_region: &crate::transcript::TranscriptConstraintRegion, + prefix: &Stage2TranscriptReplayV1, + window: SlotId, + instance: &Stage2PcsInstanceV1, +) -> Result { + let alpha = prefix_region.challenges.pcs_alpha; + let zeta = prefix_region.challenges.zeta; + arithmetic.assert_canonical(builder, alpha); + arithmetic.assert_canonical(builder, zeta); + + // The native PCS maintains an independent exponent counter per height, + // traversing batches, matrices, points, then columns. All counters use + // the SAME alpha, so one power table suffices for every height and query. + let mut sizes = BTreeMap::::new(); + for matrix in instance.batches.iter().flat_map(|batch| &batch.matrices) { + let size = matrix + .width + .checked_mul(matrix.opening_points.len()) + .ok_or_else(|| anyhow::anyhow!("PCS alpha-power count overflow"))?; + let count = sizes.entry(matrix.log_height).or_default(); + *count = count + .checked_add(size) + .ok_or_else(|| anyhow::anyhow!("PCS alpha-power count overflow"))?; + } + let maximum = sizes.values().copied().max().unwrap_or(0); + let mut alpha_powers = vec![fixed.one]; + // Include alpha^maximum for a zero-width point at a bucket's end. + for _ in 0..maximum { + alpha_powers.push(arithmetic.ext2_mul( + builder, + *alpha_powers.last().unwrap(), + alpha, + )); + } + let mut offsets = BTreeMap::::new(); + let mut next_points = BTreeMap::new(); + let query_point_basis = PcsQueryPointBasis::declare( + builder, + inputs, + public, + instance.batches.iter().flat_map(|batch| { + batch + .matrices + .iter() + .filter(|matrix| !matrix.opening_points.is_empty()) + .map(|matrix| matrix.log_height) + }), + ); + let mut batches = Vec::with_capacity(instance.batches.len()); + for batch in &instance.batches { + let commitment = bound_transcript_digest( + builder, + window, + fixed.data_zero, + inputs, + public, + prefix_region, + batch.commitment, + ); + let mut matrices = Vec::with_capacity(batch.matrices.len()); + for matrix in &batch.matrices { + let mut points = Vec::with_capacity(matrix.opening_points.len()); + for (point_index, point_kind) in matrix.opening_points.iter().enumerate() + { + let point = match *point_kind { + Stage2PcsOpeningPointV1::Zeta => zeta, + Stage2PcsOpeningPointV1::ZetaNext { log_degree } => { + *next_points.entry(log_degree).or_insert_with(|| { + let generator = Val::TWO_ADIC_GENERATORS[usize::from(log_degree)] + .as_canonical_u64(); + let generator = + record_fixed(builder, inputs, public, F128::new(generator, 0)); + arithmetic.ext2_mul(builder, zeta, generator) + }) + }, + }; + // Horner's rule computes sum_j alpha^j p_j(point), with every p_j + // still bound to its exact transcript bytes and canonicality check. + let mut opened_sum = None; + for column in (0..matrix.width).rev() { + let value = bound_transcript_extension( + builder, + window, + fixed.data_zero, + inputs, + public, + prefix_region, + matrix.opened_values, + point_index * matrix.width + column, + ); + arithmetic.assert_canonical(builder, value); + opened_sum = Some(match opened_sum { + None => value, + Some(sum) => { + let scaled = arithmetic.ext2_mul(builder, sum, alpha); + arithmetic.add(builder, scaled, value) + }, + }); + } + let offset = offsets.entry(matrix.log_height).or_default(); + points.push(Stage2PcsPointWires { + point, + alpha_offset: alpha_powers[*offset], + opened_sum: opened_sum.unwrap_or(fixed.data_zero), + }); + *offset += matrix.width; // bounded by the checked bucket size above + } + matrices.push(points); + } + batches.push(Stage2PcsBatchWires { commitment, matrices }); + } + Ok(Stage2PcsSharedWires { + alpha: native_extension(prefix.challenges()?.pcs_alpha), + alpha_powers, + query_point_basis, + batches, + }) +} + +fn weighted_quotient(quotients: &[[u64; 2]], alpha: ExtVal) -> ExtVal { + quotients.iter().rev().fold(ExtVal::ZERO, |sum, "ient| { + sum * alpha + native_extension(quotient) + }) +} + +#[allow(clippy::too_many_arguments)] +fn constrain_stage2_pcs_denominator( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + equality: SlotId, + equality_zero: Wire, + one: Wire, + inputs: &mut Vec, + public: &mut Vec, + x: Wire, + point: Wire, + denominator_value: [u64; 2], +) -> Result { + let denominator = + record_public(builder, inputs, public, pack_extension(denominator_value)); + arithmetic.assert_canonical(builder, denominator); + let denominator_check = arithmetic.add(builder, denominator, x); + assert_f128_equal(builder, equality, equality_zero, denominator_check, point); + // Enforce a nonzero denominator, including for empty column sets. + // For D != 0, Q = sum_j alpha^j (p_j(z)-p_j(x))/D is uniquely + // equivalent to D*Q + sum_j alpha^j p_j(x) = sum_j alpha^j p_j(z). + // This eliminates auxiliary per-column quotients, not a PCS check. + let inverse_value = native_extension(denominator_value) + .try_inverse() + .ok_or_else(|| anyhow::anyhow!("PCS denominator is zero"))?; + let inverse = record_public( + builder, + inputs, + public, + pack_extension(extension_words(inverse_value)), + ); + arithmetic.assert_canonical(builder, inverse); + let inverse_check = arithmetic.ext2_mul(builder, denominator, inverse); + assert_f128_equal(builder, equality, equality_zero, inverse_check, one); + Ok(denominator) +} + +#[allow(clippy::too_many_arguments)] +fn constrain_stage2_pcs_point( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + equality: SlotId, + equality_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + denominator: Wire, + row_sum: Wire, + computation: &Stage2PcsPointComputation, + point: &Stage2PcsPointWires, + alpha: ExtVal, +) -> Wire { + let quotient = record_public( + builder, + inputs, + public, + pack_extension(extension_words(weighted_quotient( + &computation.quotients, + alpha, + ))), + ); + arithmetic.assert_canonical(builder, quotient); + let product = arithmetic.ext2_mul(builder, denominator, quotient); + let reconstructed = arithmetic.add(builder, product, row_sum); + assert_f128_equal( + builder, + equality, + equality_zero, + reconstructed, + point.opened_sum, + ); + arithmetic.ext2_mul(builder, point.alpha_offset, quotient) +} + +#[allow(clippy::too_many_arguments)] +fn constrain_stage2_pcs_query( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + fixed: TranscriptBoundFriFixedWires, + inputs: &mut Vec, + public: &mut Vec, + fri_region: &crate::transcript::FriTranscriptConstraintRegion, + instance: &Stage2PcsInstanceV1, + shared: &Stage2PcsSharedWires, + query_number: usize, + query: &Stage2PcsQueryV1, + computation: &Stage2PcsQueryComputation, +) -> Result> { + let index_bits = &fri_region.query_index_bits[query_number]; + let query_points = shared.query_point_basis.constrain_query( + builder, + arithmetic, + fixed.order, + fixed.one, + fixed.data_zero, + instance.log_global_height, + index_bits, + )?; + + let mut all_rows = Vec::with_capacity(instance.batches.len()); + for (batch, opening) in instance.batches.iter().zip(&query.batch_openings) { + let mut batch_rows = Vec::with_capacity(batch.matrices.len()); + for row in &opening.opened_rows { + let mut lanes = Vec::with_capacity(row.len()); + for &value in row { + let lane = + record_public(builder, inputs, public, F128::new(value, value)); + arithmetic.assert_canonical(builder, lane); + // Both hashing and scalar multiplication use [x,x] derived from + // one lane by the repack constraints, not an assumed host duplicate. + let duplicated = + builder.gate(arithmetic.repack, &[lane, fixed.data_zero])[0]; + lanes.push(duplicated); + } + batch_rows.push(Stage2PcsRowWires { lanes }); + } + all_rows.push(batch_rows); + } + + // Authenticate every multi-height batch. Rows sharing a height are + // concatenated in matrix order before hashing; shorter-height leaves are + // injected on the right after the corresponding path compression. + for ((((batch, opening), batch_computation), rows), shared_batch) in instance + .batches + .iter() + .zip(&query.batch_openings) + .zip(&computation.batches) + .zip(&all_rows) + .zip(&shared.batches) + { + let log_batch_height = + batch.matrices.iter().map(|matrix| matrix.log_height).max().unwrap(); + let mut leaves = BTreeMap::new(); + for height in 0..=log_batch_height { + let leaf_lanes: Vec<_> = batch + .matrices + .iter() + .zip(rows) + .filter(|(matrix, _)| matrix.log_height == height) + .flat_map(|(_, row)| row.lanes.iter().copied()) + .collect(); + if leaf_lanes.is_empty() { + continue; + } + let message: Vec<_> = leaf_lanes + .chunks(2) + .map(|pair| { + builder.gate( + arithmetic.repack, + &[pair[0], pair.get(1).copied().unwrap_or(fixed.data_zero)], + )[3] + }) + .collect(); + let trace = hash_trace(leaf_lanes.len() * 8); + let parameters: Vec<_> = trace + .rows + .iter() + .map(|&(_cv, _message, counter, block_len, flags)| { + record_fixed( + builder, + inputs, + public, + pack_params(counter, block_len, flags), + ) + }) + .collect(); + let leaf = constrain_hash( + builder, + fixed.blake3, + &trace, + ¶meters, + fixed.iv, + fixed.data_zero, + &message, + )?; + leaves.insert(height, leaf); + } + + let mut current = leaves[&log_batch_height]; + let bit_offset = usize::from(instance.log_global_height - log_batch_height); + for (level, sibling) in opening.opening_proof.iter().enumerate() { + let sibling = pack_digest(sibling); + let sibling = [ + record_public(builder, inputs, public, sibling[0]), + record_public(builder, inputs, public, sibling[1]), + ]; + let ordered = builder.gate( + fixed.order, + &[ + index_bits[bit_offset + level], + current[0], + current[1], + sibling[0], + sibling[1], + ], + ); + let parent = builder.gate( + fixed.blake3, + &[ + fixed.iv[0], + fixed.iv[1], + ordered[0], + ordered[1], + ordered[2], + ordered[3], + fixed.node_params, + ], + ); + current = [parent[0], parent[1]]; + let next_height = log_batch_height + - 1 + - u8::try_from(level).expect("bounded Merkle path level"); + if let Some(&injected) = leaves.get(&next_height) { + let parent = builder.gate( + fixed.blake3, + &[ + fixed.iv[0], + fixed.iv[1], + current[0], + current[1], + injected[0], + injected[1], + fixed.node_params, + ], + ); + current = [parent[0], parent[1]]; + } + } + let expected = shared_batch.commitment; + for lane in 0..2 { + let residual = + builder.gate(fixed.equality, &[current[lane], expected[lane]])[0]; + builder.connect(fixed.equality_zero, residual); + } + let _ = batch_computation.root; + } + + let mut buckets: BTreeMap = instance + .batches + .iter() + .flat_map(|batch| batch.matrices.iter().map(|matrix| matrix.log_height)) + .map(|height| (height, fixed.data_zero)) + .collect(); + // A denominator depends on the query, matrix HEIGHT, and semantic opening + // point (including its next-row generator), not the matrix/batch itself. + // Keep the cache local to this query, and never key it by wire/value equality. + let mut denominators = BTreeMap::new(); + for ((((batch, opening), batch_computation), rows), shared_batch) in instance + .batches + .iter() + .zip(&query.batch_openings) + .zip(&computation.batches) + .zip(&all_rows) + .zip(&shared.batches) + { + for ((((matrix, _row_values), matrix_computation), row), shared_points) in + batch + .matrices + .iter() + .zip(&opening.opened_rows) + .zip(&batch_computation.matrices) + .zip(rows) + .zip(&shared_batch.matrices) + { + // Inactive preprocessed matrices still participate in authentication, + // but contribute no opening quotient to their height bucket. + if shared_points.is_empty() { + continue; + } + let x = query_points[&matrix.log_height]; + + // Sum_j alpha^j p_j(x) once per matrix/query, shared by its opening + // points. p_j(x) is a BASE-field scalar: [x,x] * [a,b] is exactly the + // extension product [x,0] * [a,b], using one lane-wise multiply. + let mut row_sum = None; + for (&lane, &power) in row.lanes.iter().zip(&shared.alpha_powers) { + let term = arithmetic.mul(builder, lane, power); + row_sum = Some(match row_sum { + None => term, + Some(sum) => arithmetic.add(builder, sum, term), + }); + } + let row_sum = row_sum.unwrap_or(fixed.data_zero); + + for ((point_kind, point_computation), shared_point) in matrix + .opening_points + .iter() + .zip(&matrix_computation.points) + .zip(shared_points) + { + let key = (matrix.log_height, *point_kind); + let denominator = if let Some(&denominator) = denominators.get(&key) { + denominator + } else { + let denominator = constrain_stage2_pcs_denominator( + builder, + arithmetic, + fixed.equality, + fixed.equality_zero, + fixed.one, + inputs, + public, + x, + shared_point.point, + point_computation.denominator, + )?; + denominators.insert(key, denominator); + denominator + }; + let term = constrain_stage2_pcs_point( + builder, + arithmetic, + fixed.equality, + fixed.equality_zero, + inputs, + public, + denominator, + row_sum, + point_computation, + shared_point, + shared.alpha, + ); + let accumulator = + arithmetic.add(builder, buckets[&matrix.log_height], term); + buckets.insert(matrix.log_height, accumulator); + } + } + } + + Ok(buckets) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn bound_transcript_extension( + builder: &mut impl CircuitEmitter, + window: SlotId, + data_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + region: &crate::transcript::TranscriptConstraintRegion, + binding: Stage2TranscriptByteBindingV1, + extension_offset: usize, +) -> Wire { + bound_transcript_window( + builder, + window, + data_zero, + inputs, + public, + region, + binding, + extension_offset * 16, + ) +} + +fn bound_transcript_digest( + builder: &mut impl CircuitEmitter, + window: SlotId, + data_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + region: &crate::transcript::TranscriptConstraintRegion, + binding: Stage2TranscriptByteBindingV1, +) -> [Wire; 2] { + [ + bound_transcript_window( + builder, window, data_zero, inputs, public, region, binding, 0, + ), + bound_transcript_window( + builder, window, data_zero, inputs, public, region, binding, 16, + ), + ] +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn bound_transcript_window( + builder: &mut impl CircuitEmitter, + window: SlotId, + data_zero: Wire, + inputs: &mut Vec, + public: &mut Vec, + region: &crate::transcript::TranscriptConstraintRegion, + binding: Stage2TranscriptByteBindingV1, + relative_byte: usize, +) -> Wire { + let byte_offset = binding.byte_offset + relative_byte; + let word_index = byte_offset / 16; + let byte_in_word = byte_offset % 16; + let words = ®ion.observation_words[binding.segment.index()]; + let first = words[word_index]; + let second = words.get(word_index + 1).copied().unwrap_or(data_zero); + let selector = + record_fixed(builder, inputs, public, F128::new(1 << byte_in_word, 0)); + builder.gate(window, &[first, second, selector])[0] +} + +pub(crate) fn assert_f128_equal( + builder: &mut impl CircuitEmitter, + equality: SlotId, + equality_zero: Wire, + left: Wire, + right: Wire, +) { + let residual = builder.gate(equality, &[left, right])[0]; + builder.connect(equality_zero, residual); +} + +pub(crate) fn record_fixed( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + public: &mut Vec, + value: F128, +) -> Wire { + inputs.push(value); + public.push(value); + builder.fixed_public_input(value) +} + +fn record_public( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + public: &mut Vec, + value: F128, +) -> Wire { + inputs.push(value); + public.push(value); + builder.public_input() +} + +#[allow(clippy::too_many_arguments)] +fn constrain_authenticated_fold( + builder: &mut impl CircuitEmitter, + arithmetic: &GoldilocksCircuitSlots, + blake3_slot: SlotId, + order_slot: SlotId, + equality_slot: SlotId, + data_zero: Wire, + equality_zero: Wire, + iv: [Wire; 2], + leaf_params: Wire, + node_params: Wire, + one: Wire, + factor_wires: &[Wire], + folded: Wire, + sibling: Wire, + beta: Wire, + index_bits: &[Wire], + path: &[[Wire; 2]], + folded_result: Wire, +) -> [Wire; 2] { + assert_eq!(index_bits.len(), path.len() + 1); + assert_eq!(factor_wires.len(), path.len()); + + // The low query bit determines which value is e0 and which is e1. + let ordered_evals = builder + .gate(order_slot, &[index_bits[0], folded, data_zero, sibling, data_zero]); + let e0 = ordered_evals[0]; + let e1 = ordered_evals[2]; + + // ExtensionMmcs serializes `[e0.c0,e0.c1,e1.c0,e1.c1]` as 32 little- + // endian bytes before hashing the leaf. + let leaf = builder.gate( + blake3_slot, + &[iv[0], iv[1], e0, e1, data_zero, data_zero, leaf_params], + ); + let mut current = [leaf[0], leaf[1]]; + for (level, sibling_digest) in path.iter().enumerate() { + let ordered = builder.gate( + order_slot, + &[ + index_bits[level + 1], + current[0], + current[1], + sibling_digest[0], + sibling_digest[1], + ], + ); + let parent = builder.gate( + blake3_slot, + &[ + iv[0], + iv[1], + ordered[0], + ordered[1], + ordered[2], + ordered[3], + node_params, + ], + ); + current = [parent[0], parent[1]]; + } + + // `s = g_(h+1)^reverse_bits(index >> 1, h)`. Each original LSB-first + // bit selects its corresponding pre-squared factor. + let mut s = one; + for (bit, factor) in index_bits[1..].iter().zip(factor_wires) { + let selected = + builder.gate(order_slot, &[*bit, one, data_zero, *factor, data_zero])[0]; + s = arithmetic.ext2_mul(builder, s, selected); + } + + let sum = arithmetic.add(builder, e0, e1); + let two_s = arithmetic.add(builder, s, s); + let lhs_fold = arithmetic.ext2_mul(builder, two_s, folded_result); + let lhs_beta = arithmetic.ext2_mul(builder, beta, e1); + let lhs = arithmetic.add(builder, lhs_fold, lhs_beta); + let rhs_sum = arithmetic.ext2_mul(builder, s, sum); + let rhs_beta = arithmetic.ext2_mul(builder, beta, e0); + let rhs = arithmetic.add(builder, rhs_sum, rhs_beta); + let equality_residual = builder.gate(equality_slot, &[lhs, rhs])[0]; + builder.connect(equality_zero, equality_residual); + current +} + +struct Stage3Linchecks { + blake3: CscCircuit, + order: CscCircuit, + add: CscCircuit, + mul: CscCircuit, + repack: CscCircuit, + canonical: CscCircuit, + equality: CscCircuit, + sample: CscCircuit, + field_sample: CscCircuit, + split: CscCircuit, + window: CscCircuit, +} + +/// CSC transposes depend only on each table's inner Boolean matrices, not on +/// the shared outer-row logarithm. Build them once for the process and reuse +/// them across proving, verification, and every Stage 3 relation size. +fn stage3_linchecks() -> &'static Stage3Linchecks { + static CACHE: OnceLock = OnceLock::new(); + CACHE.get_or_init(|| { + let builders: [fn(usize) -> BlockR1cs; 11] = [ + flock_blake3::build_block_r1cs, + build_digest_order_r1cs, + build_goldilocks_add_r1cs, + build_goldilocks_mul_r1cs, + build_lane_repack_r1cs, + build_canonical_quad_r1cs, + build_f128_equality_r1cs, + build_hash_sample_r1cs, + build_goldilocks_sample_r1cs, + build_u64_split_r1cs, + build_byte_window_r1cs, + ]; + let circuits: Vec<_> = builders + .into_par_iter() + .map(|build| { + let r1cs = build(NU); + CscCircuit::from_matrices(&r1cs.a_0, &r1cs.b_0) + .with_const_pin(r1cs.const_pin) + }) + .collect(); + let [ + blake3, + order, + add, + mul, + repack, + canonical, + equality, + sample, + field_sample, + split, + window, + ] = circuits.try_into().expect("eleven Stage 3 lincheck circuits"); + Stage3Linchecks { + blake3, + order, + add, + mul, + repack, + canonical, + equality, + sample, + field_sample, + split, + window, + } + }) +} + +/// Keep every committed slot word deterministic even when Flock lends this +/// relation a dirty recycled buffer. Flock's merged prover can advertise +/// padding as unread, but the heterogeneous Stage 3 conformance suite has +/// exposed zerocheck reads of those words across relation shapes. Clearing +/// the slot storage here preserves the documented witness contract and +/// prevents allocator contents from affecting proofs; generators may still +/// elide their redundant padding writes and lincheck-stripe initialization. +fn initialize_slot_padding(dst: SlotWitnessDest<'_>) -> SlotWitnessDest<'_> { + const ZERO_CHUNK_WORDS: usize = 1 << 16; + let z = &mut *dst.z; + let a = &mut *dst.a; + let b = &mut *dst.b; + rayon::join( + || { + z.par_chunks_mut(ZERO_CHUNK_WORDS) + .for_each(|chunk| chunk.fill(F128::ZERO)); + }, + || { + rayon::join( + || { + a.par_chunks_mut(ZERO_CHUNK_WORDS) + .for_each(|chunk| chunk.fill(F128::ZERO)); + }, + || { + b.par_chunks_mut(ZERO_CHUNK_WORDS) + .for_each(|chunk| chunk.fill(F128::ZERO)); + }, + ); + }, + ); + dst +} + +#[allow(clippy::too_many_arguments)] +fn prove_fri_circuit( + shape: &CircuitShape, + slots: FriTableSlots, + sample_slot: Option, + split_slot: Option, + window_slot: Option, + nu: usize, + inputs: &[F128], + expected_public: &[F128], + transcript_domain: &[u8], +) -> Result> { + let trace = std::env::var_os("IX_FLOCK_TIMING").is_some(); + let total_started = std::time::Instant::now(); + let phase_started = std::time::Instant::now(); + let witness = shape.run(inputs, &[]); + if witness.public != expected_public { + bail!("Flock authenticated-FRI circuit disagrees with native semantics"); + } + if trace { + eprintln!( + " [stage3-circuit] evaluate rows: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let blake3_rows = witness.rows::(slots.blake3); + let order_rows = witness.rows::(slots.order); + let add_rows = witness.rows::(slots.add); + let mul_rows = witness.rows::(slots.mul); + let repack_rows = witness.rows::(slots.repack); + let canonical_rows = + witness.rows::(slots.canonical); + let equality_rows = witness.rows::(slots.equality); + let sample_rows = + sample_slot.map(|slot| witness.rows::(slot)); + let field_sample_rows = + slots.field_sample.map(|slot| witness.rows::(slot)); + let split_rows = split_slot.map(|slot| witness.rows::(slot)); + let window_rows = + window_slot.map(|slot| witness.rows::(slot)); + + let phase_started = std::time::Instant::now(); + let linchecks = stage3_linchecks(); + let blake3_lincheck = &linchecks.blake3; + let order_lincheck = &linchecks.order; + let add_lincheck = &linchecks.add; + let mul_lincheck = &linchecks.mul; + let repack_lincheck = &linchecks.repack; + let canonical_lincheck = &linchecks.canonical; + let equality_lincheck = &linchecks.equality; + let sample_lincheck = &linchecks.sample; + let field_sample_lincheck = &linchecks.field_sample; + let split_lincheck = &linchecks.split; + let window_lincheck = &linchecks.window; + if trace { + eprintln!( + " [stage3-circuit] compile R1CS/lincheck: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + + let phase_started = std::time::Instant::now(); + let mut slot_inputs = vec![ + ( + shape.registry_slot(slots.blake3), + UnionSlotProverInput::in_place( + move |dst| { + flock_blake3::generate_witness_batch_major_partial_into( + blake3_rows, + nu, + initialize_slot_padding(dst), + ) + }, + blake3_lincheck, + ), + ), + ( + shape.registry_slot(slots.order), + UnionSlotProverInput::in_place( + move |dst| { + generate_digest_order_witness_into( + order_rows, + nu, + initialize_slot_padding(dst), + ) + }, + order_lincheck, + ), + ), + ( + shape.registry_slot(slots.add), + UnionSlotProverInput::in_place( + move |dst| { + generate_goldilocks_add_witness_into( + add_rows, + nu, + initialize_slot_padding(dst), + ) + }, + add_lincheck, + ), + ), + ( + shape.registry_slot(slots.mul), + UnionSlotProverInput::in_place( + move |dst| { + generate_goldilocks_mul_witness_into( + mul_rows, + nu, + initialize_slot_padding(dst), + ) + }, + mul_lincheck, + ), + ), + ( + shape.registry_slot(slots.repack), + UnionSlotProverInput::in_place( + move |dst| { + generate_lane_repack_witness_into( + repack_rows, + nu, + initialize_slot_padding(dst), + ) + }, + repack_lincheck, + ), + ), + ( + shape.registry_slot(slots.canonical), + UnionSlotProverInput::in_place( + move |dst| { + generate_canonical_quad_witness_into( + canonical_rows, + nu, + initialize_slot_padding(dst), + ) + }, + canonical_lincheck, + ), + ), + ( + shape.registry_slot(slots.equality), + UnionSlotProverInput::in_place( + move |dst| { + generate_f128_equality_witness_into( + equality_rows, + nu, + initialize_slot_padding(dst), + ) + }, + equality_lincheck, + ), + ), + ]; + if let (Some(slot), Some(rows)) = (sample_slot, sample_rows) { + slot_inputs.push(( + shape.registry_slot(slot), + UnionSlotProverInput::in_place( + move |dst| { + generate_hash_sample_witness_into( + rows, + nu, + initialize_slot_padding(dst), + ) + }, + sample_lincheck, + ), + )); + } + if let (Some(slot), Some(rows)) = (slots.field_sample, field_sample_rows) { + slot_inputs.push(( + shape.registry_slot(slot), + UnionSlotProverInput::in_place( + move |dst| { + generate_goldilocks_sample_witness_into( + rows, + nu, + initialize_slot_padding(dst), + ) + }, + field_sample_lincheck, + ), + )); + } + if let (Some(slot), Some(rows)) = (split_slot, split_rows) { + slot_inputs.push(( + shape.registry_slot(slot), + UnionSlotProverInput::in_place( + move |dst| { + generate_u64_split_witness_into( + rows, + nu, + initialize_slot_padding(dst), + ) + }, + split_lincheck, + ), + )); + } + if let (Some(slot), Some(rows)) = (window_slot, window_rows) { + slot_inputs.push(( + shape.registry_slot(slot), + UnionSlotProverInput::in_place( + move |dst| { + generate_byte_window_witness_into( + rows, + nu, + initialize_slot_padding(dst), + ) + }, + window_lincheck, + ), + )); + } + sort_and_validate_slots(&mut slot_inputs)?; + let slot_inputs = slot_inputs.into_iter().map(|(_, input)| input).collect(); + let union = UnionInstance::new(&shape.registry, shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = FsChallenger::with_chained_blake3(transcript_domain); + if trace { + eprintln!( + " [stage3-circuit] assemble prover inputs: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let (proof, commitment, _) = prover::prove_fast_ligerito_union_circuit( + &union, + &shape.circuit, + &witness.public, + ¶ms, + slot_inputs, + Vec::new(), + &mut challenger, + ); + if trace { + eprintln!( + " [stage3-circuit] Flock prove: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + } + let phase_started = std::time::Instant::now(); + let proof_bundle_bytes = + encode_bundle(&FriFoldProofBundle { commitment, proof })?; + if proof_bundle_bytes.len() > MAX_BUNDLE_BYTES { + bail!("Flock authenticated-FRI proof exceeds {MAX_BUNDLE_BYTES} bytes"); + } + if trace { + eprintln!( + " [stage3-circuit] encode proof: {:.2} ms", + phase_started.elapsed().as_secs_f64() * 1e3, + ); + eprintln!( + " [stage3-circuit] total: {:.2} ms", + total_started.elapsed().as_secs_f64() * 1e3, + ); + } + Ok(proof_bundle_bytes) +} + +#[allow(clippy::too_many_arguments)] +fn verify_fri_circuit( + shape: &CircuitShape, + slots: FriTableSlots, + sample_slot: Option, + split_slot: Option, + window_slot: Option, + _nu: usize, + public: &[F128], + proof_bundle_bytes: &[u8], + transcript_domain: &[u8], +) -> Result<()> { + let bundle = decode_bundle(proof_bundle_bytes) + .context("decode Flock authenticated-FRI conformance proof bundle")?; + let lincheck_cache = stage3_linchecks(); + let blake3_lincheck = &lincheck_cache.blake3; + let order_lincheck = &lincheck_cache.order; + let add_lincheck = &lincheck_cache.add; + let mul_lincheck = &lincheck_cache.mul; + let repack_lincheck = &lincheck_cache.repack; + let canonical_lincheck = &lincheck_cache.canonical; + let equality_lincheck = &lincheck_cache.equality; + let sample_lincheck = &lincheck_cache.sample; + let field_sample_lincheck = &lincheck_cache.field_sample; + let split_lincheck = &lincheck_cache.split; + let window_lincheck = &lincheck_cache.window; + + let mut linchecks: Vec<(usize, &dyn LincheckCircuit)> = vec![ + (shape.registry_slot(slots.blake3), blake3_lincheck), + (shape.registry_slot(slots.order), order_lincheck), + (shape.registry_slot(slots.add), add_lincheck), + (shape.registry_slot(slots.mul), mul_lincheck), + (shape.registry_slot(slots.repack), repack_lincheck), + (shape.registry_slot(slots.canonical), canonical_lincheck), + (shape.registry_slot(slots.equality), equality_lincheck), + ]; + if let Some(slot) = sample_slot { + linchecks.push((shape.registry_slot(slot), sample_lincheck)); + } + if let Some(slot) = slots.field_sample { + linchecks.push((shape.registry_slot(slot), field_sample_lincheck)); + } + if let Some(slot) = split_slot { + linchecks.push((shape.registry_slot(slot), split_lincheck)); + } + if let Some(slot) = window_slot { + linchecks.push((shape.registry_slot(slot), window_lincheck)); + } + sort_and_validate_slots(&mut linchecks)?; + let linchecks: Vec<&dyn LincheckCircuit> = + linchecks.into_iter().map(|(_, lincheck)| lincheck).collect(); + let union = UnionInstance::new(&shape.registry, shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = FsChallenger::with_chained_blake3(transcript_domain); + verifier::verify_ligerito_union_circuit( + &union, + &shape.circuit, + public, + &linchecks, + &bundle.commitment, + &bundle.proof, + ¶ms, + &mut challenger, + ) + .map_err(|error| { + anyhow::anyhow!("Flock authenticated-FRI proof rejected: {error:?}") + })?; + Ok(()) +} + +fn relation_inputs( + query: &FriFoldQueryV1, + folded_result: [u64; 2], +) -> Vec { + let packed_iv = pack8(&IV); + let mut inputs = Vec::with_capacity(10 + 4 * usize::from(query.log_height)); + inputs.push(F128::ZERO); + inputs.push(F128::ZERO); + inputs.push(F128::ZERO); + inputs.extend_from_slice(&packed_iv); + inputs.push(pack_params(0, 32, CHUNK_START | CHUNK_END | ROOT)); + inputs.push(pack_params(0, 64, CHUNK_START | CHUNK_END | ROOT)); + inputs.push(F128::new(1, 0)); + inputs.extend( + twiddle_factors(query.log_height) + .into_iter() + .map(|factor| F128::new(factor, 0)), + ); + inputs.push(pack_extension(query.folded)); + inputs.push(pack_extension(query.sibling)); + inputs.push(pack_extension(query.beta)); + inputs.extend( + (0..=query.log_height) + .map(|bit| F128::new(u64::from((query.query_index >> bit) & 1), 0)), + ); + for sibling in &query.opening_proof { + inputs.extend_from_slice(&pack_digest(sibling)); + } + inputs.push(pack_extension(folded_result)); + inputs +} + +fn relation_public( + query: &FriFoldQueryV1, + folded_result: [u64; 2], + root: &[u8; 32], +) -> Vec { + let mut public = relation_inputs(query, folded_result); + public.extend_from_slice(&pack_digest(root)); + public +} + +struct FriCommitPhaseComputation { + round_queries: Vec, + fold_results: Vec<[u64; 2]>, + results: Vec<[u64; 2]>, + roots: Vec<[u8; 32]>, +} + +fn compute_commit_phase( + query: &FriCommitPhaseQueryV1, +) -> Result { + validate_commit_phase_structure(query)?; + let mut folded = query.initial_folded; + let mut round_queries = Vec::with_capacity(query.rounds.len()); + let mut fold_results = Vec::with_capacity(query.rounds.len()); + let mut results = Vec::with_capacity(query.rounds.len()); + let mut roots = Vec::with_capacity(query.rounds.len()); + for (round_index, round) in query.rounds.iter().enumerate() { + let round_u8 = u8::try_from(round_index).expect("bounded FRI round count"); + let fold_query = FriFoldQueryV1 { + log_height: query.initial_log_height - round_u8, + query_index: query.query_index >> round_index, + folded, + sibling: round.sibling, + beta: round.beta, + opening_proof: round.opening_proof.clone(), + }; + validate_query(&fold_query)?; + let fold_result = native_fold(&fold_query); + let result = round.reduced_opening.map_or(fold_result, |reduced_opening| { + let beta = native_extension(round.beta); + extension_words( + native_extension(fold_result) + + beta * beta * native_extension(reduced_opening), + ) + }); + roots.push(native_root(&fold_query)); + fold_results.push(fold_result); + results.push(result); + round_queries.push(fold_query); + folded = result; + } + Ok(FriCommitPhaseComputation { round_queries, fold_results, results, roots }) +} + +fn ensure_final_polynomial( + query: &FriCommitPhaseQueryV1, + computation: &FriCommitPhaseComputation, +) -> Result<()> { + if computation.results.last().copied() != Some(query.final_polynomial) { + bail!("FRI commit-phase fold chain does not equal the final polynomial"); + } + Ok(()) +} + +fn commit_phase_relation_inputs( + query: &FriCommitPhaseQueryV1, + computation: &FriCommitPhaseComputation, +) -> Vec { + let packed_iv = pack8(&IV); + let factor_count = computation + .round_queries + .iter() + .map(|round| usize::from(round.log_height)) + .sum::(); + let path_words = computation + .round_queries + .iter() + .map(|round| 2 * round.opening_proof.len()) + .sum::(); + let mut inputs = Vec::with_capacity( + 8 + factor_count + + 1 + + usize::from(query.initial_log_height) + + 1 + + 3 * query.rounds.len() + + query + .rounds + .iter() + .filter(|round| round.reduced_opening.is_some()) + .count() + + path_words + + 1, + ); + inputs.extend_from_slice(&[F128::ZERO, F128::ZERO, F128::ZERO]); + inputs.extend_from_slice(&packed_iv); + inputs.push(pack_params(0, 32, CHUNK_START | CHUNK_END | ROOT)); + inputs.push(pack_params(0, 64, CHUNK_START | CHUNK_END | ROOT)); + inputs.push(F128::new(1, 0)); + for round in &computation.round_queries { + inputs.extend( + twiddle_factors(round.log_height) + .into_iter() + .map(|factor| F128::new(factor, 0)), + ); + } + inputs.push(pack_extension(query.initial_folded)); + inputs.extend( + (0..=query.initial_log_height) + .map(|bit| F128::new(u64::from((query.query_index >> bit) & 1), 0)), + ); + for ((round, source), fold_result) in computation + .round_queries + .iter() + .zip(&query.rounds) + .zip(&computation.fold_results) + { + inputs.push(pack_extension(round.sibling)); + inputs.push(pack_extension(round.beta)); + if let Some(reduced_opening) = source.reduced_opening { + inputs.push(pack_extension(reduced_opening)); + } + for sibling in &round.opening_proof { + inputs.extend_from_slice(&pack_digest(sibling)); + } + inputs.push(pack_extension(*fold_result)); + } + inputs.push(pack_extension(query.final_polynomial)); + inputs +} + +fn commit_phase_relation_public( + query: &FriCommitPhaseQueryV1, + computation: &FriCommitPhaseComputation, +) -> Vec { + let mut public = commit_phase_relation_inputs(query, computation); + for root in &computation.roots { + public.extend_from_slice(&pack_digest(root)); + } + public +} + +fn commit_phase_nu(query: &FriCommitPhaseQueryV1) -> usize { + let rounds = query.rounds.len(); + let rollins = + query.rounds.iter().filter(|round| round.reduced_opening.is_some()).count(); + let height_sum = (0..rounds) + .map(|round| usize::from(query.initial_log_height) - round) + .sum::(); + let extension_multiplications = height_sum + 4 * rounds + 2 * rollins; + let row_bound = [ + 5 * extension_multiplications + 4 * rounds + rollins, + 2 * extension_multiplications, + 3 * extension_multiplications, + 9 * extension_multiplications + 4 * rounds + rollins, + 2 * height_sum + rounds, + height_sum + rounds, + rounds + 1, + ] + .into_iter() + .max() + .unwrap(); + usize::try_from(row_bound.next_power_of_two().ilog2()).unwrap().max(NU) +} + +fn validate_commit_phase_structure( + query: &FriCommitPhaseQueryV1, +) -> Result<()> { + validate_log_height(query.initial_log_height)?; + validate_commit_phase_round_count( + query.initial_log_height, + query.rounds.len(), + )?; + if u64::from(query.query_index) >= 1u64 << (query.initial_log_height + 1) { + bail!( + "FRI commit-phase query index {} does not fit {} bits", + query.query_index, + query.initial_log_height + 1 + ); + } + validate_extension(query.initial_folded, "initial folded evaluation")?; + validate_extension(query.final_polynomial, "final polynomial")?; + for (round_index, round) in query.rounds.iter().enumerate() { + let expected_depth = usize::from(query.initial_log_height) - round_index; + if round.opening_proof.len() != expected_depth { + bail!( + "FRI commit-phase round {round_index} has path depth {}; expected {expected_depth}", + round.opening_proof.len() + ); + } + validate_extension(round.sibling, "FRI round sibling")?; + validate_extension(round.beta, "FRI round challenge")?; + if let Some(reduced_opening) = round.reduced_opening { + validate_extension(reduced_opening, "FRI reduced opening")?; + } + } + Ok(()) +} + +fn validate_commit_phase_round_count( + initial_log_height: u8, + round_count: usize, +) -> Result<()> { + // Every round lowers the authenticated tree by one level. The height is + // already capped by `MAX_LOG_HEIGHT`, so this is a protocol-derived bound + // rather than a second, arbitrary implementation ceiling. The + // transcript-bound path additionally fixes the exact count through its + // folding-arity schedule and FRI parameters. + let maximum = usize::from(initial_log_height); + if !(1..=maximum).contains(&round_count) { + bail!("FRI commit-phase round count {round_count}; expected 1..={maximum}"); + } + Ok(()) +} + +fn commit_phase_rounds_bytes( + initial_log_height: u8, + round_count: usize, +) -> Result { + validate_commit_phase_round_count(initial_log_height, round_count)?; + (0..round_count).try_fold(0usize, |length, round| { + let depth = usize::from(initial_log_height) - round; + length + .checked_add(49 + depth * 32) + .ok_or_else(|| anyhow::anyhow!("FRI commit-phase round bytes overflow")) + }) +} + +fn build_stage2_pcs_fri_witness( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + typed: &Stage3TypedProofWitnessV1, +) -> Result { + typed.ensure_profile(prepared.advice_profile())?; + let key = AiurVerifyingKey::from_bytes(prepared.verifying_key_bytes()) + .map_err(|error| anyhow::anyhow!("decode Aiur PCS key: {error}"))?; + if key.to_bytes() != prepared.verifying_key_bytes() { + bail!("Aiur PCS key is not canonically encoded"); + } + if key.commitment_parameters().cap_height != 0 { + bail!("Stage 3 PCS lowering currently requires cap height zero"); + } + if fri_parameter_words(&key.fri_parameters()) != fri_parameter_words(fri) { + bail!("Stage 3 PCS lowering uses different FRI parameters"); + } + + let prefix = + Stage2TranscriptReplayV1::from_prepared_and_typed(prepared, fri, typed)?; + let fri_transcript = + Stage2FriTranscriptReplayV1::from_prepared_and_typed(prepared, fri, typed)?; + let metadata = key.pcs_circuit_metadata(); + if metadata.len() != typed.active.len() { + bail!("Aiur PCS metadata and activation lengths disagree"); + } + let active_indices: Vec<_> = typed + .active + .iter() + .enumerate() + .filter_map(|(index, &active)| active.then_some(index)) + .collect(); + if active_indices.len() != typed.log_degrees.len() { + bail!("Aiur PCS active-circuit and log-degree counts disagree"); + } + let mut active_position = vec![None; typed.active.len()]; + for (position, &circuit) in active_indices.iter().enumerate() { + active_position[circuit] = Some(position); + } + + let preprocessed_roots = key.preprocessed_commitment_roots(); + let initial_preprocessed_offset = key + .transcript_seed_and_shape_bytes() + .len() + .checked_add(typed.active.len() * 8) + .ok_or_else(|| anyhow::anyhow!("initial transcript offset overflow"))?; + let initial_stage_1_offset = initial_preprocessed_offset + .checked_add( + preprocessed_roots.as_ref().map_or(0, |roots| roots.len() * 32), + ) + .ok_or_else(|| anyhow::anyhow!("Stage 1 commitment offset overflow"))?; + ensure_single_root(&typed.commitments.stage_1_trace, "Stage 1")?; + ensure_single_root(&typed.commitments.stage_2_trace, "Stage 2")?; + ensure_single_root(&typed.commitments.quotient_chunks, "quotient")?; + + let log_blowup = u8::try_from(key.commitment_parameters().log_blowup) + .map_err(|_| anyhow::anyhow!("PCS blowup height exceeds u8"))?; + let mut opening_offset = 0usize; + + let mut stage_1_matrices = Vec::with_capacity(active_indices.len()); + for (position, &circuit_index) in active_indices.iter().enumerate() { + let circuit = metadata[circuit_index]; + ensure_opened_matrix_shape( + &typed.stage_1_opened_values, + position, + 2, + circuit.main_width, + "Stage 1", + )?; + let log_degree = typed.log_degrees[position]; + stage_1_matrices.push(stage2_pcs_matrix( + log_degree, + log_blowup, + circuit.main_width, + vec![ + Stage2PcsOpeningPointV1::Zeta, + Stage2PcsOpeningPointV1::ZetaNext { log_degree }, + ], + &mut opening_offset, + )?); + } + + let mut stage_2_matrices = Vec::with_capacity(active_indices.len()); + for (position, &circuit_index) in active_indices.iter().enumerate() { + let circuit = metadata[circuit_index]; + ensure_opened_matrix_shape( + &typed.stage_2_opened_values, + position, + 2, + circuit.stage_2_width, + "Stage 2", + )?; + let log_degree = typed.log_degrees[position]; + stage_2_matrices.push(stage2_pcs_matrix( + log_degree, + log_blowup, + circuit.stage_2_width, + vec![ + Stage2PcsOpeningPointV1::Zeta, + Stage2PcsOpeningPointV1::ZetaNext { log_degree }, + ], + &mut opening_offset, + )?); + } + + let mut quotient_matrices = Vec::with_capacity(active_indices.len()); + for (position, &circuit_index) in active_indices.iter().enumerate() { + let circuit = metadata[circuit_index]; + ensure_opened_matrix_shape( + &typed.quotient_opened_values, + position, + 1, + circuit.quotient_width, + "quotient", + )?; + quotient_matrices.push(stage2_pcs_matrix( + typed.log_degrees[position], + log_blowup, + circuit.quotient_width, + vec![Stage2PcsOpeningPointV1::Zeta], + &mut opening_offset, + )?); + } + + let mut batches = vec![ + Stage2PcsBatchV1 { + commitment: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Initial, + initial_stage_1_offset, + ), + matrices: stage_1_matrices, + }, + Stage2PcsBatchV1 { + commitment: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Stage2AndAccumulator, + 0, + ), + matrices: stage_2_matrices, + }, + Stage2PcsBatchV1 { + commitment: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::QuotientCommitment, + 0, + ), + matrices: quotient_matrices, + }, + ]; + + if let Some(roots) = &preprocessed_roots { + ensure_single_root(roots, "preprocessed")?; + let opened = + typed.preprocessed_opened_values.as_ref().ok_or_else(|| { + anyhow::anyhow!("preprocessed commitment has no opened-value round") + })?; + let mut preprocessed: Vec<_> = metadata + .iter() + .enumerate() + .filter_map(|(circuit, metadata)| { + metadata.preprocessed_slot.map(|slot| (slot, circuit, *metadata)) + }) + .collect(); + preprocessed.sort_by_key(|(slot, _, _)| *slot); + if opened.len() != preprocessed.len() { + bail!("preprocessed PCS metadata and opened-value counts disagree"); + } + let mut matrices = Vec::with_capacity(preprocessed.len()); + for (expected_slot, (slot, circuit_index, circuit)) in + preprocessed.into_iter().enumerate() + { + if slot != expected_slot { + bail!("preprocessed PCS slots are not contiguous"); + } + let (log_degree, opening_points) = + if let Some(position) = active_position[circuit_index] { + ensure_opened_matrix_shape( + opened, + slot, + 2, + circuit.preprocessed_width, + "preprocessed", + )?; + let log_degree = typed.log_degrees[position]; + ( + log_degree, + vec![ + Stage2PcsOpeningPointV1::Zeta, + Stage2PcsOpeningPointV1::ZetaNext { log_degree }, + ], + ) + } else { + ensure_opened_matrix_shape( + opened, + slot, + 0, + circuit.preprocessed_width, + "inactive preprocessed", + )?; + let height = circuit.preprocessed_height; + if !height.is_power_of_two() { + bail!("preprocessed matrix height is not a power of two"); + } + ( + u8::try_from(height.ilog2()) + .map_err(|_| anyhow::anyhow!("preprocessed height exceeds u8"))?, + Vec::new(), + ) + }; + matrices.push(stage2_pcs_matrix( + log_degree, + log_blowup, + circuit.preprocessed_width, + opening_points, + &mut opening_offset, + )?); + } + batches.push(Stage2PcsBatchV1 { + commitment: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Initial, + initial_preprocessed_offset, + ), + matrices, + }); + } else if typed.preprocessed_opened_values.is_some() { + bail!("proof has preprocessed openings but the key has no commitment"); + } + if opening_offset != prefix.pcs_opening_observations.len() { + bail!( + "Stage 2 PCS adapter consumed {opening_offset} opening bytes; transcript has {}", + prefix.pcs_opening_observations.len() + ); + } + + let pcs_instance = Stage2PcsInstanceV1 { + log_global_height: fri_transcript.query_index_bits, + log_blowup, + batches, + }; + validate_stage2_pcs_instance(&prefix, &pcs_instance)?; + let prefix_challenges = prefix.challenges()?; + let fri_challenges = fri_transcript.challenges(&prefix)?; + if typed.opening_proof.query_proofs.len() + != fri_challenges.query_indices.len() + { + bail!("typed PCS query count disagrees with transcript samples"); + } + if typed.opening_proof.final_poly.as_slice() + != fri_transcript.final_polynomial + { + bail!("typed and transcript final polynomials disagree"); + } + let final_polynomial = + *typed.opening_proof.final_poly.first().ok_or_else(|| { + anyhow::anyhow!("Stage 2 FRI final polynomial is empty") + })?; + if typed.opening_proof.final_poly.len() != 1 { + bail!( + "Stage 3 PCS/FRI adapter currently requires a constant final polynomial" + ); + } + + let mut queries = Vec::with_capacity(fri_challenges.query_indices.len()); + for (query_number, (typed_query, &query_index)) in typed + .opening_proof + .query_proofs + .iter() + .zip(&fri_challenges.query_indices) + .enumerate() + { + if typed_query.input_proof.len() != pcs_instance.batches.len() { + bail!("typed PCS query {query_number} has the wrong batch count"); + } + let pcs = Stage2PcsQueryV1 { + batch_openings: typed_query + .input_proof + .iter() + .map(|opening| Stage2PcsBatchOpeningV1 { + opened_rows: opening.opened_values.clone(), + opening_proof: opening.opening_proof.clone(), + }) + .collect(), + }; + if typed_query.commit_phase_openings.len() != fri_challenges.betas.len() { + bail!("typed FRI query {query_number} has the wrong round count"); + } + let rounds: Vec<_> = typed_query + .commit_phase_openings + .iter() + .zip(&fri_challenges.betas) + .enumerate() + .map(|(round, (opening, &beta))| { + if opening.log_arity != 1 || opening.sibling_values.len() != 1 { + bail!("typed FRI query {query_number} round {round} is not binary"); + } + Ok(FriCommitPhaseRoundV1 { + sibling: opening.sibling_values[0], + beta, + reduced_opening: None, + opening_proof: opening.opening_proof.clone(), + }) + }) + .collect::>()?; + let mut query = TranscriptBoundPcsFriQueryV1 { + pcs, + fri: FriCommitPhaseQueryV1 { + initial_log_height: pcs_instance + .log_global_height + .checked_sub(1) + .ok_or_else(|| anyhow::anyhow!("FRI global height is zero"))?, + query_index: u32::try_from(query_index) + .map_err(|_| anyhow::anyhow!("FRI query index exceeds u32"))?, + initial_folded: [0, 0], + rounds, + final_polynomial, + }, + }; + let computation = compute_stage2_pcs_query( + &prefix, + &pcs_instance, + &query, + prefix_challenges, + )?; + query.fri.initial_folded = *computation + .reduced_openings + .get(&pcs_instance.log_global_height) + .ok_or_else(|| { + anyhow::anyhow!("typed PCS query has no initial bucket") + })?; + for (round, opening) in query.fri.rounds.iter_mut().enumerate() { + let height = pcs_instance.log_global_height + - 1 + - u8::try_from(round).expect("bounded FRI round index"); + opening.reduced_opening = + computation.reduced_openings.get(&height).copied(); + } + ensure_stage2_pcs_feeds_fri(&pcs_instance, &query, &computation)?; + let fri_computation = compute_commit_phase(&query.fri)?; + ensure_final_polynomial(&query.fri, &fri_computation)?; + ensure_transcript_binds_fri_query( + &fri_transcript, + &fri_challenges, + query_number, + &query.fri, + )?; + queries.push(query); + } + + Ok(Stage2PcsFriWitnessV1 { prefix, fri_transcript, pcs_instance, queries }) +} + +fn ensure_single_root(roots: &[[u8; 32]], label: &str) -> Result<()> { + if roots.len() != 1 { + bail!("{label} PCS commitment has {} roots; expected one", roots.len()); + } + Ok(()) +} + +fn ensure_opened_matrix_shape( + round: &[Vec>], + matrix: usize, + points: usize, + width: usize, + label: &str, +) -> Result<()> { + let opened = round.get(matrix).ok_or_else(|| { + anyhow::anyhow!("{label} opened matrix {matrix} is missing") + })?; + if opened.len() != points { + bail!( + "{label} opened matrix {matrix} has {} points; expected {points}", + opened.len() + ); + } + if opened.iter().any(|values| values.len() != width) { + bail!("{label} opened matrix {matrix} has the wrong width"); + } + Ok(()) +} + +fn stage2_pcs_matrix( + log_degree: u8, + log_blowup: u8, + width: usize, + opening_points: Vec, + opening_offset: &mut usize, +) -> Result { + let opened_values = Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::PcsOpening, + *opening_offset, + ); + *opening_offset = opening_points + .len() + .checked_mul(width) + .and_then(|count| count.checked_mul(16)) + .and_then(|bytes| opening_offset.checked_add(bytes)) + .ok_or_else(|| anyhow::anyhow!("PCS opening transcript offset overflow"))?; + Ok(Stage2PcsMatrixV1 { + log_height: log_degree + .checked_add(log_blowup) + .ok_or_else(|| anyhow::anyhow!("PCS matrix height overflow"))?, + width, + opening_points, + opened_values, + }) +} + +struct Stage2PcsPointComputation { + denominator: [u64; 2], + quotients: Vec<[u64; 2]>, +} + +struct Stage2PcsMatrixComputation { + points: Vec, +} + +struct Stage2PcsBatchComputation { + root: [u8; 32], + matrices: Vec, +} + +struct Stage2PcsQueryComputation { + batches: Vec, + reduced_openings: BTreeMap, +} + +fn validate_stage2_pcs_instance( + replay: &Stage2TranscriptReplayV1, + instance: &Stage2PcsInstanceV1, +) -> Result<()> { + validate_log_height(instance.log_global_height)?; + if instance.log_blowup >= instance.log_global_height { + bail!( + "Stage 2 PCS blowup height {} must be below global height {}", + instance.log_blowup, + instance.log_global_height + ); + } + if instance.batches.is_empty() { + bail!("Stage 2 PCS instance has no input batches"); + } + + let mut observed_global_height = 0u8; + for (batch_index, batch) in instance.batches.iter().enumerate() { + validate_transcript_binding( + replay, + batch.commitment, + 4, + &format!("PCS batch {batch_index} commitment"), + )?; + if batch.matrices.is_empty() { + bail!("Stage 2 PCS batch {batch_index} has no matrices"); + } + let batch_max = + batch.matrices.iter().map(|matrix| matrix.log_height).max().unwrap(); + observed_global_height = observed_global_height.max(batch_max); + if batch_max > instance.log_global_height { + bail!( + "Stage 2 PCS batch {batch_index} height {batch_max} exceeds global height {}", + instance.log_global_height + ); + } + for (matrix_index, matrix) in batch.matrices.iter().enumerate() { + if matrix.log_height > batch_max { + unreachable!("batch maximum was computed from all matrices"); + } + validate_reduced_opening_width(matrix.width)?; + for point in &matrix.opening_points { + if let Stage2PcsOpeningPointV1::ZetaNext { log_degree } = point + && usize::from(*log_degree) >= Val::TWO_ADIC_GENERATORS.len() + { + bail!( + "Stage 2 PCS batch {batch_index} matrix {matrix_index} opening generator exceeds Goldilocks two-adicity" + ); + } + } + let lane_count = matrix + .opening_points + .len() + .checked_mul(matrix.width) + .and_then(|count| count.checked_mul(2)) + .ok_or_else(|| { + anyhow::anyhow!("Stage 2 PCS OOD binding length overflow") + })?; + validate_transcript_binding( + replay, + matrix.opened_values, + lane_count, + &format!("PCS batch {batch_index} matrix {matrix_index} OOD values"), + )?; + } + } + if observed_global_height != instance.log_global_height { + bail!( + "Stage 2 PCS global height is {}; tallest matrix has height {observed_global_height}", + instance.log_global_height + ); + } + Ok(()) +} + +fn validate_stage2_pcs_query( + replay: &Stage2TranscriptReplayV1, + instance: &Stage2PcsInstanceV1, + query: &TranscriptBoundPcsFriQueryV1, +) -> Result<()> { + validate_stage2_pcs_instance(replay, instance)?; + if query.pcs.batch_openings.len() != instance.batches.len() { + bail!( + "Stage 2 PCS query has {} batch openings; expected {}", + query.pcs.batch_openings.len(), + instance.batches.len() + ); + } + if query.fri.initial_log_height + 1 != instance.log_global_height { + bail!("Stage 2 PCS and FRI global heights disagree"); + } + if usize::from(instance.log_global_height - instance.log_blowup) + != query.fri.rounds.len() + { + bail!("Stage 2 PCS and FRI final heights disagree"); + } + for (batch_index, (batch, opening)) in + instance.batches.iter().zip(&query.pcs.batch_openings).enumerate() + { + if opening.opened_rows.len() != batch.matrices.len() { + bail!( + "Stage 2 PCS batch {batch_index} has {} opened rows; expected {}", + opening.opened_rows.len(), + batch.matrices.len() + ); + } + let batch_max = + batch.matrices.iter().map(|matrix| matrix.log_height).max().unwrap(); + if opening.opening_proof.len() != usize::from(batch_max) { + bail!( + "Stage 2 PCS batch {batch_index} path has {} siblings; expected {batch_max}", + opening.opening_proof.len() + ); + } + for (matrix_index, (matrix, row)) in + batch.matrices.iter().zip(&opening.opened_rows).enumerate() + { + if row.len() != matrix.width { + bail!( + "Stage 2 PCS batch {batch_index} matrix {matrix_index} row width is {}; expected {}", + row.len(), + matrix.width + ); + } + for (column, &value) in row.iter().enumerate() { + if value >= GOLDILOCKS_MODULUS { + bail!( + "Stage 2 PCS batch {batch_index} matrix {matrix_index} column {column} is not canonical Goldilocks" + ); + } + } + } + } + Ok(()) +} + +fn compute_stage2_pcs_query( + replay: &Stage2TranscriptReplayV1, + instance: &Stage2PcsInstanceV1, + query: &TranscriptBoundPcsFriQueryV1, + challenges: crate::Stage2TranscriptChallengesV1, +) -> Result { + validate_stage2_pcs_query(replay, instance, query)?; + let alpha = native_extension(challenges.pcs_alpha); + let zeta = native_extension(challenges.zeta); + let mut buckets: BTreeMap = instance + .batches + .iter() + .flat_map(|batch| batch.matrices.iter().map(|matrix| matrix.log_height)) + .map(|height| (height, (ExtVal::ONE, ExtVal::ZERO))) + .collect(); + let mut batches = Vec::with_capacity(instance.batches.len()); + + for (batch, opening) in instance.batches.iter().zip(&query.pcs.batch_openings) + { + let root = native_stage2_pcs_batch_root( + instance.log_global_height, + query.fri.query_index, + batch, + opening, + ); + if root != read_bound_digest(replay, batch.commitment)? { + bail!( + "Stage 2 PCS input opening does not authenticate to its transcript commitment" + ); + } + let mut matrices = Vec::with_capacity(batch.matrices.len()); + for (matrix_index, (matrix, row)) in + batch.matrices.iter().zip(&opening.opened_rows).enumerate() + { + let local_index = query.fri.query_index + >> (instance.log_global_height - matrix.log_height); + let x = ExtVal::new([ + Val::from_u64(pcs_query_point(matrix.log_height, local_index)), + Val::ZERO, + ]); + let opened_at_z = read_bound_extensions( + replay, + matrix.opened_values, + matrix.opening_points.len() * matrix.width, + )?; + let mut points = Vec::with_capacity(matrix.opening_points.len()); + for (point_index, point_kind) in matrix.opening_points.iter().enumerate() + { + let point = native_stage2_pcs_point(zeta, *point_kind); + let denominator = point - x; + if denominator == ExtVal::ZERO { + bail!( + "Stage 2 PCS batch matrix {matrix_index} opening point {point_index} equals its query point" + ); + } + let values = &opened_at_z + [point_index * matrix.width..(point_index + 1) * matrix.width]; + let mut quotients = Vec::with_capacity(matrix.width); + let (alpha_power, accumulator) = buckets + .get_mut(&matrix.log_height) + .expect("one PCS bucket per matrix height"); + for (&p_at_x, &p_at_z) in row.iter().zip(values) { + let p_at_x = ExtVal::new([Val::from_u64(p_at_x), Val::ZERO]); + let quotient = (native_extension(p_at_z) - p_at_x) / denominator; + *accumulator += *alpha_power * quotient; + *alpha_power *= alpha; + quotients.push(extension_words(quotient)); + } + points.push(Stage2PcsPointComputation { + denominator: extension_words(denominator), + quotients, + }); + } + matrices.push(Stage2PcsMatrixComputation { points }); + } + batches.push(Stage2PcsBatchComputation { root, matrices }); + } + + let reduced_openings = buckets + .into_iter() + .map(|(height, (_, accumulator))| (height, extension_words(accumulator))) + .collect(); + Ok(Stage2PcsQueryComputation { batches, reduced_openings }) +} + +fn ensure_stage2_pcs_feeds_fri( + instance: &Stage2PcsInstanceV1, + query: &TranscriptBoundPcsFriQueryV1, + computation: &Stage2PcsQueryComputation, +) -> Result<()> { + let initial = + computation.reduced_openings.get(&instance.log_global_height).ok_or_else( + || anyhow::anyhow!("missing initial Stage 2 reduced opening"), + )?; + if query.fri.initial_folded != *initial { + bail!("FRI initial value is not the authenticated PCS reduced opening"); + } + for (round, fri_round) in query.fri.rounds.iter().enumerate() { + let height = instance.log_global_height + - 1 + - u8::try_from(round).expect("bounded FRI round index"); + let expected = computation.reduced_openings.get(&height).copied(); + if fri_round.reduced_opening != expected { + bail!( + "FRI round {round} reduced-opening schedule disagrees with PCS buckets" + ); + } + } + let covered_minimum = instance.log_global_height + - u8::try_from(query.fri.rounds.len()).expect("bounded FRI round count"); + if computation.reduced_openings.keys().any(|&height| { + height != instance.log_global_height && height < covered_minimum + }) { + bail!("PCS reduced opening remains below the FRI final height"); + } + Ok(()) +} + +fn native_stage2_pcs_point( + zeta: ExtVal, + point: Stage2PcsOpeningPointV1, +) -> ExtVal { + match point { + Stage2PcsOpeningPointV1::Zeta => zeta, + Stage2PcsOpeningPointV1::ZetaNext { log_degree } => { + let generator = Val::TWO_ADIC_GENERATORS[usize::from(log_degree)]; + zeta * ExtVal::new([generator, Val::ZERO]) + }, + } +} + +fn native_stage2_pcs_batch_root( + log_global_height: u8, + query_index: u32, + batch: &Stage2PcsBatchV1, + opening: &Stage2PcsBatchOpeningV1, +) -> [u8; 32] { + let log_batch_height = + batch.matrices.iter().map(|matrix| matrix.log_height).max().unwrap(); + let local_index = query_index >> (log_global_height - log_batch_height); + let mut current = native_stage2_pcs_leaf(batch, opening, log_batch_height); + for (level, sibling) in opening.opening_proof.iter().enumerate() { + let mut message = [0u8; 64]; + let (left, right) = if (local_index >> level) & 1 == 0 { + (¤t, sibling) + } else { + (sibling, ¤t) + }; + message[..32].copy_from_slice(left); + message[32..].copy_from_slice(right); + current = *native_blake3::hash(&message).as_bytes(); + let next_height = log_batch_height + - 1 + - u8::try_from(level).expect("bounded Merkle path level"); + if batch.matrices.iter().any(|matrix| matrix.log_height == next_height) { + let injected = native_stage2_pcs_leaf(batch, opening, next_height); + let mut message = [0u8; 64]; + message[..32].copy_from_slice(¤t); + message[32..].copy_from_slice(&injected); + current = *native_blake3::hash(&message).as_bytes(); + } + } + current +} + +fn native_stage2_pcs_leaf( + batch: &Stage2PcsBatchV1, + opening: &Stage2PcsBatchOpeningV1, + log_height: u8, +) -> [u8; 32] { + let mut bytes = Vec::new(); + for (matrix, row) in batch.matrices.iter().zip(&opening.opened_rows) { + if matrix.log_height == log_height { + for &value in row { + bytes.extend_from_slice(&value.to_le_bytes()); + } + } + } + *native_blake3::hash(&bytes).as_bytes() +} + +fn validate_transcript_binding( + replay: &Stage2TranscriptReplayV1, + binding: Stage2TranscriptByteBindingV1, + lane_count: usize, + label: &str, +) -> Result<()> { + let bytes = transcript_segment(replay, binding.segment); + let start = binding.byte_offset; + let end = lane_count + .checked_mul(8) + .and_then(|length| start.checked_add(length)) + .ok_or_else(|| anyhow::anyhow!("{label} byte range overflow"))?; + if end > bytes.len() { + bail!( + "{label} binding ends at byte {end}; transcript segment has {} bytes", + bytes.len() + ); + } + Ok(()) +} + +fn read_bound_digest( + replay: &Stage2TranscriptReplayV1, + binding: Stage2TranscriptByteBindingV1, +) -> Result<[u8; 32]> { + validate_transcript_binding(replay, binding, 4, "PCS commitment")?; + let bytes = transcript_segment(replay, binding.segment); + let start = binding.byte_offset; + Ok(bytes[start..start + 32].try_into().unwrap()) +} + +fn read_bound_extensions( + replay: &Stage2TranscriptReplayV1, + binding: Stage2TranscriptByteBindingV1, + count: usize, +) -> Result> { + validate_transcript_binding(replay, binding, count * 2, "PCS OOD opening")?; + let bytes = transcript_segment(replay, binding.segment); + let start = binding.byte_offset; + (0..count) + .map(|index| { + let offset = start + index * 16; + let value = [ + u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()), + u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap()), + ]; + validate_extension(value, "transcript-bound PCS OOD value")?; + Ok(value) + }) + .collect() +} + +fn transcript_segment( + replay: &Stage2TranscriptReplayV1, + segment: Stage2TranscriptSegmentV1, +) -> &[u8] { + match segment { + Stage2TranscriptSegmentV1::Initial => &replay.initial_observations, + Stage2TranscriptSegmentV1::Stage2AndAccumulator => { + &replay.stage2_and_accumulator_observations + }, + Stage2TranscriptSegmentV1::QuotientCommitment => { + &replay.quotient_commitment_observations + }, + Stage2TranscriptSegmentV1::PcsOpening => &replay.pcs_opening_observations, + } +} + +struct PcsReductionComputation { + denominator: [u64; 2], + quotients: Vec<[u64; 2]>, + accumulator: [u64; 2], + alpha_power: [u64; 2], + root: [u8; 32], +} + +fn compute_pcs_reduction( + opening: &PcsReducedOpeningV1, +) -> Result { + validate_pcs_reduction(opening)?; + let x = ExtVal::new([ + Val::from_u64(pcs_query_point(opening.log_height, opening.query_index)), + Val::ZERO, + ]); + let zeta = native_extension(opening.zeta); + let denominator = zeta - x; + if denominator == ExtVal::ZERO { + bail!("PCS reduced opening has zeta equal to the query-domain point"); + } + let alpha = native_extension(opening.alpha); + let mut alpha_power = native_extension(opening.initial_alpha_power); + let mut accumulator = native_extension(opening.initial_accumulator); + let mut quotients = Vec::with_capacity(opening.opened_values.len()); + for (&px, &pz) in opening.opened_values.iter().zip(&opening.opened_at_z) { + let px = ExtVal::new([Val::from_u64(px), Val::ZERO]); + let pz = native_extension(pz); + let quotient = (pz - px) / denominator; + accumulator += alpha_power * quotient; + alpha_power *= alpha; + quotients.push(extension_words(quotient)); + } + Ok(PcsReductionComputation { + denominator: extension_words(denominator), + quotients, + accumulator: extension_words(accumulator), + alpha_power: extension_words(alpha_power), + root: native_pcs_row_root(opening), + }) +} + +fn pcs_reduction_relation_inputs( + opening: &PcsReducedOpeningV1, + computation: &PcsReductionComputation, +) -> Vec { + let packed_iv = pack8(&IV); + let width = opening.opened_values.len(); + let mut inputs = Vec::with_capacity( + 9 + usize::from(opening.log_height) + + width.div_ceil(2) + + width + + 4 + + usize::from(opening.log_height) * 3 + + 1 + + width + + 2, + ); + inputs.extend_from_slice(&[F128::ZERO, F128::ZERO, F128::ZERO]); + inputs.extend_from_slice(&packed_iv); + inputs.extend(hash_trace(width * 8).rows.iter().map( + |&(_cv, _message, counter, block_len, flags)| { + pack_params(counter, block_len, flags) + }, + )); + inputs.push(pack_params(0, 64, CHUNK_START | CHUNK_END | ROOT)); + inputs.push(F128::new(1, 0)); + inputs.push(F128::new(7, 0)); + inputs.extend( + pcs_x_factors(opening.log_height) + .into_iter() + .map(|factor| F128::new(factor, 0)), + ); + for pair in opening.opened_values.chunks(2) { + inputs.push(F128::new(pair[0], pair.get(1).copied().unwrap_or(0))); + } + inputs.extend(opening.opened_at_z.iter().copied().map(pack_extension)); + inputs.push(pack_extension(opening.zeta)); + inputs.push(pack_extension(opening.alpha)); + inputs.push(pack_extension(opening.initial_alpha_power)); + inputs.push(pack_extension(opening.initial_accumulator)); + inputs.extend( + (0..opening.log_height) + .map(|bit| F128::new(u64::from((opening.query_index >> bit) & 1), 0)), + ); + for sibling in &opening.opening_proof { + inputs.extend_from_slice(&pack_digest(sibling)); + } + inputs.push(pack_extension(computation.denominator)); + inputs.extend(computation.quotients.iter().copied().map(pack_extension)); + inputs.push(pack_extension(computation.accumulator)); + inputs.push(pack_extension(computation.alpha_power)); + inputs +} + +fn pcs_reduction_relation_public( + opening: &PcsReducedOpeningV1, + computation: &PcsReductionComputation, +) -> Vec { + let mut public = pcs_reduction_relation_inputs(opening, computation); + public.extend_from_slice(&pack_digest(&computation.root)); + public +} + +fn validate_pcs_reduction(opening: &PcsReducedOpeningV1) -> Result<()> { + validate_log_height(opening.log_height)?; + validate_reduced_opening_width(opening.opened_values.len())?; + if opening.opened_at_z.len() != opening.opened_values.len() { + bail!( + "PCS reduced opening has {} base values but {} OOD values", + opening.opened_values.len(), + opening.opened_at_z.len() + ); + } + if opening.opening_proof.len() != usize::from(opening.log_height) { + bail!( + "PCS reduced opening has path depth {}; expected {}", + opening.opening_proof.len(), + opening.log_height + ); + } + if u64::from(opening.query_index) >= 1u64 << opening.log_height { + bail!( + "PCS query index {} does not fit {} bits", + opening.query_index, + opening.log_height + ); + } + for (column, &value) in opening.opened_values.iter().enumerate() { + if value >= GOLDILOCKS_MODULUS { + bail!("PCS opened value {column} is not canonical Goldilocks"); + } + } + for (column, &value) in opening.opened_at_z.iter().enumerate() { + validate_extension(value, &format!("PCS OOD value {column}"))?; + } + for (value, name) in [ + (opening.zeta, "PCS zeta"), + (opening.alpha, "PCS alpha"), + (opening.initial_alpha_power, "PCS initial alpha power"), + (opening.initial_accumulator, "PCS initial accumulator"), + ] { + validate_extension(value, name)?; + } + Ok(()) +} + +fn validate_reduced_opening_width(width: usize) -> Result<()> { + if !(1..=MAX_REDUCED_OPENING_WIDTH).contains(&width) { + bail!( + "PCS reduced-opening width {width}; expected 1..={MAX_REDUCED_OPENING_WIDTH}" + ); + } + Ok(()) +} + +fn pcs_reduction_nu(opening: &PcsReducedOpeningV1) -> usize { + let width = opening.opened_values.len(); + let height = usize::from(opening.log_height); + // This deliberately over-approximates the busiest shared slot. It keeps the + // circuit builder fail-closed while supporting tree-hashed rows wider than a + // single BLAKE3 block. + let row_bound = width + .saturating_mul(128) + .saturating_add(height.saturating_mul(64)) + .saturating_add(hash_trace(width * 8).rows.len()) + .max(1); + usize::try_from(row_bound.next_power_of_two().ilog2()).unwrap().max(NU) +} + +fn native_pcs_row_root(opening: &PcsReducedOpeningV1) -> [u8; 32] { + let mut leaf = Vec::with_capacity(opening.opened_values.len() * 8); + for value in &opening.opened_values { + leaf.extend_from_slice(&value.to_le_bytes()); + } + let mut current = *native_blake3::hash(&leaf).as_bytes(); + for (level, sibling) in opening.opening_proof.iter().enumerate() { + let mut block = [0u8; 64]; + let (left, right) = if (opening.query_index >> level) & 1 == 0 { + (¤t, sibling) + } else { + (sibling, ¤t) + }; + block[..32].copy_from_slice(left); + block[32..].copy_from_slice(right); + current = *native_blake3::hash(&block).as_bytes(); + } + current +} + +fn pcs_query_point(log_height: u8, query_index: u32) -> u64 { + pcs_x_factors(log_height) + .into_iter() + .enumerate() + .filter(|(bit, _)| (query_index >> bit) & 1 == 1) + .fold(7, |point, (_, factor)| goldilocks_mul(point, factor)) +} + +fn twiddle_factors(log_height: u8) -> Vec { + reversed_exponent_factors(log_height + 1, log_height) +} + +fn pcs_x_factors(log_height: u8) -> Vec { + reversed_exponent_factors(log_height, log_height) +} + +fn reversed_exponent_factors(generator_log: u8, exponent_bits: u8) -> Vec { + let generator = + Val::TWO_ADIC_GENERATORS[usize::from(generator_log)].as_canonical_u64(); + (0..exponent_bits) + .map(|bit| { + let squarings = usize::from(exponent_bits - 1 - bit); + (0..squarings).fold(generator, |value, _| goldilocks_mul(value, value)) + }) + .collect() +} + +fn subgroup_point(query: &FriFoldQueryV1) -> u64 { + let index = query.query_index >> 1; + twiddle_factors(query.log_height) + .into_iter() + .enumerate() + .filter(|(bit, _)| (index >> bit) & 1 == 1) + .fold(1, |point, (_, factor)| goldilocks_mul(point, factor)) +} + +fn ordered_evaluations(query: &FriFoldQueryV1) -> ([u64; 2], [u64; 2]) { + if query.query_index & 1 == 0 { + (query.folded, query.sibling) + } else { + (query.sibling, query.folded) + } +} + +fn native_fold(query: &FriFoldQueryV1) -> [u64; 2] { + let (e0_words, e1_words) = ordered_evaluations(query); + let e0 = native_extension(e0_words); + let e1 = native_extension(e1_words); + let beta = native_extension(query.beta); + let s = Val::from_u64(subgroup_point(query)); + let two = Val::ONE + Val::ONE; + let half = ExtVal::new([two, Val::ZERO]); + let two_s = ExtVal::new([two * s, Val::ZERO]); + extension_words((e0 + e1) / half + beta * ((e0 - e1) / two_s)) +} + +fn native_root(query: &FriFoldQueryV1) -> [u8; 32] { + let (e0, e1) = ordered_evaluations(query); + let mut leaf = [0u8; 32]; + for (chunk, word) in + leaf.as_chunks_mut::<8>().0.iter_mut().zip([e0[0], e0[1], e1[0], e1[1]]) + { + chunk.copy_from_slice(&word.to_le_bytes()); + } + let mut current = *native_blake3::hash(&leaf).as_bytes(); + for (level, sibling) in query.opening_proof.iter().enumerate() { + let mut block = [0u8; 64]; + let direction = (query.query_index >> (level + 1)) & 1; + let (left, right) = + if direction == 0 { (¤t, sibling) } else { (sibling, ¤t) }; + block[..32].copy_from_slice(left); + block[32..].copy_from_slice(right); + current = *native_blake3::hash(&block).as_bytes(); + } + current +} + +fn native_extension(value: [u64; 2]) -> ExtVal { + ExtVal::new(value.map(Val::from_u64)) +} + +fn extension_words(value: ExtVal) -> [u64; 2] { + let coefficients: &[Val] = value.as_basis_coefficients_slice(); + [coefficients[0].as_canonical_u64(), coefficients[1].as_canonical_u64()] +} + +fn pack_extension(value: [u64; 2]) -> F128 { + F128::new(value[0], value[1]) +} + +fn pack_digest(digest: &[u8; 32]) -> [F128; 2] { + [pack_bytes(&digest[..16]), pack_bytes(&digest[16..])] +} + +fn validate_query(query: &FriFoldQueryV1) -> Result<()> { + validate_log_height(query.log_height)?; + if query.opening_proof.len() != usize::from(query.log_height) { + bail!( + "FRI-fold opening has depth {}; expected {} for cap height zero", + query.opening_proof.len(), + query.log_height + ); + } + if u64::from(query.query_index) >= 1u64 << (query.log_height + 1) { + bail!( + "FRI query index {} does not fit {} bits", + query.query_index, + query.log_height + 1 + ); + } + validate_extension(query.folded, "folded evaluation")?; + validate_extension(query.sibling, "sibling evaluation")?; + validate_extension(query.beta, "FRI challenge")?; + Ok(()) +} + +fn validate_log_height(log_height: u8) -> Result<()> { + if !(MIN_LOG_HEIGHT..=MAX_LOG_HEIGHT).contains(&log_height) { + bail!( + "FRI fold log height {log_height}; expected {MIN_LOG_HEIGHT}..={MAX_LOG_HEIGHT}" + ); + } + Ok(()) +} + +fn validate_extension(value: [u64; 2], name: &str) -> Result<()> { + for (coordinate, word) in value.into_iter().enumerate() { + if word >= GOLDILOCKS_MODULUS { + bail!("{name} coordinate {coordinate} is not canonical Goldilocks"); + } + } + Ok(()) +} + +fn encode_extension(bytes: &mut Vec, value: [u64; 2]) { + bytes.extend_from_slice(&value[0].to_le_bytes()); + bytes.extend_from_slice(&value[1].to_le_bytes()); +} + +fn decode_extension(bytes: &[u8]) -> [u64; 2] { + [ + u64::from_le_bytes(bytes[..8].try_into().unwrap()), + u64::from_le_bytes(bytes[8..16].try_into().unwrap()), + ] +} + +fn sort_and_validate_slots(slots: &mut [(usize, T)]) -> Result<()> { + slots.sort_by_key(|(slot, _)| *slot); + for (expected, (observed, _)) in slots.iter().enumerate() { + if *observed != expected { + bail!( + "Flock FRI-fold table registry is incomplete: expected slot {expected}, observed {observed}" + ); + } + } + Ok(()) +} + +fn encode_bundle(bundle: &FriFoldProofBundle) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .serialize(bundle) + .context("encode Flock FRI-fold conformance proof bundle") +} + +fn decode_bundle(bytes: &[u8]) -> Result { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(MAX_BUNDLE_BYTES as u64) + .reject_trailing_bytes() + .deserialize(bytes) + .context("invalid Flock FRI-fold conformance proof bundle") +} + +#[cfg(test)] +mod tests { + use aiur::vk_codec::aiur_config_system_to_bytes; + use ix_terminal::validate_and_expand_root_inputs; + use multi_stark::{ + expr::Expr, + lookup::Lookup, + p3_matrix::dense::RowMajorMatrix, + system::{CircuitInputs, System, SystemWitness}, + types::{CommitmentParameters, GoldilocksBlake3Config}, + }; + + use super::*; + + fn prepared_stage2_pcs_fixture() + -> (ValidatedStage2RootV1, FriParameters, Vec, Vec, Vec) { + let commitment = CommitmentParameters { log_blowup: 1, cap_height: 0 }; + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 2, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 0, + }; + prepared_stage2_pcs_fixture_with(commitment, fri) + } + + fn prepared_stage2_pcs_fixture_with( + commitment: CommitmentParameters, + fri: FriParameters, + ) -> (ValidatedStage2RootV1, FriParameters, Vec, Vec, Vec) { + prepared_stage2_pcs_fixture_with_lookup_group(commitment, fri, 1) + } + + fn prepared_stage2_pcs_fixture_with_lookup_group( + commitment: CommitmentParameters, + fri: FriParameters, + lookup_group_size: usize, + ) -> (ValidatedStage2RootV1, FriParameters, Vec, Vec, Vec) { + const CLAIM_WORDS: usize = 18; + const CLAIM_CIRCUIT_WIDTH: usize = CLAIM_WORDS + 2; + const TALL_HEIGHT: usize = 8; + const SHORT_HEIGHT: usize = 4; + + let claim: Vec<_> = (0..CLAIM_WORDS) + .map(|word| Val::from_u64(0x100 + u64::try_from(word).unwrap())) + .collect(); + // Split the same claim multiplicity across one whole lookup group. + // The default size-one fixture and its transport remain unchanged. + let claim_lookup = Lookup::pull( + Expr::main(0) + * Expr::constant(Val::from_usize(lookup_group_size).inverse()), + (1..=CLAIM_WORDS) + .map(|column| Expr::main(u32::try_from(column).unwrap())) + .collect(), + ); + let multiplicity = Expr::main(0); + let multiplicity_is_boolean = + multiplicity.clone() * (multiplicity - Expr::constant(Val::ONE)); + let preprocessed_matches = + Expr::main(u32::try_from(CLAIM_CIRCUIT_WIDTH - 1).unwrap()) + - Expr::preprocessed(0); + let short_first = + Expr::IsFirstRow * (Expr::main(0) - Expr::constant(Val::from_u64(7))); + let short_transition = Expr::IsTransition + * (Expr::main_next(0) - Expr::main(0) - Expr::constant(Val::from_u64(9))); + let config = GoldilocksBlake3Config::new(commitment, fri); + let (system, key) = System::new( + config, + [ + CircuitInputs { main_width: 1, ..Default::default() }, + CircuitInputs { + main_width: CLAIM_CIRCUIT_WIDTH, + preprocessed: Some(RowMajorMatrix::new( + (0..TALL_HEIGHT) + .map(|row| Val::from_u64(5 * u64::try_from(row).unwrap() + 3)) + .collect(), + 1, + )), + constraints: vec![multiplicity_is_boolean, preprocessed_matches], + lookups: vec![claim_lookup; lookup_group_size], + lookup_group_size, + ..Default::default() + }, + CircuitInputs { + main_width: 3, + constraints: vec![short_first, short_transition], + ..Default::default() + }, + ], + ); + + let mut tall_values = vec![Val::ZERO; TALL_HEIGHT * CLAIM_CIRCUIT_WIDTH]; + tall_values[0] = Val::ONE; + tall_values[1..=CLAIM_WORDS].copy_from_slice(&claim); + for row in 0..TALL_HEIGHT { + tall_values[row * CLAIM_CIRCUIT_WIDTH + CLAIM_CIRCUIT_WIDTH - 1] = + Val::from_u64(5 * u64::try_from(row).unwrap() + 3); + } + let tall_trace = RowMajorMatrix::new(tall_values, CLAIM_CIRCUIT_WIDTH); + let short_trace = RowMajorMatrix::new( + (0..SHORT_HEIGHT * 3) + .map(|word| Val::from_u64(3 * u64::try_from(word).unwrap() + 7)) + .collect(), + 3, + ); + let proof = system.prove( + &key, + &claim, + SystemWitness::from_stage_1( + vec![RowMajorMatrix::new(Vec::new(), 1), tall_trace, short_trace], + &system, + ), + ); + system.verify(&claim, &proof).expect("fixture proof must verify"); + + let vk_bytes = aiur_config_system_to_bytes(&system, commitment, fri); + let claim_bytes: Vec<_> = claim + .iter() + .flat_map(|word| word.as_canonical_u64().to_le_bytes()) + .collect(); + let proof_bytes = proof.to_bytes().expect("encode fixture proof"); + let prepared = validate_and_expand_root_inputs( + &vk_bytes, + &claim_bytes, + &proof_bytes, + &fri, + ) + .expect("validate and expand fixture proof"); + (prepared, fri, vk_bytes, claim_bytes, proof_bytes) + } + + fn fixture() -> FriFoldQueryV1 { + FriFoldQueryV1 { + log_height: 4, + query_index: 0b1_0110, + folded: [0x1234_5678_9abc_def0, 0x0fed_cba9_8765_4321], + sibling: [17, GOLDILOCKS_MODULUS - 9], + beta: [0x1111_2222_3333_4444, 0x5555_6666_7777_8888], + opening_proof: (0..4u8) + .map(|level| { + *native_blake3::hash(&[b'f', b'r', b'i', level]).as_bytes() + }) + .collect(), + } + } + + fn commit_phase_fixture() -> FriCommitPhaseQueryV1 { + let mut query = FriCommitPhaseQueryV1 { + initial_log_height: 4, + query_index: 0b1_0110, + initial_folded: [0x1234_5678_9abc_def0, 0x0fed_cba9_8765_4321], + rounds: (0..3u8) + .map(|round| { + let depth = 4 - usize::from(round); + FriCommitPhaseRoundV1 { + sibling: [ + 17 + u64::from(round), + GOLDILOCKS_MODULUS - 9 - u64::from(round), + ], + beta: [ + 0x1111_2222_3333_4444 + u64::from(round), + 0x5555_6666_7777_8888 + u64::from(round), + ], + reduced_opening: (round != 1) + .then_some([100 + u64::from(round), 200 + u64::from(round)]), + opening_proof: (0..depth) + .map(|level| { + *native_blake3::hash(&[ + b'q', + round, + u8::try_from(level).unwrap(), + ]) + .as_bytes() + }) + .collect(), + } + }) + .collect(), + final_polynomial: [0, 0], + }; + let computation = compute_commit_phase(&query).unwrap(); + query.final_polynomial = *computation.results.last().unwrap(); + query + } + + fn pcs_reduction_fixture() -> PcsReducedOpeningV1 { + PcsReducedOpeningV1 { + log_height: 4, + query_index: 0b1010, + opened_values: vec![3, 5, 8, 13, 21], + opened_at_z: vec![ + [34, 55], + [89, 144], + [233, 377], + [610, 987], + [1597, 2584], + ], + zeta: [0x1020_3040_5060_7080, 0x1122_3344_5566_7788], + alpha: [0x3141_5926_5358_9793, 0x2384_6264_3383_2795], + initial_alpha_power: [7, 11], + initial_accumulator: [17, 19], + opening_proof: (0..4u8) + .map(|level| { + *native_blake3::hash(&[b'p', b'c', b's', level]).as_bytes() + }) + .collect(), + } + } + + fn wide_pcs_reduction_fixture() -> PcsReducedOpeningV1 { + let mut opening = pcs_reduction_fixture(); + opening.opened_values = (0..129u64).map(|column| 3 * column + 5).collect(); + opening.opened_at_z = + (0..129u64).map(|column| [7 * column + 11, 13 * column + 17]).collect(); + opening + } + + fn transcript_replay_fixture() -> Stage2TranscriptReplayV1 { + let mut initial_observations = b"multi-stark/v0".to_vec(); + for value in 0..19u64 { + initial_observations.extend_from_slice(&(value * 17 + 3).to_le_bytes()); + } + initial_observations.extend_from_slice(&[0xa5, 0x5a, 0x11]); + Stage2TranscriptReplayV1 { + initial_observations, + stage2_and_accumulator_observations: (0..79u8) + .map(|value| value.wrapping_mul(29)) + .collect(), + quotient_commitment_observations: (0..32u8) + .map(|value| value ^ 0x6d) + .collect(), + pcs_opening_observations: (0..117u8) + .map(|value| value.wrapping_mul(7).wrapping_add(1)) + .collect(), + } + } + + fn transcript_bound_pcs_fixture() + -> (Stage2TranscriptReplayV1, PcsReducedOpeningV1) { + let replay = transcript_replay_fixture(); + let challenges = replay.challenges().unwrap(); + let mut opening = pcs_reduction_fixture(); + opening.zeta = challenges.zeta; + opening.alpha = challenges.pcs_alpha; + (replay, opening) + } + + fn zero_extension_tree(log_height: u8) -> (Vec<[u8; 32]>, [u8; 32]) { + let mut current = *native_blake3::hash(&[0u8; 32]).as_bytes(); + let mut path = Vec::with_capacity(usize::from(log_height)); + for _ in 0..log_height { + path.push(current); + let mut message = [0u8; 64]; + message[..32].copy_from_slice(¤t); + message[32..].copy_from_slice(¤t); + current = *native_blake3::hash(&message).as_bytes(); + } + (path, current) + } + + fn transcript_bound_fri_fixture_with_round_count( + round_count: usize, + ) -> ( + Stage2TranscriptReplayV1, + Stage2FriTranscriptReplayV1, + FriCommitPhaseQueryV1, + ) { + let prefix = transcript_replay_fixture(); + // Model the production binary schedule with logBlowup=2 and a constant + // final polynomial: global height = rounds + 2, while the first folded + // height is global height - 1. + assert!((1..=30).contains(&round_count)); + let initial_log_height = u8::try_from(round_count + 1).unwrap(); + let trees: Vec<_> = (0..round_count) + .map(|round| { + zero_extension_tree(initial_log_height - u8::try_from(round).unwrap()) + }) + .collect(); + let mut fri_transcript = Stage2FriTranscriptReplayV1 { + commit_phase_commitments: trees + .iter() + .map(|(_, root)| vec![*root]) + .collect(), + commit_pow_witnesses: vec![0; round_count], + final_polynomial: vec![[0, 0]], + log_arities: vec![1; round_count], + query_pow_witness: 0, + commit_pow_bits: 0, + query_pow_bits: 4, + num_queries: 5, + query_index_bits: initial_log_height + 1, + }; + let challenges = (0..1_000u64) + .find_map(|witness| { + fri_transcript.query_pow_witness = witness; + fri_transcript.challenges(&prefix).ok() + }) + .expect("small query-PoW fixture has a witness"); + let query = FriCommitPhaseQueryV1 { + initial_log_height, + query_index: u32::try_from(challenges.query_indices[0]).unwrap(), + initial_folded: [0, 0], + rounds: trees + .into_iter() + .zip(challenges.betas) + .map(|((opening_proof, _), beta)| FriCommitPhaseRoundV1 { + sibling: [0, 0], + beta, + reduced_opening: None, + opening_proof, + }) + .collect(), + final_polynomial: [0, 0], + }; + (prefix, fri_transcript, query) + } + + fn transcript_bound_fri_fixture() -> ( + Stage2TranscriptReplayV1, + Stage2FriTranscriptReplayV1, + FriCommitPhaseQueryV1, + ) { + transcript_bound_fri_fixture_with_round_count(3) + } + + fn transcript_bound_fri_all_queries_fixture() -> ( + Stage2TranscriptReplayV1, + Stage2FriTranscriptReplayV1, + Vec, + ) { + let (prefix, mut fri_transcript, template) = transcript_bound_fri_fixture(); + fri_transcript.num_queries = 2; + let challenges = fri_transcript.challenges(&prefix).unwrap(); + let queries = challenges + .query_indices + .iter() + .map(|&query_index| { + let mut query = template.clone(); + query.query_index = u32::try_from(query_index).unwrap(); + query + }) + .collect(); + (prefix, fri_transcript, queries) + } + + fn linear_base_value(slope: u64, intercept: u64, x: u64) -> u64 { + (Val::from_u64(slope) * Val::from_u64(x) + Val::from_u64(intercept)) + .as_canonical_u64() + } + + fn linear_extension_value(slope: u64, intercept: u64, x: ExtVal) -> [u64; 2] { + extension_words( + ExtVal::new([Val::from_u64(slope), Val::ZERO]) * x + + ExtVal::new([Val::from_u64(intercept), Val::ZERO]), + ) + } + + fn hash_base_rows(rows: &[&[u64]]) -> [u8; 32] { + let mut bytes = Vec::new(); + for row in rows { + for &value in *row { + bytes.extend_from_slice(&value.to_le_bytes()); + } + } + *native_blake3::hash(&bytes).as_bytes() + } + + fn hash_children(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] { + let mut bytes = [0u8; 64]; + bytes[..32].copy_from_slice(left); + bytes[32..].copy_from_slice(right); + *native_blake3::hash(&bytes).as_bytes() + } + + fn multiheight_batch_root_and_path( + matrix_heights: &[u8], + matrix_rows: &[Vec>], + query_index: usize, + ) -> ([u8; 32], Vec<[u8; 32]>) { + let log_max = *matrix_heights.iter().max().unwrap(); + let leaf_layer = |height: u8| { + (0..1usize << height) + .map(|row_index| { + let rows: Vec<_> = matrix_heights + .iter() + .zip(matrix_rows) + .filter(|(matrix_height, _)| **matrix_height == height) + .map(|(_, rows)| rows[row_index].as_slice()) + .collect(); + hash_base_rows(&rows) + }) + .collect::>() + }; + let mut current = leaf_layer(log_max); + let mut index = query_index; + let mut path = Vec::with_capacity(usize::from(log_max)); + for next_height in (0..log_max).rev() { + path.push(current[index ^ 1]); + let mut parents: Vec<_> = current + .as_chunks::<2>() + .0 + .iter() + .map(|children| hash_children(&children[0], &children[1])) + .collect(); + if matrix_heights.contains(&next_height) { + for (parent, injected) in + parents.iter_mut().zip(leaf_layer(next_height)) + { + *parent = hash_children(parent, &injected); + } + } + current = parents; + index >>= 1; + } + (current[0], path) + } + + fn constant_extension_tree_root_and_path( + value: [u64; 2], + log_height: u8, + query_index: usize, + ) -> ([u8; 32], Vec<[u8; 32]>) { + let mut leaf_bytes = [0u8; 32]; + for (chunk, word) in leaf_bytes + .as_chunks_mut::<8>() + .0 + .iter_mut() + .zip([value[0], value[1], value[0], value[1]]) + { + chunk.copy_from_slice(&word.to_le_bytes()); + } + let leaf = *native_blake3::hash(&leaf_bytes).as_bytes(); + let mut current = vec![leaf; 1usize << log_height]; + let mut index = query_index; + let mut path = Vec::with_capacity(usize::from(log_height)); + for _ in 0..log_height { + path.push(current[index ^ 1]); + current = current + .as_chunks::<2>() + .0 + .iter() + .map(|children| hash_children(&children[0], &children[1])) + .collect(); + index >>= 1; + } + (current[0], path) + } + + fn transcript_bound_pcs_fri_fixture() -> ( + Stage2TranscriptReplayV1, + Stage2FriTranscriptReplayV1, + Stage2PcsInstanceV1, + Vec, + ) { + const LOG_GLOBAL: u8 = 4; + const LOG_BLOWUP: u8 = 1; + let matrix_heights = [4u8, 3u8]; + let matrix_rows = vec![ + (0..1u32 << matrix_heights[0]) + .map(|index| { + let x = pcs_query_point(matrix_heights[0], index); + vec![linear_base_value(2, 11, x), linear_base_value(3, 17, x)] + }) + .collect::>(), + (0..1u32 << matrix_heights[1]) + .map(|index| { + let x = pcs_query_point(matrix_heights[1], index); + vec![linear_base_value(5, 23, x)] + }) + .collect::>(), + ]; + let (input_root, _) = + multiheight_batch_root_and_path(&matrix_heights, &matrix_rows, 0); + + let mut prefix = Stage2TranscriptReplayV1 { + initial_observations: input_root.to_vec(), + stage2_and_accumulator_observations: vec![0x31; 48], + quotient_commitment_observations: vec![0x52; 32], + pcs_opening_observations: vec![0; 3 * 16], + }; + let zeta = native_extension(prefix.challenges().unwrap().zeta); + let opened_values = [ + linear_extension_value(2, 11, zeta), + linear_extension_value(3, 17, zeta), + linear_extension_value(5, 23, zeta), + ]; + prefix.pcs_opening_observations.clear(); + for value in opened_values { + encode_extension(&mut prefix.pcs_opening_observations, value); + } + + let prefix_challenges = prefix.challenges().unwrap(); + let alpha = native_extension(prefix_challenges.pcs_alpha); + let max_reduced = ExtVal::new([Val::from_u64(2), Val::ZERO]) + + alpha * ExtVal::new([Val::from_u64(3), Val::ZERO]); + let shorter_reduced = ExtVal::new([Val::from_u64(5), Val::ZERO]); + let mut current = max_reduced; + let mut round_values = Vec::new(); + + let (round_0_root, _) = + constant_extension_tree_root_and_path(extension_words(current), 3, 0); + let mut fri_transcript = Stage2FriTranscriptReplayV1 { + commit_phase_commitments: vec![ + vec![round_0_root], + vec![[0; 32]], + vec![[0; 32]], + ], + commit_pow_witnesses: vec![0; 3], + final_polynomial: vec![[0, 0]], + log_arities: vec![1; 3], + query_pow_witness: 0, + commit_pow_bits: 0, + query_pow_bits: 0, + num_queries: 1, + query_index_bits: LOG_GLOBAL, + }; + let beta_0 = + native_extension(fri_transcript.challenges(&prefix).unwrap().betas[0]); + round_values.push(extension_words(current)); + current += beta_0 * beta_0 * shorter_reduced; + let (round_1_root, _) = + constant_extension_tree_root_and_path(extension_words(current), 2, 0); + fri_transcript.commit_phase_commitments[1][0] = round_1_root; + + let _beta_1 = fri_transcript.challenges(&prefix).unwrap().betas[1]; + round_values.push(extension_words(current)); + let (round_2_root, _) = + constant_extension_tree_root_and_path(extension_words(current), 1, 0); + fri_transcript.commit_phase_commitments[2][0] = round_2_root; + round_values.push(extension_words(current)); + fri_transcript.final_polynomial[0] = extension_words(current); + let challenges = fri_transcript.challenges(&prefix).unwrap(); + let query_index = u32::try_from(challenges.query_indices[0]).unwrap(); + + let (_, input_path) = multiheight_batch_root_and_path( + &matrix_heights, + &matrix_rows, + usize::try_from(query_index).unwrap(), + ); + let batch_opening = Stage2PcsBatchOpeningV1 { + opened_rows: vec![ + matrix_rows[0][usize::try_from(query_index).unwrap()].clone(), + matrix_rows[1][usize::try_from(query_index >> 1).unwrap()].clone(), + ], + opening_proof: input_path, + }; + let rounds = (0..3usize) + .map(|round| { + let tree_height = 3 - u8::try_from(round).unwrap(); + let row_index = usize::try_from(query_index >> (round + 1)).unwrap(); + let (_, path) = constant_extension_tree_root_and_path( + round_values[round], + tree_height, + row_index, + ); + FriCommitPhaseRoundV1 { + sibling: round_values[round], + beta: challenges.betas[round], + reduced_opening: (round == 0) + .then_some(extension_words(shorter_reduced)), + opening_proof: path, + } + }) + .collect(); + let fri_query = FriCommitPhaseQueryV1 { + initial_log_height: LOG_GLOBAL - 1, + query_index, + initial_folded: extension_words(max_reduced), + rounds, + final_polynomial: extension_words(current), + }; + let instance = Stage2PcsInstanceV1 { + log_global_height: LOG_GLOBAL, + log_blowup: LOG_BLOWUP, + batches: vec![Stage2PcsBatchV1 { + commitment: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::Initial, + 0, + ), + matrices: vec![ + Stage2PcsMatrixV1 { + log_height: matrix_heights[0], + width: 2, + opening_points: vec![Stage2PcsOpeningPointV1::Zeta], + opened_values: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::PcsOpening, + 0, + ), + }, + Stage2PcsMatrixV1 { + log_height: matrix_heights[1], + width: 1, + opening_points: vec![Stage2PcsOpeningPointV1::Zeta], + opened_values: Stage2TranscriptByteBindingV1::new( + Stage2TranscriptSegmentV1::PcsOpening, + 32, + ), + }, + ], + }], + }; + let query = TranscriptBoundPcsFriQueryV1 { + pcs: Stage2PcsQueryV1 { batch_openings: vec![batch_opening] }, + fri: fri_query, + }; + (prefix, fri_transcript, instance, vec![query]) + } + + #[test] + fn recycled_slot_storage_is_zero_initialized() { + let poison = F128::new(0xdead_beef_dead_beef, 0xa5a5_a5a5_a5a5_a5a5); + let (mut z, mut a, mut b) = + (vec![poison; 257], vec![poison; 257], vec![poison; 257]); + let dst = initialize_slot_padding(SlotWitnessDest { + z: &mut z, + a: &mut a, + b: &mut b, + elide_padding_writes: true, + }); + assert!(dst.z.iter().all(|word| *word == F128::ZERO)); + assert!(dst.a.iter().all(|word| *word == F128::ZERO)); + assert!(dst.b.iter().all(|word| *word == F128::ZERO)); + assert!(dst.elide_padding_writes); + } + + #[test] + fn every_production_generator_tolerates_poisoned_storage_and_shape_changes() { + fn check( + rows: &[T], + k_log: usize, + generate: impl Fn(&[T], usize, SlotWitnessDest<'_>) -> Vec, + ) { + for (nu, count) in [(8, 3), (9, 1), (8, 0)] { + let rows = &rows[..count.min(rows.len())]; + let words = 1usize << (nu + k_log - 7); + let poison = F128::new(0xdead_beef_0123_4567, 0xa5a5_5a5a_dead_beef); + let mut clean = [ + vec![F128::ZERO; words], + vec![F128::ZERO; words], + vec![F128::ZERO; words], + ]; + let mut dirty = + [vec![poison; words], vec![poison; words], vec![poison; words]]; + let [z, a, b] = &mut clean; + let expected_stripes = generate( + rows, + nu, + SlotWitnessDest { z, a, b, elide_padding_writes: true }, + ); + let [z, a, b] = &mut dirty; + let stripes = generate( + rows, + nu, + initialize_slot_padding(SlotWitnessDest { + z, + a, + b, + elide_padding_writes: true, + }), + ); + assert_eq!(stripes, expected_stripes); + assert_eq!(dirty, clean, "nu={nu}, count={count}, k_log={k_log}"); + } + } + let (prepared, fri, _, _, _) = prepared_stage2_pcs_fixture(); + let witness = + Stage2AirPcsFriWitnessV1::from_prepared(&prepared, &fri).unwrap(); + let compiled = + CompiledStage3Relation::build(&witness, Default::default()).unwrap(); + let relation = &compiled.relation; + let evaluated = relation.shape.run(&relation.inputs, &[]); + macro_rules! check_slot { + ($slot:expr, $gate:ty, $generate:path) => {{ + let slot = $slot; + let table = + &relation.shape.registry.types()[relation.shape.registry_slot(slot)]; + check(evaluated.rows::<$gate>(slot), table.k_log, $generate); + }}; + } + check_slot!( + relation.slots.blake3, + Blake3Gate, + flock_blake3::generate_witness_batch_major_partial_into + ); + check_slot!( + relation.slots.order, + DigestOrderGate, + generate_digest_order_witness_into + ); + check_slot!( + relation.slots.add, + GoldilocksAddPairGate, + generate_goldilocks_add_witness_into + ); + check_slot!( + relation.slots.mul, + GoldilocksMulPairGate, + generate_goldilocks_mul_witness_into + ); + check_slot!( + relation.slots.repack, + GoldilocksLaneRepackGate, + generate_lane_repack_witness_into + ); + check_slot!( + relation.slots.canonical, + CanonicalGoldilocksQuadGate, + generate_canonical_quad_witness_into + ); + check_slot!( + relation.slots.equality, + F128EqualityGate, + generate_f128_equality_witness_into + ); + check_slot!( + relation.sample_slot, + HashSampleGate, + generate_hash_sample_witness_into + ); + check_slot!( + relation.slots.field_sample.unwrap(), + GoldilocksSampleGate, + generate_goldilocks_sample_witness_into + ); + check_slot!( + relation.split_slot, + U64SplitGate, + generate_u64_split_witness_into + ); + check_slot!( + relation.window_slot.unwrap(), + ByteWindowGate, + generate_byte_window_witness_into + ); + } + + #[test] + fn native_fold_satisfies_denominator_free_identity() { + for query_index in [0, 1, 0b1_0110, 0b1_1111] { + let mut query = fixture(); + query.query_index = query_index; + let result = native_extension(query.folded_result().unwrap()); + let (e0, e1) = ordered_evaluations(&query); + let e0 = native_extension(e0); + let e1 = native_extension(e1); + let beta = native_extension(query.beta); + let s = ExtVal::new([Val::from_u64(subgroup_point(&query)), Val::ZERO]); + let two_s = s + s; + assert_eq!(two_s * result + beta * e1, s * (e0 + e1) + beta * e0); + } + } + + #[test] + fn leaf_and_path_bind_pair_order_and_all_index_bits() { + let query = fixture(); + let root = query.commitment_root().unwrap(); + let mut pair_bit = query.clone(); + pair_bit.query_index ^= 1; + assert_ne!(pair_bit.commitment_root().unwrap(), root); + let mut path_bit = query.clone(); + path_bit.query_index ^= 1 << 3; + assert_ne!(path_bit.commitment_root().unwrap(), root); + assert_ne!( + pair_bit.folded_result().unwrap(), + query.folded_result().unwrap() + ); + } + + #[test] + fn parser_is_strict_before_crypto() { + let query = fixture(); + let artifact = FriFoldConformanceArtifactV1 { + folded_result: query.folded_result().unwrap(), + commitment_root: query.commitment_root().unwrap(), + query, + circuit_digest: [7; 32], + proof_bundle_bytes: vec![1, 2, 3], + }; + let mut bytes = artifact.to_bytes(); + assert!(FriFoldConformanceArtifactV1::from_bytes(&bytes).is_err()); + bytes[0] ^= 1; + assert!(FriFoldConformanceArtifactV1::from_bytes(&bytes).is_err()); + } + + #[test] + fn commit_phase_threads_shifted_index_and_folded_results() { + let query = commit_phase_fixture(); + let computation = compute_commit_phase(&query).unwrap(); + ensure_final_polynomial(&query, &computation).unwrap(); + assert_eq!(computation.round_queries.len(), 3); + assert_eq!( + computation.round_queries[1].query_index, + query.query_index >> 1 + ); + assert_eq!(computation.round_queries[1].folded, computation.results[0]); + assert_ne!(computation.fold_results[0], computation.results[0]); + assert_eq!(computation.fold_results[1], computation.results[1]); + assert_eq!(computation.results[2], query.final_polynomial); + assert_eq!(query.commitment_roots().unwrap(), computation.roots); + + let mut wrong_beta = query.clone(); + wrong_beta.rounds[1].beta[0] ^= 1; + assert!(wrong_beta.folded_results().is_err()); + } + + #[test] + fn deep_binary_fri_schedules_construct_and_evaluate() { + for round_count in [9, 16, 30] { + let (prefix, fri_transcript, query) = + transcript_bound_fri_fixture_with_round_count(round_count); + validate_commit_phase_structure(&query).unwrap(); + assert_eq!(query.rounds.len(), round_count); + + let challenges = fri_transcript.challenges(&prefix).unwrap(); + ensure_transcript_binds_fri_query( + &fri_transcript, + &challenges, + 0, + &query, + ) + .unwrap(); + let computation = compute_commit_phase(&query).unwrap(); + ensure_final_polynomial(&query, &computation).unwrap(); + + let relation = TranscriptBoundFriCommitPhaseRelation::build( + &prefix, + &fri_transcript, + &challenges, + 0, + &query, + &computation, + ) + .unwrap(); + let witness = relation.shape.run(&relation.inputs, &[]); + assert_eq!(witness.public, relation.public); + + let mut missing_last_round = query.clone(); + missing_last_round.rounds.pop(); + assert!( + ensure_transcript_binds_fri_query( + &fri_transcript, + &challenges, + 0, + &missing_last_round, + ) + .is_err() + ); + + let mut wrong_last_path = query; + wrong_last_path.rounds.last_mut().unwrap().opening_proof[0][0] ^= 1; + assert!( + ensure_transcript_binds_fri_query( + &fri_transcript, + &challenges, + 0, + &wrong_last_path, + ) + .is_err() + ); + } + } + + #[test] + fn commit_phase_parser_is_strict_before_crypto() { + let query = commit_phase_fixture(); + let artifact = FriCommitPhaseConformanceArtifactV1 { + commitment_roots: query.commitment_roots().unwrap(), + query, + circuit_digest: [9; 32], + proof_bundle_bytes: vec![1, 2, 3], + }; + let mut bytes = artifact.to_bytes(); + assert!(FriCommitPhaseConformanceArtifactV1::from_bytes(&bytes).is_err()); + bytes[0] ^= 1; + assert!(FriCommitPhaseConformanceArtifactV1::from_bytes(&bytes).is_err()); + } + + #[test] + fn pcs_reduction_quotients_and_accumulator_match_reference_field() { + let opening = pcs_reduction_fixture(); + let computation = compute_pcs_reduction(&opening).unwrap(); + let denominator = native_extension(computation.denominator); + for (((&px, &pz), "ient), column) in opening + .opened_values + .iter() + .zip(&opening.opened_at_z) + .zip(&computation.quotients) + .zip(0..opening.opened_values.len()) + { + let px = ExtVal::new([Val::from_u64(px), Val::ZERO]); + assert_eq!( + denominator * native_extension(quotient) + px, + native_extension(pz), + "column {column}" + ); + } + assert_eq!(opening.reduced_accumulator().unwrap(), computation.accumulator); + assert_eq!(opening.next_alpha_power().unwrap(), computation.alpha_power); + assert_eq!(opening.commitment_root().unwrap(), computation.root); + + let mut changed_index = opening; + changed_index.query_index ^= 1; + assert_ne!(changed_index.commitment_root().unwrap(), computation.root); + } + + #[test] + fn pcs_leaf_hash_supports_multiple_blocks_and_blake3_chunks() { + let opening = wide_pcs_reduction_fixture(); + assert!(opening.opened_values.len() * 8 > 1_024); + let computation = compute_pcs_reduction(&opening).unwrap(); + let relation = PcsReductionRelation::build(&opening).unwrap(); + let inputs = pcs_reduction_relation_inputs(&opening, &computation); + let public = pcs_reduction_relation_public(&opening, &computation); + let witness = relation.shape.run(&inputs, &[]); + assert_eq!(witness.public, public); + assert!( + witness.rows::(relation.slots.blake3).len() + > opening.opening_proof.len() + 1 + ); + assert_eq!(computation.root, native_pcs_row_root(&opening)); + } + + #[test] + fn pcs_reduction_parser_is_strict_before_crypto() { + let opening = pcs_reduction_fixture(); + let computation = compute_pcs_reduction(&opening).unwrap(); + let artifact = PcsReductionConformanceArtifactV1 { + opening, + reduced_accumulator: computation.accumulator, + next_alpha_power: computation.alpha_power, + circuit_digest: [11; 32], + commitment_root: computation.root, + proof_bundle_bytes: vec![1, 2, 3], + }; + let mut bytes = artifact.to_bytes(); + assert!(PcsReductionConformanceArtifactV1::from_bytes(&bytes).is_err()); + bytes[0] ^= 1; + assert!(PcsReductionConformanceArtifactV1::from_bytes(&bytes).is_err()); + } + + #[test] + fn transcript_challenges_feed_pcs_wires_in_one_circuit() { + let (replay, opening) = transcript_bound_pcs_fixture(); + let challenges = replay.challenges().unwrap(); + let computation = compute_pcs_reduction(&opening).unwrap(); + let relation = TranscriptBoundPcsReductionRelation::build( + &replay, + &opening, + &computation, + challenges, + ) + .unwrap(); + let witness = relation.shape.run(&relation.inputs, &[]); + assert_eq!(witness.public, relation.public); + + let mut wrong_opening = opening; + wrong_opening.alpha[0] ^= 1; + assert!( + TranscriptBoundPcsReductionRelation::build( + &replay, + &wrong_opening, + &compute_pcs_reduction(&wrong_opening).unwrap(), + challenges, + ) + .is_err() + ); + } + + #[test] + fn transcript_betas_indices_caps_and_final_poly_feed_one_fri_circuit() { + let (prefix, fri_transcript, query) = transcript_bound_fri_fixture(); + let challenges = fri_transcript.challenges(&prefix).unwrap(); + assert_eq!(challenges.query_indices.len(), 5); + let computation = compute_commit_phase(&query).unwrap(); + let relation = TranscriptBoundFriCommitPhaseRelation::build( + &prefix, + &fri_transcript, + &challenges, + 0, + &query, + &computation, + ) + .unwrap(); + let witness = relation.shape.run(&relation.inputs, &[]); + assert_eq!(witness.public, relation.public); + + let mut wrong_beta = query.clone(); + wrong_beta.rounds[0].beta[0] ^= 1; + assert!( + ensure_transcript_binds_fri_query( + &fri_transcript, + &challenges, + 0, + &wrong_beta, + ) + .is_err() + ); + let mut wrong_index = query; + wrong_index.query_index ^= 1; + assert!( + ensure_transcript_binds_fri_query( + &fri_transcript, + &challenges, + 0, + &wrong_index, + ) + .is_err() + ); + } + + #[test] + fn one_transcript_drives_every_fri_query_in_one_circuit() { + let (prefix, fri_transcript, queries) = + transcript_bound_fri_all_queries_fixture(); + let challenges = fri_transcript.challenges(&prefix).unwrap(); + let computations = validate_all_transcript_bound_fri_queries( + &fri_transcript, + &challenges, + &queries, + ) + .unwrap(); + let relation = TranscriptBoundFriCommitPhaseRelation::build_all( + &prefix, + &fri_transcript, + &challenges, + &queries, + &computations, + ) + .unwrap(); + let witness = relation.shape.run(&relation.inputs, &[]); + assert_eq!(witness.public, relation.public); + + let mut missing = queries.clone(); + missing.pop(); + assert!( + validate_all_transcript_bound_fri_queries( + &fri_transcript, + &challenges, + &missing, + ) + .is_err() + ); + } + + #[test] + fn authenticated_multiheight_pcs_buckets_feed_fri_in_one_circuit() { + let (prefix, fri_transcript, pcs_instance, queries) = + transcript_bound_pcs_fri_fixture(); + let prefix_challenges = prefix.challenges().unwrap(); + let fri_challenges = fri_transcript.challenges(&prefix).unwrap(); + let (fri_computations, pcs_computations) = + validate_all_transcript_bound_pcs_fri_queries( + &prefix, + &fri_transcript, + &fri_challenges, + prefix_challenges, + &pcs_instance, + &queries, + ) + .unwrap(); + assert_eq!(pcs_computations[0].reduced_openings.len(), 2); + let relation = TranscriptBoundFriCommitPhaseRelation::build_all_with_pcs( + &prefix, + &fri_transcript, + &fri_challenges, + &pcs_instance, + &queries, + &fri_computations, + &pcs_computations, + ) + .unwrap(); + let witness = relation.shape.run(&relation.inputs, &[]); + assert_eq!(witness.public, relation.public); + assert!(witness.rows::(relation.slots.blake3).len() > 20); + + let mut wrong_row = queries.clone(); + wrong_row[0].pcs.batch_openings[0].opened_rows[1][0] ^= 1; + assert!( + validate_all_transcript_bound_pcs_fri_queries( + &prefix, + &fri_transcript, + &fri_challenges, + prefix_challenges, + &pcs_instance, + &wrong_row, + ) + .is_err() + ); + let mut wrong_rollin = queries; + wrong_rollin[0].fri.rounds[0].reduced_opening.as_mut().unwrap()[0] ^= 1; + assert!( + validate_all_transcript_bound_pcs_fri_queries( + &prefix, + &fri_transcript, + &fri_challenges, + prefix_challenges, + &pcs_instance, + &wrong_rollin, + ) + .is_err() + ); + } + + #[test] + fn real_stage2_root_lowers_to_the_combined_pcs_fri_relation() { + let (prepared, fri, vk_bytes, claim_bytes, proof_bytes) = + prepared_stage2_pcs_fixture(); + let lowered = + Stage2PcsFriWitnessV1::from_prepared(&prepared, &fri).unwrap(); + + assert_eq!(lowered.pcs_instance.batches.len(), 4); + assert!( + lowered + .pcs_instance + .batches + .iter() + .take(3) + .all(|batch| batch.matrices.len() == 2) + ); + assert_eq!(lowered.pcs_instance.batches.get(3).unwrap().matrices.len(), 1); + assert_eq!(lowered.queries.len(), fri.num_queries); + + let prefix_challenges = lowered.prefix.challenges().unwrap(); + let fri_challenges = + lowered.fri_transcript.challenges(&lowered.prefix).unwrap(); + let report = crate::FlockStage3Backend + .preflight_stage2(&vk_bytes, &claim_bytes, &proof_bytes, &fri) + .unwrap(); + let census = &report.relation; + assert_eq!(census.nu, 12); + let busiest = + report.resources.tables.iter().map(|table| table.rows).max().unwrap(); + assert_eq!(census.table_capacity, busiest.next_power_of_two()); + assert_eq!(report.resources.padded_union_witness_bytes, 192 * 1024 * 1024); + assert!(census.blake3_rows > 0); + assert!(census.total_rows() > census.blake3_rows); + assert_eq!(report.advice.queries, u64::try_from(fri.num_queries).unwrap()); + assert_eq!(report.stage2_root_digest, prepared.statement().digest()); + assert!(report.to_string().contains("gate rows: blake3=")); + assert_eq!( + report.resources.tables.iter().map(|table| table.rows).sum::(), + census.total_rows() + ); + assert!( + report.resources.pcs_codeword_bytes >= report.resources.pcs_message_bytes + ); + assert!( + report.resources.padded_union_witness_bytes + >= report.resources.dense_witness_bytes * 3 + ); + assert_eq!( + report.resources.padded_union_witness_bytes, + production_padded_witness_bytes(census.nu as usize).unwrap() + ); + assert!( + report + .resources + .tables + .iter() + .map(|table| table.padded_witness_bytes) + .sum::() + <= report.resources.padded_union_witness_bytes + ); + let json = report.to_json_value(); + assert_eq!(json["schema"], "ix.flock-stage3.preflight"); + assert_eq!(json["version"], 1); + assert_eq!(json["relation_cache"], "none"); + assert_eq!( + json["specialization"]["activation"], + serde_json::json!([false, true, true]) + ); + assert_eq!( + json["specialization"]["active_log_degrees"], + serde_json::json!([3, 2]) + ); + assert_eq!( + json["config_digest"], + crate::report::hex(FlockConfigV1.digest()) + ); + assert_eq!( + json["transport"]["compact_proof_digest"], + crate::report::hex(*blake3::hash(&proof_bytes).as_bytes()) + ); + // Measurements are diagnostic only: changing them cannot change a + // cryptographic root or relation identity. + let mut changed = report.clone(); + changed.timings.total_us += 1; + changed.process_peak_rss_bytes = None; + assert_eq!(changed.stage3_statement_digest, report.stage3_statement_digest); + assert_ne!(changed.to_json_value(), json); + + let expected = crate::Stage3StatementV1::new( + prepared.statement(), + report.relation_digest, + ); + let mismatched_payload = crate::artifact::Stage3ProductionPayloadV1::new( + &vk_bytes, + &claim_bytes, + b"different compact proof", + report.relation.circuit_digest, + b"unused proof bundle", + ) + .unwrap() + .encode() + .unwrap(); + let mismatched_artifact = + crate::Stage3ArtifactV1::new(expected, mismatched_payload).unwrap(); + let error = crate::FlockStage3Backend + .verify_stage2_for_root( + &mismatched_artifact, + &vk_bytes, + &claim_bytes, + &proof_bytes, + &fri, + ) + .unwrap_err() + .to_string(); + assert!(error.contains("different Stage 2 compact proof transport")); + + let mut wrong_row = lowered.queries; + wrong_row[0].pcs.batch_openings[0].opened_rows[0][0] ^= 1; + assert!( + validate_all_transcript_bound_pcs_fri_queries( + &lowered.prefix, + &lowered.fri_transcript, + &fri_challenges, + prefix_challenges, + &lowered.pcs_instance, + &wrong_row, + ) + .is_err() + ); + } + + #[test] + fn four_message_stage2_lookups_lower_to_the_flock_relation() { + let (prepared, fri, vk, _, _) = + prepared_stage2_pcs_fixture_with_lookup_group( + CommitmentParameters { log_blowup: 2, cap_height: 0 }, + FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 2, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 0, + }, + 4, + ); + let metadata = + AiurVerifyingKey::from_bytes(&vk).unwrap().air_circuit_metadata(); + assert_eq!(metadata[1].lookup_group_size, 4); + assert_eq!(metadata[1].quotient_degree, 4); + let witness = + Stage2AirPcsFriWitnessV1::from_prepared(&prepared, &fri).unwrap(); + let compiled = + CompiledStage3Relation::build(&witness, Default::default()).unwrap(); + compiled.evaluate().unwrap(); + } + + #[test] + fn prepared_root_rejects_work_at_each_admission_boundary() { + let (_, fri, vk, claim, proof) = prepared_stage2_pcs_fixture(); + for (limits, label) in [ + ( + crate::Stage3ResourceLimitsV1 { + max_advice_bytes: 1, + ..Default::default() + }, + "advice upper bound", + ), + ( + crate::Stage3ResourceLimitsV1 { + max_table_capacity: 1, + ..Default::default() + }, + "table capacity", + ), + ( + crate::Stage3ResourceLimitsV1 { + max_union_witness_bytes: 1, + ..Default::default() + }, + "padded union witness", + ), + ] { + let error = crate::FlockStage3Backend + .prepare_stage2_with_limits(&vk, &claim, &proof, &fri, limits) + .err() + .expect("reject oversized work before proving") + .to_string(); + assert!(error.contains(label), "{error}"); + } + // A canonical-looking but invalid native proof still fails expansion + // after removal of the redundant outer native verifier call. + let mut wrong_claim = claim; + wrong_claim[0] ^= 1; + assert!( + crate::FlockStage3Backend + .prepare_witness(&vk, &wrong_claim, &proof, &fri) + .is_err() + ); + } + + #[test] + fn grouped_pcs_quotients_match_columnwise_height_buckets() { + for alpha in [[0, 0], [1, 0], [17, 29], [GOLDILOCKS_MODULUS - 1, 2]] { + let alpha = native_extension(alpha); + let mut reference = BTreeMap::::new(); + let mut grouped = BTreeMap::::new(); + for batch in 0..3u64 { + // Interleaved heights, repeated matrices and two opening points + // exercise the per-height exponent counter, including empty sets. + for (height, width) in [(5, 0), (3, 1), (5, 2), (3, 3), (5, 7), (3, 31)] + { + for point in 0..2u64 { + let denominator = native_extension([4 + batch, 2 + point]); + let rows: Vec<_> = (0..width) + .map(|column| { + native_extension([GOLDILOCKS_MODULUS - 1 - column, 0]) + }) + .collect(); + let opened: Vec<_> = (0..width) + .map(|column| native_extension([column + batch, column + point])) + .collect(); + let quotients: Vec<_> = opened + .iter() + .zip(&rows) + .map(|(&at_z, &at_x)| { + extension_words((at_z - at_x) / denominator) + }) + .collect(); + let (power, accumulator) = + reference.entry(height).or_insert((ExtVal::ONE, ExtVal::ZERO)); + for "ient in "ients { + *accumulator += *power * native_extension(quotient); + *power *= alpha; + } + let row_sum = rows + .iter() + .rev() + .fold(ExtVal::ZERO, |sum, &value| sum * alpha + value); + let opened_sum = opened + .iter() + .rev() + .fold(ExtVal::ZERO, |sum, &value| sum * alpha + value); + let quotient = weighted_quotient("ients, alpha); + assert_eq!(denominator * quotient + row_sum, opened_sum); + let (offset, accumulator) = + grouped.entry(height).or_insert((0, ExtVal::ZERO)); + *accumulator += alpha.exp_u64(*offset) * quotient; + *offset += width; + } + } + } + for (height, (power, accumulator)) in reference { + let (offset, grouped_accumulator) = grouped[&height]; + assert_eq!(power, alpha.exp_u64(offset)); + assert_eq!(accumulator, grouped_accumulator); + } + } + } + + #[test] + fn shared_pcs_query_points_match_bit_reversed_domains_and_reject_mutations() { + const NU: usize = 10; + const GLOBAL_HEIGHT: u8 = 32; + // Repeated, interleaved heights must declare factors only once. Include + // both the empty exponent and Goldilocks' maximum two-adic subgroup. + const HEIGHTS: [u8; 9] = [32, 0, 1, 9, 32, 16, 2, 16, 31]; + struct Positions { + bits: Vec, + points: Vec<(u8, usize)>, + } + fn emit( + builder: &mut impl CircuitEmitter, + nu: usize, + ) -> (Vec, Vec) { + let arithmetic = GoldilocksCircuitSlots::declare(builder, nu); + let order = builder.slot(DigestOrderGate { nu }); + let equality = builder.slot(F128EqualityGate { nu }); + let mut inputs = vec![F128::ZERO]; + let mut public = inputs.clone(); + let zero = record_fixed(builder, &mut inputs, &mut public, F128::ZERO); + let equality_zero = + record_fixed(builder, &mut inputs, &mut public, F128::ZERO); + let one = + record_fixed(builder, &mut inputs, &mut public, F128::new(1, 0)); + let basis = + PcsQueryPointBasis::declare(builder, &mut inputs, &mut public, HEIGHTS); + assert_eq!(basis.factors.len(), 7); + assert_eq!(basis.factors.values().map(Vec::len).sum::(), 91); + let mut positions = Vec::new(); + for _ in 0..2 { + let bits = (0..GLOBAL_HEIGHT) + .map(|_| { + let index = inputs.len(); + let wire = + record_public(builder, &mut inputs, &mut public, F128::ZERO); + (wire, index) + }) + .collect::>(); + let bit_wires: Vec<_> = bits.iter().map(|&(wire, _)| wire).collect(); + assert!( + basis + .constrain_query( + builder, + &arithmetic, + order, + one, + zero, + GLOBAL_HEIGHT, + &bit_wires[..31], + ) + .is_err() + ); + let points = basis + .constrain_query( + builder, + &arithmetic, + order, + one, + zero, + GLOBAL_HEIGHT, + &bit_wires, + ) + .unwrap(); + let points = points + .into_iter() + .map(|(height, actual)| { + let index = inputs.len(); + let expected = + record_public(builder, &mut inputs, &mut public, F128::new(7, 0)); + assert_f128_equal( + builder, + equality, + equality_zero, + actual, + expected, + ); + (height, index) + }) + .collect(); + positions.push(Positions { + bits: bits.into_iter().map(|(_, index)| index).collect(), + points, + }); + } + arithmetic.finish_canonical(builder); + (inputs, positions) + } + + let mut count = CountingEmitter::new(); + emit(&mut count, CountingEmitter::COUNT_NU); + // With all counting wires identical, the count still reflects two + // distinct queries and one multiplication per UNIQUE-height factor. + assert_eq!( + count.table_rows().find(|&(name, _)| name == "GoldilocksMulPairGate"), + Some(("GoldilocksMulPairGate", 182)), + ); + let mut builder = ShapeBuilder::new(NU); + let (template, positions) = emit(&mut builder, NU); + let shape = builder.finish().unwrap(); + count.ensure_matches(&shape).unwrap(); + + for indices in [ + [0u32, u32::MAX], + [1, 2], + [0xaaaa_5555, 0x5555_aaaa], + [0x8000_0000, 0x7fff_ffff], + ] { + let mut inputs = template.clone(); + for (&query_index, positions) in indices.iter().zip(&positions) { + for (bit, &index) in positions.bits.iter().enumerate() { + inputs[index] = F128::new(u64::from((query_index >> bit) & 1), 0); + } + for &(height, index) in &positions.points { + let local = u64::from(query_index) >> (GLOBAL_HEIGHT - height); + let exponent = + if height == 0 { 0 } else { local.reverse_bits() >> (64 - height) }; + // Independent exponentiation oracle, not pcs_x_factors or its + // native fold. Truncation must happen BEFORE bit reversal. + let expected = Val::from_u8(7) + * Val::TWO_ADIC_GENERATORS[usize::from(height)].exp_u64(exponent); + inputs[index] = F128::new(expected.as_canonical_u64(), 0); + } + } + shape.run(&inputs, &[]); + let rejects = |mutated: Vec| { + // No native PCS validation or public-vector comparison: equality, + // selector and arithmetic checks are in the compiled relation. + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + shape.run(&mutated, &[]) + })) + .is_err() + ); + }; + for positions in &positions { + for bit in [0, 1, 15, 31] { + let mut changed = inputs.clone(); + changed[positions.bits[bit]].lo ^= 1; + rejects(changed); + } + for &(height, index) in &positions.points { + let mut changed = inputs.clone(); + changed[index].lo ^= 1; + rejects(changed); + if height > 0 { + let mut upper_lane = inputs.clone(); + upper_lane[index].hi = 1; + rejects(upper_lane); + } + } + for invalid_bit in [F128::new(2, 0), F128::new(0, 1)] { + let mut changed = inputs.clone(); + changed[positions.bits[0]] = invalid_bit; + rejects(changed); + } + } + } + } + + #[test] + fn compiled_pcs_point_rejects_auxiliary_and_zero_denominator_mutations() { + for quotients in [vec![], vec![[11, 13], [23, 31], [41, 47]]] { + const NU: usize = 10; + let mut builder = ShapeBuilder::new(NU); + let arithmetic = GoldilocksCircuitSlots::declare(&mut builder, NU); + let equality = builder.slot(F128EqualityGate { nu: NU }); + let mut inputs = vec![F128::ZERO]; + let mut public = inputs.clone(); + let equality_zero = + record_fixed(&mut builder, &mut inputs, &mut public, F128::ZERO); + let one = + record_fixed(&mut builder, &mut inputs, &mut public, F128::new(1, 0)); + let alpha = native_extension([17, 19]); + let quotient = weighted_quotient("ients, alpha); + let denominator = native_extension([4, 2]); + let row_value = if quotients.is_empty() { + ExtVal::ZERO + } else { + native_extension([9, 4]) + }; + let point_index = inputs.len(); + let point = + record_public(&mut builder, &mut inputs, &mut public, F128::new(7, 2)); + let opened_index = inputs.len(); + let opened_sum = record_public( + &mut builder, + &mut inputs, + &mut public, + pack_extension(extension_words(denominator * quotient + row_value)), + ); + let alpha_offset = record_fixed( + &mut builder, + &mut inputs, + &mut public, + pack_extension(extension_words(alpha.exp_u64(7))), + ); + let x = + record_fixed(&mut builder, &mut inputs, &mut public, F128::new(3, 0)); + let row_sum = record_fixed( + &mut builder, + &mut inputs, + &mut public, + pack_extension(extension_words(row_value)), + ); + let auxiliary_start = inputs.len(); + let denominator_wire = constrain_stage2_pcs_denominator( + &mut builder, + &arithmetic, + equality, + equality_zero, + one, + &mut inputs, + &mut public, + x, + point, + extension_words(denominator), + ) + .unwrap(); + let term = constrain_stage2_pcs_point( + &mut builder, + &arithmetic, + equality, + equality_zero, + &mut inputs, + &mut public, + denominator_wire, + row_sum, + &Stage2PcsPointComputation { + denominator: extension_words(denominator), + quotients, + }, + &Stage2PcsPointWires { point, alpha_offset, opened_sum }, + alpha, + ); + // These exact indices identify D, 1/D and Q, without guessing by value. + assert_eq!(inputs.len() - auxiliary_start, 3); + builder.publish(term); + public.push(pack_extension(extension_words(alpha.exp_u64(7) * quotient))); + arithmetic.finish_canonical(&mut builder); + let shape = builder.finish().unwrap(); + assert_eq!(shape.run(&inputs, &[]).public, public); + let rejects = |mutated: Vec| { + // No native verifier, native recomputation or public-vector comparison: + // failure must come from the compiled gates and connected wires. + assert!( + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + shape.run(&mutated, &[]) + })) + .is_err() + ); + }; + for index in auxiliary_start..auxiliary_start + 3 { + let mut altered = inputs.clone(); + altered[index].lo ^= 1; + rejects(altered); + for bad in + [F128::new(GOLDILOCKS_MODULUS, 0), F128::new(0, GOLDILOCKS_MODULUS)] + { + let mut noncanonical = inputs.clone(); + noncanonical[index] = bad; + rejects(noncanonical); + } + } + let mut zero_denominator = inputs; + zero_denominator[auxiliary_start] = F128::ZERO; + zero_denominator[point_index] = F128::new(3, 0); + zero_denominator[opened_index] = + pack_extension(extension_words(row_value)); + // D+x=z and D*Q+row_sum=opened_sum both hold in this attack. Only + // the compiled inverse constraint excludes the degenerate denominator. + rejects(zero_denominator); + } + } + + #[test] + fn compiled_relation_rejects_mutations_without_native_prevalidation() { + let (prepared, fri, _, _, _) = prepared_stage2_pcs_fixture(); + let witness = + Stage2AirPcsFriWitnessV1::from_prepared(&prepared, &fri).unwrap(); + let compiled = + CompiledStage3Relation::build(&witness, Default::default()).unwrap(); + compiled.evaluate().unwrap(); + let relation = &compiled.relation; + let query = &witness.pcs_fri.queries[0]; + let cases = [ + ( + "PCS Merkle sibling", + pack_digest(&query.pcs.batch_openings[0].opening_proof[0])[0], + ), + ("FRI evaluation", pack_extension(query.fri.rounds[0].sibling)), + ( + "FRI Merkle sibling", + pack_digest(&query.fri.rounds[0].opening_proof[0])[0], + ), + ]; + for (label, word) in cases { + let index = + relation.inputs.iter().position(|input| *input == word).expect(label); + let mut mutated = relation.inputs.clone(); + mutated[index].lo ^= 1; + // run() asserts connected-wire consistency. We do not compare the + // public vector or call a native verifier here: rejection must come + // from the already compiled relation's gates and wiring. + let result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + relation.shape.run(&mutated, &[]) + })); + assert!(result.is_err(), "compiled relation accepted mutated {label}"); + } + } + + #[test] + fn production_parameters_generate_and_lower_canonical_stage2_transport() { + let commitment = CommitmentParameters { log_blowup: 2, cap_height: 0 }; + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + }; + let (prepared, fri, _, _, _) = + prepared_stage2_pcs_fixture_with(commitment, fri); + let witness = Stage2AirPcsFriWitnessV1::from_prepared(&prepared, &fri) + .expect("lower production-parameter Stage 2 transport"); + + assert_eq!(prepared.advice_profile().queries, 100); + assert_eq!(witness.pcs_fri.queries.len(), 100); + assert_eq!(witness.pcs_fri.fri_transcript.query_pow_bits, 20); + assert_eq!(witness.pcs_fri.pcs_instance.log_blowup, 2); + let error = CompiledStage3Relation::build( + &witness, + crate::Stage3ResourceLimitsV1 { + max_union_witness_bytes: 1, + ..Default::default() + }, + ) + .err() + .expect("reject oversized union before wiring compilation") + .to_string(); + assert!(error.contains("padded union witness"), "{error}"); + assert!(error.contains("3221225472"), "{error}"); + eprintln!("Production-parameter early sizing: {error}"); + } + + #[cfg(feature = "production-measurements")] + #[test] + #[ignore = "expensive 100-query relation compilation; no prover is run"] + fn production_parameter_relation_census() { + let (prepared, fri, _, _, _) = prepared_stage2_pcs_fixture_with( + CommitmentParameters { log_blowup: 2, cap_height: 0 }, + FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + }, + ); + let witness = + Stage2AirPcsFriWitnessV1::from_prepared(&prepared, &fri).unwrap(); + // The exact row count admits this fixture at 3 GiB, with no limit + // increase. No prover buffers are allocated by this experiment. + let limits = crate::Stage3ResourceLimitsV1 { + max_table_capacity: 1 << 16, + max_union_witness_bytes: 3 * 1024 * 1024 * 1024, + ..Default::default() + }; + let compiled = CompiledStage3Relation::build(&witness, limits).unwrap(); + compiled.evaluate().unwrap(); + let resources = compiled.resources().unwrap(); + assert_eq!(compiled.census().unwrap().nu, 16); + assert_eq!( + resources.padded_union_witness_bytes, + limits.max_union_witness_bytes + ); + for rejected_limits in [ + crate::Stage3ResourceLimitsV1 { + max_table_capacity: limits.max_table_capacity - 1, + ..limits + }, + crate::Stage3ResourceLimitsV1 { + max_union_witness_bytes: limits.max_union_witness_bytes - 1, + ..limits + }, + ] { + assert!( + CompiledStage3Relation::build(&witness, rejected_limits).is_err() + ); + } + eprintln!( + "Production-parameter census: {}", + serde_json::to_string(&compiled.census().unwrap()).unwrap() + ); + eprintln!( + "Production-parameter resources: {}", + serde_json::to_string(&resources).unwrap() + ); + assert_eq!( + resources.tables.iter().map(|table| table.rows).sum::(), + compiled.census().unwrap().total_rows() + ); + assert_eq!( + resources.padded_union_witness_bytes, + production_padded_witness_bytes(compiled.census().unwrap().nu as usize) + .unwrap() + ); + } + + #[test] + #[ignore = "real Flock proof of the complete Stage 2 integration fixture"] + fn real_stage2_integration_artifact_round_trip() { + stage2_integration_artifact_round_trip(false); + } + + #[cfg(feature = "production-measurements")] + #[test] + #[ignore = "100-query proof with 3 GiB padded buffers; run separately"] + fn production_parameter_artifact_round_trip() { + stage2_integration_artifact_round_trip(true); + } + + fn stage2_integration_artifact_round_trip(production_parameters: bool) { + let test_name = if production_parameters { + "fri::tests::production_parameter_artifact_round_trip" + } else { + "fri::tests::real_stage2_integration_artifact_round_trip" + }; + if let Some(directory) = std::env::var_os("IX_FLOCK_STAGE3_VERIFY_CHILD") { + let directory = std::path::PathBuf::from(directory); + let (vk, claim, proof): (Vec, Vec, Vec) = + bincode::deserialize( + &std::fs::read(directory.join("root.transport")).unwrap(), + ) + .unwrap(); + let fri = AiurVerifyingKey::from_bytes(&vk).unwrap().fri_parameters(); + let artifact = + crate::Stage3ArtifactV1::read_from_path(directory.join("root.flock")) + .unwrap(); + let started = std::time::Instant::now(); + crate::FlockStage3Backend + .verify_stage2_for_root(&artifact, &vk, &claim, &proof, &fri) + .expect("fresh-process verification against external root transport"); + eprintln!( + "Flock fresh-process external-root verification: {:.3} seconds", + started.elapsed().as_secs_f64() + ); + return; + } + let total_started = std::time::Instant::now(); + + let fixture_started = std::time::Instant::now(); + let (prepared, fri, vk_bytes, claim_bytes, proof_bytes) = + if production_parameters { + prepared_stage2_pcs_fixture_with( + CommitmentParameters { log_blowup: 2, cap_height: 0 }, + FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + }, + ) + } else { + prepared_stage2_pcs_fixture() + }; + let fixture_elapsed = fixture_started.elapsed(); + + let backend = crate::FlockStage3Backend; + + let prove_started = std::time::Instant::now(); + let handle = backend + .prepare_stage2(&vk_bytes, &claim_bytes, &proof_bytes, &fri) + .expect("preflight complete Stage 3 relation"); + let preflight = handle.report().clone(); + eprintln!("{}", preflight.to_json_value()); + let (artifact, timings) = + handle.prove_with_timings().expect("prove complete Stage 3 relation"); + eprintln!( + "Flock post-proof process peak RSS: {:?} bytes", + crate::stage3_process_peak_rss_bytes() + ); + eprintln!( + "Flock proof timings (us): {}", + serde_json::to_string(&timings).unwrap() + ); + let reused_started = std::time::Instant::now(); + handle.verify(&artifact).expect("verify with explicit relation reuse"); + eprintln!( + "Flock explicitly reused verifier: {:.3} seconds", + reused_started.elapsed().as_secs_f64() + ); + drop(handle); + let prove_elapsed = prove_started.elapsed(); + assert_eq!( + artifact.statement().stage2_root_digest(), + &preflight.stage2_root_digest, + ); + assert_eq!( + artifact.statement().relation_digest(), + &preflight.relation_digest, + ); + assert_eq!( + artifact.statement().digest(), + preflight.stage3_statement_digest + ); + + let encode_started = std::time::Instant::now(); + let encoded = artifact.to_bytes(); + let encode_elapsed = encode_started.elapsed(); + eprintln!( + "Flock complete Stage 3 artifact: {} bytes (payload: {} bytes)", + encoded.len(), + artifact.proof_bytes().len(), + ); + + let decode_started = std::time::Instant::now(); + let decoded = crate::Stage3ArtifactV1::from_bytes(&encoded).unwrap(); + let decode_elapsed = decode_started.elapsed(); + + // A separate process has no relation or lincheck cache from proving. + // External inputs live in a separate trusted test file, not the artifact. + let directory = std::env::temp_dir().join(format!( + "ix-flock-stage3-cold-{}-{production_parameters}", + std::process::id() + )); + std::fs::create_dir(&directory).unwrap(); + let artifact_path = directory.join("root.flock"); + let transport_path = directory.join("root.transport"); + artifact.write_atomic(&artifact_path).unwrap(); + std::fs::write( + &transport_path, + bincode::serialize(&(&vk_bytes, &claim_bytes, &proof_bytes)).unwrap(), + ) + .unwrap(); + let child = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", test_name, "--ignored", "--nocapture"]) + .env("IX_FLOCK_STAGE3_VERIFY_CHILD", &directory) + .output() + .unwrap(); + eprint!("{}", String::from_utf8_lossy(&child.stderr)); + std::fs::remove_file(artifact_path).unwrap(); + std::fs::remove_file(transport_path).unwrap(); + std::fs::remove_dir(directory).unwrap(); + assert!( + child.status.success(), + "{}", + String::from_utf8_lossy(&child.stdout) + ); + + let valid_verify_started = std::time::Instant::now(); + backend + .verify_stage2_for_root( + &decoded, + &vk_bytes, + &claim_bytes, + &proof_bytes, + &fri, + ) + .expect("verify complete Stage 3 relation against its aggregate root"); + let valid_verify_elapsed = valid_verify_started.elapsed(); + + let wrong_relation = + crate::Stage3StatementV1::new(prepared.statement(), [0xa5; 32]); + let wrong_relation_started = std::time::Instant::now(); + assert!(backend.verify_stage2(&decoded, &wrong_relation).is_err()); + let wrong_relation_elapsed = wrong_relation_started.elapsed(); + + let corrupt_decode_started = std::time::Instant::now(); + let mut corrupted = encoded; + let flip_at = corrupted.len() - 1; + corrupted[flip_at] ^= 1; + let corrupted = crate::Stage3ArtifactV1::from_bytes(&corrupted).unwrap(); + let corrupt_decode_elapsed = corrupt_decode_started.elapsed(); + + let corrupt_verify_started = std::time::Instant::now(); + assert!(backend.verify_stage2(&corrupted, corrupted.statement()).is_err()); + let corrupt_verify_elapsed = corrupt_verify_started.elapsed(); + + let total_elapsed = total_started.elapsed(); + let negative_checks_elapsed = + wrong_relation_elapsed + corrupt_decode_elapsed + corrupt_verify_elapsed; + eprintln!( + concat!( + "Flock complete Stage 3 timings (seconds):\n", + " fixture setup: {:>10.3}\n", + " preflight + self-verifying proof: {:>10.3}\n", + " artifact encode: {:>10.3}\n", + " artifact decode: {:>10.3}\n", + " valid external-root verification: {:>10.3}\n", + " reject wrong relation statement: {:>10.6}\n", + " corrupt and decode artifact: {:>10.3}\n", + " reject corrupted proof: {:>10.3}\n", + " all negative checks: {:>10.3}\n", + " total: {:>10.3}", + ), + fixture_elapsed.as_secs_f64(), + prove_elapsed.as_secs_f64(), + encode_elapsed.as_secs_f64(), + decode_elapsed.as_secs_f64(), + valid_verify_elapsed.as_secs_f64(), + wrong_relation_elapsed.as_secs_f64(), + corrupt_decode_elapsed.as_secs_f64(), + corrupt_verify_elapsed.as_secs_f64(), + negative_checks_elapsed.as_secs_f64(), + total_elapsed.as_secs_f64(), + ); + } + + #[test] + #[ignore = "real Flock authenticated FRI-fold proof; run explicitly"] + fn real_authenticated_fri_fold_round_trip_and_mutations() { + let artifact = prove_fri_fold_conformance(&fixture()).expect("prove fold"); + eprintln!( + "Flock authenticated FRI-fold conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_fri_fold_conformance(&artifact).expect("verify fold"); + let decoded = + FriFoldConformanceArtifactV1::from_bytes(&artifact.to_bytes()).unwrap(); + verify_fri_fold_conformance(&decoded).expect("verify decoded fold"); + + let mut wrong_sibling = decoded.clone(); + wrong_sibling.query.opening_proof[2][7] ^= 1; + assert!(verify_fri_fold_conformance(&wrong_sibling).is_err()); + let mut wrong_beta = decoded.clone(); + wrong_beta.query.beta[1] ^= 1; + assert!(verify_fri_fold_conformance(&wrong_beta).is_err()); + let mut wrong_result = decoded.clone(); + wrong_result.folded_result[0] ^= 1; + assert!(verify_fri_fold_conformance(&wrong_result).is_err()); + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_fri_fold_conformance(&wrong_proof).is_err()); + } + + #[test] + #[ignore = "real Flock FRI commit-phase query proof; run explicitly"] + fn real_fri_commit_phase_round_trip_and_mutations() { + let artifact = prove_fri_commit_phase_conformance(&commit_phase_fixture()) + .expect("prove commit phase"); + eprintln!( + "Flock FRI commit-phase conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_fri_commit_phase_conformance(&artifact) + .expect("verify commit phase"); + let decoded = + FriCommitPhaseConformanceArtifactV1::from_bytes(&artifact.to_bytes()) + .unwrap(); + verify_fri_commit_phase_conformance(&decoded) + .expect("verify decoded commit phase"); + + let mut wrong_path = decoded.clone(); + wrong_path.query.rounds[1].opening_proof[0][5] ^= 1; + assert!(verify_fri_commit_phase_conformance(&wrong_path).is_err()); + let mut wrong_final = decoded.clone(); + wrong_final.query.final_polynomial[1] ^= 1; + assert!(verify_fri_commit_phase_conformance(&wrong_final).is_err()); + let mut wrong_root = decoded.clone(); + wrong_root.commitment_roots[2][3] ^= 1; + assert!(verify_fri_commit_phase_conformance(&wrong_root).is_err()); + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_fri_commit_phase_conformance(&wrong_proof).is_err()); + } + + #[test] + #[ignore = "real Flock authenticated PCS-reduction proof; run explicitly"] + fn real_pcs_reduction_round_trip_and_mutations() { + let artifact = prove_pcs_reduction_conformance(&pcs_reduction_fixture()) + .expect("prove PCS reduction"); + eprintln!( + "Flock PCS-reduction conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_pcs_reduction_conformance(&artifact).expect("verify PCS reduction"); + let decoded = + PcsReductionConformanceArtifactV1::from_bytes(&artifact.to_bytes()) + .unwrap(); + verify_pcs_reduction_conformance(&decoded) + .expect("verify decoded PCS reduction"); + + let mut wrong_value = decoded.clone(); + wrong_value.opening.opened_values[2] ^= 1; + assert!(verify_pcs_reduction_conformance(&wrong_value).is_err()); + let mut wrong_ood = decoded.clone(); + wrong_ood.opening.opened_at_z[1][0] ^= 1; + assert!(verify_pcs_reduction_conformance(&wrong_ood).is_err()); + let mut wrong_result = decoded.clone(); + wrong_result.reduced_accumulator[0] ^= 1; + assert!(verify_pcs_reduction_conformance(&wrong_result).is_err()); + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_pcs_reduction_conformance(&wrong_proof).is_err()); + } + + #[test] + #[ignore = "real transcript-bound Flock PCS proof; run explicitly"] + fn real_transcript_bound_pcs_round_trip_and_mutations() { + let (replay, opening) = transcript_bound_pcs_fixture(); + let artifact = + prove_transcript_bound_pcs_reduction_conformance(&replay, &opening) + .expect("prove transcript-bound PCS reduction"); + eprintln!( + "Flock transcript-bound PCS conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_transcript_bound_pcs_reduction_conformance(&artifact) + .expect("verify transcript-bound PCS reduction"); + + let mut wrong_transcript = artifact.clone(); + wrong_transcript.replay.pcs_opening_observations[0] ^= 1; + assert!( + verify_transcript_bound_pcs_reduction_conformance(&wrong_transcript) + .is_err() + ); + let mut wrong_opening = artifact.clone(); + wrong_opening.opening.opened_values[0] ^= 1; + assert!( + verify_transcript_bound_pcs_reduction_conformance(&wrong_opening) + .is_err() + ); + let mut wrong_proof = artifact; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!( + verify_transcript_bound_pcs_reduction_conformance(&wrong_proof).is_err() + ); + } + + #[test] + #[ignore = "real transcript-bound Flock FRI-query proof; run explicitly"] + fn real_transcript_bound_fri_round_trip_and_mutations() { + let (prefix, fri_transcript, query) = transcript_bound_fri_fixture(); + let artifact = prove_transcript_bound_fri_commit_phase_conformance( + &prefix, + &fri_transcript, + 0, + &query, + ) + .expect("prove transcript-bound FRI query"); + eprintln!( + "Flock transcript-bound FRI-query conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_transcript_bound_fri_commit_phase_conformance(&artifact) + .expect("verify transcript-bound FRI query"); + + let mut wrong_cap = artifact.clone(); + wrong_cap.fri_transcript.commit_phase_commitments[1][0][7] ^= 1; + assert!( + verify_transcript_bound_fri_commit_phase_conformance(&wrong_cap).is_err() + ); + let mut wrong_query = artifact.clone(); + wrong_query.query.query_index ^= 1; + assert!( + verify_transcript_bound_fri_commit_phase_conformance(&wrong_query) + .is_err() + ); + let mut wrong_final = artifact.clone(); + wrong_final.fri_transcript.final_polynomial[0][0] ^= 1; + assert!( + verify_transcript_bound_fri_commit_phase_conformance(&wrong_final) + .is_err() + ); + let mut wrong_proof = artifact; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!( + verify_transcript_bound_fri_commit_phase_conformance(&wrong_proof) + .is_err() + ); + } + + #[test] + #[ignore = "real all-query transcript-bound Flock FRI proof; run explicitly"] + fn real_transcript_bound_fri_all_queries_round_trip_and_mutations() { + let (prefix, fri_transcript, queries) = + transcript_bound_fri_all_queries_fixture(); + let artifact = prove_transcript_bound_fri_queries_conformance( + &prefix, + &fri_transcript, + &queries, + ) + .expect("prove every transcript-bound FRI query"); + eprintln!( + "Flock all-query transcript-bound FRI bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_transcript_bound_fri_queries_conformance(&artifact) + .expect("verify every transcript-bound FRI query"); + + let mut wrong_query = artifact.clone(); + wrong_query.queries[1].query_index ^= 1; + assert!( + verify_transcript_bound_fri_queries_conformance(&wrong_query).is_err() + ); + let mut missing_query = artifact.clone(); + missing_query.queries.pop(); + assert!( + verify_transcript_bound_fri_queries_conformance(&missing_query).is_err() + ); + let mut wrong_proof = artifact; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!( + verify_transcript_bound_fri_queries_conformance(&wrong_proof).is_err() + ); + } + + #[test] + #[ignore = "real transcript-bound PCS-to-FRI Flock proof; run explicitly"] + fn real_transcript_bound_pcs_fri_round_trip_and_mutations() { + let (prefix, fri_transcript, pcs_instance, queries) = + transcript_bound_pcs_fri_fixture(); + let artifact = prove_transcript_bound_pcs_fri_queries_conformance( + &prefix, + &fri_transcript, + &pcs_instance, + &queries, + ) + .expect("prove transcript-bound PCS-to-FRI relation"); + eprintln!( + "Flock transcript-bound PCS-to-FRI bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_transcript_bound_pcs_fri_queries_conformance(&artifact) + .expect("verify transcript-bound PCS-to-FRI relation"); + + let mut wrong_row = artifact.clone(); + wrong_row.queries[0].pcs.batch_openings[0].opened_rows[0][0] ^= 1; + assert!( + verify_transcript_bound_pcs_fri_queries_conformance(&wrong_row).is_err() + ); + let mut wrong_ood = artifact.clone(); + wrong_ood.prefix.pcs_opening_observations[0] ^= 1; + assert!( + verify_transcript_bound_pcs_fri_queries_conformance(&wrong_ood).is_err() + ); + let mut wrong_proof = artifact; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!( + verify_transcript_bound_pcs_fri_queries_conformance(&wrong_proof) + .is_err() + ); + } +} diff --git a/flock-stage3/host/src/goldilocks.rs b/flock-stage3/host/src/goldilocks.rs new file mode 100644 index 00000000..7de4afe0 --- /dev/null +++ b/flock-stage3/host/src/goldilocks.rs @@ -0,0 +1,663 @@ +//! Boolean R1CS gadgets for Goldilocks values carried as little-endian u64s. +//! +//! Each canonicality row checks two `F128` words (four Goldilocks limbs) +//! and exposes all 128 violation bits. Packing the independent checks uses +//! 508 of the same 512 Boolean columns as the former two-limb row. + +use std::sync::OnceLock; + +use flock_prover::{ + circuit::builder::{GateType, SlotWitness}, + field::F128, + lincheck::pack_z_lincheck, + r1cs::{BlockR1cs, SparseBinaryMatrix, WitnessLayout}, + schedule::{IoWord, TableType}, + union::SlotWitnessDest, +}; + +use crate::boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_rows_into, + generate_boolean_witness, generate_boolean_witness_into, + write_f128 as write_boolean_f128, +}; + +pub(crate) const GOLDILOCKS_MODULUS: u64 = 0xffff_ffff_0000_0001; +const K_LOG: usize = 9; +const K: usize = 1 << K_LOG; +const K_SKIP: usize = 6; +const INPUT_BASE: usize = 0; +const VIOLATION_BASE: usize = 256; +const FIRST_CHAIN_BASE: usize = 384; +const LIMBS: usize = 4; +const USEFUL_BITS: usize = FIRST_CHAIN_BASE + LIMBS * 31; + +const ADD_K_LOG: usize = 11; +const ADD_LEFT_BASE: usize = 0; +const ADD_RIGHT_BASE: usize = 128; +const ADD_RESULT_BASE: usize = 256; +const ADD_VIOLATION_BASE: usize = 384; +const ADD_TOP_VIOLATION_BASE: usize = 512; +const ADD_RESERVED_COLUMNS: usize = 640; + +/// One R1CS row record for four little-endian Goldilocks candidates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct CanonicalGoldilocksQuadRow([F128; 2]); + +/// A word-aligned Boolean gate that checks four canonical Goldilocks values. +/// +/// Its sole output is zero exactly when all four input u64 limbs are below +/// `2^64 - 2^32 + 1`. Callers must connect that output to a fixed zero wire; +/// the table alone intentionally exposes, rather than silently pins, the +/// violation bits. +#[derive(Clone, Copy, Debug)] +pub(crate) struct CanonicalGoldilocksQuadGate { + pub(crate) nu: usize, +} + +impl GateType for CanonicalGoldilocksQuadGate { + type Row = CanonicalGoldilocksQuadRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_canonical_quad_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::output(2), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let value = [inputs[0], inputs[1]]; + outputs.push(violation_word(value)); + CanonicalGoldilocksQuadRow(value) + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +/// Build the Boolean relation used by [`CanonicalGoldilocksQuadGate`]. +pub(crate) fn build_canonical_quad_r1cs(nu: usize) -> BlockR1cs { + assert!(nu >= 3, "Flock lincheck requires at least eight rows"); + + let mut a_rows = vec![Vec::new(); K]; + let mut b_rows = vec![Vec::new(); K]; + + // Input bits are free Boolean values: x * x = x over GF(2). + for bit in 0..256 { + a_rows[INPUT_BASE + bit].push(INPUT_BASE + bit); + b_rows[INPUT_BASE + bit].push(INPUT_BASE + bit); + } + + // Fold each limb's high 32 bits to one `high_is_all_ones` bit. + for limb in 0..LIMBS { + add_and_chain( + &mut a_rows, + &mut b_rows, + limb * 64 + 32, + FIRST_CHAIN_BASE + limb * 31, + ); + } + + // x >= p iff its high 32 bits are all one and at least one low bit is one. + // Materialize all 32 products. Wiring pins the complete output word to zero. + for limb in 0..LIMBS { + let high_all = FIRST_CHAIN_BASE + limb * 31 + 30; + for low_bit in 0..32 { + a_rows[VIOLATION_BASE + limb * 32 + low_bit].push(high_all); + b_rows[VIOLATION_BASE + limb * 32 + low_bit].push(limb * 64 + low_bit); + } + } + + let identity_rows = (0..K).map(|row| vec![row]).collect(); + BlockR1cs { + m: K_LOG + nu, + k_log: K_LOG, + k_skip: K_SKIP, + useful_bits: USEFUL_BITS, + a_0: sparse_matrix(a_rows), + b_0: sparse_matrix(b_rows), + c_0: sparse_matrix(identity_rows), + layout: WitnessLayout::BatchMajor, + const_pin: None, + digest_cache: OnceLock::new(), + csc_cache: OnceLock::new(), + } +} + +/// Produce Flock's batch-major `(z, A z, B z, lincheck stripe)` tuple. +pub(crate) fn generate_canonical_quad_witness( + rows: &[CanonicalGoldilocksQuadRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + let capacity = 1usize << nu; + assert!(rows.len() <= capacity); + let r1cs = build_canonical_quad_r1cs(nu); + let mut z = vec![false; r1cs.n()]; + for (outer, row) in rows.iter().enumerate() { + let range = outer * K..(outer + 1) * K; + fill_logical_row(&mut z[range], row.0); + } + let a = r1cs.apply_a(&z); + let b = r1cs.apply_b(&z); + debug_assert!( + a.iter() + .zip(&b) + .zip(&z) + .all(|((a_bit, b_bit), z_bit)| (*a_bit & *b_bit) == *z_bit) + ); + let stripe = pack_z_lincheck(&z, r1cs.m, r1cs.k_log); + ( + pack_batch_major(&z, nu), + pack_batch_major(&a, nu), + pack_batch_major(&b, nu), + stripe, + ) +} + +pub(crate) fn generate_canonical_quad_witness_into( + rows: &[CanonicalGoldilocksQuadRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + let r1cs = build_canonical_quad_r1cs(nu); + generate_boolean_rows_into( + r1cs.k_log, + r1cs.useful_bits, + &r1cs.a_0.rows, + &r1cs.b_0.rows, + rows, + nu, + dst, + |row, bits| fill_logical_row(bits, row.0), + ) +} + +/// One row of two lane-wise Goldilocks additions packed into `F128` words. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct GoldilocksAddPairRow { + left: F128, + right: F128, +} + +/// Two independent canonical Goldilocks additions, one per u64 lane. +/// +/// The gate exposes the result plus two zero-valued equation-residual words. +/// Callers connect both residuals to a fixed zero wire and pass every input +/// and result through [`CanonicalGoldilocksQuadGate`]. Keeping canonicality a +/// shared table avoids duplicating its constraints in every arithmetic table. +#[derive(Clone, Copy, Debug)] +pub(crate) struct GoldilocksAddPairGate { + pub(crate) nu: usize, +} + +impl GateType for GoldilocksAddPairGate { + type Row = GoldilocksAddPairRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_goldilocks_add_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::output(2), + IoWord::output(3), + IoWord::output(4), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let left = inputs[0]; + let right = inputs[1]; + outputs.extend_from_slice(&[ + F128::new( + goldilocks_add(left.lo, right.lo), + goldilocks_add(left.hi, right.hi), + ), + F128::ZERO, + F128::ZERO, + ]); + GoldilocksAddPairRow { left, right } + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +struct GoldilocksAddPlan { + boolean: BooleanR1csPlan, + quotient_bits: [usize; 2], +} + +pub(crate) fn build_goldilocks_add_r1cs(nu: usize) -> BlockR1cs { + goldilocks_add_plan().boolean.block_r1cs(nu) +} + +pub(crate) fn generate_goldilocks_add_witness( + rows: &[GoldilocksAddPairRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + let plan = goldilocks_add_plan(); + generate_boolean_witness(&plan.boolean, rows, nu, |row, bits| { + fill_goldilocks_add_row(plan, *row, bits) + }) +} + +pub(crate) fn generate_goldilocks_add_witness_into( + rows: &[GoldilocksAddPairRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + let plan = goldilocks_add_plan(); + generate_boolean_witness_into(&plan.boolean, rows, nu, dst, |row, bits| { + fill_goldilocks_add_row(plan, *row, bits) + }) +} + +fn goldilocks_add_plan() -> &'static GoldilocksAddPlan { + static PLAN: OnceLock = OnceLock::new(); + PLAN.get_or_init(build_goldilocks_add_plan) +} + +fn build_goldilocks_add_plan() -> GoldilocksAddPlan { + let mut builder = BooleanR1csBuilder::new(ADD_K_LOG, ADD_RESERVED_COLUMNS); + for column in ADD_LEFT_BASE..ADD_RESULT_BASE + 128 { + builder.free_boolean_at(column); + } + let one = builder.alloc_constant_one(); + let quotient_bits = + [builder.alloc_free_boolean(), builder.alloc_free_boolean()]; + + for (lane, "ient) in quotient_bits.iter().enumerate() { + let lane_offset = lane * 64; + let left: [usize; 64] = + std::array::from_fn(|bit| ADD_LEFT_BASE + lane_offset + bit); + let right: [usize; 64] = + std::array::from_fn(|bit| ADD_RIGHT_BASE + lane_offset + bit); + let result: [usize; 64] = + std::array::from_fn(|bit| ADD_RESULT_BASE + lane_offset + bit); + let right_terms = right.map(Some); + let modulus_terms: [Option; 64] = std::array::from_fn(|bit| { + (((GOLDILOCKS_MODULUS >> bit) & 1) == 1).then_some(quotient) + }); + let (left_sum, left_carry) = + ripple_add(&mut builder, &left, &right_terms, one); + let (right_sum, right_carry) = + ripple_add(&mut builder, &result, &modulus_terms, one); + + for bit in 0..64 { + builder.write_xor( + ADD_VIOLATION_BASE + lane_offset + bit, + &[left_sum[bit], right_sum[bit]], + one, + ); + } + builder.write_xor( + ADD_TOP_VIOLATION_BASE + lane, + &[left_carry, right_carry], + one, + ); + } + + GoldilocksAddPlan { boolean: builder.finish(), quotient_bits } +} + +fn ripple_add( + builder: &mut BooleanR1csBuilder, + left: &[usize; 64], + right: &[Option; 64], + one: usize, +) -> (Vec, usize) { + let mut sums = Vec::with_capacity(64); + let mut carry = None; + for bit in 0..64 { + let xor_lr = right[bit] + .map_or(left[bit], |right| builder.xor(&[left[bit], right], one)); + let sum = carry.map_or(xor_lr, |carry| builder.xor(&[xor_lr, carry], one)); + sums.push(sum); + + let left_and_right = right[bit].map(|right| builder.and(left[bit], right)); + let carry_and_xor = carry.map(|carry| builder.and(carry, xor_lr)); + carry = match (left_and_right, carry_and_xor) { + (Some(first), Some(second)) => Some(builder.xor(&[first, second], one)), + (Some(carry), None) | (None, Some(carry)) => Some(carry), + (None, None) => None, + }; + } + (sums, carry.expect("addition has a carry variable from bit zero")) +} + +fn fill_goldilocks_add_row( + plan: &GoldilocksAddPlan, + row: GoldilocksAddPairRow, + bits: &mut [bool], +) { + let result = F128::new( + goldilocks_add(row.left.lo, row.right.lo), + goldilocks_add(row.left.hi, row.right.hi), + ); + write_boolean_f128(bits, ADD_LEFT_BASE, row.left); + write_boolean_f128(bits, ADD_RIGHT_BASE, row.right); + write_boolean_f128(bits, ADD_RESULT_BASE, result); + for (lane, quotient) in plan.quotient_bits.iter().enumerate() { + let (left, right) = if lane == 0 { + (row.left.lo, row.right.lo) + } else { + (row.left.hi, row.right.hi) + }; + bits[*quotient] = + left as u128 + right as u128 >= GOLDILOCKS_MODULUS as u128; + } +} + +pub(crate) fn goldilocks_add(left: u64, right: u64) -> u64 { + ((left as u128 + right as u128) % GOLDILOCKS_MODULUS as u128) as u64 +} + +fn add_and_chain( + a_rows: &mut [Vec], + b_rows: &mut [Vec], + high_base: usize, + chain_base: usize, +) { + for step in 0..31 { + let output = chain_base + step; + let lhs = if step == 0 { high_base } else { output - 1 }; + let rhs = high_base + step + 1; + a_rows[output].push(lhs); + b_rows[output].push(rhs); + } +} + +fn sparse_matrix(rows: Vec>) -> SparseBinaryMatrix { + SparseBinaryMatrix { num_rows: K, num_cols: K, rows } +} + +fn fill_logical_row(bits: &mut [bool], value: [F128; 2]) { + assert_eq!(bits.len(), K); + write_f128(bits, INPUT_BASE, value[0]); + write_f128(bits, INPUT_BASE + 128, value[1]); + write_f128(bits, VIOLATION_BASE, violation_word(value)); + for (limb, value) in + [value[0].lo, value[0].hi, value[1].lo, value[1].hi].into_iter().enumerate() + { + fill_and_chain(bits, value, FIRST_CHAIN_BASE + limb * 31); + } +} + +fn fill_and_chain(bits: &mut [bool], limb: u64, chain_base: usize) { + let mut accumulator = bit(limb, 32); + for step in 0..31 { + accumulator &= bit(limb, 33 + step); + bits[chain_base + step] = accumulator; + } +} + +fn violation_word(value: [F128; 2]) -> F128 { + let packed = value.map(|word| { + u64::from(limb_violation_bits(word.lo)) + | (u64::from(limb_violation_bits(word.hi)) << 32) + }); + F128::new(packed[0], packed[1]) +} + +fn limb_violation_bits(value: u64) -> u32 { + if value >= GOLDILOCKS_MODULUS { value as u32 } else { 0 } +} + +fn write_f128(bits: &mut [bool], offset: usize, value: F128) { + for local in 0..64 { + bits[offset + local] = bit(value.lo, local); + bits[offset + 64 + local] = bit(value.hi, local); + } +} + +fn bit(value: u64, index: usize) -> bool { + (value >> index) & 1 == 1 +} + +fn pack_batch_major(bits: &[bool], nu: usize) -> Vec { + let capacity = 1usize << nu; + assert_eq!(bits.len(), capacity * K); + let chunks = K / 128; + let mut packed = vec![F128::ZERO; chunks * capacity]; + for chunk in 0..chunks { + for outer in 0..capacity { + let start = outer * K + chunk * 128; + let mut lo = 0u64; + let mut hi = 0u64; + for local in 0..64 { + lo |= u64::from(bits[start + local]) << local; + hi |= u64::from(bits[start + 64 + local]) << local; + } + packed[(chunk << nu) + outer] = F128::new(lo, hi); + } + } + packed +} + +#[cfg(test)] +mod tests { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + use flock_prover::circuit::builder::ShapeBuilder; + use multi_stark::{ + p3_field::{PrimeCharacteristicRing, PrimeField64}, + p3_goldilocks::Goldilocks, + }; + + use super::*; + + #[test] + fn canonicality_boundary_matches_goldilocks_modulus() { + for value in [0, 1, GOLDILOCKS_MODULUS - 1] { + assert_eq!(violation_word([F128::new(value, value); 2]), F128::ZERO); + } + for limb in 0..LIMBS { + for bad in [GOLDILOCKS_MODULUS, GOLDILOCKS_MODULUS + 1, u64::MAX] { + let mut values = [0; LIMBS]; + values[limb] = bad; + assert_ne!(violation_word(pack_limbs(values)), F128::ZERO); + } + } + } + + fn pack_limbs(values: [u64; LIMBS]) -> [F128; 2] { + [F128::new(values[0], values[1]), F128::new(values[2], values[3])] + } + + #[test] + fn r1cs_recomputes_every_violation_bit() { + let r1cs = build_canonical_quad_r1cs(3); + assert_eq!(r1cs.k_log, 9); + assert_eq!(r1cs.useful_bits, 508); + for violation_bit in 0..128 { + let mut limbs = [GOLDILOCKS_MODULUS - 1; LIMBS]; + limbs[violation_bit / 32] |= 1 << (violation_bit % 32); + let value = pack_limbs(limbs); + let mut row = vec![false; K]; + fill_logical_row(&mut row, value); + let mut witness = vec![false; r1cs.n()]; + witness[..K].copy_from_slice(&row); + assert!(r1cs.satisfies(&witness)); + + assert!(witness[VIOLATION_BASE + violation_bit]); + witness[VIOLATION_BASE + violation_bit] = false; + assert!(!r1cs.satisfies(&witness), "violation bit {violation_bit}"); + } + } + + #[test] + fn circuit_wiring_pins_violation_output_to_zero() { + let nu = 3; + let mut builder = ShapeBuilder::new(nu); + let slot = builder.slot(CanonicalGoldilocksQuadGate { nu }); + let candidates = [builder.input(), builder.input()]; + let zero = builder.fixed_public_input(F128::ZERO); + let violation = builder.gate(slot, &candidates)[0]; + builder.connect(violation, zero); + let shape = builder.finish().unwrap(); + + for value in [0, 1, (1 << 32) - 1, 1 << 32, GOLDILOCKS_MODULUS - 1] { + let values = pack_limbs([value; LIMBS]); + shape.run(&[values[0], values[1], F128::ZERO], &[]); + } + for limb in 0..LIMBS { + for bad in [GOLDILOCKS_MODULUS, GOLDILOCKS_MODULUS + 1, u64::MAX] { + let mut values = [17; LIMBS]; + values[limb] = bad; + let values = pack_limbs(values); + assert!( + catch_unwind(AssertUnwindSafe(|| { + shape.run(&[values[0], values[1], F128::ZERO], &[]) + })) + .is_err(), + "limb {limb}: {bad}" + ); + } + } + } + + #[test] + fn batch_major_witness_has_zero_dummy_rows() { + let rows = [ + CanonicalGoldilocksQuadRow([F128::new(1, 2), F128::new(3, 4)]), + CanonicalGoldilocksQuadRow([F128::new(5, 6), F128::new(7, 8)]), + ]; + let (z, a, b, stripe) = generate_canonical_quad_witness(&rows, 3); + assert_eq!(z.len(), 32); + assert_eq!(a.len(), z.len()); + assert_eq!(b.len(), z.len()); + assert_eq!(stripe.len(), K); + for chunk in 0..K / 128 { + for outer in rows.len()..8 { + assert_eq!(z[(chunk << 3) + outer], F128::ZERO); + assert_eq!(a[(chunk << 3) + outer], F128::ZERO); + assert_eq!(b[(chunk << 3) + outer], F128::ZERO); + } + } + } + + #[test] + fn modular_add_matches_reference_goldilocks() { + let boundary = [ + 0, + 1, + 2, + (1u64 << 32) - 1, + 1u64 << 32, + GOLDILOCKS_MODULUS - 2, + GOLDILOCKS_MODULUS - 1, + ]; + for &left in &boundary { + for &right in &boundary { + let expected = (Goldilocks::from_u64(left) + + Goldilocks::from_u64(right)) + .as_canonical_u64(); + assert_eq!(goldilocks_add(left, right), expected); + } + } + + let mut state = 0x6a09_e667_f3bc_c909u64; + for _ in 0..256 { + state = state + .wrapping_mul(0x9e37_79b9_7f4a_7c15) + .wrapping_add(0xbf58_476d_1ce4_e5b9); + let left = state % GOLDILOCKS_MODULUS; + state ^= state.rotate_left(29); + let right = state % GOLDILOCKS_MODULUS; + let expected = (Goldilocks::from_u64(left) + Goldilocks::from_u64(right)) + .as_canonical_u64(); + assert_eq!(goldilocks_add(left, right), expected); + } + } + + #[test] + fn modular_add_r1cs_rejects_wrong_result_and_quotient() { + let plan = build_goldilocks_add_plan(); + let r1cs = plan.boolean.block_r1cs(3); + let cases = [ + GoldilocksAddPairRow { + left: F128::new(0, GOLDILOCKS_MODULUS - 1), + right: F128::new(0, 0), + }, + GoldilocksAddPairRow { + left: F128::new(GOLDILOCKS_MODULUS - 1, 1 << 32), + right: F128::new(GOLDILOCKS_MODULUS - 1, u64::MAX >> 32), + }, + ]; + for row in cases { + let mut logical = vec![false; plan.boolean.k()]; + plan.boolean.fill_row(&mut logical, |bits| { + fill_goldilocks_add_row(&plan, row, bits) + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.boolean.k()].copy_from_slice(&logical); + assert!(r1cs.satisfies(&witness)); + + let mut wrong_result = witness.clone(); + wrong_result[ADD_RESULT_BASE + 17] ^= true; + assert!(!r1cs.satisfies(&wrong_result)); + + let mut wrong_quotient = witness; + wrong_quotient[plan.quotient_bits[0]] ^= true; + assert!(!r1cs.satisfies(&wrong_quotient)); + } + } + + #[test] + fn modular_add_gate_pins_equation_residuals() { + let nu = 3; + let mut builder = ShapeBuilder::new(nu); + let slot = builder.slot(GoldilocksAddPairGate { nu }); + let left = builder.input(); + let right = builder.input(); + let zero = builder.fixed_public_input(F128::ZERO); + let outputs = builder.gate(slot, &[left, right]); + builder.connect(outputs[1], zero); + builder.connect(outputs[2], zero); + builder.publish(outputs[0]); + let shape = builder.finish().unwrap(); + + let left = F128::new(GOLDILOCKS_MODULUS - 1, 7); + let right = F128::new(2, GOLDILOCKS_MODULUS - 3); + let witness = shape.run(&[left, right, F128::ZERO], &[]); + assert_eq!(*witness.public.last().unwrap(), F128::new(1, 4)); + assert_eq!( + witness.rows::(slot), + &[GoldilocksAddPairRow { left, right }] + ); + } + + #[test] + fn modular_add_batch_witness_zeroes_dummy_rows() { + let rows = + [GoldilocksAddPairRow { left: F128::new(1, 2), right: F128::new(3, 4) }]; + let (z, a, b, stripe) = generate_goldilocks_add_witness(&rows, 3); + let chunks = (1usize << ADD_K_LOG) / 128; + assert_eq!(z.len(), chunks * 8); + assert_eq!(a.len(), z.len()); + assert_eq!(b.len(), z.len()); + assert_eq!(stripe.len(), 1usize << ADD_K_LOG); + for chunk in 0..chunks { + for outer in rows.len()..8 { + assert_eq!(z[(chunk << 3) + outer], F128::ZERO); + assert_eq!(a[(chunk << 3) + outer], F128::ZERO); + assert_eq!(b[(chunk << 3) + outer], F128::ZERO); + } + } + } +} diff --git a/flock-stage3/host/src/lib.rs b/flock-stage3/host/src/lib.rs new file mode 100644 index 00000000..dcbbbc81 --- /dev/null +++ b/flock-stage3/host/src/lib.rs @@ -0,0 +1,495 @@ +//! Ix Stage 3 backend for a specialised Aiur verifier over Flock's binary +//! field proof system. + +mod air; +mod arithmetic; +mod artifact; +mod binding; +mod boolean; +mod config; +mod conformance; +mod equality; +mod extension; +mod fri; +mod goldilocks; +mod limits; +mod merkle; +mod multiplication; +mod prepared; +mod relation; +mod report; +mod sizing; +mod transcript; +mod typed_witness; +mod window; + +#[cfg(test)] +mod test_support; + +use aiur::vk_codec::AiurVerifyingKey; +use anyhow::{Result, bail}; +use ix_terminal::{ + Stage2AdviceProfileV1, ValidatedStage2RootV1, + validate_and_expand_root_inputs_bounded, +}; +use multi_stark::types::FriParameters; +use std::fmt; + +pub use air::{Stage2ActiveAirCircuitV1, Stage2AirProgramV1}; +pub use arithmetic::{ + ARITHMETIC_CONFORMANCE_ARTIFACT_MAGIC, ArithmeticConformanceArtifactV1, + GoldilocksAddPairV1, GoldilocksExt2MulV1, GoldilocksMulPairV1, + prove_arithmetic_conformance, verify_arithmetic_conformance, +}; +use artifact::Stage3ProductionPayloadV1; +pub use artifact::{ + MAX_STAGE3_ARTIFACT_BYTES, MAX_STAGE3_PROOF_BYTES, STAGE3_STATEMENT_BYTES, + STAGE3_STATEMENT_DOMAIN, Stage3ArtifactV1, Stage3ArtifactWriterV1, + Stage3StatementV1, +}; +pub use binding::{ + STAGE3_BINDING_ARTIFACT_MAGIC, Stage3BindingArtifactV1, + prove_stage3_statement_binding, stage3_statement_binding_circuit_digest, + verify_stage3_statement_binding, verify_stage3_statement_binding_for, +}; +pub use config::{ + ARITHMETIC_CONFORMANCE_TRANSCRIPT_DOMAIN, + ENGINE_CONFORMANCE_TRANSCRIPT_DOMAIN, FLOCK_UPSTREAM_REVISION, + FRI_FOLD_CONFORMANCE_TRANSCRIPT_DOMAIN, + FRI_QUERY_CONFORMANCE_TRANSCRIPT_DOMAIN, FlockConfigV1, + MERKLE_CONFORMANCE_TRANSCRIPT_DOMAIN, + PCS_REDUCTION_CONFORMANCE_TRANSCRIPT_DOMAIN, + STAGE2_AIR_PCS_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + STAGE2_TRANSCRIPT_CONFORMANCE_TRANSCRIPT_DOMAIN, STAGE3_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_FRI_CONFORMANCE_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_PCS_CONFORMANCE_TRANSCRIPT_DOMAIN, + TRANSCRIPT_BOUND_PCS_FRI_QUERIES_CONFORMANCE_TRANSCRIPT_DOMAIN, +}; +pub use conformance::{ + EngineConformanceArtifact, prove_engine_conformance, + verify_engine_conformance, +}; +pub use flock_prover::r1cs_hashes::blake3::Compression; +pub use fri::{ + FRI_COMMIT_PHASE_CONFORMANCE_ARTIFACT_MAGIC, + FRI_FOLD_CONFORMANCE_ARTIFACT_MAGIC, FriCommitPhaseConformanceArtifactV1, + FriCommitPhaseQueryV1, FriCommitPhaseRoundV1, FriFoldConformanceArtifactV1, + FriFoldQueryV1, PCS_REDUCTION_CONFORMANCE_ARTIFACT_MAGIC, + PcsReducedOpeningV1, PcsReductionConformanceArtifactV1, + Stage2AirPcsFriArtifactV1, Stage2AirPcsFriWitnessV1, Stage2PcsBatchOpeningV1, + Stage2PcsBatchV1, Stage2PcsFriWitnessV1, Stage2PcsInstanceV1, + Stage2PcsMatrixV1, Stage2PcsOpeningPointV1, Stage2PcsQueryV1, + Stage3RelationCensusV1, TranscriptBoundFriCommitPhaseArtifactV1, + TranscriptBoundFriQueriesArtifactV1, TranscriptBoundPcsFriQueriesArtifactV1, + TranscriptBoundPcsFriQueryV1, TranscriptBoundPcsReductionArtifactV1, + prove_fri_commit_phase_conformance, prove_fri_fold_conformance, + prove_pcs_reduction_conformance, prove_stage2_air_pcs_fri_conformance, + prove_transcript_bound_fri_commit_phase_conformance, + prove_transcript_bound_fri_queries_conformance, + prove_transcript_bound_pcs_fri_queries_conformance, + prove_transcript_bound_pcs_reduction_conformance, + verify_fri_commit_phase_conformance, verify_fri_fold_conformance, + verify_pcs_reduction_conformance, verify_stage2_air_pcs_fri_conformance, + verify_stage2_air_pcs_fri_conformance_for, + verify_transcript_bound_fri_commit_phase_conformance, + verify_transcript_bound_fri_queries_conformance, + verify_transcript_bound_pcs_fri_queries_conformance, + verify_transcript_bound_pcs_reduction_conformance, +}; +pub use limits::Stage3ResourceLimitsV1; +pub use merkle::{ + MERKLE_CONFORMANCE_ARTIFACT_MAGIC, MerkleConformanceArtifactV1, MerklePathV1, + prove_merkle_conformance, verify_merkle_conformance, +}; +pub use prepared::{Stage3PreparedRootV1, Stage3ProofTimingsV1}; +pub use relation::{ + STAGE3_RELATION_MANIFEST_DOMAIN, STAGE3_VERIFIER_PHASES_V1, + Stage3LoweringStatusV1, Stage3RelationBoundsV1, Stage3RelationManifestV1, + Stage3VerifierPhaseV1, +}; +pub use report::{ + Stage3PreflightTimingsV1, Stage3ResourceReportV1, Stage3TableReportV1, + process_peak_rss_bytes as stage3_process_peak_rss_bytes, +}; +pub use transcript::{ + STAGE2_TRANSCRIPT_CONFORMANCE_ARTIFACT_MAGIC, + Stage2FriTranscriptChallengesV1, Stage2FriTranscriptReplayV1, + Stage2TranscriptByteBindingV1, Stage2TranscriptChallengesV1, + Stage2TranscriptConformanceArtifactV1, Stage2TranscriptReplayV1, + Stage2TranscriptSegmentV1, prove_stage2_transcript_conformance, + verify_stage2_transcript_conformance, +}; +pub use typed_witness::{ + STAGE3_TYPED_WITNESS_LAYOUT_DOMAIN, Stage3DigestV1, Stage3ExtensionValueV1, + Stage3OpenedRoundV1, Stage3TypedBatchOpeningV1, Stage3TypedCommitPhaseStepV1, + Stage3TypedCommitmentsV1, Stage3TypedFriProofV1, Stage3TypedProofCountsV1, + Stage3TypedProofWitnessV1, Stage3TypedQueryProofV1, +}; + +/// Result of compiling and evaluating the complete Stage 3 relation without +/// invoking the Flock prover. This is the mandatory cost/compatibility gate +/// before attempting a production-sized aggregate root. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3PreflightReportV1 { + pub stage2_root_digest: [u8; 32], + pub relation_digest: [u8; 32], + pub stage3_statement_digest: [u8; 32], + pub verifying_key_digest: [u8; 32], + pub compact_proof_digest: [u8; 32], + pub typed_witness_layout_digest: [u8; 32], + pub activation: Vec, + pub log_degrees: Vec, + pub fri_parameter_words: [u64; 5], + pub verifying_key_bytes: u64, + pub claim_bytes: u64, + pub compact_proof_bytes: u64, + pub advice: Stage2AdviceProfileV1, + pub relation: Stage3RelationCensusV1, + pub resources: Stage3ResourceReportV1, + pub limits: Stage3ResourceLimitsV1, + pub timings: Stage3PreflightTimingsV1, + pub process_peak_rss_bytes: Option, +} + +impl fmt::Display for Stage3PreflightReportV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex = |digest| blake3::Hash::from_bytes(digest).to_hex(); + writeln!(formatter, "Flock Stage 3 preflight accepted the aggregate root")?; + writeln!(formatter, " Stage 2 root: {}", hex(self.stage2_root_digest))?; + writeln!(formatter, " relation: {}", hex(self.relation_digest))?; + writeln!( + formatter, + " Stage 3 stmt: {}", + hex(self.stage3_statement_digest) + )?; + writeln!( + formatter, + " transport: vk={} B, claim={} B, compact proof={} B, advice={} B", + self.verifying_key_bytes, + self.claim_bytes, + self.compact_proof_bytes, + self.advice.advice_bytes, + )?; + writeln!( + formatter, + " Stage 2 shape: circuits={}/{} active, queries={}, FRI rounds={}, input rounds/query={}", + self.advice.active_circuits, + self.advice.total_circuits, + self.advice.queries, + self.advice.fri_rounds, + self.advice.input_rounds_per_query, + )?; + writeln!( + formatter, + " openings: input siblings={}, FRI siblings={}, base values={}, FRI extension siblings={}, other extensions={}", + self.advice.input_merkle_siblings, + self.advice.fri_merkle_siblings, + self.advice.opened_base_values, + self.advice.fri_sibling_extension_values, + self.advice.other_extension_values, + )?; + writeln!( + formatter, + " Flock relation: nu={}, capacity/table={}, inputs={}, public={}, rows={}", + self.relation.nu, + self.relation.table_capacity, + self.relation.relation_inputs, + self.relation.public_values, + self.relation.total_rows(), + )?; + writeln!( + formatter, + " gate rows: blake3={}, order={}, add={}, mul={}, repack={}, canonical={}, equality={}, hash-sample={}, field-sample={}, split={}, window={}", + self.relation.blake3_rows, + self.relation.digest_order_rows, + self.relation.goldilocks_add_rows, + self.relation.goldilocks_mul_rows, + self.relation.lane_repack_rows, + self.relation.canonical_goldilocks_rows, + self.relation.equality_rows, + self.relation.hash_sample_rows, + self.relation.field_sample_rows, + self.relation.u64_split_rows, + self.relation.byte_window_rows, + )?; + writeln!( + formatter, + " union: virtual log={}, committed log={}, dense={} B, padded z/a/b={} B", + self.resources.virtual_union_log, + self.resources.committed_union_log, + self.resources.dense_witness_bytes, + self.resources.padded_union_witness_bytes, + )?; + writeln!( + formatter, + " PCS: message={} B, codeword={} B, lanes={}, log inverse rate={}", + self.resources.pcs_message_bytes, + self.resources.pcs_codeword_bytes, + self.resources.pcs_lanes, + self.resources.pcs_log_inverse_rate, + )?; + writeln!( + formatter, + " fresh preparation (us): native={}, lowering={}, compile={}, evaluate={}, total={}", + self.timings.native_prepare_us, + self.timings.lowering_us, + self.timings.compile_us, + self.timings.evaluate_us, + self.timings.total_us, + )?; + write!( + formatter, + " memory: process lifetime peak={:?} B; PCS/compiler scratch is additional to padded witness", + self.process_peak_rss_bytes, + ) + } +} + +/// Host facade for the production Stage 3 relation. +#[derive(Clone, Copy, Debug, Default)] +pub struct FlockStage3Backend; + +impl FlockStage3Backend { + /// Verify the compact Stage 2 root and produce the exact vk/claims/advice + /// transport that the Flock relation must consume. This is usable while the + /// relation itself is still being lowered. + pub fn prepare_witness( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result { + self.prepare_witness_with_limits( + vk_bytes, + claim_bytes, + proof_bytes, + fri, + Stage3ResourceLimitsV1::default(), + ) + } + + pub fn prepare_witness_with_limits( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + limits: Stage3ResourceLimitsV1, + ) -> Result { + limits.ensure_transport(vk_bytes, claim_bytes, proof_bytes, fri)?; + // Fail before relation construction on an invalid compact root. The Flock + // relation repeats verification; this native validation/expansion pass is + // the inexpensive guard needed before allocating a production-scale + // circuit. + let prepared = validate_and_expand_root_inputs_bounded( + vk_bytes, + claim_bytes, + proof_bytes, + fri, + limits.max_advice_bytes, + )?; + limits.ensure_prepared(&prepared)?; + Ok(prepared) + } + + /// Admit, compile, and evaluate once. The caller owns the relation and may + /// explicitly reuse it for proving or verification; no root is retained in + /// a process-global cache by the production workflow. + pub fn prepare_stage2( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result { + self.prepare_stage2_with_limits( + vk_bytes, + claim_bytes, + proof_bytes, + fri, + Stage3ResourceLimitsV1::default(), + ) + } + + pub fn prepare_stage2_with_limits( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + limits: Stage3ResourceLimitsV1, + ) -> Result { + Stage3LoweringStatusV1::current().ensure_complete()?; + Stage3PreparedRootV1::new(vk_bytes, claim_bytes, proof_bytes, fri, limits) + } + + /// Validate a compact aggregate root, compile the complete specialised + /// relation, and evaluate every gate without running the Flock prover. + pub fn preflight_stage2( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result { + Ok( + self + .prepare_stage2(vk_bytes, claim_bytes, proof_bytes, fri)? + .report() + .clone(), + ) + } + + /// Compile and content-address the complete relation for a prepared root. + /// This builds the circuit but does not run the expensive Flock prover. + pub fn relation_manifest( + self, + prepared: &ValidatedStage2RootV1, + ) -> Result { + Stage3RelationManifestV1::for_prepared(prepared) + } + + /// Decode the verified advice transport into the primitive, fixed-schema + /// witness consumed by the no-RISC-V Flock lowering. + pub fn prepare_typed_proof_witness( + self, + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + ) -> Result { + Stage3TypedProofWitnessV1::from_prepared(prepared, fri) + } + + /// Construct a public Stage 3 statement only from a complete manifest that + /// is specialised to the exact shape of this prepared root. + pub fn prepare_statement( + self, + prepared: &ValidatedStage2RootV1, + manifest: &Stage3RelationManifestV1, + ) -> Result { + manifest.ensure_matches(prepared)?; + Ok(Stage3StatementV1::new( + prepared.statement(), + manifest.relation_digest()?, + )) + } + + /// Validate and lower a compact Stage 2 proof, prove the complete + /// statement/AIR/PCS/FRI relation using the production transcript domain, + /// verify it, and return its strictly framed Stage 3 artifact. + pub fn prove_stage2( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result { + self.prepare_stage2(vk_bytes, claim_bytes, proof_bytes, fri)?.prove() + } + + /// Run the mandatory no-prove gate and then prove using the same validated + /// root, typed witness, and explicitly owned relation. The returned artifact + /// has also passed the production Flock verifier. + pub fn preflight_and_prove_stage2( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result<(Stage3PreflightReportV1, Stage3ArtifactV1)> { + let prepared = + self.prepare_stage2(vk_bytes, claim_bytes, proof_bytes, fri)?; + let report = prepared.report().clone(); + let artifact = prepared.prove()?; + Ok((report, artifact)) + } + + /// Verify the expected public statement, reconstruct the fixed relation + /// from canonical Stage 2 inputs, pin its manifest digest, and verify the + /// Flock proof under the production transcript domain. + pub fn verify_stage2( + self, + artifact: &Stage3ArtifactV1, + expected: &Stage3StatementV1, + ) -> Result<()> { + artifact.ensure_statement(expected)?; + let payload = Stage3ProductionPayloadV1::decode(artifact.proof_bytes())?; + Stage3ResourceLimitsV1::default().ensure_raw_transport( + payload.vk_bytes(), + payload.claim_bytes(), + payload.stage2_proof_bytes(), + )?; + let key = AiurVerifyingKey::from_bytes(payload.vk_bytes()) + .map_err(|error| anyhow::anyhow!("decode Stage 3 Aiur key: {error}"))?; + let fri = key.fri_parameters(); + let prepared = self.prepare_stage2( + payload.vk_bytes(), + payload.claim_bytes(), + payload.stage2_proof_bytes(), + &fri, + )?; + if prepared.statement() != expected { + bail!("Stage 3 relation manifest does not match the expected relation"); + } + prepared.verify(artifact) + } + + /// Verify an artifact against the exact canonical aggregate-root transport + /// from which it was produced. The expected statement and relation digest + /// are rebuilt from these external inputs; no artifact-supplied statement is + /// used as its own trust anchor. + /// + /// Requiring the embedded compact transport to match also lets this path + /// reuse one native validation and one relation build. + pub fn verify_stage2_for_root( + self, + artifact: &Stage3ArtifactV1, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result<()> { + let payload = Stage3ProductionPayloadV1::decode(artifact.proof_bytes())?; + for (observed, external, label) in [ + (payload.vk_bytes(), vk_bytes, "verifying key"), + (payload.claim_bytes(), claim_bytes, "claim"), + (payload.stage2_proof_bytes(), proof_bytes, "compact proof"), + ] { + if observed != external { + bail!("Stage 3 artifact embeds a different Stage 2 {label} transport"); + } + } + + self + .prepare_stage2(vk_bytes, claim_bytes, proof_bytes, fri)? + .verify(artifact) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn malformed_stage2_input_fails_before_proving() { + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + }; + assert!( + FlockStage3Backend + .prove_stage2(b"vk", &[0; 144], b"proof", &fri) + .is_err() + ); + assert!( + FlockStage3Backend + .preflight_and_prove_stage2(b"vk", &[0; 144], b"proof", &fri) + .is_err() + ); + assert!(Stage3LoweringStatusV1::current().is_complete()); + } +} diff --git a/flock-stage3/host/src/limits.rs b/flock-stage3/host/src/limits.rs new file mode 100644 index 00000000..a14cd3c2 --- /dev/null +++ b/flock-stage3/host/src/limits.rs @@ -0,0 +1,316 @@ +use anyhow::{Result, bail}; +use ix_terminal::{ + OUTER_CLAIM_BYTES, STAGE2_CLAIMS_BYTES, Stage2AdviceProfileV1, + ValidatedStage2RootV1, +}; +use multi_stark::types::FriParameters; +use serde::{Deserialize, Serialize}; + +const MIB: u64 = 1024 * 1024; + +/// Host-side admission limits applied before a Stage 3 relation is built. +/// +/// These limits are denial-of-service guards, not cryptographic parameters and +/// not a claim that every admitted proof will fit a particular machine. The +/// exact relation census printed by preflight remains the operator's sizing +/// input. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Stage3ResourceLimitsV1 { + pub max_verifying_key_bytes: u64, + pub max_compact_proof_bytes: u64, + pub max_advice_bytes: u64, + pub max_total_circuits: u64, + pub max_fri_queries: u64, + pub max_fri_rounds: u64, + pub max_profile_items: u64, + /// Capacity guard checked before building the circuit's wiring. + pub max_table_capacity: u64, + /// Combined size of the three padded union buffers, excluding PCS and + /// compiler scratch space. This is not a total process RSS limit. + pub max_union_witness_bytes: u64, +} + +impl Default for Stage3ResourceLimitsV1 { + fn default() -> Self { + Self { + max_verifying_key_bytes: 16 * MIB, + max_compact_proof_bytes: 64 * MIB, + max_advice_bytes: 256 * MIB, + max_total_circuits: 1 << 16, + max_fri_queries: 1_024, + max_fri_rounds: 32, + max_profile_items: 1 << 24, + max_table_capacity: 1 << 22, + max_union_witness_bytes: 32 * 1024 * MIB, + } + } +} + +impl Stage3ResourceLimitsV1 { + pub(crate) fn ensure_table_capacity(self, nu: usize) -> Result<()> { + let capacity = 1u64 + .checked_shl(u32::try_from(nu)?) + .ok_or_else(|| anyhow::anyhow!("Stage 3 table capacity overflows u64"))?; + if capacity > self.max_table_capacity { + bail!( + "Stage 3 table capacity {capacity} (nu={nu}) exceeds admission limit {}", + self.max_table_capacity + ); + } + Ok(()) + } + + pub(crate) fn ensure_union_witness(self, bytes: u64) -> Result<()> { + if bytes > self.max_union_witness_bytes { + bail!( + "Stage 3 padded union witness requires {bytes} bytes; admission limit is {} (PCS/compiler scratch is additional)", + self.max_union_witness_bytes + ); + } + Ok(()) + } + /// Reject impossible or oversized raw inputs before decoding/verifying them. + pub fn ensure_transport( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + ) -> Result<()> { + self.ensure_raw_transport(vk_bytes, claim_bytes, proof_bytes)?; + self.ensure_fri(fri) + } + + pub(crate) fn ensure_raw_transport( + self, + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + ) -> Result<()> { + ensure_nonempty_bounded( + vk_bytes.len(), + self.max_verifying_key_bytes, + "Stage 2 verifying key", + )?; + if claim_bytes.len() != OUTER_CLAIM_BYTES { + bail!( + "Stage 2 outer claim is {} bytes; expected {OUTER_CLAIM_BYTES}", + claim_bytes.len() + ); + } + ensure_nonempty_bounded( + proof_bytes.len(), + self.max_compact_proof_bytes, + "compact Stage 2 proof", + )?; + Ok(()) + } + + fn ensure_fri(self, fri: &FriParameters) -> Result<()> { + if fri.log_final_poly_len != 0 { + bail!("Stage 3 currently requires a constant final FRI polynomial"); + } + if fri.max_log_arity != 1 { + bail!("Stage 3 currently requires binary FRI (maximum log arity 1)"); + } + let queries = as_u64(fri.num_queries, "FRI query count")?; + if queries == 0 || queries > self.max_fri_queries { + bail!( + "Stage 3 FRI query count {queries} is outside 1..={}", + self.max_fri_queries + ); + } + for (label, bits) in [ + ("commit", fri.commit_proof_of_work_bits), + ("query", fri.query_proof_of_work_bits), + ] { + if bits >= 64 { + bail!("Stage 3 FRI {label} PoW width {bits} is outside 0..64"); + } + } + Ok(()) + } + + /// Reject expanded proof geometry that is too large to hand to the relation + /// compiler. This runs immediately after native proof expansion. + pub fn ensure_prepared(self, prepared: &ValidatedStage2RootV1) -> Result<()> { + ensure_nonempty_bounded( + prepared.verifying_key_bytes().len(), + self.max_verifying_key_bytes, + "expanded Stage 2 verifying key", + )?; + if prepared.claims_bytes().len() != STAGE2_CLAIMS_BYTES { + bail!( + "expanded Stage 2 claims are {} bytes; expected {STAGE2_CLAIMS_BYTES}", + prepared.claims_bytes().len() + ); + } + ensure_nonempty_bounded( + prepared.advice_bytes().len(), + self.max_advice_bytes, + "expanded Stage 2 advice", + )?; + + let profile = prepared.advice_profile(); + if profile.advice_bytes + != as_u64(prepared.advice_bytes().len(), "expanded advice length")? + { + bail!("Stage 2 advice profile byte length disagrees with its transport"); + } + ensure_range( + profile.total_circuits, + self.max_total_circuits, + "total circuit count", + )?; + if profile.active_circuits == 0 + || profile.active_circuits > profile.total_circuits + { + bail!( + "Stage 2 active circuit count {} is outside 1..={}", + profile.active_circuits, + profile.total_circuits + ); + } + ensure_range(profile.queries, self.max_fri_queries, "FRI query count")?; + ensure_range(profile.fri_rounds, self.max_fri_rounds, "FRI round count")?; + + for (label, value) in profile_items(profile) { + if value > self.max_profile_items { + bail!( + "Stage 2 {label} count {value} exceeds host admission limit {}", + self.max_profile_items + ); + } + } + Ok(()) + } +} + +fn profile_items(profile: &Stage2AdviceProfileV1) -> [(&'static str, u64); 7] { + [ + ("input rounds per query", profile.input_rounds_per_query), + ("commitment cap digest", profile.commitment_cap_digests), + ("input Merkle sibling", profile.input_merkle_siblings), + ("FRI Merkle sibling", profile.fri_merkle_siblings), + ("opened base value", profile.opened_base_values), + ("FRI sibling extension value", profile.fri_sibling_extension_values), + ("other extension value", profile.other_extension_values), + ] +} + +fn ensure_nonempty_bounded( + observed: usize, + maximum: u64, + label: &str, +) -> Result<()> { + let observed = as_u64(observed, label)?; + if observed == 0 || observed > maximum { + bail!("{label} length {observed} is outside 1..={maximum} bytes"); + } + Ok(()) +} + +fn ensure_range(observed: u64, maximum: u64, label: &str) -> Result<()> { + if observed == 0 || observed > maximum { + bail!("Stage 2 {label} {observed} is outside 1..={maximum}"); + } + Ok(()) +} + +fn as_u64(value: usize, label: &str) -> Result { + u64::try_from(value) + .map_err(|error| anyhow::anyhow!("{label} exceeds u64: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn production_fri() -> FriParameters { + FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 100, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 20, + } + } + + #[test] + fn production_parameters_pass_early_admission() { + Stage3ResourceLimitsV1::default() + .ensure_transport( + b"canonical-looking vk transport", + &[0; OUTER_CLAIM_BYTES], + b"canonical-looking compact proof transport", + &production_fri(), + ) + .unwrap(); + } + + #[test] + fn transport_admission_rejects_unsupported_or_oversized_work() { + let limits = Stage3ResourceLimitsV1 { + max_verifying_key_bytes: 2, + max_compact_proof_bytes: 3, + ..Stage3ResourceLimitsV1::default() + }; + let claim = [0; OUTER_CLAIM_BYTES]; + assert!( + limits + .ensure_transport(b"too long", &claim, b"ok", &production_fri()) + .is_err() + ); + assert!( + limits + .ensure_transport(b"ok", &claim, b"long", &production_fri()) + .is_err() + ); + + let mut non_binary = production_fri(); + non_binary.max_log_arity = 2; + assert!( + limits.ensure_transport(b"ok", &claim, b"ok", &non_binary).is_err() + ); + + let mut non_constant = production_fri(); + non_constant.log_final_poly_len = 1; + assert!( + limits.ensure_transport(b"ok", &claim, b"ok", &non_constant).is_err() + ); + + let mut too_many_queries = production_fri(); + too_many_queries.num_queries = 1_025; + assert!( + limits.ensure_transport(b"ok", &claim, b"ok", &too_many_queries).is_err() + ); + + let mut invalid_pow = production_fri(); + invalid_pow.query_proof_of_work_bits = 64; + assert!( + limits.ensure_transport(b"ok", &claim, b"ok", &invalid_pow).is_err() + ); + } + + #[test] + fn resource_limits_are_strict_configurable_and_checked_at_boundaries() { + let defaults = Stage3ResourceLimitsV1::default(); + assert_eq!( + serde_json::from_str::("{}").unwrap(), + defaults + ); + let limits: Stage3ResourceLimitsV1 = serde_json::from_str( + r#"{"max_table_capacity":1024,"max_union_witness_bytes":2048}"#, + ) + .unwrap(); + assert!(limits.ensure_table_capacity(10).is_ok()); + assert!(limits.ensure_table_capacity(11).is_err()); + assert!(limits.ensure_table_capacity(64).is_err()); + assert!(limits.ensure_union_witness(2048).is_ok()); + assert!(limits.ensure_union_witness(2049).is_err()); + for json in [r#"{"max_witness_bytes":1}"#, r#"{"max_advice_bytes":-1}"#] { + assert!(serde_json::from_str::(json).is_err()); + } + } +} diff --git a/flock-stage3/host/src/merkle.rs b/flock-stage3/host/src/merkle.rs new file mode 100644 index 00000000..5c0186c1 --- /dev/null +++ b/flock-stage3/host/src/merkle.rs @@ -0,0 +1,623 @@ +//! Circuit-bound BLAKE3 Merkle authentication paths. +//! +//! Plonky3's `CompressionFunctionFromHasher` hashes the +//! concatenation of two 32-byte digests. One constrained direction bit orders +//! the current and sibling digests at every level, then the existing Flock +//! BLAKE3 compression table computes the parent. + +use ::blake3 as native_blake3; +use anyhow::{Context, Result, bail}; +use bincode::Options; +use flock_prover::{ + challenger::FsChallenger, + circuit::builder::{ + CircuitShape, GateType, ShapeBuilder, SlotId, SlotWitness, + }, + field::F128, + pcs::Commitment, + proof::R1csProofCircuitMerged, + prover::{self, UnionSlotProverInput}, + r1cs::BlockR1cs, + r1cs_hashes::blake3 as flock_blake3, + schedule::{IoWord, TableType}, + union::{SlotWitnessDest, UnionInstance}, + verifier, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + FlockConfigV1, MERKLE_CONFORMANCE_TRANSCRIPT_DOMAIN, + binding::{ + Blake3Gate, CHUNK_END, CHUNK_START, IV, ROOT, pack_bytes, pack_params, + pack8, pcs_params, + }, + boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_witness, + generate_boolean_witness_into, write_f128, + }, +}; + +pub const MERKLE_CONFORMANCE_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLKMP1"; +const ARTIFACT_VERSION: u16 = 1; +const CONFIG_OFFSET: usize = 10; +const DEPTH_OFFSET: usize = CONFIG_OFFSET + 32; +const INDEX_OFFSET: usize = DEPTH_OFFSET + 1; +const LEAF_OFFSET: usize = INDEX_OFFSET + 4; +const PATH_OFFSET: usize = LEAF_OFFSET + 32; +const FIXED_SUFFIX_BYTES: usize = 32 + 32 + 8; +const MAX_DEPTH: usize = 32; +const MAX_BUNDLE_BYTES: usize = 64 * 1024 * 1024; + +const NU: usize = 8; +const ORDER_K_LOG: usize = 11; +const BIT_BASE: usize = 0; +const CURRENT_BASE: usize = 128; +const SIBLING_BASE: usize = 384; +const LEFT_BASE: usize = 640; +const RIGHT_BASE: usize = 896; +const ORDER_RESERVED_COLUMNS: usize = 1152; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MerklePathV1 { + pub leaf: [u8; 32], + pub siblings: Vec<[u8; 32]>, + /// Leaf index; level zero consumes its least-significant bit. + pub index: u32, +} + +impl MerklePathV1 { + pub fn root(&self) -> Result<[u8; 32]> { + validate_path(self)?; + Ok(native_root(self)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MerkleConformanceArtifactV1 { + path: MerklePathV1, + circuit_digest: [u8; 32], + root: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl MerkleConformanceArtifactV1 { + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity( + PATH_OFFSET + + 32 * self.path.siblings.len() + + FIXED_SUFFIX_BYTES + + self.proof_bundle_bytes.len(), + ); + bytes.extend_from_slice(MERKLE_CONFORMANCE_ARTIFACT_MAGIC); + bytes.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.push(u8::try_from(self.path.siblings.len()).expect("Merkle depth")); + bytes.extend_from_slice(&self.path.index.to_le_bytes()); + bytes.extend_from_slice(&self.path.leaf); + for sibling in &self.path.siblings { + bytes.extend_from_slice(sibling); + } + bytes.extend_from_slice(&self.circuit_digest); + bytes.extend_from_slice(&self.root); + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < PATH_OFFSET + FIXED_SUFFIX_BYTES { + bail!("truncated Flock Merkle conformance artifact"); + } + if &bytes[..8] != MERKLE_CONFORMANCE_ARTIFACT_MAGIC { + bail!("invalid Flock Merkle conformance artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != ARTIFACT_VERSION { + bail!("unsupported Flock Merkle artifact version {version}"); + } + if bytes[CONFIG_OFFSET..DEPTH_OFFSET] != FlockConfigV1.digest() { + bail!("Flock Merkle artifact configuration mismatch"); + } + let depth = usize::from(bytes[DEPTH_OFFSET]); + validate_depth(depth)?; + let index = + u32::from_le_bytes(bytes[INDEX_OFFSET..LEAF_OFFSET].try_into().unwrap()); + let path_end = PATH_OFFSET + .checked_add(depth * 32) + .ok_or_else(|| anyhow::anyhow!("Merkle path length overflow"))?; + let suffix_end = path_end + .checked_add(FIXED_SUFFIX_BYTES) + .ok_or_else(|| anyhow::anyhow!("Merkle artifact length overflow"))?; + if bytes.len() < suffix_end { + bail!("truncated Flock Merkle path or proof header"); + } + let mut leaf = [0u8; 32]; + leaf.copy_from_slice(&bytes[LEAF_OFFSET..PATH_OFFSET]); + let siblings = bytes[PATH_OFFSET..path_end].as_chunks::<32>().0.to_vec(); + let path = MerklePathV1 { leaf, siblings, index }; + validate_path(&path)?; + let mut circuit_digest = [0u8; 32]; + circuit_digest.copy_from_slice(&bytes[path_end..path_end + 32]); + let mut root = [0u8; 32]; + root.copy_from_slice(&bytes[path_end + 32..path_end + 64]); + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[path_end + 64..suffix_end].try_into().unwrap(), + )) + .map_err(|error| { + anyhow::anyhow!("Merkle proof bundle length does not fit usize: {error}") + })?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock Merkle proof bundle length {bundle_len}"); + } + let expected_len = suffix_end + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("Merkle proof length overflow"))?; + if bytes.len() != expected_len { + bail!( + "Flock Merkle artifact is {} bytes; header declares {expected_len}", + bytes.len() + ); + } + let proof_bundle_bytes = bytes[suffix_end..].to_vec(); + decode_bundle(&proof_bundle_bytes) + .context("decode Flock Merkle conformance proof bundle")?; + Ok(Self { path, circuit_digest, root, proof_bundle_bytes }) + } + + pub fn path(&self) -> &MerklePathV1 { + &self.path + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn root(&self) -> &[u8; 32] { + &self.root + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } +} + +#[derive(Serialize, Deserialize)] +struct MerkleProofBundle { + commitment: Commitment, + proof: R1csProofCircuitMerged, +} + +pub fn prove_merkle_conformance( + path: &MerklePathV1, +) -> Result { + validate_path(path)?; + let relation = MerkleRelation::build(path.siblings.len())?; + relation.ensure_registry_order()?; + let witness = relation.shape.run(&relation_inputs(path), &[]); + let root = native_root(path); + if witness.public != relation_public(path, &root) { + bail!("Flock Merkle circuit output disagrees with native BLAKE3 root"); + } + let blake3_rows = witness.rows::(relation.blake3_slot); + let order_rows = witness.rows::(relation.order_slot); + let blake3_r1cs = flock_blake3::build_block_r1cs(NU); + let blake3_lincheck = blake3_r1cs.csc_lincheck_circuit(); + let order_r1cs = build_digest_order_r1cs(NU); + let order_lincheck = order_r1cs.csc_lincheck_circuit(); + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = + FsChallenger::with_chained_blake3(MERKLE_CONFORMANCE_TRANSCRIPT_DOMAIN); + let (proof, commitment, _) = prover::prove_fast_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &witness.public, + ¶ms, + vec![ + UnionSlotProverInput::new( + flock_blake3::generate_witness_batch_major_partial(blake3_rows, NU), + blake3_lincheck, + ), + UnionSlotProverInput::new( + generate_digest_order_witness(order_rows, NU), + order_lincheck, + ), + ], + Vec::new(), + &mut challenger, + ); + let proof_bundle_bytes = + encode_bundle(&MerkleProofBundle { commitment, proof })?; + if proof_bundle_bytes.len() > MAX_BUNDLE_BYTES { + bail!("Flock Merkle proof bundle exceeds {MAX_BUNDLE_BYTES} bytes"); + } + Ok(MerkleConformanceArtifactV1 { + path: path.clone(), + circuit_digest: relation.shape.circuit.digest(), + root, + proof_bundle_bytes, + }) +} + +pub fn verify_merkle_conformance( + artifact: &MerkleConformanceArtifactV1, +) -> Result<()> { + validate_path(&artifact.path)?; + let relation = MerkleRelation::build(artifact.path.siblings.len())?; + relation.ensure_registry_order()?; + if artifact.circuit_digest != relation.shape.circuit.digest() { + bail!("Flock Merkle conformance circuit digest mismatch"); + } + let bundle = decode_bundle(&artifact.proof_bundle_bytes) + .context("decode Flock Merkle conformance proof bundle")?; + let public = relation_public(&artifact.path, &artifact.root); + let blake3_r1cs = flock_blake3::build_block_r1cs(NU); + let blake3_lincheck = blake3_r1cs.csc_lincheck_circuit(); + let order_r1cs = build_digest_order_r1cs(NU); + let order_lincheck = order_r1cs.csc_lincheck_circuit(); + let linchecks: [&dyn flock_prover::lincheck::LincheckCircuit; 2] = + [blake3_lincheck, order_lincheck]; + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = + FsChallenger::with_chained_blake3(MERKLE_CONFORMANCE_TRANSCRIPT_DOMAIN); + verifier::verify_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &public, + &linchecks, + &bundle.commitment, + &bundle.proof, + ¶ms, + &mut challenger, + ) + .map_err(|error| { + anyhow::anyhow!("Flock Merkle conformance proof rejected: {error:?}") + })?; + Ok(()) +} + +struct MerkleRelation { + shape: CircuitShape, + blake3_slot: SlotId, + order_slot: SlotId, +} + +impl MerkleRelation { + fn build(depth: usize) -> Result { + validate_depth(depth)?; + let mut builder = ShapeBuilder::new(NU); + let blake3_slot = builder.slot(Blake3Gate { nu: NU }); + let order_slot = builder.slot(DigestOrderGate { nu: NU }); + let packed_iv = pack8(&IV); + let iv = [ + builder.fixed_public_input(packed_iv[0]), + builder.fixed_public_input(packed_iv[1]), + ]; + let params = builder.fixed_public_input(pack_params( + 0, + 64, + CHUNK_START | CHUNK_END | ROOT, + )); + let mut current = [builder.public_input(), builder.public_input()]; + for _ in 0..depth { + let direction = builder.public_input(); + let sibling = [builder.public_input(), builder.public_input()]; + let ordered = builder.gate( + order_slot, + &[direction, current[0], current[1], sibling[0], sibling[1]], + ); + let parent = builder.gate( + blake3_slot, + &[iv[0], iv[1], ordered[0], ordered[1], ordered[2], ordered[3], params], + ); + current = [parent[0], parent[1]]; + } + builder.publish(current[0]); + builder.publish(current[1]); + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock Merkle conformance circuit: {error:?}") + })?; + Ok(Self { shape, blake3_slot, order_slot }) + } + + fn ensure_registry_order(&self) -> Result<()> { + if self.shape.registry_slot(self.blake3_slot) != 0 + || self.shape.registry_slot(self.order_slot) != 1 + { + bail!("unexpected Flock Merkle table registry order"); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct DigestOrderRow { + direction: bool, + current: [F128; 2], + sibling: [F128; 2], +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct DigestOrderGate { + pub(crate) nu: usize, +} + +impl GateType for DigestOrderGate { + type Row = DigestOrderRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_digest_order_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::input(2), + IoWord::input(3), + IoWord::input(4), + IoWord::output(5), + IoWord::output(6), + IoWord::output(7), + IoWord::output(8), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let direction_word = inputs[0]; + assert_eq!(direction_word.hi, 0); + assert!(direction_word.lo <= 1); + let direction = direction_word.lo == 1; + let current = [inputs[1], inputs[2]]; + let sibling = [inputs[3], inputs[4]]; + if direction { + outputs + .extend_from_slice(&[sibling[0], sibling[1], current[0], current[1]]); + } else { + outputs + .extend_from_slice(&[current[0], current[1], sibling[0], sibling[1]]); + } + DigestOrderRow { direction, current, sibling } + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_digest_order_r1cs(nu: usize) -> BlockR1cs { + build_digest_order_plan().block_r1cs(nu) +} + +pub(crate) fn generate_digest_order_witness( + rows: &[DigestOrderRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + let plan = build_digest_order_plan(); + generate_boolean_witness(&plan, rows, nu, |row, bits| { + bits[BIT_BASE] = row.direction; + write_f128(bits, CURRENT_BASE, row.current[0]); + write_f128(bits, CURRENT_BASE + 128, row.current[1]); + write_f128(bits, SIBLING_BASE, row.sibling[0]); + write_f128(bits, SIBLING_BASE + 128, row.sibling[1]); + }) +} + +pub(crate) fn generate_digest_order_witness_into( + rows: &[DigestOrderRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + let plan = build_digest_order_plan(); + generate_boolean_witness_into(&plan, rows, nu, dst, |row, bits| { + bits[BIT_BASE] = row.direction; + write_f128(bits, CURRENT_BASE, row.current[0]); + write_f128(bits, CURRENT_BASE + 128, row.current[1]); + write_f128(bits, SIBLING_BASE, row.sibling[0]); + write_f128(bits, SIBLING_BASE + 128, row.sibling[1]); + }) +} + +fn build_digest_order_plan() -> BooleanR1csPlan { + let mut builder = + BooleanR1csBuilder::new(ORDER_K_LOG, ORDER_RESERVED_COLUMNS); + builder.free_boolean_at(BIT_BASE); + for column in CURRENT_BASE..SIBLING_BASE + 256 { + builder.free_boolean_at(column); + } + let one = builder.alloc_constant_one(); + for bit in 0..256 { + let current = CURRENT_BASE + bit; + let sibling = SIBLING_BASE + bit; + let selected = + builder.product_of_parities(&[BIT_BASE], &[current, sibling]); + builder.write_xor(LEFT_BASE + bit, &[current, selected], one); + builder.write_xor( + RIGHT_BASE + bit, + &[current, sibling, LEFT_BASE + bit], + one, + ); + } + builder.finish() +} + +fn relation_inputs(path: &MerklePathV1) -> Vec { + let packed_iv = pack8(&IV); + let mut inputs = Vec::with_capacity(5 + 3 * path.siblings.len()); + inputs.extend_from_slice(&packed_iv); + inputs.push(pack_params(0, 64, CHUNK_START | CHUNK_END | ROOT)); + inputs.extend_from_slice(&pack_digest(&path.leaf)); + for (level, sibling) in path.siblings.iter().enumerate() { + inputs.push(F128::new(u64::from((path.index >> level) & 1), 0)); + inputs.extend_from_slice(&pack_digest(sibling)); + } + inputs +} + +fn relation_public(path: &MerklePathV1, root: &[u8; 32]) -> Vec { + let mut public = relation_inputs(path); + public.extend_from_slice(&pack_digest(root)); + public +} + +fn pack_digest(digest: &[u8; 32]) -> [F128; 2] { + [pack_bytes(&digest[..16]), pack_bytes(&digest[16..])] +} + +fn native_root(path: &MerklePathV1) -> [u8; 32] { + let mut current = path.leaf; + for (level, sibling) in path.siblings.iter().enumerate() { + let mut input = [0u8; 64]; + let (left, right) = if (path.index >> level) & 1 == 0 { + (¤t, sibling) + } else { + (sibling, ¤t) + }; + input[..32].copy_from_slice(left); + input[32..].copy_from_slice(right); + current = *native_blake3::hash(&input).as_bytes(); + } + current +} + +fn validate_path(path: &MerklePathV1) -> Result<()> { + validate_depth(path.siblings.len())?; + if u64::from(path.index) >= 1u64 << path.siblings.len() { + bail!( + "Merkle index {} does not fit depth {}", + path.index, + path.siblings.len() + ); + } + Ok(()) +} + +fn validate_depth(depth: usize) -> Result<()> { + if !(1..=MAX_DEPTH).contains(&depth) { + bail!("Merkle depth {depth}; expected 1..={MAX_DEPTH}"); + } + Ok(()) +} + +fn encode_bundle(bundle: &MerkleProofBundle) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .serialize(bundle) + .context("encode Flock Merkle conformance proof bundle") +} + +fn decode_bundle(bytes: &[u8]) -> Result { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(MAX_BUNDLE_BYTES as u64) + .reject_trailing_bytes() + .deserialize(bytes) + .context("invalid Flock Merkle conformance proof bundle") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture() -> MerklePathV1 { + MerklePathV1 { + leaf: *native_blake3::hash(b"ix-stage3-merkle-leaf").as_bytes(), + siblings: (0..4u8) + .map(|level| *native_blake3::hash(&[0xa5, level]).as_bytes()) + .collect(), + index: 0b1010, + } + } + + #[test] + fn native_path_matches_manual_blake3_compression() { + let path = fixture(); + let root = path.root().unwrap(); + assert_ne!(root, path.leaf); + let mut changed = path; + changed.index ^= 1; + assert_ne!(changed.root().unwrap(), root); + } + + #[test] + fn digest_order_r1cs_rejects_direction_and_output_mutations() { + let plan = build_digest_order_plan(); + let r1cs = plan.block_r1cs(3); + let row = DigestOrderRow { + direction: true, + current: [F128::new(1, 2), F128::new(3, 4)], + sibling: [F128::new(5, 6), F128::new(7, 8)], + }; + let mut logical = vec![false; plan.k()]; + plan.fill_row(&mut logical, |bits| { + bits[BIT_BASE] = row.direction; + write_f128(bits, CURRENT_BASE, row.current[0]); + write_f128(bits, CURRENT_BASE + 128, row.current[1]); + write_f128(bits, SIBLING_BASE, row.sibling[0]); + write_f128(bits, SIBLING_BASE + 128, row.sibling[1]); + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.k()].copy_from_slice(&logical); + assert!(r1cs.satisfies(&witness)); + + let mut wrong_direction = witness.clone(); + wrong_direction[BIT_BASE] ^= true; + assert!(!r1cs.satisfies(&wrong_direction)); + let mut wrong_output = witness; + wrong_output[LEFT_BASE + 17] ^= true; + assert!(!r1cs.satisfies(&wrong_output)); + } + + #[test] + fn artifact_parser_is_strict_before_crypto() { + let path = fixture(); + let artifact = MerkleConformanceArtifactV1 { + root: path.root().unwrap(), + path, + circuit_digest: [7; 32], + proof_bundle_bytes: vec![1, 2, 3], + }; + let mut bytes = artifact.to_bytes(); + assert!(MerkleConformanceArtifactV1::from_bytes(&bytes).is_err()); + bytes[0] ^= 1; + assert!(MerkleConformanceArtifactV1::from_bytes(&bytes).is_err()); + } + + #[test] + #[ignore = "real Flock BLAKE3 Merkle circuit proof; run explicitly"] + fn real_merkle_path_round_trip_and_mutations() { + let artifact = prove_merkle_conformance(&fixture()).expect("prove path"); + eprintln!( + "Flock BLAKE3-Merkle conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_merkle_conformance(&artifact).expect("verify path"); + let decoded = + MerkleConformanceArtifactV1::from_bytes(&artifact.to_bytes()).unwrap(); + verify_merkle_conformance(&decoded).expect("verify decoded path"); + + let mut wrong_sibling = decoded.clone(); + wrong_sibling.path.siblings[1][7] ^= 1; + assert!(verify_merkle_conformance(&wrong_sibling).is_err()); + let mut wrong_index = decoded.clone(); + wrong_index.path.index ^= 1; + assert!(verify_merkle_conformance(&wrong_index).is_err()); + let mut wrong_root = decoded.clone(); + wrong_root.root[0] ^= 1; + assert!(verify_merkle_conformance(&wrong_root).is_err()); + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_merkle_conformance(&wrong_proof).is_err()); + } +} diff --git a/flock-stage3/host/src/multiplication.rs b/flock-stage3/host/src/multiplication.rs new file mode 100644 index 00000000..9f8f42e0 --- /dev/null +++ b/flock-stage3/host/src/multiplication.rs @@ -0,0 +1,441 @@ +//! Boolean R1CS relation for multiplication in the Goldilocks base field. +//! +//! For canonical `a`, `b`, and `c`, the gate proves that a private 64-bit +//! quotient `q` satisfies the exact non-negative integer identity +//! +//! ```text +//! a * b + (q << 32) = c + q + (q << 64). +//! ``` +//! +//! This is `a*b = c + q*(2^64 - 2^32 + 1)`, so canonical `c` is exactly +//! `a*b mod p`. The multiplication bits are reduced with a carry-save tree +//! before one ripple pass; this is substantially smaller than adding 64 +//! shifted partial-product rows sequentially. + +use std::sync::OnceLock; + +use flock_prover::{ + circuit::builder::{GateType, SlotWitness}, + field::F128, + r1cs::BlockR1cs, + schedule::{IoWord, TableType}, + union::SlotWitnessDest, +}; + +use crate::{ + boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_witness, + generate_boolean_witness_into, write_f128, + }, + goldilocks::GOLDILOCKS_MODULUS, +}; + +const MUL_K_LOG: usize = 16; +const LEFT_BASE: usize = 0; +const RIGHT_BASE: usize = 128; +const RESULT_BASE: usize = 256; +const LOW_RESIDUAL_BASE: usize = 384; +const HIGH_RESIDUAL_BASE: usize = 512; +const TOP_RESIDUAL_BASE: usize = 640; +const RESERVED_COLUMNS: usize = 768; + +// Both sides of the quotient identity are below 2^129 for all 64-bit +// inputs. Comparing 130 sum bits is therefore an exact integer comparison, +// not merely equality modulo a power of two. +const INTEGER_SUM_BITS: usize = 130; + +/// One row of two independent Goldilocks multiplications packed by u64 lane. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct GoldilocksMulPairRow { + left: F128, + right: F128, +} + +/// Two lane-wise Goldilocks multiplications with explicit equation outputs. +/// +/// The result is the first output. Callers must connect all residual words +/// to zero and route the two inputs and result through the shared canonical +/// Goldilocks table. +#[derive(Clone, Copy, Debug)] +pub(crate) struct GoldilocksMulPairGate { + pub(crate) nu: usize, +} + +impl GateType for GoldilocksMulPairGate { + type Row = GoldilocksMulPairRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_goldilocks_mul_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::output(2), + IoWord::output(3), + IoWord::output(4), + IoWord::output(5), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let left = inputs[0]; + let right = inputs[1]; + outputs.extend_from_slice(&[ + F128::new( + goldilocks_mul(left.lo, right.lo), + goldilocks_mul(left.hi, right.hi), + ), + F128::ZERO, + F128::ZERO, + F128::ZERO, + ]); + GoldilocksMulPairRow { left, right } + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +struct GoldilocksMulPlan { + boolean: BooleanR1csPlan, + quotient_bits: [[usize; 64]; 2], +} + +pub(crate) fn build_goldilocks_mul_r1cs(nu: usize) -> BlockR1cs { + goldilocks_mul_plan().boolean.block_r1cs(nu) +} + +pub(crate) fn generate_goldilocks_mul_witness( + rows: &[GoldilocksMulPairRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + let plan = goldilocks_mul_plan(); + generate_boolean_witness(&plan.boolean, rows, nu, |row, bits| { + fill_goldilocks_mul_row(plan, *row, bits) + }) +} + +pub(crate) fn generate_goldilocks_mul_witness_into( + rows: &[GoldilocksMulPairRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + let plan = goldilocks_mul_plan(); + generate_boolean_witness_into(&plan.boolean, rows, nu, dst, |row, bits| { + fill_goldilocks_mul_row(plan, *row, bits) + }) +} + +fn goldilocks_mul_plan() -> &'static GoldilocksMulPlan { + static PLAN: OnceLock = OnceLock::new(); + PLAN.get_or_init(build_goldilocks_mul_plan) +} + +fn build_goldilocks_mul_plan() -> GoldilocksMulPlan { + let mut builder = BooleanR1csBuilder::new(MUL_K_LOG, RESERVED_COLUMNS); + for column in LEFT_BASE..RESULT_BASE + 128 { + builder.free_boolean_at(column); + } + let one = builder.alloc_constant_one(); + let quotient_bits = std::array::from_fn(|_| { + std::array::from_fn(|_| builder.alloc_free_boolean()) + }); + + for (lane, quotient) in quotient_bits.iter().enumerate() { + let lane_offset = lane * 64; + let left: [usize; 64] = + std::array::from_fn(|bit| LEFT_BASE + lane_offset + bit); + let right: [usize; 64] = + std::array::from_fn(|bit| RIGHT_BASE + lane_offset + bit); + let result: [usize; 64] = + std::array::from_fn(|bit| RESULT_BASE + lane_offset + bit); + + let mut left_columns = vec![Vec::new(); INTEGER_SUM_BITS + 1]; + for (left_bit, &left_column) in left.iter().enumerate() { + for (right_bit, &right_column) in right.iter().enumerate() { + let product = builder.and(left_column, right_column); + left_columns[left_bit + right_bit].push(product); + } + } + for (bit, "ient_bit) in quotient.iter().enumerate() { + left_columns[bit + 32].push(quotient_bit); + } + + let mut right_columns = vec![Vec::new(); INTEGER_SUM_BITS + 1]; + for bit in 0..64 { + right_columns[bit].push(result[bit]); + right_columns[bit].push(quotient[bit]); + right_columns[bit + 64].push(quotient[bit]); + } + + let left_sum = + sum_bit_columns(&mut builder, left_columns, one, INTEGER_SUM_BITS); + let right_sum = + sum_bit_columns(&mut builder, right_columns, one, INTEGER_SUM_BITS); + for bit in 0..INTEGER_SUM_BITS { + let residual = if bit < 128 { + if bit < 64 { + LOW_RESIDUAL_BASE + lane_offset + bit + } else { + HIGH_RESIDUAL_BASE + lane_offset + bit - 64 + } + } else { + TOP_RESIDUAL_BASE + lane_offset + bit - 128 + }; + let terms: Vec<_> = + [left_sum[bit], right_sum[bit]].into_iter().flatten().collect(); + if !terms.is_empty() { + builder.write_xor(residual, &terms, one); + } + } + } + + GoldilocksMulPlan { boolean: builder.finish(), quotient_bits } +} + +/// Convert a set of same-weight Boolean terms into canonical binary bits. +fn sum_bit_columns( + builder: &mut BooleanR1csBuilder, + mut columns: Vec>, + one: usize, + output_bits: usize, +) -> Vec> { + assert!(columns.len() > output_bits); + + // Carry-save reduction leaves at most two bits in each weight column. + for bit in 0..output_bits { + while columns[bit].len() > 2 { + let third = columns[bit].pop().unwrap(); + let second = columns[bit].pop().unwrap(); + let first = columns[bit].pop().unwrap(); + let (sum, carry) = full_adder(builder, first, second, third, one); + columns[bit].push(sum); + columns[bit + 1].push(carry); + } + } + + // Add the final two carry-save rows with one ripple pass. + let mut result = Vec::with_capacity(output_bits); + let mut carry = None; + for column in columns.iter().take(output_bits) { + let mut terms = column.clone(); + if let Some(carry_bit) = carry.take() { + terms.push(carry_bit); + } + match terms.as_slice() { + [] => result.push(None), + &[only] => result.push(Some(only)), + &[first, second] => { + let (sum, next_carry) = half_adder(builder, first, second, one); + result.push(Some(sum)); + carry = Some(next_carry); + }, + &[first, second, third] => { + let (sum, next_carry) = full_adder(builder, first, second, third, one); + result.push(Some(sum)); + carry = Some(next_carry); + }, + _ => unreachable!("carry-save column contains more than two bits"), + } + } + // The represented integers are strictly below 2^129. Any structurally + // allocated carry at weight 2^130 is therefore the constant-zero Boolean + // function; all of its source operations remain constrained in the table. + result +} + +fn half_adder( + builder: &mut BooleanR1csBuilder, + first: usize, + second: usize, + one: usize, +) -> (usize, usize) { + (builder.xor(&[first, second], one), builder.and(first, second)) +} + +fn full_adder( + builder: &mut BooleanR1csBuilder, + first: usize, + second: usize, + third: usize, + one: usize, +) -> (usize, usize) { + let sum = builder.xor(&[first, second, third], one); + let first_and_second = builder.and(first, second); + let third_and_difference = + builder.product_of_parities(&[third], &[first, second]); + let carry = builder.xor(&[first_and_second, third_and_difference], one); + (sum, carry) +} + +fn fill_goldilocks_mul_row( + plan: &GoldilocksMulPlan, + row: GoldilocksMulPairRow, + bits: &mut [bool], +) { + let result = F128::new( + goldilocks_mul(row.left.lo, row.right.lo), + goldilocks_mul(row.left.hi, row.right.hi), + ); + write_f128(bits, LEFT_BASE, row.left); + write_f128(bits, RIGHT_BASE, row.right); + write_f128(bits, RESULT_BASE, result); + for (lane, quotient_columns) in plan.quotient_bits.iter().enumerate() { + let (left, right) = if lane == 0 { + (row.left.lo, row.right.lo) + } else { + (row.left.hi, row.right.hi) + }; + let quotient = (left as u128 * right as u128) / GOLDILOCKS_MODULUS as u128; + for (bit, &column) in quotient_columns.iter().enumerate() { + bits[column] = (quotient >> bit) & 1 == 1; + } + } +} + +pub(crate) fn goldilocks_mul(left: u64, right: u64) -> u64 { + ((left as u128 * right as u128) % GOLDILOCKS_MODULUS as u128) as u64 +} + +#[cfg(test)] +mod tests { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + use flock_prover::circuit::builder::ShapeBuilder; + use multi_stark::{ + p3_field::{PrimeCharacteristicRing, PrimeField64}, + p3_goldilocks::Goldilocks, + }; + + use super::*; + + #[test] + fn modular_mul_matches_reference_goldilocks() { + let boundary = [ + 0, + 1, + 2, + (1u64 << 32) - 1, + 1u64 << 32, + GOLDILOCKS_MODULUS - 2, + GOLDILOCKS_MODULUS - 1, + ]; + for &left in &boundary { + for &right in &boundary { + let expected = (Goldilocks::from_u64(left) + * Goldilocks::from_u64(right)) + .as_canonical_u64(); + assert_eq!(goldilocks_mul(left, right), expected); + } + } + + let mut state = 0xbb67_ae85_84ca_a73bu64; + for _ in 0..256 { + state = state + .wrapping_mul(0x9e37_79b9_7f4a_7c15) + .wrapping_add(0x94d0_49bb_1331_11eb); + let left = state % GOLDILOCKS_MODULUS; + state ^= state.rotate_left(23); + let right = state % GOLDILOCKS_MODULUS; + let expected = (Goldilocks::from_u64(left) * Goldilocks::from_u64(right)) + .as_canonical_u64(); + assert_eq!(goldilocks_mul(left, right), expected); + } + } + + #[test] + fn modular_mul_r1cs_rejects_wrong_result_and_quotient() { + let plan = build_goldilocks_mul_plan(); + eprintln!( + "Goldilocks multiplication table uses {} Boolean columns", + plan.boolean.useful_bits() + ); + let r1cs = plan.boolean.block_r1cs(3); + let cases = [ + GoldilocksMulPairRow { + left: F128::new(0, GOLDILOCKS_MODULUS - 1), + right: F128::new(GOLDILOCKS_MODULUS - 1, GOLDILOCKS_MODULUS - 1), + }, + GoldilocksMulPairRow { + left: F128::new(1 << 32, 0x1234_5678_9abc_def0), + right: F128::new(GOLDILOCKS_MODULUS - 2, 0xfedc_ba98_7654_3210), + }, + ]; + for row in cases { + let mut logical = vec![false; plan.boolean.k()]; + plan.boolean.fill_row(&mut logical, |bits| { + fill_goldilocks_mul_row(&plan, row, bits) + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.boolean.k()].copy_from_slice(&logical); + assert!(r1cs.satisfies(&witness)); + + let mut wrong_result = witness.clone(); + wrong_result[RESULT_BASE + 17] ^= true; + assert!(!r1cs.satisfies(&wrong_result)); + + let mut wrong_quotient = witness; + wrong_quotient[plan.quotient_bits[0][31]] ^= true; + assert!(!r1cs.satisfies(&wrong_quotient)); + } + } + + #[test] + fn multiplication_gate_pins_equation_residuals() { + let nu = 3; + let mut builder = ShapeBuilder::new(nu); + let slot = builder.slot(GoldilocksMulPairGate { nu }); + let left = builder.input(); + let right = builder.input(); + let zero = builder.fixed_public_input(F128::ZERO); + let outputs = builder.gate(slot, &[left, right]); + builder.connect(outputs[1], zero); + builder.connect(outputs[2], zero); + builder.connect(outputs[3], zero); + let shape = builder.finish().unwrap(); + shape.run( + &[ + F128::new(3, GOLDILOCKS_MODULUS - 1), + F128::new(7, GOLDILOCKS_MODULUS - 1), + F128::ZERO, + ], + &[], + ); + + let invalid = catch_unwind(AssertUnwindSafe(|| { + shape.run( + &[F128::new(GOLDILOCKS_MODULUS, 1), F128::new(1, 1), F128::ZERO], + &[], + ) + })); + // The multiplication identity itself accepts any u64 representation; + // canonicality is deliberately a shared, separately wired gate. + assert!(invalid.is_ok()); + } + + #[test] + fn modular_mul_batch_witness_zeroes_dummy_rows() { + let rows = + [GoldilocksMulPairRow { left: F128::new(3, 5), right: F128::new(7, 11) }]; + let plan = build_goldilocks_mul_plan(); + let (z, a, b, stripe) = generate_goldilocks_mul_witness(&rows, 3); + let chunks = plan.boolean.k() / 128; + assert_eq!(z.len(), chunks * 8); + assert_eq!(a.len(), z.len()); + assert_eq!(b.len(), z.len()); + assert_eq!(stripe.len(), plan.boolean.k()); + for chunk in 0..chunks { + for outer in rows.len()..8 { + assert_eq!(z[(chunk << 3) + outer], F128::ZERO); + assert_eq!(a[(chunk << 3) + outer], F128::ZERO); + assert_eq!(b[(chunk << 3) + outer], F128::ZERO); + } + } + } +} diff --git a/flock-stage3/host/src/prepared.rs b/flock-stage3/host/src/prepared.rs new file mode 100644 index 00000000..e0d30f97 --- /dev/null +++ b/flock-stage3/host/src/prepared.rs @@ -0,0 +1,178 @@ +//! Explicit ownership of one admitted, compiled and evaluated aggregate root. + +use anyhow::{Result, bail}; +use multi_stark::types::FriParameters; +use serde::Serialize; +use std::time::Instant; + +use crate::{ + FlockStage3Backend, Stage2AirPcsFriWitnessV1, Stage3ArtifactV1, + Stage3PreflightReportV1, Stage3PreflightTimingsV1, Stage3RelationManifestV1, + Stage3ResourceLimitsV1, Stage3StatementV1, Stage3TypedProofWitnessV1, + artifact::Stage3ProductionPayloadV1, + fri::CompiledStage3Relation, + report::{elapsed_us, process_peak_rss_bytes}, +}; + +/// Reusable within an operation; dropping it releases its relation and root +/// transport. Only invariant R1CS/lincheck tables are shared across roots. +pub struct Stage3PreparedRootV1 { + report: Stage3PreflightReportV1, + statement: Stage3StatementV1, + relation: CompiledStage3Relation, + vk_bytes: Vec, + claim_bytes: Vec, + proof_bytes: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Stage3ProofTimingsV1 { + pub prove_us: u64, + pub self_verify_us: u64, + pub package_us: u64, + pub total_us: u64, +} + +impl Stage3PreparedRootV1 { + pub(crate) fn new( + vk_bytes: &[u8], + claim_bytes: &[u8], + proof_bytes: &[u8], + fri: &FriParameters, + limits: Stage3ResourceLimitsV1, + ) -> Result { + let total = Instant::now(); + let started = Instant::now(); + let prepared = FlockStage3Backend.prepare_witness_with_limits( + vk_bytes, + claim_bytes, + proof_bytes, + fri, + limits, + )?; + let native_prepare_us = elapsed_us(started); + let started = Instant::now(); + let typed = Stage3TypedProofWitnessV1::from_prepared(&prepared, fri)?; + let witness = Stage2AirPcsFriWitnessV1::from_prepared_and_typed( + &prepared, fri, &typed, + )?; + let lowering_us = elapsed_us(started); + let started = Instant::now(); + let relation = CompiledStage3Relation::build(&witness, limits)?; + let census = relation.census()?; + let resources = relation.resources()?; + limits.ensure_union_witness(resources.padded_union_witness_bytes)?; + let manifest = Stage3RelationManifestV1::for_prepared_and_typed( + &prepared, + &typed, + census.circuit_digest, + )?; + let relation_digest = manifest.relation_digest()?; + let statement = + Stage3StatementV1::new(prepared.statement(), relation_digest); + let compile_us = elapsed_us(started); + let started = Instant::now(); + relation.evaluate()?; + let evaluate_us = elapsed_us(started); + let report = Stage3PreflightReportV1 { + stage2_root_digest: prepared.statement().digest(), + relation_digest, + stage3_statement_digest: statement.digest(), + verifying_key_digest: *prepared.statement().verifying_key_digest(), + compact_proof_digest: *blake3::hash(proof_bytes).as_bytes(), + typed_witness_layout_digest: typed.layout_digest(), + activation: typed.active, + log_degrees: typed.log_degrees, + fri_parameter_words: *prepared.statement().fri_parameter_words(), + verifying_key_bytes: vk_bytes.len() as u64, + claim_bytes: claim_bytes.len() as u64, + compact_proof_bytes: proof_bytes.len() as u64, + advice: prepared.advice_profile().clone(), + relation: census, + resources, + limits, + timings: Stage3PreflightTimingsV1 { + native_prepare_us, + lowering_us, + compile_us, + evaluate_us, + total_us: elapsed_us(total), + }, + process_peak_rss_bytes: process_peak_rss_bytes(), + }; + Ok(Self { + report, + statement, + relation, + vk_bytes: vk_bytes.to_vec(), + claim_bytes: claim_bytes.to_vec(), + proof_bytes: proof_bytes.to_vec(), + }) + } + + pub fn report(&self) -> &Stage3PreflightReportV1 { + &self.report + } + + pub fn statement(&self) -> &Stage3StatementV1 { + &self.statement + } + + pub fn prove(&self) -> Result { + self.prove_with_timings().map(|(artifact, _)| artifact) + } + + pub fn prove_with_timings( + &self, + ) -> Result<(Stage3ArtifactV1, Stage3ProofTimingsV1)> { + let total = Instant::now(); + let started = Instant::now(); + let bundle = self.relation.prove()?; + let prove_us = elapsed_us(started); + let started = Instant::now(); + self.relation.verify(self.report.relation.circuit_digest, &bundle)?; + let self_verify_us = elapsed_us(started); + let started = Instant::now(); + let payload = Stage3ProductionPayloadV1::new( + &self.vk_bytes, + &self.claim_bytes, + &self.proof_bytes, + self.report.relation.circuit_digest, + &bundle, + )? + .encode()?; + let artifact = Stage3ArtifactV1::new(self.statement.clone(), payload)?; + Ok(( + artifact, + Stage3ProofTimingsV1 { + prove_us, + self_verify_us, + package_us: elapsed_us(started), + total_us: elapsed_us(total), + }, + )) + } + + /// Verify against the already admitted external root, without repeating + /// native verification, lowering, or compilation. + pub fn verify(&self, artifact: &Stage3ArtifactV1) -> Result<()> { + artifact.ensure_statement(&self.statement)?; + let payload = Stage3ProductionPayloadV1::decode(artifact.proof_bytes())?; + for (observed, external, label) in [ + (payload.vk_bytes(), self.vk_bytes.as_slice(), "verifying key"), + (payload.claim_bytes(), self.claim_bytes.as_slice(), "claim"), + ( + payload.stage2_proof_bytes(), + self.proof_bytes.as_slice(), + "compact proof", + ), + ] { + if observed != external { + bail!("Stage 3 artifact embeds a different Stage 2 {label} transport"); + } + } + self + .relation + .verify(payload.circuit_digest(), payload.flock_proof_bundle_bytes()) + } +} diff --git a/flock-stage3/host/src/relation.rs b/flock-stage3/host/src/relation.rs new file mode 100644 index 00000000..901385f9 --- /dev/null +++ b/flock-stage3/host/src/relation.rs @@ -0,0 +1,491 @@ +use anyhow::{Result, bail}; +use ix_terminal::{Stage2AdviceProfileV1, ValidatedStage2RootV1}; +use multi_stark::types::FriParameters; + +use crate::{ + FlockConfigV1, Stage2AirPcsFriWitnessV1, Stage3TypedProofWitnessV1, + fri::stage2_air_pcs_fri_circuit_digest, +}; + +pub const STAGE3_RELATION_MANIFEST_DOMAIN: &[u8; 8] = b"IXFLKR01"; +const STAGE3_RELATION_MANIFEST_VERSION: u16 = 1; + +/// A semantic obligation that the production Flock relation must enforce. +/// +/// These are deliberately coarser than individual helper functions, but fine +/// grained enough that a partial port cannot silently omit an entire verifier +/// phase. A phase bit may only be enabled together with tests that compare the +/// Flock lowering against the existing Aiur verifier. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum Stage3VerifierPhaseV1 { + TypedProofWitnessShape = 0, + SpecializedVerifyingKeyBinding = 1, + ClaimsDecodeAndCanonicality = 2, + Stage2StatementBinding = 3, + ShapeAndActivation = 4, + LookupAccumulatorBalance = 5, + FiatShamirReplay = 6, + AirOodEvaluation = 7, + PcsOpeningReduction = 8, + MerkleMmcs = 9, + FriGrindingFoldAndFinalPolynomial = 10, +} + +pub const STAGE3_VERIFIER_PHASES_V1: [Stage3VerifierPhaseV1; 11] = [ + Stage3VerifierPhaseV1::TypedProofWitnessShape, + Stage3VerifierPhaseV1::SpecializedVerifyingKeyBinding, + Stage3VerifierPhaseV1::ClaimsDecodeAndCanonicality, + Stage3VerifierPhaseV1::Stage2StatementBinding, + Stage3VerifierPhaseV1::ShapeAndActivation, + Stage3VerifierPhaseV1::LookupAccumulatorBalance, + Stage3VerifierPhaseV1::FiatShamirReplay, + Stage3VerifierPhaseV1::AirOodEvaluation, + Stage3VerifierPhaseV1::PcsOpeningReduction, + Stage3VerifierPhaseV1::MerkleMmcs, + Stage3VerifierPhaseV1::FriGrindingFoldAndFinalPolynomial, +]; + +const REQUIRED_PHASE_MASK: u16 = (1 << STAGE3_VERIFIER_PHASES_V1.len()) - 1; + +// Every phase is consumed by the single statement/AIR/PCS/FRI relation. The +// manifest still refuses to identify a deployable relation until the concrete +// compiled circuit digest has been installed. +const IMPLEMENTED_PHASE_MASK: u16 = REQUIRED_PHASE_MASK; + +impl Stage3VerifierPhaseV1 { + const fn bit(self) -> u16 { + 1 << self as u8 + } + + pub const fn name(self) -> &'static str { + match self { + Self::TypedProofWitnessShape => "typed-proof-witness-shape", + Self::SpecializedVerifyingKeyBinding => { + "specialized-verifying-key-binding" + }, + Self::ClaimsDecodeAndCanonicality => "claims-decode-and-canonicality", + Self::Stage2StatementBinding => "stage2-statement-binding", + Self::ShapeAndActivation => "shape-and-activation", + Self::LookupAccumulatorBalance => "lookup-accumulator-balance", + Self::FiatShamirReplay => "fiat-shamir-replay", + Self::AirOodEvaluation => "air-ood-evaluation", + Self::PcsOpeningReduction => "pcs-opening-reduction", + Self::MerkleMmcs => "merkle-mmcs", + Self::FriGrindingFoldAndFinalPolynomial => { + "fri-grinding-fold-and-final-polynomial" + }, + } + } +} + +/// Auditable progress gate for the verifier lowering. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stage3LoweringStatusV1 { + implemented_phase_mask: u16, +} + +impl Stage3LoweringStatusV1 { + pub const fn current() -> Self { + Self { implemented_phase_mask: IMPLEMENTED_PHASE_MASK } + } + + pub const fn required_phase_mask(self) -> u16 { + REQUIRED_PHASE_MASK + } + + pub const fn implemented_phase_mask(self) -> u16 { + self.implemented_phase_mask + } + + pub const fn is_complete(self) -> bool { + self.implemented_phase_mask == REQUIRED_PHASE_MASK + } + + pub fn missing_phases(self) -> Vec { + STAGE3_VERIFIER_PHASES_V1 + .into_iter() + .filter(|phase| self.implemented_phase_mask & phase.bit() == 0) + .collect() + } + + pub fn ensure_complete(self) -> Result<()> { + if self.is_complete() { + return Ok(()); + } + let missing = self + .missing_phases() + .into_iter() + .map(Stage3VerifierPhaseV1::name) + .collect::>() + .join(", "); + bail!("Flock Stage 3 verifier lowering is incomplete; missing: {missing}") + } +} + +/// Exact transport and advice shape of one compiled Stage 3 verifier relation. +/// +/// The current relation has no padding/activation layer that would make these +/// values reusable maxima. Every word therefore identifies the exact witness +/// shape used to compile the relation. A future capacity-based relation needs a +/// new manifest version and explicit in-circuit padding constraints. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3RelationBoundsV1 { + pub verifying_key_bytes: u64, + pub claims_bytes: u64, + pub advice: Stage2AdviceProfileV1, +} + +impl Stage3RelationBoundsV1 { + fn for_prepared(prepared: &ValidatedStage2RootV1) -> Result { + Ok(Self { + verifying_key_bytes: as_u64( + prepared.verifying_key_bytes().len(), + "verifying-key bytes", + )?, + claims_bytes: as_u64(prepared.claims_bytes().len(), "claims bytes")?, + advice: prepared.advice_profile().clone(), + }) + } + + fn canonical_words(&self) -> [u64; 14] { + [ + self.verifying_key_bytes, + self.claims_bytes, + self.advice.advice_bytes, + self.advice.total_circuits, + self.advice.active_circuits, + self.advice.queries, + self.advice.fri_rounds, + self.advice.input_rounds_per_query, + self.advice.commitment_cap_digests, + self.advice.input_merkle_siblings, + self.advice.fri_merkle_siblings, + self.advice.opened_base_values, + self.advice.fri_sibling_extension_values, + self.advice.other_extension_values, + ] + } + + fn ensure_matches(&self, prepared: &ValidatedStage2RootV1) -> Result<()> { + let observed = Self::for_prepared(prepared)?; + self.ensure_same_shape(&observed) + } + + fn ensure_same_shape(&self, observed: &Self) -> Result<()> { + let labels = [ + "verifying-key bytes", + "claims bytes", + "advice bytes", + "total circuits", + "active circuits", + "queries", + "FRI rounds", + "input rounds per query", + "commitment cap digests", + "input Merkle siblings", + "FRI Merkle siblings", + "opened base values", + "FRI sibling extension values", + "other extension values", + ]; + let expected_words = self.canonical_words(); + let observed_words = observed.canonical_words(); + if let Some((label, (expected, observed))) = labels + .into_iter() + .zip(expected_words.into_iter().zip(observed_words)) + .find(|(_, (expected, observed))| expected != observed) + { + bail!( + "Stage 2 {label} differs from the exact relation shape: expected {expected}, observed {observed}" + ); + } + Ok(()) + } +} + +/// Canonical identity of a specialised Stage 3 verifier relation. +/// +/// The constructor compiles the relation and installs its circuit digest. +/// `relation_digest` additionally binds the Flock configuration, specialised +/// Stage 2 key, witness layout, phase mask, and exact measured shape. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3RelationManifestV1 { + stage2_verifying_key_digest: [u8; 32], + typed_witness_layout_digest: [u8; 32], + // These values are already bound by the compiled circuit digest. Retain + // them here as well so reuse checks cover specialization, not just lengths. + activation: Vec, + log_degrees: Vec, + relation_program_digest: Option<[u8; 32]>, + bounds: Stage3RelationBoundsV1, + lowering_status: Stage3LoweringStatusV1, +} + +impl Stage3RelationManifestV1 { + pub fn for_prepared(prepared: &ValidatedStage2RootV1) -> Result { + let fri = statement_fri_parameters(prepared)?; + let witness = Stage2AirPcsFriWitnessV1::from_prepared(prepared, &fri)?; + let relation_program_digest = stage2_air_pcs_fri_circuit_digest(&witness)?; + Self::for_prepared_and_program_digest(prepared, relation_program_digest) + } + + pub(crate) fn for_prepared_and_program_digest( + prepared: &ValidatedStage2RootV1, + relation_program_digest: [u8; 32], + ) -> Result { + let fri = statement_fri_parameters(prepared)?; + let typed_witness = + Stage3TypedProofWitnessV1::from_prepared(prepared, &fri)?; + Self::for_prepared_and_typed( + prepared, + &typed_witness, + relation_program_digest, + ) + } + + pub(crate) fn for_prepared_and_typed( + prepared: &ValidatedStage2RootV1, + typed_witness: &Stage3TypedProofWitnessV1, + relation_program_digest: [u8; 32], + ) -> Result { + typed_witness.ensure_profile(prepared.advice_profile())?; + Ok(Self { + stage2_verifying_key_digest: *prepared.statement().verifying_key_digest(), + typed_witness_layout_digest: typed_witness.layout_digest(), + activation: typed_witness.active.clone(), + log_degrees: typed_witness.log_degrees.clone(), + relation_program_digest: Some(relation_program_digest), + bounds: Stage3RelationBoundsV1::for_prepared(prepared)?, + lowering_status: Stage3LoweringStatusV1::current(), + }) + } + + pub fn stage2_verifying_key_digest(&self) -> &[u8; 32] { + &self.stage2_verifying_key_digest + } + + pub fn typed_witness_layout_digest(&self) -> &[u8; 32] { + &self.typed_witness_layout_digest + } + + pub fn bounds(&self) -> &Stage3RelationBoundsV1 { + &self.bounds + } + + pub const fn lowering_status(&self) -> Stage3LoweringStatusV1 { + self.lowering_status + } + + pub fn ensure_matches(&self, prepared: &ValidatedStage2RootV1) -> Result<()> { + if prepared.statement().verifying_key_digest() + != &self.stage2_verifying_key_digest + { + bail!("Stage 2 verifying key differs from the specialised relation"); + } + self.bounds.ensure_matches(prepared)?; + + let fri = statement_fri_parameters(prepared)?; + let observed = Stage3TypedProofWitnessV1::from_prepared(prepared, &fri)?; + self.ensure_layout_digest(observed.layout_digest())?; + if observed.active != self.activation { + bail!("Stage 2 activation pattern differs from the specialised relation"); + } + if observed.log_degrees != self.log_degrees { + bail!( + "Stage 2 active trace heights differ from the specialised relation" + ); + } + Ok(()) + } + + /// Compatibility spelling retained for callers written against the earlier + /// capacity terminology. The check is exact, not a less-than-or-equal test. + pub fn ensure_accommodates( + &self, + prepared: &ValidatedStage2RootV1, + ) -> Result<()> { + self.ensure_matches(prepared) + } + + fn ensure_layout_digest(&self, observed: [u8; 32]) -> Result<()> { + if observed != self.typed_witness_layout_digest { + bail!( + "Stage 2 typed witness layout differs from the exact relation shape" + ); + } + Ok(()) + } + + /// Return the digest used in `Stage3StatementV1` for the complete, + /// content-addressed relation program and its exact witness shape. + pub fn relation_digest(&self) -> Result<[u8; 32]> { + self.lowering_status.ensure_complete()?; + if self.relation_program_digest.is_none() { + bail!("Flock Stage 3 relation program has not been built and digested"); + } + Ok(*blake3::hash(&self.canonical_bytes()).as_bytes()) + } + + fn canonical_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(8 + 2 + 32 * 4 + 2 + 2 + 14 * 8); + bytes.extend_from_slice(STAGE3_RELATION_MANIFEST_DOMAIN); + bytes.extend_from_slice(&STAGE3_RELATION_MANIFEST_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + bytes.extend_from_slice(&self.stage2_verifying_key_digest); + bytes.extend_from_slice(&self.typed_witness_layout_digest); + bytes.extend_from_slice(&self.relation_program_digest.unwrap_or([0; 32])); + bytes.extend_from_slice( + &self.lowering_status.required_phase_mask().to_le_bytes(), + ); + bytes.extend_from_slice( + &self.lowering_status.implemented_phase_mask().to_le_bytes(), + ); + for word in self.bounds.canonical_words() { + bytes.extend_from_slice(&word.to_le_bytes()); + } + bytes + } +} + +fn statement_fri_parameters( + prepared: &ValidatedStage2RootV1, +) -> Result { + let [log_final_poly_len, max_log_arity, num_queries, commit_pow, query_pow] = + *prepared.statement().fri_parameter_words(); + let convert = |value, label| { + usize::try_from(value).map_err(|error| { + anyhow::anyhow!("Stage 2 {label} does not fit usize: {error}") + }) + }; + Ok(FriParameters { + log_final_poly_len: convert(log_final_poly_len, "final polynomial log")?, + max_log_arity: convert(max_log_arity, "maximum FRI arity log")?, + num_queries: convert(num_queries, "query count")?, + commit_proof_of_work_bits: convert(commit_pow, "commit PoW bits")?, + query_proof_of_work_bits: convert(query_pow, "query PoW bits")?, + }) +} + +fn as_u64(value: usize, label: &str) -> Result { + u64::try_from(value) + .map_err(|error| anyhow::anyhow!("{label} exceeds u64: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::interchangeable_root; + + #[test] + fn manifest_reuse_checks_specialization_even_when_layout_matches() { + for (first, second, message) in [ + ([4, 0], [0, 4], "activation pattern"), + ([8, 4], [4, 8], "active trace heights"), + ] { + let first = interchangeable_root(first, 256); + let second = interchangeable_root(second, 256); + assert_eq!(first.advice_profile(), second.advice_profile()); + let manifest = Stage3RelationManifestV1::for_prepared(&first).unwrap(); + let fri = statement_fri_parameters(&second).unwrap(); + let typed = + Stage3TypedProofWitnessV1::from_prepared(&second, &fri).unwrap(); + assert_eq!( + manifest.typed_witness_layout_digest(), + &typed.layout_digest() + ); + assert!( + manifest + .ensure_matches(&second) + .unwrap_err() + .to_string() + .contains(message) + ); + assert!( + crate::FlockStage3Backend + .prepare_statement(&second, &manifest) + .is_err() + ); + } + } + + #[test] + fn different_claims_preserve_the_same_specialized_relation() { + let first = interchangeable_root([4, 0], 256); + let second = interchangeable_root([4, 0], 512); + let first_manifest = + Stage3RelationManifestV1::for_prepared(&first).unwrap(); + let second_manifest = + Stage3RelationManifestV1::for_prepared(&second).unwrap(); + assert_ne!(first.statement(), second.statement()); + first_manifest.ensure_matches(&second).unwrap(); + assert_eq!( + first_manifest.relation_digest().unwrap(), + second_manifest.relation_digest().unwrap() + ); + } + + fn relation_shape() -> Stage3RelationBoundsV1 { + Stage3RelationBoundsV1 { + verifying_key_bytes: 10, + claims_bytes: 160, + advice: Stage2AdviceProfileV1 { + advice_bytes: 1_000, + total_circuits: 8, + active_circuits: 3, + queries: 100, + fri_rounds: 20, + input_rounds_per_query: 4, + commitment_cap_digests: 23, + input_merkle_siblings: 2_400, + fri_merkle_siblings: 19_000, + opened_base_values: 12_000, + fri_sibling_extension_values: 2_000, + other_extension_values: 900, + }, + } + } + + #[test] + fn phase_registry_is_complete_and_unique() { + let status = Stage3LoweringStatusV1::current(); + assert_eq!(status.required_phase_mask(), 0x07ff); + assert_eq!(status.implemented_phase_mask(), 0x07ff); + assert!(status.is_complete()); + assert!(status.missing_phases().is_empty()); + status.ensure_complete().unwrap(); + } + + #[test] + fn relation_shape_is_exact_instead_of_a_maximum() { + let expected = relation_shape(); + assert!(expected.ensure_same_shape(&expected).is_ok()); + + let mut smaller = expected.clone(); + smaller.advice.active_circuits -= 1; + let error = expected.ensure_same_shape(&smaller).unwrap_err().to_string(); + assert!(error.contains("active circuits")); + assert!(error.contains("expected 3, observed 2")); + + let mut larger = expected.clone(); + larger.advice.fri_merkle_siblings += 1; + let error = expected.ensure_same_shape(&larger).unwrap_err().to_string(); + assert!(error.contains("FRI Merkle siblings")); + assert!(error.contains("expected 19000, observed 19001")); + } + + #[test] + fn manifest_rejects_a_different_nested_layout_digest() { + let manifest = Stage3RelationManifestV1 { + stage2_verifying_key_digest: [0; 32], + typed_witness_layout_digest: [1; 32], + activation: vec![], + log_degrees: vec![], + relation_program_digest: Some([2; 32]), + bounds: relation_shape(), + lowering_status: Stage3LoweringStatusV1::current(), + }; + assert!(manifest.ensure_layout_digest([1; 32]).is_ok()); + assert!(manifest.ensure_layout_digest([3; 32]).is_err()); + } +} diff --git a/flock-stage3/host/src/report.rs b/flock-stage3/host/src/report.rs new file mode 100644 index 00000000..4cfffd80 --- /dev/null +++ b/flock-stage3/host/src/report.rs @@ -0,0 +1,110 @@ +//! Versioned diagnostics. These encodings are not cryptographic statements. + +use serde::{Serialize, Serializer}; +use serde_json::{Value, json}; + +use crate::{FLOCK_UPSTREAM_REVISION, FlockConfigV1, Stage3PreflightReportV1}; + +pub(crate) fn hex(digest: [u8; 32]) -> String { + blake3::Hash::from_bytes(digest).to_hex().to_string() +} + +pub(crate) fn serialize_digest( + digest: &[u8; 32], + serializer: S, +) -> Result { + serializer.serialize_str(&hex(*digest)) +} + +pub(crate) fn elapsed_us(start: std::time::Instant) -> u64 { + u64::try_from(start.elapsed().as_micros()).unwrap_or(u64::MAX) +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] +pub struct Stage3PreflightTimingsV1 { + pub native_prepare_us: u64, + pub lowering_us: u64, + pub compile_us: u64, + pub evaluate_us: u64, + pub total_us: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Stage3ResourceReportV1 { + pub virtual_union_log: u64, + pub committed_union_log: u64, + pub dense_witness_bytes: u64, + pub padded_union_witness_bytes: u64, + pub pcs_message_bytes: u64, + pub pcs_codeword_bytes: u64, + pub pcs_log_batch_size: u64, + pub pcs_lanes: u64, + pub pcs_log_inverse_rate: u64, + pub tables: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Stage3TableReportV1 { + pub registry_slot: u64, + pub rows: u64, + pub boolean_columns: u64, + pub useful_boolean_columns: u64, + pub padded_witness_bytes: u64, +} + +impl Stage3PreflightReportV1 { + /// Stable JSON object, suitable for one JSONL record per aggregate root. + /// Durations and process memory are diagnostic and excluded from digests. + pub fn to_json_value(&self) -> Value { + let advice = &self.advice; + json!({ + "schema": "ix.flock-stage3.preflight", "version": 1, + "stage2_root_digest": hex(self.stage2_root_digest), + "relation_digest": hex(self.relation_digest), + "stage3_statement_digest": hex(self.stage3_statement_digest), + "config_digest": hex(FlockConfigV1.digest()), + "flock_revision": FLOCK_UPSTREAM_REVISION, + "profile": "fast128", "merkle_hash": "blake3", + "transcript": "chained-blake3", + "transcript_domain": String::from_utf8_lossy(crate::STAGE3_TRANSCRIPT_DOMAIN), + "transport": { + "verifying_key_digest": hex(self.verifying_key_digest), + "compact_proof_digest": hex(self.compact_proof_digest), + "verifying_key_bytes": self.verifying_key_bytes, + "claim_bytes": self.claim_bytes, + "compact_proof_bytes": self.compact_proof_bytes, + }, + "specialization": { + "typed_witness_layout_digest": hex(self.typed_witness_layout_digest), + "activation": self.activation, "active_log_degrees": self.log_degrees, + "fri_parameter_words": self.fri_parameter_words, + }, + "advice": { + "advice_bytes": advice.advice_bytes, + "total_circuits": advice.total_circuits, + "active_circuits": advice.active_circuits, + "queries": advice.queries, "fri_rounds": advice.fri_rounds, + "input_rounds_per_query": advice.input_rounds_per_query, + "commitment_cap_digests": advice.commitment_cap_digests, + "input_merkle_siblings": advice.input_merkle_siblings, + "fri_merkle_siblings": advice.fri_merkle_siblings, + "opened_base_values": advice.opened_base_values, + "fri_sibling_extension_values": advice.fri_sibling_extension_values, + "other_extension_values": advice.other_extension_values, + }, + "relation": self.relation, "resources": self.resources, + "limits": self.limits, "timings": self.timings, + "relation_cache": "none", + "process_peak_rss_bytes": self.process_peak_rss_bytes, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + }) + } +} + +/// Linux's process high-water mark. A batch's later records include previous +/// roots; use separate processes when measuring individual peak memory. +pub fn process_peak_rss_bytes() -> Option { + let status = std::fs::read_to_string("/proc/self/status").ok()?; + let line = status.lines().find(|line| line.starts_with("VmHWM:"))?; + line.split_whitespace().nth(1)?.parse::().ok()?.checked_mul(1024) +} diff --git a/flock-stage3/host/src/sizing.rs b/flock-stage3/host/src/sizing.rs new file mode 100644 index 00000000..f0ad5b30 --- /dev/null +++ b/flock-stage3/host/src/sizing.rs @@ -0,0 +1,268 @@ +//! Count the same constraint emission used for compilation, without building +//! R1CS tables, wire classes, or capacity-sized permutation buffers. +//! +//! Counting wires are opaque placeholders: the emitter must not branch on wire +//! identity or inspect wire values. Native witness-dependent branches still +//! run normally in both passes. The finished circuit's counts and schemas are +//! checked against the count pass before it can be used. + +use anyhow::{Result, bail}; +use flock_prover::{ + circuit::builder::{ + CircuitShape, GateType, ShapeBuilder, SlotId, SlotWitness, Wire, + }, + field::F128, + r1cs::SparseBinaryMatrix, + schedule::{IoDirection, IoWord, TableClass, TableType}, +}; + +/// Cheap arity metadata, checked against every finished production table. +pub(crate) trait CountedGate: GateType { + const INPUTS: usize; + const OUTPUTS: usize; +} + +macro_rules! counted_gates { + ($($gate:ty => ($inputs:literal, $outputs:literal)),+ $(,)?) => { + $(impl CountedGate for $gate { + const INPUTS: usize = $inputs; + const OUTPUTS: usize = $outputs; + })+ + }; +} + +counted_gates! { + crate::binding::Blake3Gate => (7, 4), + crate::merkle::DigestOrderGate => (5, 4), + crate::goldilocks::GoldilocksAddPairGate => (2, 3), + crate::multiplication::GoldilocksMulPairGate => (2, 4), + crate::extension::GoldilocksLaneRepackGate => (2, 4), + crate::goldilocks::CanonicalGoldilocksQuadGate => (2, 1), + crate::equality::F128EqualityGate => (2, 1), + crate::transcript::HashSampleGate => (1, 1), + crate::transcript::GoldilocksSampleGate => (4, 9), + crate::transcript::U64SplitGate => (1, 2), + crate::window::ByteWindowGate => (3, 1), +} + +/// The gate-emission subset shared by the real builder and the census pass. +pub(crate) trait CircuitEmitter { + fn slot(&mut self, gate: G) -> SlotId + where + G: CountedGate + Send + Sync + 'static, + G::Row: Send + 'static, + G::Hint: 'static; + fn input(&mut self) -> Wire; + fn public_input(&mut self) -> Wire; + fn fixed_public_input(&mut self, value: F128) -> Wire; + fn gate(&mut self, slot: SlotId, inputs: &[Wire]) -> Vec; + fn publish(&mut self, wire: Wire); + fn connect(&mut self, first: Wire, second: Wire); +} + +impl CircuitEmitter for ShapeBuilder { + fn slot(&mut self, gate: G) -> SlotId + where + G: CountedGate + Send + Sync + 'static, + G::Row: Send + 'static, + G::Hint: 'static, + { + self.slot(gate) + } + fn input(&mut self) -> Wire { + self.input() + } + fn public_input(&mut self) -> Wire { + self.public_input() + } + fn fixed_public_input(&mut self, value: F128) -> Wire { + self.fixed_public_input(value) + } + fn gate(&mut self, slot: SlotId, inputs: &[Wire]) -> Vec { + self.gate(slot, inputs) + } + fn publish(&mut self, wire: Wire) { + self.publish(wire); + } + fn connect(&mut self, first: Wire, second: Wire) { + self.connect(first, second); + } +} + +struct SlotCount { + id: SlotId, + name: &'static str, + inputs: usize, + outputs: usize, + rows: usize, +} + +pub(crate) struct CountingEmitter { + // Upstream IDs have private constructors. This builder only allocates IDs; + // it never emits a gate, evaluates a witness, or finishes a circuit. + ids: ShapeBuilder, + placeholder: Wire, + slots: Vec, +} + +impl CountingEmitter { + /// Capacity checks in the shared emitter must not truncate the count pass. + /// No allocation uses this capacity: all real work is admitted afterwards. + pub(crate) const COUNT_NU: usize = usize::BITS as usize - 1; + + pub(crate) fn new() -> Self { + let mut ids = ShapeBuilder::new(0); + let placeholder = ids.input(); + Self { ids, placeholder, slots: Vec::new() } + } + + pub(crate) fn required_nu(&self, minimum: usize) -> Result { + let rows = self.slots.iter().map(|slot| slot.rows).max().unwrap_or(0); + let capacity = + rows.max(1).checked_next_power_of_two().ok_or_else(|| { + anyhow::anyhow!("Stage 3 exact table row count overflow") + })?; + Ok((capacity.ilog2() as usize).max(minimum)) + } + + pub(crate) fn table_rows( + &self, + ) -> impl Iterator { + self.slots.iter().map(|slot| (slot.name, slot.rows)) + } + + pub(crate) fn ensure_matches(&self, shape: &CircuitShape) -> Result<()> { + if self.slots.len() != shape.counts.len() { + bail!("Stage 3 counting/compiled slot counts disagree"); + } + for slot in &self.slots { + let index = shape.registry_slot(slot.id); + let schema = &shape.registry.types()[index].io_schema; + let inputs = schema.iter().filter(|io| io.dir == IoDirection::In).count(); + if shape.counts[index] != slot.rows + || inputs != slot.inputs + || schema.len() - inputs != slot.outputs + { + bail!("Stage 3 counting/compiled table {index} disagrees"); + } + } + Ok(()) + } +} + +impl CircuitEmitter for CountingEmitter { + fn slot(&mut self, _gate: G) -> SlotId + where + G: CountedGate + Send + Sync + 'static, + G::Row: Send + 'static, + G::Hint: 'static, + { + let id = self.ids.slot(IdOnlyGate); + self.slots.push(SlotCount { + id, + name: std::any::type_name::().rsplit("::").next().unwrap(), + inputs: G::INPUTS, + outputs: G::OUTPUTS, + rows: 0, + }); + id + } + fn input(&mut self) -> Wire { + self.placeholder + } + fn public_input(&mut self) -> Wire { + self.placeholder + } + fn fixed_public_input(&mut self, _value: F128) -> Wire { + self.placeholder + } + fn gate(&mut self, slot: SlotId, inputs: &[Wire]) -> Vec { + let count = self.slots.iter_mut().find(|count| count.id == slot).unwrap(); + assert_eq!(inputs.len(), count.inputs, "counted gate input arity"); + // Saturation is fail-closed: required_nu rejects usize::MAX, including + // when the emitter goes on to make more calls after an overflow. + count.rows = count.rows.saturating_add(1); + vec![self.placeholder; count.outputs] + } + fn publish(&mut self, _wire: Wire) {} + fn connect(&mut self, _first: Wire, _second: Wire) {} +} + +/// An ID allocator token, not a circuit table. It cannot escape this module +/// through CountingEmitter and its builder is never finished. +struct IdOnlyGate; + +impl GateType for IdOnlyGate { + type Row = (); + type Hint = (); + + fn table(&self) -> TableType { + let empty = + || SparseBinaryMatrix { num_rows: 0, num_cols: 0, rows: Vec::new() }; + TableType { + k_log: 0, + useful_bits: 0, + a_0: empty(), + b_0: empty(), + c_0: empty(), + const_pin: None, + class: TableClass::Boolean, + io_schema: vec![IoWord::input(0)], + } + } + + fn eval(&self, _: &[F128], _: &(), _: &mut Vec) { + unreachable!("counting IDs cannot evaluate a circuit") + } + + fn witness(&self, _: &[()], _: usize) -> SlotWitness { + unreachable!("counting IDs cannot produce a witness") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::goldilocks::CanonicalGoldilocksQuadGate; + + #[test] + fn capacity_uses_the_largest_table_and_checks_rounding_overflow() { + let mut count = CountingEmitter::new(); + for _ in 0..2 { + count.slot(CanonicalGoldilocksQuadGate { nu: 10 }); + } + assert_eq!(count.required_nu(10).unwrap(), 10); + for (rows, nu) in [(1023, 10), (1024, 10), (1025, 11)] { + for slot in &mut count.slots { + slot.rows = rows; + } + assert_eq!(count.required_nu(10).unwrap(), nu); + } + count.slots[0].rows = usize::MAX; + assert!(count.required_nu(10).is_err()); + } + + #[test] + fn compiled_count_and_schema_drift_are_rejected() { + let mut count = CountingEmitter::new(); + let mut real = ShapeBuilder::new(10); + fn emit(builder: &mut impl CircuitEmitter) { + let slot = builder.slot(CanonicalGoldilocksQuadGate { nu: 10 }); + let input = builder.fixed_public_input(F128::ZERO); + builder.gate(slot, &[input, input]); + } + emit(&mut count); + emit(&mut real); + assert_eq!( + count.table_rows().collect::>(), + vec![("CanonicalGoldilocksQuadGate", 1)], + ); + let shape = real.finish().unwrap(); + count.ensure_matches(&shape).unwrap(); + count.slots[0].rows += 1; + assert!(count.ensure_matches(&shape).is_err()); + count.slots[0].rows -= 1; + count.slots[0].outputs += 1; + assert!(count.ensure_matches(&shape).is_err()); + } +} diff --git a/flock-stage3/host/src/test_support.rs b/flock-stage3/host/src/test_support.rs new file mode 100644 index 00000000..429b8f37 --- /dev/null +++ b/flock-stage3/host/src/test_support.rs @@ -0,0 +1,65 @@ +//! Small native proofs for specialization and verifier-boundary regressions. + +use aiur::vk_codec::aiur_config_system_to_bytes; +use ix_terminal::ValidatedStage2RootV1; +use multi_stark::{ + expr::Expr, + lookup::Lookup, + p3_field::{PrimeCharacteristicRing, PrimeField64}, + p3_matrix::dense::RowMajorMatrix, + system::{CircuitInputs, System, SystemWitness}, + types::{CommitmentParameters, FriParameters, GoldilocksBlake3Config, Val}, +}; + +/// Two interchangeable circuits under one key. A zero height makes a circuit +/// inactive; the first active circuit consumes the claim exactly once. +pub(crate) fn interchangeable_root( + heights: [usize; 2], + claim_seed: u64, +) -> ValidatedStage2RootV1 { + let commitment = CommitmentParameters { log_blowup: 1, cap_height: 0 }; + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 2, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 0, + }; + let circuit = || CircuitInputs { + main_width: 19, + lookups: vec![Lookup::pull( + Expr::main(0), + (1..=18).map(Expr::main).collect(), + )], + ..Default::default() + }; + let (system, key) = System::new( + GoldilocksBlake3Config::new(commitment, fri), + [circuit(), circuit()], + ); + let claim: Vec<_> = + (0..18).map(|word| Val::from_u64(claim_seed + word)).collect(); + let first_active = heights.iter().position(|&height| height != 0).unwrap(); + let traces = heights + .into_iter() + .enumerate() + .map(|(index, height)| { + let mut values = vec![Val::ZERO; height * 19]; + if index == first_active { + values[0] = Val::ONE; + values[1..19].copy_from_slice(&claim); + } + RowMajorMatrix::new(values, 19) + }) + .collect(); + let proof = + system.prove(&key, &claim, SystemWitness::from_stage_1(traces, &system)); + let vk = aiur_config_system_to_bytes(&system, commitment, fri); + let claim_bytes: Vec<_> = claim + .iter() + .flat_map(|word| word.as_canonical_u64().to_le_bytes()) + .collect(); + crate::FlockStage3Backend + .prepare_witness(&vk, &claim_bytes, &proof.to_bytes().unwrap(), &fri) + .unwrap() +} diff --git a/flock-stage3/host/src/transcript.rs b/flock-stage3/host/src/transcript.rs new file mode 100644 index 00000000..60e8f2c2 --- /dev/null +++ b/flock-stage3/host/src/transcript.rs @@ -0,0 +1,2835 @@ +//! Exact BLAKE3 `HashChallenger` replay for the Stage 2 verifier transcript. +//! +//! Plonky3's byte challenger replaces its input with `BLAKE3(input)` whenever +//! an empty output buffer is sampled, then pops sample bytes from the END of +//! that digest. An observation discards any unused output bytes. This module +//! lowers the protocol-shaped prefix through the PCS opening-batch sample: +//! +//! 1. sample and re-observe the lookup challenge; +//! 2. sample and re-observe the fingerprint challenge; +//! 3. observe Stage 2 data and sample the constraint challenge; +//! 4. observe the quotient commitment and sample zeta; +//! 5. observe all PCS openings, then sample the FRI/PCS batching challenge +//! used to reduce every opening before the commit-phase folds. +//! +//! The replay can then continue through FRI commitments, commit grinding and +//! betas, the final polynomial/arity observations, query grinding, and every +//! masked query draw. Every BLAKE3 compression is constrained, including +//! chunk-tree parents for messages longer than 1,024 bytes. Sampled Goldilocks +//! limbs are checked canonical. Field sampling constrains one chained digest +//! refill and fails closed only if those eight candidates still contain fewer +//! than two canonical Goldilocks values (seven candidates after a raw PoW +//! draw). + +use aiur::vk_codec::AiurVerifyingKey; +use anyhow::{Context, Result, bail}; +use bincode::Options; +use flock_prover::{ + challenger::FsChallenger, + circuit::builder::{ + CircuitShape, GateType, ShapeBuilder, SlotId, SlotWitness, Wire, + }, + field::F128, + lincheck::LincheckCircuit, + pcs::Commitment, + proof::R1csProofCircuitMerged, + prover::{self, UnionSlotProverInput}, + r1cs_hashes::{ + blake3 as flock_blake3, + fs_chain::{CvSource, FsChain, FsChainTrace}, + }, + schedule::{IoWord, TableType}, + union::{SlotWitnessDest, UnionInstance}, + verifier, +}; +use ix_terminal::{ValidatedStage2RootV1, fri_parameter_words}; +use multi_stark::types::FriParameters; +use serde::{Deserialize, Serialize}; + +use crate::sizing::CircuitEmitter; + +use crate::{ + FlockConfigV1, STAGE2_TRANSCRIPT_CONFORMANCE_TRANSCRIPT_DOMAIN, + binding::{Blake3Gate, IV, pack_bytes, pack_params, pack8, pcs_params}, + boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_witness, + generate_boolean_witness_into, write_f128, + }, + goldilocks::{ + CanonicalGoldilocksQuadGate, GOLDILOCKS_MODULUS, build_canonical_quad_r1cs, + generate_canonical_quad_witness, + }, + typed_witness::{Stage3OpenedRoundV1, Stage3TypedProofWitnessV1}, +}; + +pub const STAGE2_TRANSCRIPT_CONFORMANCE_ARTIFACT_MAGIC: &[u8; 8] = b"IXFLTR01"; + +const ARTIFACT_VERSION: u16 = 1; +const CONFIG_OFFSET: usize = 10; +const LENGTHS_OFFSET: usize = CONFIG_OFFSET + 32; +const SEGMENT_COUNT: usize = 4; +const LENGTH_BYTES: usize = SEGMENT_COUNT * 4; +const SEGMENTS_OFFSET: usize = LENGTHS_OFFSET + LENGTH_BYTES; +const CHALLENGE_COUNT: usize = 5; +const CHALLENGE_BYTES: usize = CHALLENGE_COUNT * 16; +const FIXED_SUFFIX_BYTES: usize = CHALLENGE_BYTES + 32 + 8; +const MAX_OBSERVATION_BYTES: usize = 16 * 1024 * 1024; +const MAX_BUNDLE_BYTES: usize = 64 * 1024 * 1024; +const WORD_BYTES: usize = 16; +const MIN_NU: usize = 8; +const MAX_NU: usize = 20; +const MAX_FRI_ROUNDS: usize = 32; +const MAX_FRI_QUERIES: usize = 1_024; +const MAX_CAP_ROOTS: usize = 256; + +const SAMPLE_K_LOG: usize = 9; +const SAMPLE_INPUT_BASE: usize = 0; +const SAMPLE_OUTPUT_BASE: usize = 128; +const SAMPLE_COLUMNS: usize = 256; + +const FIELD_SAMPLE_K_LOG: usize = 14; +const FIELD_SAMPLE_HIGH_BASE: usize = 0; +const FIELD_SAMPLE_LOW_BASE: usize = 128; +const FIELD_SAMPLE_REFILL_HIGH_BASE: usize = 256; +const FIELD_SAMPLE_REFILL_LOW_BASE: usize = 384; +const FIELD_SAMPLE_OUTPUT_BASE: usize = 512; +const FIELD_SAMPLE_FAILURE_BASE: usize = 640; +const FIELD_SAMPLE_RAW_FIRST_BASE: usize = 768; +const FIELD_SAMPLE_SKIP_OUTPUT_BASE: usize = 896; +const FIELD_SAMPLE_SKIP_FAILURE_BASE: usize = 1_024; +const FIELD_SAMPLE_STATE_LOW_BASE: usize = 1_152; +const FIELD_SAMPLE_STATE_HIGH_BASE: usize = 1_280; +const FIELD_SAMPLE_SKIP_STATE_LOW_BASE: usize = 1_408; +const FIELD_SAMPLE_SKIP_STATE_HIGH_BASE: usize = 1_536; +const FIELD_SAMPLE_COLUMNS: usize = 1_664; + +/// The variable observation segments around the fixed challenger operations. +/// +/// `initial_observations` is the complete seed/shape/activation/stage-1/ +/// claims prefix. The other fields correspond to the comments on their +/// names and are already serialized exactly as the Stage 2 challenger sees +/// them. The production composition will build these byte words directly +/// from the typed proof and verifying-key constants. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2TranscriptReplayV1 { + pub initial_observations: Vec, + pub stage2_and_accumulator_observations: Vec, + pub quotient_commitment_observations: Vec, + pub pcs_opening_observations: Vec, +} + +/// Challenges derived by [`Stage2TranscriptReplayV1`], in protocol order. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stage2TranscriptChallengesV1 { + pub lookup: [u64; 2], + pub fingerprint: [u64; 2], + pub constraint: [u64; 2], + pub zeta: [u64; 2], + pub pcs_alpha: [u64; 2], +} + +/// One of the four byte segments consumed by the constrained Stage 2 +/// transcript prefix. +/// +/// PCS composition uses these identifiers to consume commitment roots and +/// out-of-domain values from the exact wires already hashed by the +/// transcript, rather than accepting duplicated public values. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Stage2TranscriptSegmentV1 { + Initial, + Stage2AndAccumulator, + QuotientCommitment, + PcsOpening, +} + +impl Stage2TranscriptSegmentV1 { + pub(crate) const fn index(self) -> usize { + match self { + Self::Initial => 0, + Self::Stage2AndAccumulator => 1, + Self::QuotientCommitment => 2, + Self::PcsOpening => 3, + } + } +} + +/// A little-endian `u64` lane inside one constrained transcript segment. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stage2TranscriptByteBindingV1 { + pub segment: Stage2TranscriptSegmentV1, + pub byte_offset: usize, +} + +impl Stage2TranscriptByteBindingV1 { + pub const fn new( + segment: Stage2TranscriptSegmentV1, + byte_offset: usize, + ) -> Self { + Self { segment, byte_offset } + } +} + +/// The FRI portion of the Stage 2 byte transcript after the opening-batch +/// challenge has been sampled. +/// +/// Commitments are kept as caps so the relation can expose and bind every cap +/// root individually. `query_index_bits` is the exact bit width passed to +/// `SerializingChallenger64::sample_bits`; it equals the global FRI height for +/// the two-adic folding strategy used by Ix. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2FriTranscriptReplayV1 { + pub commit_phase_commitments: Vec>, + pub commit_pow_witnesses: Vec, + pub final_polynomial: Vec<[u64; 2]>, + pub log_arities: Vec, + pub query_pow_witness: u64, + pub commit_pow_bits: u8, + pub query_pow_bits: u8, + pub num_queries: usize, + pub query_index_bits: u8, +} + +/// Challenges sampled by the FRI verifier after the PCS batching challenge. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2FriTranscriptChallengesV1 { + pub betas: Vec<[u64; 2]>, + pub query_indices: Vec, +} + +impl Stage2TranscriptReplayV1 { + /// Replay the exact native byte challenger through the first FRI challenge. + pub fn challenges(&self) -> Result { + compute_challenges(self) + } + + /// Build the exact transcript segments from an already validated Stage 2 + /// root and its serializer-independent typed proof witness. + pub fn from_prepared( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + ) -> Result { + let typed = Stage3TypedProofWitnessV1::from_prepared(prepared, fri)?; + Self::from_prepared_and_typed(prepared, fri, &typed) + } + + pub fn from_prepared_and_typed( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + typed: &Stage3TypedProofWitnessV1, + ) -> Result { + if prepared.statement().fri_parameter_words() != &fri_parameter_words(fri) { + bail!("Stage 2 transcript uses different FRI parameters"); + } + typed.ensure_profile(prepared.advice_profile())?; + let key = AiurVerifyingKey::from_bytes(prepared.verifying_key_bytes()) + .map_err(|error| { + anyhow::anyhow!("decode Aiur transcript key: {error}") + })?; + if key.to_bytes() != prepared.verifying_key_bytes() { + bail!("Aiur transcript key is not canonically encoded"); + } + if fri_parameter_words(&key.fri_parameters()) != fri_parameter_words(fri) { + bail!("Aiur transcript key uses different FRI parameters"); + } + if key.num_circuits() != typed.active.len() { + bail!( + "Aiur transcript key has {} circuits but activation has {} bits", + key.num_circuits(), + typed.active.len() + ); + } + + let mut initial_observations = key.transcript_seed_and_shape_bytes(); + for &active in &typed.active { + push_u64_observation(&mut initial_observations, u64::from(active)); + } + if let Some(preprocessed) = key.preprocessed_commitment_roots() { + push_cap_observations(&mut initial_observations, &preprocessed); + } + push_cap_observations( + &mut initial_observations, + &typed.commitments.stage_1_trace, + ); + for &log_degree in &typed.log_degrees { + push_u64_observation(&mut initial_observations, u64::from(log_degree)); + } + initial_observations.extend_from_slice(prepared.claims_bytes()); + + let mut stage2_and_accumulator_observations = Vec::new(); + push_cap_observations( + &mut stage2_and_accumulator_observations, + &typed.commitments.stage_2_trace, + ); + for &accumulator in &typed.intermediate_accumulators { + push_extension_observation( + &mut stage2_and_accumulator_observations, + accumulator, + ); + } + + let mut quotient_commitment_observations = Vec::new(); + push_cap_observations( + &mut quotient_commitment_observations, + &typed.commitments.quotient_chunks, + ); + + let mut pcs_opening_observations = Vec::new(); + push_opened_round_observations( + &mut pcs_opening_observations, + &typed.stage_1_opened_values, + ); + push_opened_round_observations( + &mut pcs_opening_observations, + &typed.stage_2_opened_values, + ); + push_opened_round_observations( + &mut pcs_opening_observations, + &typed.quotient_opened_values, + ); + if let Some(preprocessed) = &typed.preprocessed_opened_values { + push_opened_round_observations( + &mut pcs_opening_observations, + preprocessed, + ); + } + + let replay = Self { + initial_observations, + stage2_and_accumulator_observations, + quotient_commitment_observations, + pcs_opening_observations, + }; + validate_replay(&replay)?; + Ok(replay) + } +} + +impl Stage2FriTranscriptReplayV1 { + /// Build the exact post-opening FRI transcript from the validated Stage 2 + /// advice transport and the commitment parameters embedded in its key. + pub fn from_prepared( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + ) -> Result { + let typed = Stage3TypedProofWitnessV1::from_prepared(prepared, fri)?; + Self::from_prepared_and_typed(prepared, fri, &typed) + } + + pub fn from_prepared_and_typed( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + typed: &Stage3TypedProofWitnessV1, + ) -> Result { + if prepared.statement().fri_parameter_words() != &fri_parameter_words(fri) { + bail!("Stage 2 FRI transcript uses different FRI parameters"); + } + typed.ensure_profile(prepared.advice_profile())?; + let key = AiurVerifyingKey::from_bytes(prepared.verifying_key_bytes()) + .map_err(|error| { + anyhow::anyhow!("decode Aiur FRI transcript key: {error}") + })?; + if key.to_bytes() != prepared.verifying_key_bytes() { + bail!("Aiur FRI transcript key is not canonically encoded"); + } + if fri_parameter_words(&key.fri_parameters()) != fri_parameter_words(fri) { + bail!("Aiur FRI transcript key uses different FRI parameters"); + } + + let first_query = typed + .opening_proof + .query_proofs + .first() + .ok_or_else(|| anyhow::anyhow!("Stage 2 FRI proof has no queries"))?; + let log_arities: Vec = first_query + .commit_phase_openings + .iter() + .map(|step| step.log_arity) + .collect(); + if typed.opening_proof.query_proofs.iter().any(|query| { + query + .commit_phase_openings + .iter() + .map(|step| step.log_arity) + .ne(log_arities.iter().copied()) + }) { + bail!("Stage 2 FRI queries disagree on the folding-arity schedule"); + } + let total_log_reduction = + log_arities.iter().try_fold(0usize, |sum, &arity| { + sum + .checked_add(usize::from(arity)) + .ok_or_else(|| anyhow::anyhow!("FRI folding-arity sum overflow")) + })?; + let query_index_bits = total_log_reduction + .checked_add(key.commitment_parameters().log_blowup) + .and_then(|height| height.checked_add(fri.log_final_poly_len)) + .ok_or_else(|| anyhow::anyhow!("FRI global height overflow"))?; + + let replay = Self { + commit_phase_commitments: typed + .opening_proof + .commit_phase_commits + .clone(), + commit_pow_witnesses: typed.opening_proof.commit_pow_witnesses.clone(), + final_polynomial: typed.opening_proof.final_poly.clone(), + log_arities, + query_pow_witness: typed.opening_proof.query_pow_witness, + commit_pow_bits: u8::try_from(fri.commit_proof_of_work_bits) + .map_err(|_| anyhow::anyhow!("commit PoW bits exceed u8"))?, + query_pow_bits: u8::try_from(fri.query_proof_of_work_bits) + .map_err(|_| anyhow::anyhow!("query PoW bits exceed u8"))?, + num_queries: fri.num_queries, + query_index_bits: u8::try_from(query_index_bits) + .map_err(|_| anyhow::anyhow!("FRI query-index width exceeds u8"))?, + }; + validate_fri_replay(&replay)?; + Ok(replay) + } + + /// Replay the native challenger from the prefix's retained digest state. + pub fn challenges( + &self, + prefix: &Stage2TranscriptReplayV1, + ) -> Result { + compute_fri_challenges(prefix, self) + } +} + +/// A real Flock proof of the transcript prefix relation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage2TranscriptConformanceArtifactV1 { + replay: Stage2TranscriptReplayV1, + challenges: Stage2TranscriptChallengesV1, + circuit_digest: [u8; 32], + proof_bundle_bytes: Vec, +} + +impl Stage2TranscriptConformanceArtifactV1 { + pub fn replay(&self) -> &Stage2TranscriptReplayV1 { + &self.replay + } + + pub const fn challenges(&self) -> Stage2TranscriptChallengesV1 { + self.challenges + } + + pub fn circuit_digest(&self) -> &[u8; 32] { + &self.circuit_digest + } + + pub fn proof_bundle_bytes(&self) -> &[u8] { + &self.proof_bundle_bytes + } + + pub fn to_bytes(&self) -> Vec { + let segments = replay_segments(&self.replay); + let segment_bytes: usize = + segments.iter().map(|segment| segment.len()).sum(); + let mut bytes = Vec::with_capacity( + SEGMENTS_OFFSET + + segment_bytes + + FIXED_SUFFIX_BYTES + + self.proof_bundle_bytes.len(), + ); + bytes.extend_from_slice(STAGE2_TRANSCRIPT_CONFORMANCE_ARTIFACT_MAGIC); + bytes.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes()); + bytes.extend_from_slice(&FlockConfigV1.digest()); + for segment in &segments { + bytes.extend_from_slice( + &u32::try_from(segment.len()) + .expect("bounded transcript segment length") + .to_le_bytes(), + ); + } + for segment in &segments { + bytes.extend_from_slice(segment); + } + encode_challenges(&mut bytes, self.challenges); + bytes.extend_from_slice(&self.circuit_digest); + bytes.extend_from_slice( + &u64::try_from(self.proof_bundle_bytes.len()) + .expect("proof bundle length") + .to_le_bytes(), + ); + bytes.extend_from_slice(&self.proof_bundle_bytes); + bytes + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < SEGMENTS_OFFSET + FIXED_SUFFIX_BYTES { + bail!("truncated Flock Stage 2 transcript artifact"); + } + if &bytes[..8] != STAGE2_TRANSCRIPT_CONFORMANCE_ARTIFACT_MAGIC { + bail!("invalid Flock Stage 2 transcript artifact magic"); + } + let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap()); + if version != ARTIFACT_VERSION { + bail!("unsupported Flock Stage 2 transcript artifact version {version}"); + } + if bytes[CONFIG_OFFSET..LENGTHS_OFFSET] != FlockConfigV1.digest() { + bail!("Flock Stage 2 transcript artifact configuration mismatch"); + } + + let mut lengths = [0usize; SEGMENT_COUNT]; + for (index, length) in lengths.iter_mut().enumerate() { + let offset = LENGTHS_OFFSET + index * 4; + *length = usize::try_from(u32::from_le_bytes( + bytes[offset..offset + 4].try_into().unwrap(), + )) + .expect("u32 fits usize"); + } + let segment_bytes = lengths.iter().try_fold(0usize, |total, &length| { + total + .checked_add(length) + .ok_or_else(|| anyhow::anyhow!("transcript segment length overflow")) + })?; + let suffix_offset = SEGMENTS_OFFSET + .checked_add(segment_bytes) + .ok_or_else(|| anyhow::anyhow!("transcript artifact length overflow"))?; + let minimum_end = suffix_offset + .checked_add(FIXED_SUFFIX_BYTES) + .ok_or_else(|| anyhow::anyhow!("transcript artifact length overflow"))?; + if bytes.len() < minimum_end { + bail!("truncated Flock Stage 2 transcript artifact segments"); + } + + let mut cursor = SEGMENTS_OFFSET; + let mut take_segment = |length: usize| { + let end = cursor + length; + let segment = bytes[cursor..end].to_vec(); + cursor = end; + segment + }; + let replay = Stage2TranscriptReplayV1 { + initial_observations: take_segment(lengths[0]), + stage2_and_accumulator_observations: take_segment(lengths[1]), + quotient_commitment_observations: take_segment(lengths[2]), + pcs_opening_observations: take_segment(lengths[3]), + }; + validate_replay(&replay)?; + debug_assert_eq!(cursor, suffix_offset); + + let challenges = + decode_challenges(&bytes[suffix_offset..suffix_offset + CHALLENGE_BYTES]); + validate_challenges(challenges)?; + let digest_offset = suffix_offset + CHALLENGE_BYTES; + let mut circuit_digest = [0u8; 32]; + circuit_digest.copy_from_slice(&bytes[digest_offset..digest_offset + 32]); + let bundle_length_offset = digest_offset + 32; + let bundle_len = usize::try_from(u64::from_le_bytes( + bytes[bundle_length_offset..bundle_length_offset + 8].try_into().unwrap(), + )) + .map_err(|_| anyhow::anyhow!("Flock proof length does not fit usize"))?; + if bundle_len == 0 || bundle_len > MAX_BUNDLE_BYTES { + bail!("invalid Flock Stage 2 transcript proof length {bundle_len}"); + } + let bundle_offset = bundle_length_offset + 8; + let declared_end = bundle_offset + .checked_add(bundle_len) + .ok_or_else(|| anyhow::anyhow!("transcript proof length overflow"))?; + if bytes.len() != declared_end { + bail!( + "Flock Stage 2 transcript artifact is {} bytes; header declares {declared_end}", + bytes.len() + ); + } + Ok(Self { + replay, + challenges, + circuit_digest, + proof_bundle_bytes: bytes[bundle_offset..].to_vec(), + }) + } +} + +#[derive(Serialize, Deserialize)] +struct TranscriptProofBundle { + commitment: Commitment, + proof: R1csProofCircuitMerged, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct HashSampleRow(F128); + +/// Convert digest bytes `[16..32]` into the first sampled extension element. +/// +/// For an input word `[LE(digest[16..24]), LE(digest[24..32])]`, popping +/// bytes from the end and then decoding each draw as LE produces +/// `[BE(digest[24..32]), BE(digest[16..24])]`. +#[derive(Clone, Copy, Debug)] +pub(crate) struct HashSampleGate { + pub(crate) nu: usize, +} + +impl GateType for HashSampleGate { + type Row = HashSampleRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_hash_sample_r1cs(self.nu)) + .with_io_schema(vec![IoWord::input(0), IoWord::output(1)]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let input = inputs[0]; + outputs.push(sample_word(input)); + HashSampleRow(input) + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_hash_sample_r1cs( + nu: usize, +) -> flock_prover::r1cs::BlockR1cs { + hash_sample_plan().block_r1cs(nu) +} + +#[cfg(test)] +pub(crate) fn generate_hash_sample_witness( + rows: &[HashSampleRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + generate_boolean_witness(hash_sample_plan(), rows, nu, |row, bits| { + write_f128(bits, SAMPLE_INPUT_BASE, row.0); + }) +} + +pub(crate) fn generate_hash_sample_witness_into( + rows: &[HashSampleRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + generate_boolean_witness_into( + hash_sample_plan(), + rows, + nu, + dst, + |row, bits| { + write_f128(bits, SAMPLE_INPUT_BASE, row.0); + }, + ) +} + +fn hash_sample_plan() -> &'static BooleanR1csPlan { + static PLAN: std::sync::OnceLock = + std::sync::OnceLock::new(); + PLAN.get_or_init(|| { + let mut builder = BooleanR1csBuilder::new(SAMPLE_K_LOG, SAMPLE_COLUMNS); + for column in SAMPLE_INPUT_BASE..SAMPLE_INPUT_BASE + 128 { + builder.free_boolean_at(column); + } + for output_bit in 0..128 { + let lane_bit = output_bit % 64; + let source_lane = if output_bit < 64 { 1 } else { 0 }; + let source = + SAMPLE_INPUT_BASE + source_lane * 64 + reverse_bytes_bit(lane_bit); + builder.write_product_of_parities( + SAMPLE_OUTPUT_BASE + output_bit, + &[source], + &[source], + ); + } + builder.finish() + }) +} + +const fn reverse_bytes_bit(bit: usize) -> usize { + (7 - bit / 8) * 8 + bit % 8 +} + +fn sample_word(input: F128) -> F128 { + F128::new(input.hi.swap_bytes(), input.lo.swap_bytes()) +} + +/// Eight raw draws from a digest and its chained refill, lowered to the first +/// two canonical Goldilocks values. The second result variant skips the first +/// raw draw, as required when commit-phase grinding consumes it before beta. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct GoldilocksSampleRow([F128; 4]); + +#[derive(Clone, Copy, Debug)] +pub(crate) struct GoldilocksSampleGate { + pub(crate) nu: usize, +} + +impl GateType for GoldilocksSampleGate { + type Row = GoldilocksSampleRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_goldilocks_sample_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::input(2), + IoWord::input(3), + IoWord::output(4), + IoWord::output(5), + IoWord::output(6), + IoWord::output(7), + IoWord::output(8), + IoWord::output(9), + IoWord::output(10), + IoWord::output(11), + IoWord::output(12), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let words = [inputs[0], inputs[1], inputs[2], inputs[3]]; + let candidates = digest_candidates(words); + let (sample, failure, used_refill) = select_two_candidates(&candidates, 4); + let (skip_sample, skip_failure, skip_used_refill) = + select_two_candidates(&candidates[1..], 3); + let state = + if used_refill { [words[3], words[2]] } else { [words[1], words[0]] }; + let skip_state = if skip_used_refill { + [words[3], words[2]] + } else { + [words[1], words[0]] + }; + outputs.extend_from_slice(&[ + sample, + F128::new(u64::from(failure), 0), + F128::new(candidates[0], 0), + skip_sample, + F128::new(u64::from(skip_failure), 0), + state[0], + state[1], + skip_state[0], + skip_state[1], + ]); + GoldilocksSampleRow(words) + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_goldilocks_sample_r1cs( + nu: usize, +) -> flock_prover::r1cs::BlockR1cs { + goldilocks_sample_plan().block_r1cs(nu) +} + +pub(crate) fn generate_goldilocks_sample_witness( + rows: &[GoldilocksSampleRow], + nu: usize, +) -> (Vec, Vec, Vec, Vec) { + generate_boolean_witness(goldilocks_sample_plan(), rows, nu, |row, bits| { + write_f128(bits, FIELD_SAMPLE_HIGH_BASE, row.0[0]); + write_f128(bits, FIELD_SAMPLE_LOW_BASE, row.0[1]); + write_f128(bits, FIELD_SAMPLE_REFILL_HIGH_BASE, row.0[2]); + write_f128(bits, FIELD_SAMPLE_REFILL_LOW_BASE, row.0[3]); + }) +} + +pub(crate) fn generate_goldilocks_sample_witness_into( + rows: &[GoldilocksSampleRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + generate_boolean_witness_into( + goldilocks_sample_plan(), + rows, + nu, + dst, + |row, bits| { + write_f128(bits, FIELD_SAMPLE_HIGH_BASE, row.0[0]); + write_f128(bits, FIELD_SAMPLE_LOW_BASE, row.0[1]); + write_f128(bits, FIELD_SAMPLE_REFILL_HIGH_BASE, row.0[2]); + write_f128(bits, FIELD_SAMPLE_REFILL_LOW_BASE, row.0[3]); + }, + ) +} + +fn goldilocks_sample_plan() -> &'static BooleanR1csPlan { + static PLAN: std::sync::OnceLock = + std::sync::OnceLock::new(); + PLAN.get_or_init(|| { + let mut builder = + BooleanR1csBuilder::new(FIELD_SAMPLE_K_LOG, FIELD_SAMPLE_COLUMNS); + let one = builder.alloc_constant_one(); + for column in FIELD_SAMPLE_HIGH_BASE..FIELD_SAMPLE_REFILL_LOW_BASE + 128 { + builder.free_boolean_at(column); + } + let candidate_bits = digest_candidate_columns(); + let rejection: Vec<_> = candidate_bits + .iter() + .map(|candidate| rejection_bit(&mut builder, candidate, one)) + .collect(); + let acceptance: Vec<_> = rejection + .iter() + .map(|&reject| builder.xor(&[reject, one], one)) + .collect(); + + let (first, second, failure) = + selection_masks(&mut builder, &acceptance, &rejection, one); + write_selected_word( + &mut builder, + FIELD_SAMPLE_OUTPUT_BASE, + &candidate_bits, + &first, + &second, + one, + ); + write_flag_word( + &mut builder, + FIELD_SAMPLE_FAILURE_BASE, + failure, + candidate_bits[0][0], + one, + ); + write_candidate_low_word( + &mut builder, + FIELD_SAMPLE_RAW_FIRST_BASE, + &candidate_bits[0], + one, + ); + let used_refill = + selection_uses_refill(&mut builder, &first, &second, 4, one); + write_selected_state( + &mut builder, + FIELD_SAMPLE_STATE_LOW_BASE, + FIELD_SAMPLE_STATE_HIGH_BASE, + used_refill, + one, + ); + + let (skip_first, skip_second, skip_failure) = + selection_masks(&mut builder, &acceptance[1..], &rejection[1..], one); + write_selected_word( + &mut builder, + FIELD_SAMPLE_SKIP_OUTPUT_BASE, + &candidate_bits[1..], + &skip_first, + &skip_second, + one, + ); + write_flag_word( + &mut builder, + FIELD_SAMPLE_SKIP_FAILURE_BASE, + skip_failure, + candidate_bits[0][0], + one, + ); + let skip_used_refill = + selection_uses_refill(&mut builder, &skip_first, &skip_second, 3, one); + write_selected_state( + &mut builder, + FIELD_SAMPLE_SKIP_STATE_LOW_BASE, + FIELD_SAMPLE_SKIP_STATE_HIGH_BASE, + skip_used_refill, + one, + ); + builder.finish() + }) +} + +fn digest_candidate_columns() -> [[usize; 64]; 8] { + [ + std::array::from_fn(|bit| { + FIELD_SAMPLE_HIGH_BASE + 64 + reverse_bytes_bit(bit) + }), + std::array::from_fn(|bit| FIELD_SAMPLE_HIGH_BASE + reverse_bytes_bit(bit)), + std::array::from_fn(|bit| { + FIELD_SAMPLE_LOW_BASE + 64 + reverse_bytes_bit(bit) + }), + std::array::from_fn(|bit| FIELD_SAMPLE_LOW_BASE + reverse_bytes_bit(bit)), + std::array::from_fn(|bit| { + FIELD_SAMPLE_REFILL_HIGH_BASE + 64 + reverse_bytes_bit(bit) + }), + std::array::from_fn(|bit| { + FIELD_SAMPLE_REFILL_HIGH_BASE + reverse_bytes_bit(bit) + }), + std::array::from_fn(|bit| { + FIELD_SAMPLE_REFILL_LOW_BASE + 64 + reverse_bytes_bit(bit) + }), + std::array::from_fn(|bit| { + FIELD_SAMPLE_REFILL_LOW_BASE + reverse_bytes_bit(bit) + }), + ] +} + +fn rejection_bit( + builder: &mut BooleanR1csBuilder, + candidate: &[usize; 64], + one: usize, +) -> usize { + let high_all = candidate[33..] + .iter() + .fold(candidate[32], |all, &bit| builder.and(all, bit)); + let low_any = candidate[1..32].iter().fold(candidate[0], |any, &bit| { + let both = builder.and(any, bit); + builder.xor(&[any, bit, both], one) + }); + builder.and(high_all, low_any) +} + +fn selection_masks( + builder: &mut BooleanR1csBuilder, + acceptance: &[usize], + rejection: &[usize], + one: usize, +) -> (Vec, Vec, usize) { + let zero = builder.xor(&[one, one], one); + let mut none = one; + let mut exactly_one = zero; + let mut first = Vec::with_capacity(acceptance.len()); + let mut second = Vec::with_capacity(acceptance.len()); + for (&accept, &reject) in acceptance.iter().zip(rejection) { + let first_here = builder.and(none, accept); + let second_here = builder.and(exactly_one, accept); + first.push(first_here); + second.push(second_here); + let one_stays = builder.and(exactly_one, reject); + exactly_one = builder.xor(&[one_stays, first_here], one); + none = builder.and(none, reject); + } + let failure = builder.xor(&[none, exactly_one], one); + (first, second, failure) +} + +fn selection_uses_refill( + builder: &mut BooleanR1csBuilder, + first: &[usize], + second: &[usize], + refill_start: usize, + one: usize, +) -> usize { + first[refill_start..].iter().chain(&second[refill_start..]).copied().fold( + builder.xor(&[one, one], one), + |used, mask| { + let both = builder.and(used, mask); + builder.xor(&[used, mask, both], one) + }, + ) +} + +fn write_selected_state( + builder: &mut BooleanR1csBuilder, + output_low_base: usize, + output_high_base: usize, + use_refill: usize, + one: usize, +) { + for (output_base, primary_base, refill_base) in [ + (output_low_base, FIELD_SAMPLE_LOW_BASE, FIELD_SAMPLE_REFILL_LOW_BASE), + (output_high_base, FIELD_SAMPLE_HIGH_BASE, FIELD_SAMPLE_REFILL_HIGH_BASE), + ] { + for bit in 0..128 { + let primary = primary_base + bit; + let refill = refill_base + bit; + let remove_primary = builder.and(use_refill, primary); + let add_refill = builder.and(use_refill, refill); + builder.write_xor( + output_base + bit, + &[primary, remove_primary, add_refill], + one, + ); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn write_selected_word( + builder: &mut BooleanR1csBuilder, + output_base: usize, + candidates: &[[usize; 64]], + first: &[usize], + second: &[usize], + one: usize, +) { + for bit in 0..64 { + let first_terms: Vec<_> = candidates + .iter() + .zip(first) + .map(|(candidate, &mask)| builder.and(candidate[bit], mask)) + .collect(); + let second_terms: Vec<_> = candidates + .iter() + .zip(second) + .map(|(candidate, &mask)| builder.and(candidate[bit], mask)) + .collect(); + builder.write_xor(output_base + bit, &first_terms, one); + builder.write_xor(output_base + 64 + bit, &second_terms, one); + } +} + +fn write_flag_word( + builder: &mut BooleanR1csBuilder, + output_base: usize, + flag: usize, + zero_source: usize, + one: usize, +) { + builder.write_product_of_parities(output_base, &[flag], &[flag]); + for bit in 1..128 { + builder.write_xor(output_base + bit, &[zero_source, zero_source], one); + } +} + +fn write_candidate_low_word( + builder: &mut BooleanR1csBuilder, + output_base: usize, + candidate: &[usize; 64], + one: usize, +) { + for (bit, &candidate_bit) in candidate.iter().enumerate() { + builder.write_product_of_parities( + output_base + bit, + &[candidate_bit], + &[candidate_bit], + ); + } + for bit in 64..128 { + builder.write_xor(output_base + bit, &[candidate[0], candidate[0]], one); + } +} + +fn digest_candidates(words: [F128; 4]) -> [u64; 8] { + [ + words[0].hi.swap_bytes(), + words[0].lo.swap_bytes(), + words[1].hi.swap_bytes(), + words[1].lo.swap_bytes(), + words[2].hi.swap_bytes(), + words[2].lo.swap_bytes(), + words[3].hi.swap_bytes(), + words[3].lo.swap_bytes(), + ] +} + +fn select_two_candidates( + candidates: &[u64], + refill_start: usize, +) -> (F128, bool, bool) { + let accepted: Vec<_> = candidates + .iter() + .copied() + .enumerate() + .filter(|&(_, candidate)| candidate < GOLDILOCKS_MODULUS) + .take(2) + .collect(); + if accepted.len() == 2 { + ( + F128::new(accepted[0].1, accepted[1].1), + false, + accepted[1].0 >= refill_start, + ) + } else { + (F128::ZERO, true, true) + } +} + +const U64_SPLIT_K_LOG: usize = 9; +const U64_SPLIT_INPUT_BASE: usize = 0; +const U64_SPLIT_BIT_BASE: usize = 128; +const U64_SPLIT_QUOTIENT_BASE: usize = 256; +const U64_SPLIT_COLUMNS: usize = 384; + +/// Split a low-lane `u64` into its least-significant bit and the remaining +/// quotient. Repeating this gate exposes exactly the low bits consumed by +/// `SerializingChallenger64::sample_bits`, without making those bits separate +/// public inputs. +#[derive(Clone, Copy, Debug)] +pub(crate) struct U64SplitGate { + pub(crate) nu: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct U64SplitRow(F128); + +impl GateType for U64SplitGate { + type Row = U64SplitRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_u64_split_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::output(1), + IoWord::output(2), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let input = inputs[0]; + assert_eq!(input.hi, 0, "u64 split input must occupy the low lane"); + outputs.push(F128::new(input.lo & 1, 0)); + outputs.push(F128::new(input.lo >> 1, 0)); + U64SplitRow(input) + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_u64_split_r1cs(nu: usize) -> flock_prover::r1cs::BlockR1cs { + u64_split_plan().block_r1cs(nu) +} + +pub(crate) fn generate_u64_split_witness_into( + rows: &[U64SplitRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + generate_boolean_witness_into(u64_split_plan(), rows, nu, dst, |row, bits| { + write_f128(bits, U64_SPLIT_INPUT_BASE, row.0); + }) +} + +fn u64_split_plan() -> &'static BooleanR1csPlan { + static PLAN: std::sync::OnceLock = + std::sync::OnceLock::new(); + PLAN.get_or_init(|| { + let mut builder = + BooleanR1csBuilder::new(U64_SPLIT_K_LOG, U64_SPLIT_COLUMNS); + let one = builder.alloc_constant_one(); + for bit in 0..64 { + builder.free_boolean_at(U64_SPLIT_INPUT_BASE + bit); + } + for bit in 64..128 { + builder.assert_zero_at(U64_SPLIT_INPUT_BASE + bit, one); + } + + let write_copy = |builder: &mut BooleanR1csBuilder, output, source| { + builder.write_product_of_parities(output, &[source], &[source]); + }; + let write_zero = |builder: &mut BooleanR1csBuilder, output| { + builder.write_xor( + output, + &[U64_SPLIT_INPUT_BASE, U64_SPLIT_INPUT_BASE], + one, + ); + }; + + write_copy(&mut builder, U64_SPLIT_BIT_BASE, U64_SPLIT_INPUT_BASE); + for bit in 1..128 { + write_zero(&mut builder, U64_SPLIT_BIT_BASE + bit); + } + for bit in 0..63 { + write_copy( + &mut builder, + U64_SPLIT_QUOTIENT_BASE + bit, + U64_SPLIT_INPUT_BASE + bit + 1, + ); + } + for bit in 63..128 { + write_zero(&mut builder, U64_SPLIT_QUOTIENT_BASE + bit); + } + builder.finish() + }) +} + +#[derive(Clone, Copy)] +pub(crate) struct TranscriptCircuitSlots { + pub(crate) blake3: SlotId, + pub(crate) sample: SlotId, + pub(crate) canonical: SlotId, +} + +#[derive(Clone, Copy)] +pub(crate) struct TranscriptChallengeWires { + pub(crate) lookup: Wire, + pub(crate) fingerprint: Wire, + pub(crate) constraint: Wire, + pub(crate) zeta: Wire, + pub(crate) pcs_alpha: Wire, +} + +impl TranscriptChallengeWires { + pub(crate) fn all(self) -> [Wire; CHALLENGE_COUNT] { + [self.lookup, self.fingerprint, self.constraint, self.zeta, self.pcs_alpha] + } +} + +pub(crate) struct TranscriptConstraintRegion { + pub(crate) inputs: Vec, + pub(crate) challenges: TranscriptChallengeWires, + /// Packed public words for each of the four observation segments. These are + /// the same wires passed to the BLAKE3 transcript gates. + pub(crate) observation_words: Vec>, + /// HashChallenger input state after sampling the PCS challenge. The low + /// half of this digest remains in the output buffer until the next + /// observation; every valid FRI proof immediately observes a non-empty cap. + pub(crate) state_digest: [Wire; 2], +} + +struct TranscriptRelation { + shape: CircuitShape, + slots: TranscriptCircuitSlots, + nu: usize, + inputs: Vec, +} + +impl TranscriptRelation { + fn build(replay: &Stage2TranscriptReplayV1) -> Result { + let nu = transcript_nu(replay)?; + let mut builder = ShapeBuilder::new(nu); + let slots = TranscriptCircuitSlots { + blake3: builder.slot(Blake3Gate { nu }), + sample: builder.slot(GoldilocksSampleGate { nu }), + canonical: builder.slot(CanonicalGoldilocksQuadGate { nu }), + }; + let region = constrain_stage2_transcript(&mut builder, slots, replay, nu)?; + for challenge in region.challenges.all() { + builder.publish(challenge); + } + let shape = builder.finish().map_err(|error| { + anyhow::anyhow!("build Flock Stage 2 transcript circuit: {error:?}") + })?; + Ok(Self { shape, slots, nu, inputs: region.inputs }) + } + + fn public(&self, challenges: Stage2TranscriptChallengesV1) -> Vec { + let mut public = self.inputs.clone(); + public.extend(challenge_words(challenges)); + public + } +} + +pub(crate) fn transcript_nu( + replay: &Stage2TranscriptReplayV1, +) -> Result { + let traces = transcript_traces(replay)?; + let blake3_rows = traces.iter().map(|trace| trace.rows.len()).sum::() + + CHALLENGE_COUNT * hash_trace(32).rows.len(); + let needed_rows = blake3_rows.max(CHALLENGE_COUNT).max(1); + let nu = MIN_NU.max(needed_rows.next_power_of_two().ilog2() as usize); + if nu > MAX_NU { + bail!( + "Stage 2 transcript needs {blake3_rows} BLAKE3 rows (nu={nu}); maximum is nu={MAX_NU}" + ); + } + Ok(nu) +} + +pub(crate) fn constrain_stage2_transcript( + builder: &mut impl CircuitEmitter, + slots: TranscriptCircuitSlots, + replay: &Stage2TranscriptReplayV1, + nu: usize, +) -> Result { + let traces = transcript_traces(replay)?; + let row_count = traces.iter().map(|trace| trace.rows.len()).sum::() + + CHALLENGE_COUNT * hash_trace(32).rows.len(); + if row_count > 1usize << nu { + bail!("Stage 2 transcript exceeds the supplied Flock row capacity"); + } + let segment_wires: Vec> = replay_segments(replay) + .iter() + .map(|segment| { + (0..segment.len().div_ceil(WORD_BYTES)) + .map(|_| builder.public_input()) + .collect() + }) + .collect(); + + let mut inputs: Vec = + replay_segments(replay).into_iter().flat_map(pack_segment).collect(); + let packed_iv = pack8(&IV); + let iv = [ + fixed(builder, &mut inputs, packed_iv[0]), + fixed(builder, &mut inputs, packed_iv[1]), + ]; + // Data padding is only consumed; assertion zero only receives residual + // outputs. Keeping the wiring classes separate preserves a directed DAG. + let data_zero = fixed(builder, &mut inputs, F128::ZERO); + let assertion_zero = fixed(builder, &mut inputs, F128::ZERO); + let parameter_wires: Vec> = traces + .iter() + .map(|trace| { + trace + .rows + .iter() + .map(|&(_cv, _message, counter, block_len, flags)| { + fixed(builder, &mut inputs, pack_params(counter, block_len, flags)) + }) + .collect() + }) + .collect(); + + let digest_1 = constrain_hash( + builder, + slots.blake3, + &traces[0], + ¶meter_wires[0], + iv, + data_zero, + &segment_wires[0], + )?; + let sampled = constrain_field_sample( + builder, + slots.blake3, + slots.sample, + slots.canonical, + assertion_zero, + iv, + data_zero, + &mut inputs, + digest_1, + false, + )?; + let lookup = sampled.value; + let state_1 = sampled.state; + + let digest_2 = constrain_hash( + builder, + slots.blake3, + &traces[1], + ¶meter_wires[1], + iv, + data_zero, + &[state_1[0], state_1[1], lookup], + )?; + let sampled = constrain_field_sample( + builder, + slots.blake3, + slots.sample, + slots.canonical, + assertion_zero, + iv, + data_zero, + &mut inputs, + digest_2, + false, + )?; + let fingerprint = sampled.value; + let state_2 = sampled.state; + + let mut message_3 = vec![state_2[0], state_2[1], fingerprint]; + message_3.extend_from_slice(&segment_wires[1]); + let digest_3 = constrain_hash( + builder, + slots.blake3, + &traces[2], + ¶meter_wires[2], + iv, + data_zero, + &message_3, + )?; + let sampled = constrain_field_sample( + builder, + slots.blake3, + slots.sample, + slots.canonical, + assertion_zero, + iv, + data_zero, + &mut inputs, + digest_3, + false, + )?; + let constraint = sampled.value; + let state_3 = sampled.state; + + let mut message_4 = vec![state_3[0], state_3[1]]; + message_4.extend_from_slice(&segment_wires[2]); + let digest_4 = constrain_hash( + builder, + slots.blake3, + &traces[3], + ¶meter_wires[3], + iv, + data_zero, + &message_4, + )?; + let sampled = constrain_field_sample( + builder, + slots.blake3, + slots.sample, + slots.canonical, + assertion_zero, + iv, + data_zero, + &mut inputs, + digest_4, + false, + )?; + let zeta = sampled.value; + let state_4 = sampled.state; + + let mut message_5 = vec![state_4[0], state_4[1]]; + message_5.extend_from_slice(&segment_wires[3]); + let digest_5 = constrain_hash( + builder, + slots.blake3, + &traces[4], + ¶meter_wires[4], + iv, + data_zero, + &message_5, + )?; + let sampled = constrain_field_sample( + builder, + slots.blake3, + slots.sample, + slots.canonical, + assertion_zero, + iv, + data_zero, + &mut inputs, + digest_5, + false, + )?; + let pcs_alpha = sampled.value; + Ok(TranscriptConstraintRegion { + inputs, + challenges: TranscriptChallengeWires { + lookup, + fingerprint, + constraint, + zeta, + pcs_alpha, + }, + observation_words: segment_wires, + state_digest: sampled.state, + }) +} + +#[derive(Clone, Copy)] +pub(crate) struct FriTranscriptCircuitSlots { + pub(crate) blake3: SlotId, + pub(crate) sample: SlotId, + pub(crate) field_sample: SlotId, + pub(crate) canonical: SlotId, + pub(crate) repack: SlotId, + pub(crate) split: SlotId, +} + +pub(crate) struct FriTranscriptConstraintRegion { + pub(crate) inputs: Vec, + pub(crate) betas: Vec, + pub(crate) query_index_bits: Vec>, + pub(crate) commitment_roots: Vec>, + pub(crate) final_polynomial: Vec, +} + +/// Continue an already constrained Stage 2 transcript through every FRI +/// challenge and query draw. The returned beta/index wires are intended to be +/// consumed directly by the PCS/FRI verifier relation. +pub(crate) fn constrain_stage2_fri_transcript( + builder: &mut impl CircuitEmitter, + slots: FriTranscriptCircuitSlots, + replay: &Stage2FriTranscriptReplayV1, + initial_digest: [Wire; 2], + nu: usize, +) -> Result { + validate_fri_replay(replay)?; + let capacity = 1usize << nu; + if fri_transcript_blake3_rows(replay)? > capacity + || fri_transcript_split_rows(replay)? > capacity + { + bail!("Stage 2 FRI transcript exceeds the supplied Flock row capacity"); + } + let mut inputs = Vec::new(); + let packed_iv = pack8(&IV); + let iv = [ + fixed(builder, &mut inputs, packed_iv[0]), + fixed(builder, &mut inputs, packed_iv[1]), + ]; + let data_zero = fixed(builder, &mut inputs, F128::ZERO); + let assertion_zero = fixed(builder, &mut inputs, F128::ZERO); + + let mut state = initial_digest; + let mut betas = Vec::with_capacity(replay.commit_phase_commitments.len()); + let mut commitment_roots = + Vec::with_capacity(replay.commit_phase_commitments.len()); + for (round, cap) in replay.commit_phase_commitments.iter().enumerate() { + let cap_bytes = cap_observation_bytes(cap); + let cap_words = declare_public_segment(builder, &mut inputs, &cap_bytes); + commitment_roots.push(cap_words.as_chunks::<2>().0.to_vec()); + let mut message = Vec::with_capacity( + 2 + cap_words.len() + usize::from(replay.commit_pow_bits != 0), + ); + message.extend_from_slice(&state); + message.extend_from_slice(&cap_words); + if replay.commit_pow_bits != 0 { + message.push(declare_public_word( + builder, + &mut inputs, + F128::new(replay.commit_pow_witnesses[round], 0), + )); + } + let message_len = + 32 + cap_bytes.len() + usize::from(replay.commit_pow_bits != 0) * 8; + let trace = hash_trace(message_len); + let parameters = declare_trace_parameters(builder, &mut inputs, &trace); + let digest = constrain_hash( + builder, + slots.blake3, + &trace, + ¶meters, + iv, + data_zero, + &message, + )?; + + let sampled = constrain_field_sample( + builder, + slots.blake3, + slots.field_sample, + slots.canonical, + assertion_zero, + iv, + data_zero, + &mut inputs, + digest, + replay.commit_pow_bits != 0, + )?; + if replay.commit_pow_bits != 0 { + constrain_low_zero_bits( + builder, + slots.split, + assertion_zero, + sampled.raw_first, + replay.commit_pow_bits, + ); + } + betas.push(sampled.value); + state = sampled.state; + } + + let mut final_suffix = final_observation_bytes(replay); + if replay.query_pow_bits != 0 { + push_u64_observation(&mut final_suffix, replay.query_pow_witness); + } + let final_words = declare_public_segment(builder, &mut inputs, &final_suffix); + let final_polynomial = final_words[..replay.final_polynomial.len()].to_vec(); + let mut final_message = Vec::with_capacity(2 + final_words.len()); + final_message.extend_from_slice(&state); + final_message.extend_from_slice(&final_words); + let final_trace = hash_trace(32 + final_suffix.len()); + let final_parameters = + declare_trace_parameters(builder, &mut inputs, &final_trace); + state = constrain_hash( + builder, + slots.blake3, + &final_trace, + &final_parameters, + iv, + data_zero, + &final_message, + )?; + + let draw_count = replay + .num_queries + .checked_add(usize::from(replay.query_pow_bits != 0)) + .ok_or_else(|| anyhow::anyhow!("FRI transcript draw count overflow"))?; + let digest_count = draw_count.div_ceil(4); + let mut draws = Vec::with_capacity(4 * digest_count); + for digest_index in 0..digest_count { + if digest_index != 0 { + let trace = hash_trace(32); + let parameters = declare_trace_parameters(builder, &mut inputs, &trace); + state = constrain_hash( + builder, + slots.blake3, + &trace, + ¶meters, + iv, + data_zero, + &state, + )?; + } + let high_samples = builder.gate(slots.sample, &[state[1]])[0]; + let low_samples = builder.gate(slots.sample, &[state[0]])[0]; + draws.extend(split_sample_lanes( + builder, + slots.repack, + data_zero, + high_samples, + )); + draws.extend(split_sample_lanes( + builder, + slots.repack, + data_zero, + low_samples, + )); + } + draws.truncate(draw_count); + let query_draws = if replay.query_pow_bits == 0 { + &draws[..] + } else { + constrain_low_zero_bits( + builder, + slots.split, + assertion_zero, + draws[0], + replay.query_pow_bits, + ); + &draws[1..] + }; + let query_index_bits = query_draws + .iter() + .map(|&draw| { + split_low_bits(builder, slots.split, draw, replay.query_index_bits) + }) + .collect(); + + Ok(FriTranscriptConstraintRegion { + inputs, + betas, + query_index_bits, + commitment_roots, + final_polynomial, + }) +} + +fn declare_public_segment( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + bytes: &[u8], +) -> Vec { + pack_segment(bytes) + .into_iter() + .map(|word| declare_public_word(builder, inputs, word)) + .collect() +} + +fn declare_public_word( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + value: F128, +) -> Wire { + inputs.push(value); + builder.public_input() +} + +fn declare_trace_parameters( + builder: &mut impl CircuitEmitter, + inputs: &mut Vec, + trace: &FsChainTrace, +) -> Vec { + trace + .rows + .iter() + .map(|&(_cv, _message, counter, block_len, flags)| { + fixed(builder, inputs, pack_params(counter, block_len, flags)) + }) + .collect() +} + +fn split_sample_lanes( + builder: &mut impl CircuitEmitter, + repack_slot: SlotId, + zero: Wire, + samples: Wire, +) -> [Wire; 2] { + let repacked = builder.gate(repack_slot, &[samples, zero]); + let low = repacked[3]; + let high_duplicate = repacked[1]; + let high = builder.gate(repack_slot, &[high_duplicate, zero])[3]; + [low, high] +} + +fn split_low_bits( + builder: &mut impl CircuitEmitter, + split_slot: SlotId, + mut value: Wire, + bits: u8, +) -> Vec { + (0..bits) + .map(|_| { + let outputs = builder.gate(split_slot, &[value]); + value = outputs[1]; + outputs[0] + }) + .collect() +} + +fn constrain_low_zero_bits( + builder: &mut impl CircuitEmitter, + split_slot: SlotId, + zero: Wire, + value: Wire, + bits: u8, +) { + for bit in split_low_bits(builder, split_slot, value, bits) { + builder.connect(zero, bit); + } +} + +fn fixed( + builder: &mut impl CircuitEmitter, + fixed_inputs: &mut Vec, + value: F128, +) -> Wire { + fixed_inputs.push(value); + builder.fixed_public_input(value) +} + +struct ConstrainedFieldSample { + value: Wire, + raw_first: Wire, + state: [Wire; 2], +} + +#[allow(clippy::too_many_arguments)] +fn constrain_field_sample( + builder: &mut impl CircuitEmitter, + blake3: SlotId, + sample: SlotId, + canonical: SlotId, + zero: Wire, + iv: [Wire; 2], + data_zero: Wire, + inputs: &mut Vec, + digest: [Wire; 2], + skip_first: bool, +) -> Result { + let trace = hash_trace(32); + let parameters = declare_trace_parameters(builder, inputs, &trace); + let refill = constrain_hash( + builder, + blake3, + &trace, + ¶meters, + iv, + data_zero, + &digest, + )?; + let sampled = + builder.gate(sample, &[digest[1], digest[0], refill[1], refill[0]]); + let (challenge, failure, state) = if skip_first { + (sampled[3], sampled[4], [sampled[7], sampled[8]]) + } else { + (sampled[0], sampled[1], [sampled[5], sampled[6]]) + }; + builder.connect(zero, failure); + let violation = builder.gate(canonical, &[challenge, data_zero])[0]; + builder.connect(zero, violation); + Ok(ConstrainedFieldSample { value: challenge, raw_first: sampled[2], state }) +} + +pub(crate) fn constrain_hash( + builder: &mut impl CircuitEmitter, + slot: SlotId, + trace: &FsChainTrace, + parameters: &[Wire], + iv: [Wire; 2], + zero: Wire, + message: &[Wire], +) -> Result<[Wire; 2]> { + if parameters.len() != trace.rows.len() { + bail!("BLAKE3 trace parameter count mismatch"); + } + let mut row_outputs = Vec::<[Wire; 4]>::with_capacity(trace.rows.len()); + for (row_index, ¶meter) in parameters.iter().enumerate() { + let link = trace.links[row_index]; + let (cv, block) = if let Some(right_row) = link.right { + let CvSource::Row(left_row) = link.cv else { + bail!("BLAKE3 parent row does not name its left child"); + }; + let left = row_outputs + .get(left_row) + .ok_or_else(|| anyhow::anyhow!("BLAKE3 left-child link is forward"))?; + let right = row_outputs + .get(right_row) + .ok_or_else(|| anyhow::anyhow!("BLAKE3 right-child link is forward"))?; + (iv, [left[0], left[1], right[0], right[1]]) + } else { + if link.repeats.is_some() { + bail!("unexpected BLAKE3 XOF row in a 32-byte transcript hash"); + } + let cv = match link.cv { + CvSource::Iv => iv, + CvSource::Row(source) => { + let output = row_outputs.get(source).ok_or_else(|| { + anyhow::anyhow!("BLAKE3 chaining-value link is forward") + })?; + [output[0], output[1]] + }, + CvSource::RowHi(source) => { + let output = row_outputs.get(source).ok_or_else(|| { + anyhow::anyhow!("BLAKE3 high-half link is forward") + })?; + [output[2], output[3]] + }, + }; + let offset = trace.block_offsets[row_index].ok_or_else(|| { + anyhow::anyhow!("BLAKE3 data row is missing its message offset") + })?; + if offset % WORD_BYTES != 0 { + bail!("BLAKE3 message block is not word aligned"); + } + let first_word = offset / WORD_BYTES; + let block = std::array::from_fn(|word| { + message.get(first_word + word).copied().unwrap_or(zero) + }); + (cv, block) + }; + let outputs = builder.gate( + slot, + &[cv[0], cv[1], block[0], block[1], block[2], block[3], parameter], + ); + row_outputs.push(outputs.try_into().expect("BLAKE3 gate has four outputs")); + } + let root_row = *trace + .squeezes + .first() + .and_then(|rows| rows.first()) + .ok_or_else(|| anyhow::anyhow!("BLAKE3 trace has no root squeeze"))?; + let root = row_outputs + .get(root_row) + .ok_or_else(|| anyhow::anyhow!("BLAKE3 root row is out of range"))?; + Ok([root[0], root[1]]) +} + +/// Prove exact Stage 2 transcript replay through the PCS opening-batch sample. +pub fn prove_stage2_transcript_conformance( + replay: &Stage2TranscriptReplayV1, +) -> Result { + let challenges = compute_challenges(replay)?; + let relation = TranscriptRelation::build(replay)?; + let inputs = relation.inputs.clone(); + let expected_public = relation.public(challenges); + let witness = relation.shape.run(&inputs, &[]); + if witness.public != expected_public { + bail!("Flock transcript circuit disagrees with native HashChallenger"); + } + + let proof_bundle_bytes = prove_relation(&relation, &witness)?; + Ok(Stage2TranscriptConformanceArtifactV1 { + replay: replay.clone(), + challenges, + circuit_digest: relation.shape.circuit.digest(), + proof_bundle_bytes, + }) +} + +/// Verify and bind a transcript conformance proof to every observation and +/// sampled challenge carried by its strict artifact. +pub fn verify_stage2_transcript_conformance( + artifact: &Stage2TranscriptConformanceArtifactV1, +) -> Result<()> { + let challenges = compute_challenges(&artifact.replay)?; + if challenges != artifact.challenges { + bail!("Flock Stage 2 transcript artifact challenge mismatch"); + } + let relation = TranscriptRelation::build(&artifact.replay)?; + if relation.shape.circuit.digest() != artifact.circuit_digest { + bail!("Flock Stage 2 transcript circuit digest mismatch"); + } + let public = relation.public(artifact.challenges); + verify_relation(&relation, &public, &artifact.proof_bundle_bytes) +} + +fn prove_relation( + relation: &TranscriptRelation, + witness: &flock_prover::circuit::builder::CircuitWitness, +) -> Result> { + let blake3_rows = witness.rows::(relation.slots.blake3); + let sample_rows = witness.rows::(relation.slots.sample); + let canonical_rows = + witness.rows::(relation.slots.canonical); + + let blake3_r1cs = flock_blake3::build_block_r1cs(relation.nu); + let blake3_lincheck = blake3_r1cs.csc_lincheck_circuit(); + let sample_r1cs = build_goldilocks_sample_r1cs(relation.nu); + let sample_lincheck = sample_r1cs.csc_lincheck_circuit(); + let canonical_r1cs = build_canonical_quad_r1cs(relation.nu); + let canonical_lincheck = canonical_r1cs.csc_lincheck_circuit(); + + let mut slots = vec![ + ( + relation.shape.registry_slot(relation.slots.blake3), + UnionSlotProverInput::new( + flock_blake3::generate_witness_batch_major_partial( + blake3_rows, + relation.nu, + ), + blake3_lincheck, + ), + ), + ( + relation.shape.registry_slot(relation.slots.sample), + UnionSlotProverInput::new( + generate_goldilocks_sample_witness(sample_rows, relation.nu), + sample_lincheck, + ), + ), + ( + relation.shape.registry_slot(relation.slots.canonical), + UnionSlotProverInput::new( + generate_canonical_quad_witness(canonical_rows, relation.nu), + canonical_lincheck, + ), + ), + ]; + sort_slots(&mut slots)?; + let slots = slots.into_iter().map(|(_, input)| input).collect(); + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = FsChallenger::with_chained_blake3( + STAGE2_TRANSCRIPT_CONFORMANCE_TRANSCRIPT_DOMAIN, + ); + let (proof, commitment, _) = prover::prove_fast_ligerito_union_circuit( + &union, + &relation.shape.circuit, + &witness.public, + ¶ms, + slots, + Vec::new(), + &mut challenger, + ); + let bytes = encode_bundle(&TranscriptProofBundle { commitment, proof })?; + if bytes.len() > MAX_BUNDLE_BYTES { + bail!("Flock Stage 2 transcript proof exceeds {MAX_BUNDLE_BYTES} bytes"); + } + Ok(bytes) +} + +fn verify_relation( + relation: &TranscriptRelation, + public: &[F128], + proof_bundle_bytes: &[u8], +) -> Result<()> { + let bundle = decode_bundle(proof_bundle_bytes) + .context("decode Flock Stage 2 transcript proof bundle")?; + let blake3_r1cs = flock_blake3::build_block_r1cs(relation.nu); + let blake3_lincheck = blake3_r1cs.csc_lincheck_circuit(); + let sample_r1cs = build_goldilocks_sample_r1cs(relation.nu); + let sample_lincheck = sample_r1cs.csc_lincheck_circuit(); + let canonical_r1cs = build_canonical_quad_r1cs(relation.nu); + let canonical_lincheck = canonical_r1cs.csc_lincheck_circuit(); + let mut linchecks: Vec<(usize, &dyn LincheckCircuit)> = vec![ + (relation.shape.registry_slot(relation.slots.blake3), blake3_lincheck), + (relation.shape.registry_slot(relation.slots.sample), sample_lincheck), + ( + relation.shape.registry_slot(relation.slots.canonical), + canonical_lincheck, + ), + ]; + sort_slots(&mut linchecks)?; + let linchecks: Vec<&dyn LincheckCircuit> = + linchecks.into_iter().map(|(_, lincheck)| lincheck).collect(); + let union = + UnionInstance::new(&relation.shape.registry, relation.shape.counts.clone()); + let params = pcs_params(&union); + let mut challenger = FsChallenger::with_chained_blake3( + STAGE2_TRANSCRIPT_CONFORMANCE_TRANSCRIPT_DOMAIN, + ); + verifier::verify_ligerito_union_circuit( + &union, + &relation.shape.circuit, + public, + &linchecks, + &bundle.commitment, + &bundle.proof, + ¶ms, + &mut challenger, + ) + .map_err(|error| { + anyhow::anyhow!("Flock Stage 2 transcript proof rejected: {error:?}") + })?; + Ok(()) +} + +fn sort_slots(slots: &mut [(usize, T)]) -> Result<()> { + slots.sort_by_key(|(index, _)| *index); + if slots.iter().enumerate().any(|(expected, (actual, _))| expected != *actual) + { + bail!("Flock Stage 2 transcript registry is not contiguous"); + } + Ok(()) +} + +pub(crate) fn hash_trace(message_len: usize) -> FsChainTrace { + let mut chain = FsChain::new(); + chain.absorb(&vec![0u8; message_len]); + let output = chain.finalize(32); + debug_assert_eq!(output.len(), 32); + chain.finish() +} + +fn transcript_traces( + replay: &Stage2TranscriptReplayV1, +) -> Result> { + validate_replay(replay)?; + let message_lengths = [ + replay.initial_observations.len(), + 32 + 16, + 32 + 16 + replay.stage2_and_accumulator_observations.len(), + 32 + replay.quotient_commitment_observations.len(), + 32 + replay.pcs_opening_observations.len(), + ]; + Ok(message_lengths.iter().copied().map(hash_trace).collect()) +} + +fn compute_challenges( + replay: &Stage2TranscriptReplayV1, +) -> Result { + Ok(compute_challenges_and_state(replay)?.0) +} + +fn compute_challenges_and_state( + replay: &Stage2TranscriptReplayV1, +) -> Result<(Stage2TranscriptChallengesV1, [u8; 32])> { + validate_replay(replay)?; + + let digest_1 = hash_parts(&[&replay.initial_observations]); + let (lookup, state_1) = sample_digest_high(&digest_1)?; + let lookup_bytes = extension_bytes(lookup); + + let digest_2 = hash_parts(&[&state_1, &lookup_bytes]); + let (fingerprint, state_2) = sample_digest_high(&digest_2)?; + let fingerprint_bytes = extension_bytes(fingerprint); + + let digest_3 = hash_parts(&[ + &state_2, + &fingerprint_bytes, + &replay.stage2_and_accumulator_observations, + ]); + let (constraint, state_3) = sample_digest_high(&digest_3)?; + + let digest_4 = + hash_parts(&[&state_3, &replay.quotient_commitment_observations]); + let (zeta, state_4) = sample_digest_high(&digest_4)?; + + let digest_5 = hash_parts(&[&state_4, &replay.pcs_opening_observations]); + let (pcs_alpha, state_5) = sample_digest_high(&digest_5)?; + Ok(( + Stage2TranscriptChallengesV1 { + lookup, + fingerprint, + constraint, + zeta, + pcs_alpha, + }, + state_5, + )) +} + +fn compute_fri_challenges( + prefix: &Stage2TranscriptReplayV1, + replay: &Stage2FriTranscriptReplayV1, +) -> Result { + validate_fri_replay(replay)?; + let (_, state) = compute_challenges_and_state(prefix)?; + let mut challenger = NativeByteChallenger { + input: state.to_vec(), + // Sampling the PCS extension challenge consumed the high sixteen bytes. + output: state[..16].to_vec(), + }; + let mut betas = Vec::with_capacity(replay.commit_phase_commitments.len()); + for (round, cap) in replay.commit_phase_commitments.iter().enumerate() { + challenger.observe(&cap_observation_bytes(cap)); + if !challenger + .check_witness(replay.commit_pow_bits, replay.commit_pow_witnesses[round]) + { + bail!("Stage 2 FRI commit PoW witness {round} is invalid"); + } + betas.push(challenger.sample_extension()?); + } + challenger.observe(&final_observation_bytes(replay)); + if !challenger.check_witness(replay.query_pow_bits, replay.query_pow_witness) + { + bail!("Stage 2 FRI query PoW witness is invalid"); + } + let query_indices = (0..replay.num_queries) + .map(|_| challenger.sample_bits(replay.query_index_bits)) + .collect(); + Ok(Stage2FriTranscriptChallengesV1 { betas, query_indices }) +} + +struct NativeByteChallenger { + input: Vec, + output: Vec, +} + +impl NativeByteChallenger { + fn observe(&mut self, bytes: &[u8]) { + if bytes.is_empty() { + return; + } + self.output.clear(); + self.input.extend_from_slice(bytes); + } + + fn sample_u64(&mut self) -> u64 { + if self.output.is_empty() { + let digest = *blake3::hash(&self.input).as_bytes(); + self.input = digest.to_vec(); + self.output = digest.to_vec(); + } + let bytes: [u8; 8] = + std::array::from_fn(|_| self.output.pop().expect("fresh digest bytes")); + u64::from_le_bytes(bytes) + } + + fn sample_extension(&mut self) -> Result<[u64; 2]> { + let mut accepted = Vec::with_capacity(2); + loop { + let value = self.sample_u64(); + if value < GOLDILOCKS_MODULUS { + accepted.push(value); + if accepted.len() == 2 { + return Ok([accepted[0], accepted[1]]); + } + } + } + } + + fn sample_bits(&mut self, bits: u8) -> u64 { + debug_assert!(bits < 64); + self.sample_u64() & ((1u64 << bits) - 1) + } + + fn check_witness(&mut self, bits: u8, witness: u64) -> bool { + if bits == 0 { + return true; + } + self.observe(&witness.to_le_bytes()); + self.sample_bits(bits) == 0 + } +} + +fn cap_observation_bytes(cap: &[[u8; 32]]) -> Vec { + cap.iter().flatten().copied().collect() +} + +fn final_observation_bytes(replay: &Stage2FriTranscriptReplayV1) -> Vec { + let mut bytes = Vec::with_capacity( + 16 * replay.final_polynomial.len() + 8 * replay.log_arities.len(), + ); + for &coefficient in &replay.final_polynomial { + push_extension_observation(&mut bytes, coefficient); + } + for &log_arity in &replay.log_arities { + push_u64_observation(&mut bytes, u64::from(log_arity)); + } + bytes +} + +pub(crate) fn fri_transcript_blake3_rows( + replay: &Stage2FriTranscriptReplayV1, +) -> Result { + validate_fri_replay(replay)?; + let commit_rows = + replay.commit_phase_commitments.iter().try_fold(0usize, |rows, cap| { + let message_len = + 32 + 32 * cap.len() + 8 * usize::from(replay.commit_pow_bits != 0); + rows + .checked_add(hash_trace(message_len).rows.len()) + .and_then(|rows| rows.checked_add(hash_trace(32).rows.len())) + .ok_or_else(|| anyhow::anyhow!("FRI transcript row count overflow")) + })?; + let final_rows = hash_trace( + 32 + final_observation_bytes(replay).len() + + 8 * usize::from(replay.query_pow_bits != 0), + ) + .rows + .len(); + let draws = replay.num_queries + usize::from(replay.query_pow_bits != 0); + let followup_digests = draws.div_ceil(4).saturating_sub(1); + commit_rows + .checked_add(final_rows) + .and_then(|rows| { + rows.checked_add(followup_digests * hash_trace(32).rows.len()) + }) + .ok_or_else(|| anyhow::anyhow!("FRI transcript row count overflow")) +} + +pub(crate) fn fri_transcript_split_rows( + replay: &Stage2FriTranscriptReplayV1, +) -> Result { + validate_fri_replay(replay)?; + usize::from(replay.commit_pow_bits) + .checked_mul(replay.commit_phase_commitments.len()) + .and_then(|rows| rows.checked_add(usize::from(replay.query_pow_bits))) + .and_then(|rows| { + rows + .checked_add(replay.num_queries * usize::from(replay.query_index_bits)) + }) + .ok_or_else(|| anyhow::anyhow!("FRI transcript split row count overflow")) +} + +fn sample_digest_high(digest: &[u8; 32]) -> Result<([u64; 2], [u8; 32])> { + let refill = hash_parts(&[digest]); + let candidates = digest_candidates([ + pack_bytes(&digest[16..]), + pack_bytes(&digest[..16]), + pack_bytes(&refill[16..]), + pack_bytes(&refill[..16]), + ]); + let (sample, failure, used_refill) = select_two_candidates(&candidates, 4); + if failure { + bail!("Stage 2 transcript needs more than eight Goldilocks candidates"); + } + Ok(([sample.lo, sample.hi], if used_refill { refill } else { *digest })) +} + +fn hash_parts(parts: &[&[u8]]) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + for part in parts { + hasher.update(part); + } + *hasher.finalize().as_bytes() +} + +fn extension_bytes(value: [u64; 2]) -> [u8; 16] { + let mut bytes = [0u8; 16]; + bytes[..8].copy_from_slice(&value[0].to_le_bytes()); + bytes[8..].copy_from_slice(&value[1].to_le_bytes()); + bytes +} + +fn push_u64_observation(bytes: &mut Vec, value: u64) { + bytes.extend_from_slice(&value.to_le_bytes()); +} + +fn push_extension_observation(bytes: &mut Vec, value: [u64; 2]) { + bytes.extend_from_slice(&extension_bytes(value)); +} + +fn push_cap_observations(bytes: &mut Vec, roots: &[[u8; 32]]) { + for root in roots { + bytes.extend_from_slice(root); + } +} + +fn push_opened_round_observations( + bytes: &mut Vec, + round: &Stage3OpenedRoundV1, +) { + for matrix in round { + for point in matrix { + for &value in point { + push_extension_observation(bytes, value); + } + } + } +} + +fn replay_segments(replay: &Stage2TranscriptReplayV1) -> [&[u8]; 4] { + [ + &replay.initial_observations, + &replay.stage2_and_accumulator_observations, + &replay.quotient_commitment_observations, + &replay.pcs_opening_observations, + ] +} + +fn pack_segment(segment: &[u8]) -> Vec { + segment + .chunks(WORD_BYTES) + .map(|chunk| { + let mut word = [0u8; WORD_BYTES]; + word[..chunk.len()].copy_from_slice(chunk); + pack_bytes(&word) + }) + .collect() +} + +pub(crate) fn transcript_challenge_words( + challenges: Stage2TranscriptChallengesV1, +) -> [F128; 5] { + [ + pack_extension(challenges.lookup), + pack_extension(challenges.fingerprint), + pack_extension(challenges.constraint), + pack_extension(challenges.zeta), + pack_extension(challenges.pcs_alpha), + ] +} + +fn challenge_words(challenges: Stage2TranscriptChallengesV1) -> [F128; 5] { + transcript_challenge_words(challenges) +} + +fn pack_extension(value: [u64; 2]) -> F128 { + F128::new(value[0], value[1]) +} + +fn validate_replay(replay: &Stage2TranscriptReplayV1) -> Result<()> { + if replay.initial_observations.is_empty() { + bail!("Stage 2 transcript initial observations are empty"); + } + let total = + replay_segments(replay).iter().try_fold(0usize, |total, segment| { + total.checked_add(segment.len()).ok_or_else(|| { + anyhow::anyhow!("Stage 2 transcript observation length overflow") + }) + })?; + if total > MAX_OBSERVATION_BYTES { + bail!( + "Stage 2 transcript carries {total} observation bytes; maximum is {MAX_OBSERVATION_BYTES}" + ); + } + Ok(()) +} + +fn validate_fri_replay(replay: &Stage2FriTranscriptReplayV1) -> Result<()> { + let rounds = replay.commit_phase_commitments.len(); + if rounds == 0 || rounds > MAX_FRI_ROUNDS { + bail!( + "Stage 2 FRI transcript has {rounds} rounds; expected 1..={MAX_FRI_ROUNDS}" + ); + } + if replay.commit_pow_witnesses.len() != rounds + || replay.log_arities.len() != rounds + { + bail!("Stage 2 FRI transcript round-vector lengths disagree"); + } + if replay.num_queries == 0 || replay.num_queries > MAX_FRI_QUERIES { + bail!( + "Stage 2 FRI transcript has {} queries; expected 1..={MAX_FRI_QUERIES}", + replay.num_queries + ); + } + if replay.query_index_bits == 0 || replay.query_index_bits >= 64 { + bail!( + "Stage 2 FRI query-index width {} is outside 1..64", + replay.query_index_bits + ); + } + for (label, bits) in + [("commit", replay.commit_pow_bits), ("query", replay.query_pow_bits)] + { + if bits >= 64 || (1u64 << bits) >= GOLDILOCKS_MODULUS { + bail!("Stage 2 FRI {label} PoW width {bits} is invalid"); + } + } + if (1u64 << replay.query_index_bits) >= GOLDILOCKS_MODULUS { + bail!("Stage 2 FRI query-index mask exceeds the field order"); + } + if replay.final_polynomial.is_empty() + || !replay.final_polynomial.len().is_power_of_two() + { + bail!( + "Stage 2 FRI final polynomial length must be a non-zero power of two" + ); + } + for coefficient in &replay.final_polynomial { + if coefficient.iter().any(|&limb| limb >= GOLDILOCKS_MODULUS) { + bail!("Stage 2 FRI final polynomial contains a non-canonical limb"); + } + } + if replay + .commit_pow_witnesses + .iter() + .chain(std::iter::once(&replay.query_pow_witness)) + .any(|&witness| witness >= GOLDILOCKS_MODULUS) + { + bail!("Stage 2 FRI transcript contains a non-canonical PoW witness"); + } + let mut cap_roots = 0usize; + for cap in &replay.commit_phase_commitments { + if cap.is_empty() || cap.len() > MAX_CAP_ROOTS { + bail!( + "Stage 2 FRI commitment cap has {} roots; expected 1..={MAX_CAP_ROOTS}", + cap.len() + ); + } + cap_roots = cap_roots + .checked_add(cap.len()) + .ok_or_else(|| anyhow::anyhow!("Stage 2 FRI cap-root count overflow"))?; + } + if replay.log_arities.iter().any(|&arity| arity == 0 || arity >= 64) { + bail!("Stage 2 FRI transcript contains an invalid folding log-arity"); + } + let observation_bytes = 32usize + .checked_mul(cap_roots) + .and_then(|bytes| bytes.checked_add(final_observation_bytes(replay).len())) + .and_then(|bytes| { + bytes.checked_add(8 * usize::from(replay.query_pow_bits != 0)) + }) + .ok_or_else(|| { + anyhow::anyhow!("Stage 2 FRI observation length overflow") + })?; + if observation_bytes > MAX_OBSERVATION_BYTES { + bail!( + "Stage 2 FRI transcript carries {observation_bytes} observation bytes; maximum is {MAX_OBSERVATION_BYTES}" + ); + } + Ok(()) +} + +fn validate_challenges(challenges: Stage2TranscriptChallengesV1) -> Result<()> { + for challenge in [ + challenges.lookup, + challenges.fingerprint, + challenges.constraint, + challenges.zeta, + challenges.pcs_alpha, + ] { + if challenge.iter().any(|&value| value >= GOLDILOCKS_MODULUS) { + bail!("non-canonical Goldilocks transcript challenge"); + } + } + Ok(()) +} + +fn encode_challenges( + bytes: &mut Vec, + challenges: Stage2TranscriptChallengesV1, +) { + for challenge in [ + challenges.lookup, + challenges.fingerprint, + challenges.constraint, + challenges.zeta, + challenges.pcs_alpha, + ] { + bytes.extend_from_slice(&extension_bytes(challenge)); + } +} + +fn decode_challenges(bytes: &[u8]) -> Stage2TranscriptChallengesV1 { + debug_assert_eq!(bytes.len(), CHALLENGE_BYTES); + let mut values = [[0u64; 2]; CHALLENGE_COUNT]; + for (value, chunk) in values.iter_mut().zip(bytes.as_chunks::<16>().0) { + value[0] = u64::from_le_bytes(chunk[..8].try_into().unwrap()); + value[1] = u64::from_le_bytes(chunk[8..].try_into().unwrap()); + } + Stage2TranscriptChallengesV1 { + lookup: values[0], + fingerprint: values[1], + constraint: values[2], + zeta: values[3], + pcs_alpha: values[4], + } +} + +fn encode_bundle(bundle: &TranscriptProofBundle) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .serialize(bundle) + .context("encode Flock Stage 2 transcript proof bundle") +} + +fn decode_bundle(bytes: &[u8]) -> Result { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .with_limit(MAX_BUNDLE_BYTES as u64) + .reject_trailing_bytes() + .deserialize(bytes) + .context("invalid Flock Stage 2 transcript proof bundle") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn replay_fixture() -> Stage2TranscriptReplayV1 { + let mut initial = b"multi-stark/v0".to_vec(); + for value in 0..19u64 { + initial.extend_from_slice(&(value * 17 + 3).to_le_bytes()); + } + // Exercise a final partial BLAKE3 word in the first flush. + initial.extend_from_slice(&[0xa5, 0x5a, 0x11]); + + let stage2_and_accumulator_observations = + (0..79u8).map(|value| value.wrapping_mul(29)).collect(); + let quotient_commitment_observations = + (0..32u8).map(|value| value ^ 0x6d).collect(); + let pcs_opening_observations = + (0..117u8).map(|value| value.wrapping_mul(7).wrapping_add(1)).collect(); + Stage2TranscriptReplayV1 { + initial_observations: initial, + stage2_and_accumulator_observations, + quotient_commitment_observations, + pcs_opening_observations, + } + } + + #[derive(Clone)] + struct ReferenceHashChallenger { + input: Vec, + output: Vec, + } + + impl ReferenceHashChallenger { + fn new(initial: Vec) -> Self { + Self { input: initial, output: Vec::new() } + } + + fn observe(&mut self, bytes: &[u8]) { + self.output.clear(); + self.input.extend_from_slice(bytes); + } + + fn sample8(&mut self) -> [u8; 8] { + if self.output.is_empty() { + let digest = *blake3::hash(&self.input).as_bytes(); + self.input = digest.to_vec(); + self.output = digest.to_vec(); + } + std::array::from_fn(|_| self.output.pop().unwrap()) + } + + fn sample_field(&mut self) -> u64 { + loop { + let value = u64::from_le_bytes(self.sample8()); + if value < GOLDILOCKS_MODULUS { + return value; + } + } + } + + fn sample_ext(&mut self) -> [u64; 2] { + [self.sample_field(), self.sample_field()] + } + } + + #[test] + fn optimized_replay_matches_hash_challenger_buffers() { + let replay = replay_fixture(); + let expected = replay.challenges().unwrap(); + let mut challenger = + ReferenceHashChallenger::new(replay.initial_observations.clone()); + let lookup = challenger.sample_ext(); + challenger.observe(&extension_bytes(lookup)); + let fingerprint = challenger.sample_ext(); + challenger.observe(&extension_bytes(fingerprint)); + challenger.observe(&replay.stage2_and_accumulator_observations); + let constraint = challenger.sample_ext(); + challenger.observe(&replay.quotient_commitment_observations); + let zeta = challenger.sample_ext(); + challenger.observe(&replay.pcs_opening_observations); + let pcs_alpha = challenger.sample_ext(); + assert_eq!( + expected, + Stage2TranscriptChallengesV1 { + lookup, + fingerprint, + constraint, + zeta, + pcs_alpha, + } + ); + } + + #[test] + fn sample_gate_is_the_reverse_pop_order_permutation() { + let input = F128::new(0x0706_0504_0302_0100, 0x0f0e_0d0c_0b0a_0908); + assert_eq!( + sample_word(input), + F128::new(0x0809_0a0b_0c0d_0e0f, 0x0001_0203_0405_0607) + ); + let rows = [HashSampleRow(input)]; + let (z, _, _, _) = generate_hash_sample_witness(&rows, MIN_NU); + assert!(!z.is_empty()); + } + + #[test] + fn goldilocks_sample_gate_redraws_and_supports_pow_skip() { + let cases = [ + ( + [GOLDILOCKS_MODULUS, 5, 6, 7, 8, 9, 10, 11], + F128::new(5, 6), + F128::new(5, 6), + false, + false, + false, + false, + ), + ( + [1, GOLDILOCKS_MODULUS, 2, 3, 4, 5, 6, 7], + F128::new(1, 2), + F128::new(2, 3), + false, + false, + false, + false, + ), + ( + [GOLDILOCKS_MODULUS, GOLDILOCKS_MODULUS, 2, 3, 4, 5, 6, 7], + F128::new(2, 3), + F128::new(2, 3), + false, + false, + false, + false, + ), + ( + [1, 2, GOLDILOCKS_MODULUS, GOLDILOCKS_MODULUS, 4, 5, 6, 7], + F128::new(1, 2), + F128::new(2, 4), + false, + false, + false, + true, + ), + ( + [ + GOLDILOCKS_MODULUS, + GOLDILOCKS_MODULUS, + GOLDILOCKS_MODULUS, + 3, + 4, + 5, + 6, + 7, + ], + F128::new(3, 4), + F128::new(3, 4), + false, + false, + true, + true, + ), + ]; + for ( + candidates, + expected, + expected_skip, + failure, + skip_failure, + used_refill, + skip_used_refill, + ) in cases + { + let words = [ + F128::new(candidates[1].swap_bytes(), candidates[0].swap_bytes()), + F128::new(candidates[3].swap_bytes(), candidates[2].swap_bytes()), + F128::new(candidates[5].swap_bytes(), candidates[4].swap_bytes()), + F128::new(candidates[7].swap_bytes(), candidates[6].swap_bytes()), + ]; + let mut outputs = Vec::new(); + GoldilocksSampleGate { nu: MIN_NU }.eval(&words, &(), &mut outputs); + assert_eq!(outputs[0], expected); + assert_eq!(outputs[1], F128::new(u64::from(failure), 0)); + assert_eq!(outputs[2], F128::new(candidates[0], 0)); + assert_eq!(outputs[3], expected_skip); + assert_eq!(outputs[4], F128::new(u64::from(skip_failure), 0)); + let state = + if used_refill { [words[3], words[2]] } else { [words[1], words[0]] }; + let skip_state = if skip_used_refill { + [words[3], words[2]] + } else { + [words[1], words[0]] + }; + assert_eq!(outputs[5..7], state); + assert_eq!(outputs[7..9], skip_state); + let rows = [GoldilocksSampleRow(words)]; + let (z, _, _, _) = generate_goldilocks_sample_witness(&rows, MIN_NU); + assert!(!z.is_empty()); + } + } + + #[test] + fn u64_split_gate_exposes_low_bits_and_rejects_a_high_lane() { + let plan = u64_split_plan(); + let r1cs = plan.block_r1cs(MIN_NU); + let value = 0x8bad_f00d_dead_beefu64; + let mut row = vec![false; plan.k()]; + plan.fill_row(&mut row, |bits| { + write_f128(bits, U64_SPLIT_INPUT_BASE, F128::new(value, 0)); + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.k()].copy_from_slice(&row); + assert!(r1cs.satisfies(&witness)); + assert_eq!(row[U64_SPLIT_BIT_BASE], value & 1 == 1); + for bit in 0..63 { + assert_eq!( + row[U64_SPLIT_QUOTIENT_BASE + bit], + (value >> (bit + 1)) & 1 == 1 + ); + } + + let mut wrong_quotient = witness; + wrong_quotient[U64_SPLIT_QUOTIENT_BASE + 17] ^= true; + assert!(!r1cs.satisfies(&wrong_quotient)); + + let mut high_lane_row = vec![false; plan.k()]; + plan.fill_row(&mut high_lane_row, |bits| { + write_f128(bits, U64_SPLIT_INPUT_BASE, F128::new(value, 1)); + }); + let mut high_lane = vec![false; r1cs.n()]; + high_lane[..plan.k()].copy_from_slice(&high_lane_row); + assert!(!r1cs.satisfies(&high_lane)); + } + + #[test] + fn circuit_matches_native_replay_and_uses_tree_hashing() { + let replay = replay_fixture(); + let challenges = replay.challenges().unwrap(); + let relation = TranscriptRelation::build(&replay).unwrap(); + let witness = relation.shape.run(&relation.inputs, &[]); + assert_eq!(witness.public, relation.public(challenges)); + + let mut long = replay; + long.initial_observations.resize(1_103, 0x42); + let long_relation = TranscriptRelation::build(&long).unwrap(); + let long_challenges = long.challenges().unwrap(); + let long_witness = long_relation.shape.run(&long_relation.inputs, &[]); + assert_eq!(long_witness.public, long_relation.public(long_challenges)); + assert!( + long_witness.rows::(long_relation.slots.blake3).len() + > witness.rows::(relation.slots.blake3).len() + ); + } + + #[test] + fn post_fri_circuit_matches_native_pow_betas_and_refilled_queries() { + let prefix = replay_fixture(); + let mut replay = Stage2FriTranscriptReplayV1 { + commit_phase_commitments: vec![ + vec![*blake3::hash(b"fri-cap-0").as_bytes()], + vec![*blake3::hash(b"fri-cap-1").as_bytes()], + ], + commit_pow_witnesses: vec![0, 0], + final_polynomial: vec![[17, 29]], + log_arities: vec![1, 1], + query_pow_witness: 0, + commit_pow_bits: 2, + query_pow_bits: 3, + num_queries: 5, + query_index_bits: 5, + }; + let challenges = (0..4_096u64) + .find_map(|nonce| { + replay.commit_pow_witnesses = vec![nonce & 15, (nonce >> 4) & 15]; + replay.query_pow_witness = (nonce >> 8) & 15; + replay.challenges(&prefix).ok() + }) + .expect("small commit/query PoW fixture has witnesses"); + + let nu = 11; + let mut builder = ShapeBuilder::new(nu); + let blake3 = builder.slot(Blake3Gate { nu }); + let sample = builder.slot(HashSampleGate { nu }); + let field_sample = builder.slot(GoldilocksSampleGate { nu }); + let canonical = builder.slot(CanonicalGoldilocksQuadGate { nu }); + let repack = + builder.slot(crate::extension::GoldilocksLaneRepackGate { nu }); + let split = builder.slot(U64SplitGate { nu }); + let prefix_region = constrain_stage2_transcript( + &mut builder, + TranscriptCircuitSlots { blake3, sample: field_sample, canonical }, + &prefix, + nu, + ) + .unwrap(); + let mut inputs = prefix_region.inputs.clone(); + let mut public = inputs.clone(); + for challenge in prefix_region.challenges.all() { + builder.publish(challenge); + } + public.extend(challenge_words(prefix.challenges().unwrap())); + + let fri_region = constrain_stage2_fri_transcript( + &mut builder, + FriTranscriptCircuitSlots { + blake3, + sample, + field_sample, + canonical, + repack, + split, + }, + &replay, + prefix_region.state_digest, + nu, + ) + .unwrap(); + inputs.extend_from_slice(&fri_region.inputs); + public.extend_from_slice(&fri_region.inputs); + for &beta in &fri_region.betas { + builder.publish(beta); + } + public.extend(challenges.betas.iter().copied().map(pack_extension)); + for bits in &fri_region.query_index_bits { + for &bit in bits { + builder.publish(bit); + } + } + for &index in &challenges.query_indices { + public.extend( + (0..replay.query_index_bits) + .map(|bit| F128::new((index >> bit) & 1, 0)), + ); + } + let shape = builder.finish().unwrap(); + let witness = shape.run(&inputs, &[]); + assert_eq!(witness.public, public); + } + + #[test] + fn artifact_parser_is_strict_before_crypto() { + let replay = replay_fixture(); + let artifact = Stage2TranscriptConformanceArtifactV1 { + challenges: replay.challenges().unwrap(), + replay, + circuit_digest: [7; 32], + proof_bundle_bytes: vec![1, 2, 3], + }; + let bytes = artifact.to_bytes(); + assert_eq!( + Stage2TranscriptConformanceArtifactV1::from_bytes(&bytes).unwrap(), + artifact + ); + + let mut trailing = bytes.clone(); + trailing.push(0); + assert!( + Stage2TranscriptConformanceArtifactV1::from_bytes(&trailing).is_err() + ); + let mut wrong_config = bytes.clone(); + wrong_config[CONFIG_OFFSET] ^= 1; + assert!( + Stage2TranscriptConformanceArtifactV1::from_bytes(&wrong_config).is_err() + ); + let mut wrong_length = bytes; + wrong_length[LENGTHS_OFFSET] ^= 1; + assert!( + Stage2TranscriptConformanceArtifactV1::from_bytes(&wrong_length).is_err() + ); + } + + #[test] + #[ignore = "large upstream Flock proof; run explicitly for transcript conformance"] + fn real_transcript_round_trip_and_mutations() { + let artifact = prove_stage2_transcript_conformance(&replay_fixture()) + .expect("prove Stage 2 transcript replay"); + eprintln!( + "Flock Stage 2 transcript conformance bundle: {} bytes", + artifact.proof_bundle_bytes().len() + ); + verify_stage2_transcript_conformance(&artifact) + .expect("verify Stage 2 transcript replay"); + + let encoded = artifact.to_bytes(); + let decoded = + Stage2TranscriptConformanceArtifactV1::from_bytes(&encoded).unwrap(); + verify_stage2_transcript_conformance(&decoded) + .expect("verify decoded Stage 2 transcript replay"); + + let mut wrong_observation = decoded.clone(); + wrong_observation.replay.pcs_opening_observations[0] ^= 1; + assert!(verify_stage2_transcript_conformance(&wrong_observation).is_err()); + + let mut wrong_challenge = decoded.clone(); + wrong_challenge.challenges.pcs_alpha[0] ^= 1; + assert!(verify_stage2_transcript_conformance(&wrong_challenge).is_err()); + + let mut wrong_proof = decoded; + let flip_at = wrong_proof.proof_bundle_bytes.len() / 2; + wrong_proof.proof_bundle_bytes[flip_at] ^= 1; + assert!(verify_stage2_transcript_conformance(&wrong_proof).is_err()); + } +} diff --git a/flock-stage3/host/src/typed_witness.rs b/flock-stage3/host/src/typed_witness.rs new file mode 100644 index 00000000..fd734aec --- /dev/null +++ b/flock-stage3/host/src/typed_witness.rs @@ -0,0 +1,451 @@ +//! Owned, serialization-independent witness consumed by the Stage 3 lowering. +//! +//! The source advice uses bincode only as an off-circuit transport. This +//! module converts it once into primitive semantic values so the Flock +//! relation never depends on Rust layout or byte-parser execution. + +use anyhow::{Result, bail}; +use ix_terminal::{ + Stage2AdviceProfileV1, ValidatedStage2RootV1, decode_stage2_advice, + fri_parameter_words, +}; +use multi_stark::{ + advice::AdviceProof, + p3_field::{BasedVectorSpace, PrimeField64}, + types::{ExtVal, FriParameters, Val}, +}; + +pub const STAGE3_TYPED_WITNESS_LAYOUT_DOMAIN: &[u8; 8] = b"IXTYPW01"; +const STAGE3_TYPED_WITNESS_LAYOUT_VERSION: u16 = 1; + +pub type Stage3DigestV1 = [u8; 32]; +pub type Stage3ExtensionValueV1 = [u64; 2]; +pub type Stage3OpenedRoundV1 = Vec>>; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3TypedCommitmentsV1 { + pub stage_1_trace: Vec, + pub stage_2_trace: Vec, + pub quotient_chunks: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3TypedBatchOpeningV1 { + pub opened_values: Vec>, + pub opening_proof: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3TypedCommitPhaseStepV1 { + pub log_arity: u8, + pub sibling_values: Vec, + pub opening_proof: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3TypedQueryProofV1 { + pub input_proof: Vec, + pub commit_phase_openings: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3TypedFriProofV1 { + pub commit_phase_commits: Vec>, + pub commit_pow_witnesses: Vec, + pub query_proofs: Vec, + pub final_poly: Vec, + pub query_pow_witness: u64, +} + +/// Primitive, typed mirror of `multi_stark::advice::AdviceProof`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stage3TypedProofWitnessV1 { + pub active: Vec, + pub commitments: Stage3TypedCommitmentsV1, + pub intermediate_accumulators: Vec, + pub log_degrees: Vec, + pub opening_proof: Stage3TypedFriProofV1, + pub quotient_opened_values: Stage3OpenedRoundV1, + pub preprocessed_opened_values: Option, + pub stage_1_opened_values: Stage3OpenedRoundV1, + pub stage_2_opened_values: Stage3OpenedRoundV1, +} + +/// Counts that must agree with the independently recorded advice profile. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stage3TypedProofCountsV1 { + pub total_circuits: u64, + pub active_circuits: u64, + pub queries: u64, + pub fri_rounds: u64, + pub input_rounds_per_query: u64, + pub commitment_cap_digests: u64, + pub input_merkle_siblings: u64, + pub fri_merkle_siblings: u64, + pub opened_base_values: u64, + pub fri_sibling_extension_values: u64, + pub other_extension_values: u64, +} + +impl Stage3TypedProofWitnessV1 { + /// Decode the strict advice transport and immediately erase its serializer + /// representation in favor of semantic primitive values. + pub fn from_advice_bytes(bytes: &[u8], fri: &FriParameters) -> Result { + Ok(Self::from_advice(decode_stage2_advice(bytes, fri)?)) + } + + /// Prepare the typed proof attached to an already validated Stage 2 root. + pub fn from_prepared( + prepared: &ValidatedStage2RootV1, + fri: &FriParameters, + ) -> Result { + if prepared.statement().fri_parameter_words() != &fri_parameter_words(fri) { + bail!("typed Stage 3 witness uses different FRI parameters"); + } + let witness = Self::from_advice_bytes(prepared.advice_bytes(), fri)?; + witness.ensure_profile(prepared.advice_profile())?; + Ok(witness) + } + + /// Digest of the exact nested vector/option layout, excluding witness + /// values. It is a capacity/compiler input, not a proof-content commitment. + pub fn layout_digest(&self) -> [u8; 32] { + let mut bytes = Vec::new(); + bytes.extend_from_slice(STAGE3_TYPED_WITNESS_LAYOUT_DOMAIN); + bytes.extend_from_slice(&STAGE3_TYPED_WITNESS_LAYOUT_VERSION.to_le_bytes()); + for word in self.layout_words() { + bytes.extend_from_slice(&word.to_le_bytes()); + } + *blake3::hash(&bytes).as_bytes() + } + + pub fn counts(&self) -> Stage3TypedProofCountsV1 { + let commitment_cap_digests = self.commitments.stage_1_trace.len() + + self.commitments.stage_2_trace.len() + + self.commitments.quotient_chunks.len() + + self + .opening_proof + .commit_phase_commits + .iter() + .map(Vec::len) + .sum::(); + let input_merkle_siblings = self + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.input_proof) + .map(|opening| opening.opening_proof.len()) + .sum(); + let fri_merkle_siblings = self + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.commit_phase_openings) + .map(|opening| opening.opening_proof.len()) + .sum(); + let opened_base_values = self + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.input_proof) + .flat_map(|opening| &opening.opened_values) + .map(Vec::len) + .sum(); + let fri_sibling_extension_values = self + .opening_proof + .query_proofs + .iter() + .flat_map(|query| &query.commit_phase_openings) + .map(|opening| opening.sibling_values.len()) + .sum(); + let other_extension_values = self.intermediate_accumulators.len() + + count_opened_values(&self.quotient_opened_values) + + self.preprocessed_opened_values.as_ref().map_or(0, count_opened_values) + + count_opened_values(&self.stage_1_opened_values) + + count_opened_values(&self.stage_2_opened_values) + + self.opening_proof.final_poly.len(); + let input_rounds_per_query = self + .opening_proof + .query_proofs + .first() + .map_or(0, |query| query.input_proof.len()); + + Stage3TypedProofCountsV1 { + total_circuits: as_u64(self.active.len()), + active_circuits: as_u64( + self.active.iter().filter(|&&active| active).count(), + ), + queries: as_u64(self.opening_proof.query_proofs.len()), + fri_rounds: as_u64(self.opening_proof.commit_phase_commits.len()), + input_rounds_per_query: as_u64(input_rounds_per_query), + commitment_cap_digests: as_u64(commitment_cap_digests), + input_merkle_siblings: as_u64(input_merkle_siblings), + fri_merkle_siblings: as_u64(fri_merkle_siblings), + opened_base_values: as_u64(opened_base_values), + fri_sibling_extension_values: as_u64(fri_sibling_extension_values), + other_extension_values: as_u64(other_extension_values), + } + } + + pub fn ensure_profile(&self, profile: &Stage2AdviceProfileV1) -> Result<()> { + let expected = Stage3TypedProofCountsV1 { + total_circuits: profile.total_circuits, + active_circuits: profile.active_circuits, + queries: profile.queries, + fri_rounds: profile.fri_rounds, + input_rounds_per_query: profile.input_rounds_per_query, + commitment_cap_digests: profile.commitment_cap_digests, + input_merkle_siblings: profile.input_merkle_siblings, + fri_merkle_siblings: profile.fri_merkle_siblings, + opened_base_values: profile.opened_base_values, + fri_sibling_extension_values: profile.fri_sibling_extension_values, + other_extension_values: profile.other_extension_values, + }; + let observed = self.counts(); + if observed != expected { + bail!( + "typed Stage 3 witness counts differ from advice profile: expected {expected:?}, observed {observed:?}" + ); + } + Ok(()) + } + + /// Structural verifier step 2: the chained lookup accumulator must end at + /// zero. The full relation will wire this value to the accumulator updates. + pub fn last_accumulator_is_zero(&self) -> bool { + self.intermediate_accumulators.last().is_some_and(|value| *value == [0, 0]) + } + + fn from_advice(proof: AdviceProof) -> Self { + Self { + active: proof.active, + commitments: Stage3TypedCommitmentsV1 { + stage_1_trace: proof.commitments.stage_1_trace.roots().to_vec(), + stage_2_trace: proof.commitments.stage_2_trace.roots().to_vec(), + quotient_chunks: proof.commitments.quotient_chunks.roots().to_vec(), + }, + intermediate_accumulators: proof + .intermediate_accumulators + .into_iter() + .map(extension_words) + .collect(), + log_degrees: proof.log_degrees, + opening_proof: Stage3TypedFriProofV1 { + commit_phase_commits: proof + .opening_proof + .commit_phase_commits + .iter() + .map(|commitment| commitment.roots().to_vec()) + .collect(), + commit_pow_witnesses: proof + .opening_proof + .commit_pow_witnesses + .into_iter() + .map(base_word) + .collect(), + query_proofs: proof + .opening_proof + .query_proofs + .into_iter() + .map(|query| Stage3TypedQueryProofV1 { + input_proof: query + .input_proof + .into_iter() + .map(|opening| Stage3TypedBatchOpeningV1 { + opened_values: opening + .opened_values + .into_iter() + .map(|row| row.into_iter().map(base_word).collect()) + .collect(), + opening_proof: opening.opening_proof, + }) + .collect(), + commit_phase_openings: query + .commit_phase_openings + .into_iter() + .map(|step| Stage3TypedCommitPhaseStepV1 { + log_arity: step.log_arity, + sibling_values: step + .sibling_values + .into_iter() + .map(extension_words) + .collect(), + opening_proof: step.opening_proof, + }) + .collect(), + }) + .collect(), + final_poly: proof + .opening_proof + .final_poly + .into_iter() + .map(extension_words) + .collect(), + query_pow_witness: base_word(proof.opening_proof.query_pow_witness), + }, + quotient_opened_values: opened_round(proof.quotient_opened_values), + preprocessed_opened_values: proof + .preprocessed_opened_values + .map(opened_round), + stage_1_opened_values: opened_round(proof.stage_1_opened_values), + stage_2_opened_values: opened_round(proof.stage_2_opened_values), + } + } + + fn layout_words(&self) -> Vec { + let mut words = Vec::new(); + push_len(&mut words, &self.active); + push_len(&mut words, &self.commitments.stage_1_trace); + push_len(&mut words, &self.commitments.stage_2_trace); + push_len(&mut words, &self.commitments.quotient_chunks); + push_len(&mut words, &self.intermediate_accumulators); + push_len(&mut words, &self.log_degrees); + push_len(&mut words, &self.opening_proof.commit_phase_commits); + for cap in &self.opening_proof.commit_phase_commits { + push_len(&mut words, cap); + } + push_len(&mut words, &self.opening_proof.commit_pow_witnesses); + push_len(&mut words, &self.opening_proof.query_proofs); + for query in &self.opening_proof.query_proofs { + push_len(&mut words, &query.input_proof); + for opening in &query.input_proof { + push_len(&mut words, &opening.opened_values); + for row in &opening.opened_values { + push_len(&mut words, row); + } + push_len(&mut words, &opening.opening_proof); + } + push_len(&mut words, &query.commit_phase_openings); + for step in &query.commit_phase_openings { + words.push(u64::from(step.log_arity)); + push_len(&mut words, &step.sibling_values); + push_len(&mut words, &step.opening_proof); + } + } + push_len(&mut words, &self.opening_proof.final_poly); + push_opened_round(&mut words, &self.quotient_opened_values); + words.push(u64::from(self.preprocessed_opened_values.is_some())); + if let Some(round) = &self.preprocessed_opened_values { + push_opened_round(&mut words, round); + } + push_opened_round(&mut words, &self.stage_1_opened_values); + push_opened_round(&mut words, &self.stage_2_opened_values); + words + } +} + +fn base_word(value: Val) -> u64 { + value.as_canonical_u64() +} + +fn extension_words(value: ExtVal) -> Stage3ExtensionValueV1 { + let coefficients = value.as_basis_coefficients_slice(); + [base_word(coefficients[0]), base_word(coefficients[1])] +} + +fn opened_round(values: Vec>>) -> Stage3OpenedRoundV1 { + values + .into_iter() + .map(|matrix| { + matrix + .into_iter() + .map(|point| point.into_iter().map(extension_words).collect()) + .collect() + }) + .collect() +} + +fn count_opened_values(values: &Stage3OpenedRoundV1) -> usize { + values.iter().flat_map(|matrix| matrix.iter()).map(Vec::len).sum() +} + +fn push_len(words: &mut Vec, values: &[T]) { + words.push(as_u64(values.len())); +} + +fn push_opened_round(words: &mut Vec, values: &Stage3OpenedRoundV1) { + push_len(words, values); + for matrix in values { + push_len(words, matrix); + for point in matrix { + push_len(words, point); + } + } +} + +fn as_u64(value: usize) -> u64 { + u64::try_from(value).expect("Stage 3 witness length fits u64") +} + +#[cfg(test)] +mod tests { + use multi_stark::{ + advice::proof_to_advice_bytes, + p3_field::PrimeCharacteristicRing, + p3_matrix::dense::RowMajorMatrix, + system::{CircuitInputs, System, SystemWitness}, + types::{CommitmentParameters, GoldilocksBlake3Config}, + }; + + use super::*; + + fn typed_fixture() -> (Stage3TypedProofWitnessV1, Stage2AdviceProfileV1) { + let commitment = CommitmentParameters { log_blowup: 1, cap_height: 0 }; + let fri = FriParameters { + log_final_poly_len: 0, + max_log_arity: 1, + num_queries: 2, + commit_proof_of_work_bits: 0, + query_proof_of_work_bits: 0, + }; + let (system, key) = System::new( + GoldilocksBlake3Config::new(commitment, fri), + [ + CircuitInputs { main_width: 2, ..Default::default() }, + CircuitInputs { main_width: 3, ..Default::default() }, + ], + ); + let trace_1 = + RowMajorMatrix::new((0..16u32).map(Val::from_u32).collect::>(), 2); + let trace_2 = RowMajorMatrix::new( + (0..12u32).map(|value| Val::from_u32(7 * value + 3)).collect(), + 3, + ); + let proof = system.prove_multiple_claims( + &key, + &[], + SystemWitness::from_stage_1(vec![trace_1, trace_2], &system), + ); + let advice = + proof_to_advice_bytes(&system, commitment, fri, &[], &proof).unwrap(); + let profile = + Stage2AdviceProfileV1::from_advice_bytes(&advice, &fri).unwrap(); + let typed = + Stage3TypedProofWitnessV1::from_advice_bytes(&advice, &fri).unwrap(); + (typed, profile) + } + + #[test] + fn typed_layout_preserves_every_profile_count() { + let (typed, profile) = typed_fixture(); + typed.ensure_profile(&profile).unwrap(); + assert!(typed.last_accumulator_is_zero()); + assert_ne!(typed.layout_digest(), [0; 32]); + } + + #[test] + fn layout_digest_changes_with_nested_shape_not_values() { + let (typed, _) = typed_fixture(); + let digest = typed.layout_digest(); + + let mut value_change = typed.clone(); + value_change.opening_proof.query_pow_witness ^= 1; + assert_eq!(value_change.layout_digest(), digest); + + let mut shape_change = typed; + shape_change.opening_proof.query_proofs[0].input_proof[0] + .opening_proof + .pop(); + assert_ne!(shape_change.layout_digest(), digest); + } +} diff --git a/flock-stage3/host/src/window.rs b/flock-stage3/host/src/window.rs new file mode 100644 index 00000000..dc459c79 --- /dev/null +++ b/flock-stage3/host/src/window.rs @@ -0,0 +1,169 @@ +//! A fixed-selector 16-byte window over two adjacent transcript words. +//! +//! Stage 2 starts its challenger seed with the 14-byte `multi-stark/v0` tag, +//! so later commitment digests are not necessarily aligned to the Flock +//! circuit's 16-byte `F128` words. This Boolean table selects one of the 16 +//! possible byte offsets and returns the exact next 16 bytes. The selector is +//! always a relation-fixed one-hot word at call sites. + +use flock_prover::{ + circuit::builder::{GateType, SlotWitness}, + field::F128, + r1cs::BlockR1cs, + schedule::{IoWord, TableType}, + union::SlotWitnessDest, +}; + +use crate::boolean::{ + BooleanR1csBuilder, BooleanR1csPlan, generate_boolean_witness_into, + write_f128, +}; + +const K_LOG: usize = 12; +const FIRST_BASE: usize = 0; +const SECOND_BASE: usize = 128; +const SELECTOR_BASE: usize = 256; +const OUTPUT_BASE: usize = 384; +const RESERVED_COLUMNS: usize = 512; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct ByteWindowGate { + pub(crate) nu: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ByteWindowRow { + first: F128, + second: F128, + selector: F128, +} + +impl GateType for ByteWindowGate { + type Row = ByteWindowRow; + type Hint = (); + + fn table(&self) -> TableType { + crate::boolean::table_from_block_r1cs(build_byte_window_r1cs(self.nu)) + .with_io_schema(vec![ + IoWord::input(0), + IoWord::input(1), + IoWord::input(2), + IoWord::output(3), + ]) + } + + fn eval( + &self, + inputs: &[F128], + _hint: &(), + outputs: &mut Vec, + ) -> Self::Row { + let first = inputs[0]; + let second = inputs[1]; + let selector = inputs[2]; + assert_eq!(selector.hi, 0, "byte-window selector high lane must be zero"); + assert_eq!( + selector.lo.count_ones(), + 1, + "byte-window selector must be one-hot" + ); + let offset = selector.lo.trailing_zeros() as usize; + assert!(offset < 16, "byte-window offset exceeds one F128 word"); + outputs.push(byte_window(first, second, offset)); + ByteWindowRow { first, second, selector } + } + + fn witness(&self, _rows: &[Self::Row], _nu: usize) -> SlotWitness { + SlotWitness::DeferredToRows + } +} + +pub(crate) fn build_byte_window_r1cs(nu: usize) -> BlockR1cs { + byte_window_plan().block_r1cs(nu) +} + +pub(crate) fn generate_byte_window_witness_into( + rows: &[ByteWindowRow], + nu: usize, + dst: SlotWitnessDest<'_>, +) -> Vec { + generate_boolean_witness_into( + byte_window_plan(), + rows, + nu, + dst, + |row, bits| { + write_f128(bits, FIRST_BASE, row.first); + write_f128(bits, SECOND_BASE, row.second); + write_f128(bits, SELECTOR_BASE, row.selector); + }, + ) +} + +fn byte_window_plan() -> &'static BooleanR1csPlan { + static PLAN: std::sync::OnceLock = + std::sync::OnceLock::new(); + PLAN.get_or_init(|| { + let mut builder = BooleanR1csBuilder::new(K_LOG, RESERVED_COLUMNS); + for column in FIRST_BASE..SELECTOR_BASE + 128 { + builder.free_boolean_at(column); + } + let one = builder.alloc_constant_one(); + for output_bit in 0..128 { + let products: Vec<_> = (0..16) + .map(|offset| { + let source_bit = offset * 8 + output_bit; + let source = if source_bit < 128 { + FIRST_BASE + source_bit + } else { + SECOND_BASE + source_bit - 128 + }; + builder.and(SELECTOR_BASE + offset, source) + }) + .collect(); + builder.write_xor(OUTPUT_BASE + output_bit, &products, one); + } + builder.finish() + }) +} + +fn byte_window(first: F128, second: F128, offset: usize) -> F128 { + let mut bytes = [0u8; 32]; + bytes[..8].copy_from_slice(&first.lo.to_le_bytes()); + bytes[8..16].copy_from_slice(&first.hi.to_le_bytes()); + bytes[16..24].copy_from_slice(&second.lo.to_le_bytes()); + bytes[24..].copy_from_slice(&second.hi.to_le_bytes()); + F128::new( + u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap()), + u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap()), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_byte_offset_matches_native_slice() { + let first = F128::new(0x0706_0504_0302_0100, 0x0f0e_0d0c_0b0a_0908); + let second = F128::new(0x1716_1514_1312_1110, 0x1f1e_1d1c_1b1a_1918); + let plan = byte_window_plan(); + let r1cs = plan.block_r1cs(3); + for offset in 0..16 { + let selector = F128::new(1 << offset, 0); + let mut logical = vec![false; plan.k()]; + plan.fill_row(&mut logical, |bits| { + write_f128(bits, FIRST_BASE, first); + write_f128(bits, SECOND_BASE, second); + write_f128(bits, SELECTOR_BASE, selector); + }); + let mut witness = vec![false; r1cs.n()]; + witness[..plan.k()].copy_from_slice(&logical); + assert!(r1cs.satisfies(&witness)); + let expected = byte_window(first, second, offset); + let mut output = vec![false; 128]; + write_f128(&mut output, 0, expected); + assert_eq!(&logical[OUTPUT_BASE..OUTPUT_BASE + 128], output); + } + } +} diff --git a/flock-stage3/measurements/packed-canonicality-2026-09-05.json b/flock-stage3/measurements/packed-canonicality-2026-09-05.json new file mode 100644 index 00000000..a1901035 --- /dev/null +++ b/flock-stage3/measurements/packed-canonicality-2026-09-05.json @@ -0,0 +1,1259 @@ +{ + "schema": "ix.flock-stage3.packed-canonical-measurement", + "version": 1, + "date": "2026-09-05", + "source_commit": "40cf786ac78990108701ac2f1ec5e3b5867f4410", + "source_worktree_dirty": true, + "source_note": "Pack two 128-bit canonicality requests per Boolean row, retaining independent per-limb checks. No Stage 2 keys, root transports, FRI parameters, dependency/configuration pins or admission defaults changed. Canonicality table schema, compiled circuits and relation identities change.", + "baseline_measurements": [ + "pcs-query-sharing-2026-09-05.json" + ], + "protocol": { + "lean_toolchain": "4.33.1", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "plonky3_revision": "3152b14a89067c83775a8076cc262ffc48a1fd7c", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "flock_config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "commitment": { + "log_blowup": 2, + "cap_height": 0 + }, + "fri": { + "query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0 + } + }, + "host": { + "cpu": "AMD Ryzen 9 7950X3D", + "mem_total_bytes": 134128111616, + "rayon_threads": 8, + "census_address_space_limit_bytes": 17179869184, + "production_parameter_proof_address_space_limit_bytes": 68719476736, + "two_query_proof_address_space_limit_bytes": 17179869184, + "timing_sample_count_per_fixture": 1 + }, + "canonicality_table": { + "before": { + "gate": "CanonicalGoldilocksPairGate", + "input_words": 1, + "input_limbs": 2, + "active_violation_bits": 64, + "useful_boolean_columns": 318, + "boolean_columns": 512 + }, + "after": { + "gate": "CanonicalGoldilocksQuadGate", + "input_words": 2, + "input_limbs": 4, + "active_violation_bits": 128, + "useful_boolean_columns": 508, + "boolean_columns": 512 + }, + "batching": "Pair requests in emission order; flush an odd tail against fixed data zero before either count or compiled emission finishes. No wire-identity or witness-value deduplication.", + "zero_anchors": "Separate canonical(0,0) output anchors for at most 256 residuals; data zero remains input-only.", + "omitted_constraints": false, + "union_column_log_unchanged": 17 + }, + "real_aggregate_roots": [ + { + "profile": "legacy", + "fixture": "Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json", + "root_wrapper": { + "file": "root.ixon-proof", + "bytes": 8565030, + "blake3": "254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406" + }, + "aggregate_vk": { + "file": "aggr.vk", + "bytes": 181630, + "blake3": "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e" + }, + "outer_claim": { + "file": "outer-claim.bin", + "bytes": 144, + "blake3": "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713" + }, + "after": { + "compiled": false, + "count_us": 155457, + "nu": 22, + "padded_union_witness_bytes": 206158430208, + "process_peak_rss_bytes": 366616576, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 4194304, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 2871375 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1629468 + }, + { + "gate": "CanonicalGoldilocksQuadGate", + "rows": 3162519 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2405370 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 104980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "version": 1 + }, + "before": { + "compiled": false, + "count_us": 175956, + "nu": 23, + "padded_union_witness_bytes": 412316860416, + "process_peak_rss_bytes": 366891008, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 8388608, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 2871375 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1629468 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 6283484 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2405370 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 104980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "version": 1 + }, + "canonical_rows_removed": 3120965, + "canonical_row_reduction_percent": 49.669339493822214, + "padded_witness_reduction_factor": 2, + "native_root_key_claim_bytes_unchanged": true, + "native_proofs_fresh_process_verified": true, + "compiled": false, + "evaluated": false, + "proved": false, + "admission": { + "admission_error": "Stage 3 padded union witness requires 206158430208 bytes; admission limit is 1 (PCS/compiler scratch is additional)", + "aggregate_lookup_policy": "legacy", + "compiled": false, + "schema": "ix.flock-stage3.fixture-count", + "version": 1 + } + }, + { + "profile": "min-opening-width-v1", + "fixture": "Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/fixture.json", + "root_wrapper": { + "file": "root.ixon-proof", + "bytes": 8002662, + "blake3": "635c8f79af8cf1a913cb9291fbd1aa1a3f2c4ff221e6ec1339795989c649910f" + }, + "aggregate_vk": { + "file": "aggr.vk", + "bytes": 181630, + "blake3": "3c740f5b7645b1f7cdf361b7de3e20628c18ea3bf9cfafd1bd8aac4aca2bf7ba" + }, + "outer_claim": { + "file": "outer-claim.bin", + "bytes": 144, + "blake3": "72e37e5dda38c176b1caaa377ea779ca285d8e614ca268aeb9130812fdc2216d" + }, + "after": { + "compiled": false, + "count_us": 149295, + "nu": 22, + "padded_union_witness_bytes": 206158430208, + "process_peak_rss_bytes": 363384832, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 4194304, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 2798642 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1561938 + }, + { + "gate": "CanonicalGoldilocksQuadGate", + "rows": 3056107 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2304075 + }, + { + "gate": "Blake3Gate", + "rows": 158899 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 104980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 32399 + } + ], + "version": 1 + }, + "before": { + "compiled": false, + "count_us": 168385, + "nu": 23, + "padded_union_witness_bytes": 412316860416, + "process_peak_rss_bytes": 364548096, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 8388608, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 2798642 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1561938 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 6072018 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2304075 + }, + { + "gate": "Blake3Gate", + "rows": 158899 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 104980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 32399 + } + ], + "version": 1 + }, + "canonical_rows_removed": 3015911, + "canonical_row_reduction_percent": 49.66900625129899, + "padded_witness_reduction_factor": 2, + "native_root_key_claim_bytes_unchanged": true, + "native_proofs_fresh_process_verified": true, + "compiled": false, + "evaluated": false, + "proved": false, + "admission": { + "admission_error": "Stage 3 padded union witness requires 206158430208 bytes; admission limit is 1 (PCS/compiler scratch is additional)", + "aggregate_lookup_policy": "min-opening-width-v1", + "compiled": false, + "schema": "ix.flock-stage3.fixture-count", + "version": 1 + } + } + ], + "production_admission_defaults": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "count_only_limits": { + "max_table_capacity": 4294967296, + "max_union_witness_bytes": 1 + }, + "default_cli_admission_errors": [ + "Stage 3 padded union witness requires 206158430208 bytes; admission limit is 34359738368 (PCS/compiler scratch is additional)", + "Stage 3 table capacity 4194304 (nu=22) exceeds admission limit 2097152" + ], + "production_parameter_toy": { + "fixture": "Small native three-circuit fixture, one inactive and two active heights 8/4; 18-word claim, 100 FRI queries and 20-bit query PoW. Not an ix_aggr corpus root.", + "before": { + "fixture": "Three native multi-STARK circuits: one inactive, active heights 8 and 4, preprocessing and an 18-word claim lookup. Not an ix_aggr corpus root.", + "stage2_root_unchanged_from_baseline": true, + "census": { + "count_us": 2891, + "test_total_seconds": 1.84, + "circuit_digest": "4a436f834faaeb4a4bcf80b7e310ea6ceee69dc45902a0800a186a54bdb3e231", + "phase_ms": { + "count_and_admit": 3.08, + "declare_slots": 755.96, + "shared_pcs_constraints": 0.24, + "pcs_query_constraints": 19.07, + "fri_query_constraints": 8.74, + "finish_builder": 934.58, + "compile_relation": 1724 + }, + "admission_limits": { + "max_table_capacity": 65536, + "max_union_witness_bytes": 3221225472 + }, + "rejected_one_row_or_byte_below": true + }, + "proved": true, + "report": { + "advice": { + "active_circuits": 2, + "advice_bytes": 143426, + "commitment_cap_digests": 6, + "fri_merkle_siblings": 900, + "fri_rounds": 3, + "fri_sibling_extension_values": 300, + "input_merkle_siblings": 2000, + "input_rounds_per_query": 4, + "opened_base_values": 3200, + "other_extension_values": 63, + "queries": 100, + "total_circuits": 3 + }, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "limits": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + "merkle_hash": "blake3", + "process_peak_rss_bytes": 999923712, + "profile": "fast128", + "relation": { + "blake3_rows": 4470, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 65268, + "circuit_digest": "4a436f834faaeb4a4bcf80b7e310ea6ceee69dc45902a0800a186a54bdb3e231", + "digest_order_rows": 5000, + "equality_rows": 3815, + "field_sample_rows": 8, + "goldilocks_add_rows": 33661, + "goldilocks_mul_rows": 14858, + "hash_sample_rows": 52, + "lane_repack_rows": 21198, + "nu": 16, + "public_values": 14289, + "relation_inputs": 13786, + "table_capacity": 65536, + "u64_split_rows": 520 + }, + "relation_cache": "none", + "relation_digest": "84e51adf14fe15746142b64ab5f2db9648a9529612ed4e4a125b7523539a03bf", + "resources": { + "committed_union_log": 30, + "dense_witness_bytes": 99271760, + "padded_union_witness_bytes": 3221225472, + "pcs_codeword_bytes": 201326592, + "pcs_lanes": 48, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 100663296, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 1610612736, + "registry_slot": 0, + "rows": 14858, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 402653184, + "registry_slot": 1, + "rows": 4470, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 402653184, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 100663296, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 50331648, + "registry_slot": 4, + "rows": 33661, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 50331648, + "registry_slot": 5, + "rows": 5000, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 25165824, + "registry_slot": 6, + "rows": 21198, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 7, + "rows": 65268, + "useful_boolean_columns": 318 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 8, + "rows": 3815, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 9, + "rows": 52, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 10, + "rows": 520, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 33 + }, + "schema": "ix.flock-stage3.preflight", + "specialization": { + "activation": [ + false, + true, + true + ], + "active_log_degrees": [ + 3, + 2 + ], + "fri_parameter_words": [ + 0, + 1, + 100, + 0, + 20 + ], + "typed_witness_layout_digest": "82fba0be5f9777bf92835a742a02e153ffb288dd53f1a2618ac82f8841cb5cfb" + }, + "stage2_root_digest": "9773a5442674c6679b2551d15dce94702964f25402094831d930fe010182320f", + "stage3_statement_digest": "dd65d7c9be6669e1501888dadf8d11c1d485c67ad1494cf68dbcf596a5d51ce2", + "timings": { + "compile_us": 1738175, + "evaluate_us": 10313, + "lowering_us": 1611, + "native_prepare_us": 1649, + "total_us": 1751791 + }, + "transcript": "chained-blake3", + "transcript_domain": "ix:flock-stage3:fri-verifier:v1", + "transport": { + "claim_bytes": 144, + "compact_proof_bytes": 43249, + "compact_proof_digest": "a553b3ec29ceb843daff22b36f7996e128082bb4de0b24fa034858e0cbfad053", + "verifying_key_bytes": 279, + "verifying_key_digest": "afa90f0a22bfaf47cf2d5b9fa47f466b46ae1eba7c5e9fbe63b6ce47e5402302" + }, + "version": 1 + }, + "proof_run": { + "prove_us": 2234030, + "self_verify_us": 16449, + "package_us": 272, + "total_us": 2250751, + "artifact_bytes": 481987, + "payload_bytes": 481861, + "post_proof_process_peak_rss_bytes": 5509857280, + "explicit_reuse_verify_seconds": 0.017, + "fresh_process_verify_seconds": 2.7, + "fresh_relation_warm_tables_verify_seconds": 1.609, + "test_total_seconds": 10.059, + "temporary_artifact_persisted_and_cold_verified": true, + "rejected_wrong_relation_and_corrupt_proof": true, + "temporary_artifact_removed_after_test": true + } + }, + "after": { + "proved": true, + "report": { + "advice": { + "active_circuits": 2, + "advice_bytes": 143426, + "commitment_cap_digests": 6, + "fri_merkle_siblings": 900, + "fri_rounds": 3, + "fri_sibling_extension_values": 300, + "input_merkle_siblings": 2000, + "input_rounds_per_query": 4, + "opened_base_values": 3200, + "other_extension_values": 63, + "queries": 100, + "total_circuits": 3 + }, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "limits": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + "merkle_hash": "blake3", + "process_peak_rss_bytes": 991858688, + "profile": "fast128", + "relation": { + "blake3_rows": 4470, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 32857, + "circuit_digest": "e0f904688d96a1acddffe34e35330553d21e96866042bfd71ec45fbd039bd42c", + "digest_order_rows": 5000, + "equality_rows": 3815, + "field_sample_rows": 8, + "goldilocks_add_rows": 33661, + "goldilocks_mul_rows": 14858, + "hash_sample_rows": 52, + "lane_repack_rows": 21198, + "nu": 16, + "public_values": 14289, + "relation_inputs": 13786, + "table_capacity": 65536, + "u64_split_rows": 520 + }, + "relation_cache": "none", + "relation_digest": "2a59cdc3bfd3ba2917655b4f7219bf9ae02153a000f67d14d62d94edd5effded", + "resources": { + "committed_union_log": 30, + "dense_witness_bytes": 98241744, + "padded_union_witness_bytes": 3221225472, + "pcs_codeword_bytes": 197132288, + "pcs_lanes": 47, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 98566144, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 1610612736, + "registry_slot": 0, + "rows": 14858, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 402653184, + "registry_slot": 1, + "rows": 4470, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 402653184, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 100663296, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 50331648, + "registry_slot": 4, + "rows": 33661, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 50331648, + "registry_slot": 5, + "rows": 5000, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 25165824, + "registry_slot": 6, + "rows": 21198, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 7, + "rows": 32857, + "useful_boolean_columns": 508 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 8, + "rows": 3815, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 9, + "rows": 52, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 10, + "rows": 520, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 33 + }, + "schema": "ix.flock-stage3.preflight", + "specialization": { + "activation": [ + false, + true, + true + ], + "active_log_degrees": [ + 3, + 2 + ], + "fri_parameter_words": [ + 0, + 1, + 100, + 0, + 20 + ], + "typed_witness_layout_digest": "82fba0be5f9777bf92835a742a02e153ffb288dd53f1a2618ac82f8841cb5cfb" + }, + "stage2_root_digest": "9773a5442674c6679b2551d15dce94702964f25402094831d930fe010182320f", + "stage3_statement_digest": "d7efcda602529e0f8bacea4e4323817679d9d873b5e7f07838f51f0d3dccea8c", + "timings": { + "compile_us": 1668684, + "evaluate_us": 10200, + "lowering_us": 1547, + "native_prepare_us": 1582, + "total_us": 1682066 + }, + "transcript": "chained-blake3", + "transcript_domain": "ix:flock-stage3:fri-verifier:v1", + "transport": { + "claim_bytes": 144, + "compact_proof_bytes": 43633, + "compact_proof_digest": "0fcce50f76c964a46e39d22c25b97297540549a5b6a299e7cc179c418fc46524", + "verifying_key_bytes": 279, + "verifying_key_digest": "afa90f0a22bfaf47cf2d5b9fa47f466b46ae1eba7c5e9fbe63b6ce47e5402302" + }, + "version": 1 + }, + "proof_run": { + "proof_timings_us": { + "prove_us": 2262482, + "self_verify_us": 18453, + "package_us": 242, + "total_us": 2281178 + }, + "post_prove_process_peak_rss_bytes": 5501505536, + "artifact_bytes": 478483, + "production_payload_bytes": 478357, + "explicit_verifier_reuse_s": 0.018, + "fresh_process_external_root_verification_s": 2.605, + "warm_invariant_tables_fresh_relation_verification_s": 1.435, + "full_test_s": 9.6, + "artifact_persisted_for_child_verification": true, + "separate_trusted_transport_persisted_for_child_verification": true, + "temporary_artifact_and_transport_removed": true, + "timing_sample_count": 1 + } + }, + "census": { + "passed": true, + "elapsed_s": 1.8, + "count_us": 2531, + "slot_declaration_ms": 787.59, + "pcs_query_emission_ms": 14.73, + "fri_query_emission_ms": 9.06, + "builder_finish_ms": 873.06, + "exact_capacity_and_witness_limits_accepted": true, + "one_row_or_byte_below_rejected": true + }, + "note": "Padded witness remains 3 GiB: 33,661 addition rows now set nu=16, even though canonicality rows fall from 65,268 to 32,857. The standalone proof peak remains about 5.1 GiB RSS." + }, + "two_query_cross_check": { + "before": { + "proved": true, + "report": { + "advice": { + "active_circuits": 2, + "advice_bytes": 3916, + "commitment_cap_digests": 6, + "fri_merkle_siblings": 12, + "fri_rounds": 3, + "fri_sibling_extension_values": 6, + "input_merkle_siblings": 32, + "input_rounds_per_query": 4, + "opened_base_values": 64, + "other_extension_values": 63, + "queries": 2, + "total_circuits": 3 + }, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "limits": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + "merkle_hash": "blake3", + "process_peak_rss_bytes": 819068928, + "profile": "fast128", + "relation": { + "blake3_rows": 119, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 3999, + "circuit_digest": "be8bbe53f1af8a04277b704875f7d3965048d7d653fc311aa23b00ec9e54b23d", + "digest_order_rows": 76, + "equality_rows": 91, + "field_sample_rows": 8, + "goldilocks_add_rows": 2173, + "goldilocks_mul_rows": 828, + "hash_sample_rows": 2, + "lane_repack_rows": 1284, + "nu": 12, + "public_values": 604, + "relation_inputs": 593, + "table_capacity": 4096, + "u64_split_rows": 8 + }, + "relation_cache": "none", + "relation_digest": "b6df063431ff94bf4d4b2fa7ae3ccd8f1cf18a79148f7af5daba4d10419010a2", + "resources": { + "committed_union_log": 26, + "dense_witness_bytes": 5440128, + "padded_union_witness_bytes": 201326592, + "pcs_codeword_bytes": 11010048, + "pcs_lanes": 42, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 5505024, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 100663296, + "registry_slot": 0, + "rows": 828, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 25165824, + "registry_slot": 1, + "rows": 119, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 25165824, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 6291456, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 3145728, + "registry_slot": 4, + "rows": 2173, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 3145728, + "registry_slot": 5, + "rows": 76, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 1572864, + "registry_slot": 6, + "rows": 1284, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 7, + "rows": 3999, + "useful_boolean_columns": 318 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 8, + "rows": 91, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 9, + "rows": 2, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 10, + "rows": 8, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 29 + }, + "schema": "ix.flock-stage3.preflight", + "specialization": { + "activation": [ + false, + true, + true + ], + "active_log_degrees": [ + 3, + 2 + ], + "fri_parameter_words": [ + 0, + 1, + 2, + 0, + 0 + ], + "typed_witness_layout_digest": "f25d2a4c87b411eac37f49c4479fada0e628c6d7f57a2c303e55af4f0e83d772" + }, + "stage2_root_digest": "e4d635cb35dee195f87c29653c58b7313d4da0ce8468b20d97d93d9bf32fff54", + "stage3_statement_digest": "c254fb22705d7e3653804d17422e8a96de9ad09e619c134b5c106882b763cf53", + "timings": { + "compile_us": 1466998, + "evaluate_us": 235, + "lowering_us": 80, + "native_prepare_us": 99, + "total_us": 1467418 + }, + "transcript": "chained-blake3", + "transcript_domain": "ix:flock-stage3:fri-verifier:v1", + "transport": { + "claim_bytes": 144, + "compact_proof_bytes": 3441, + "compact_proof_digest": "a513eb6ca3f53f0437d5e81cbb910f735e7deb1520c7ba3ebcf85e6b7ca296f2", + "verifying_key_bytes": 279, + "verifying_key_digest": "c6c140a56ee9662394c92a6ca6edfa131c523254f958b3f94de3d0f75eb7d37d" + }, + "version": 1 + }, + "proof_run": { + "prove_us": 1102599, + "self_verify_us": 13126, + "package_us": 171, + "total_us": 1115897, + "artifact_bytes": 343851, + "payload_bytes": 343725, + "post_proof_process_peak_rss_bytes": 1701462016, + "explicit_reuse_verify_seconds": 0.012, + "fresh_process_verify_seconds": 2.422, + "fresh_relation_warm_tables_verify_seconds": 1.237, + "test_total_seconds": 7.6, + "temporary_artifact_persisted_and_cold_verified": true, + "rejected_wrong_relation_and_corrupt_proof": true, + "temporary_artifact_removed_after_test": true + } + }, + "after": { + "proved": true, + "report": { + "advice": { + "active_circuits": 2, + "advice_bytes": 3916, + "commitment_cap_digests": 6, + "fri_merkle_siblings": 12, + "fri_rounds": 3, + "fri_sibling_extension_values": 6, + "input_merkle_siblings": 32, + "input_rounds_per_query": 4, + "opened_base_values": 64, + "other_extension_values": 63, + "queries": 2, + "total_circuits": 3 + }, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "limits": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + "merkle_hash": "blake3", + "process_peak_rss_bytes": 818257920, + "profile": "fast128", + "relation": { + "blake3_rows": 119, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 2017, + "circuit_digest": "420b5a821b55b859fe04d30f2a796f1002536e85765a23d81ea3676557f908cb", + "digest_order_rows": 76, + "equality_rows": 91, + "field_sample_rows": 8, + "goldilocks_add_rows": 2173, + "goldilocks_mul_rows": 828, + "hash_sample_rows": 2, + "lane_repack_rows": 1284, + "nu": 12, + "public_values": 604, + "relation_inputs": 593, + "table_capacity": 4096, + "u64_split_rows": 8 + }, + "relation_cache": "none", + "relation_digest": "0d5124f504868d673825f808ecdfc32a58804834139bf4fa865d4653125ced80", + "resources": { + "committed_union_log": 26, + "dense_witness_bytes": 5377264, + "padded_union_witness_bytes": 201326592, + "pcs_codeword_bytes": 11010048, + "pcs_lanes": 42, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 5505024, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 100663296, + "registry_slot": 0, + "rows": 828, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 25165824, + "registry_slot": 1, + "rows": 119, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 25165824, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 6291456, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 3145728, + "registry_slot": 4, + "rows": 2173, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 3145728, + "registry_slot": 5, + "rows": 76, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 1572864, + "registry_slot": 6, + "rows": 1284, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 7, + "rows": 2017, + "useful_boolean_columns": 508 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 8, + "rows": 91, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 9, + "rows": 2, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 10, + "rows": 8, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 29 + }, + "schema": "ix.flock-stage3.preflight", + "specialization": { + "activation": [ + false, + true, + true + ], + "active_log_degrees": [ + 3, + 2 + ], + "fri_parameter_words": [ + 0, + 1, + 2, + 0, + 0 + ], + "typed_witness_layout_digest": "f25d2a4c87b411eac37f49c4479fada0e628c6d7f57a2c303e55af4f0e83d772" + }, + "stage2_root_digest": "e4d635cb35dee195f87c29653c58b7313d4da0ce8468b20d97d93d9bf32fff54", + "stage3_statement_digest": "b8d6aec8c989ddfddd6040afd83d30610585a89fb43d306404f9321bc03bd990", + "timings": { + "compile_us": 1463455, + "evaluate_us": 203, + "lowering_us": 94, + "native_prepare_us": 136, + "total_us": 1463895 + }, + "transcript": "chained-blake3", + "transcript_domain": "ix:flock-stage3:fri-verifier:v1", + "transport": { + "claim_bytes": 144, + "compact_proof_bytes": 3441, + "compact_proof_digest": "a513eb6ca3f53f0437d5e81cbb910f735e7deb1520c7ba3ebcf85e6b7ca296f2", + "verifying_key_bytes": 279, + "verifying_key_digest": "c6c140a56ee9662394c92a6ca6edfa131c523254f958b3f94de3d0f75eb7d37d" + }, + "version": 1 + }, + "proof_run": { + "proof_timings_us": { + "prove_us": 1117621, + "self_verify_us": 12357, + "package_us": 256, + "total_us": 1130235 + }, + "post_prove_process_peak_rss_bytes": 1695559680, + "artifact_bytes": 343867, + "production_payload_bytes": 343741, + "explicit_verifier_reuse_s": 0.011, + "fresh_process_external_root_verification_s": 2.485, + "warm_invariant_tables_fresh_relation_verification_s": 1.208, + "full_test_s": 7.65, + "artifact_persisted_for_child_verification": true, + "separate_trusted_transport_persisted_for_child_verification": true, + "temporary_artifact_and_transport_removed": true, + "timing_sample_count": 1 + } + } + }, + "validation": { + "fast_tests": { + "passed": 75, + "failed": 0, + "ignored": 13, + "elapsed_s": 5.42 + }, + "serial_cryptographic_vectors": { + "passed": 13, + "failed": 0, + "elapsed_s": 73.92 + }, + "all_four_input_lanes_reject_noncanonical_boundaries": true, + "all_128_violation_bits_recomputed_by_boolean_r1cs": true, + "odd_final_requests_and_group_boundary_mutations_rejected": true, + "count_compiled_arity_and_row_parity": true, + "data_zero_and_bounded_assertion_wiring_classes_checked": true, + "poisoned_recycled_storage_vectors_passed": true, + "production_parameter_full_proof_and_fresh_process_verification": true, + "separate_two_query_full_proof_and_fresh_process_verification": true, + "real_native_fixtures_fresh_verified": true, + "flock_enabled_cli_build_and_regressions": true, + "isolated_release_clippy_all_targets_all_features": true, + "root_ix_ffi_release_clippy_parallel_net_flock_all_targets": true, + "root_ix_ffi_no_default_features_check": true, + "both_workspaces_format_check": true, + "git_diff_check": true + }, + "notes": [ + "Real aggregate results are count-only, before wiring compilation. 192 GiB is padded z/a/b, not a total resident-memory prediction. Compiler, PCS, lincheck, allocator scratch and OS headroom remain additional.", + "Real-root counts were collected in separate fresh processes, sequentially, with 8 Rayon threads and 16 GiB address-space caps.", + "Toy 100-query and two-query proof measurements are separate standalone processes. Timings are single-run diagnostics, not throughput or memory guarantees.", + "Toy native transports, typed-witness layouts and public/input counts remain unchanged; compiled circuit and relation/Stage 3 statement digests change.", + "Canonicality row count includes zero-output anchors and unbatched transcript samples, so total rows are slightly more than half the preceding table count.", + "The experimental Stage 2 profile remains explicit opt-in. No fixture or old measurement JSON was rewritten.", + "No independent soundness audit or production-root Flock proof has been completed." + ] +} diff --git a/flock-stage3/measurements/pcs-query-sharing-2026-09-05.json b/flock-stage3/measurements/pcs-query-sharing-2026-09-05.json new file mode 100644 index 00000000..4e097692 --- /dev/null +++ b/flock-stage3/measurements/pcs-query-sharing-2026-09-05.json @@ -0,0 +1,920 @@ +{ + "schema": "ix.flock-stage3.query-sharing-measurement", + "version": 1, + "date": "2026-09-05", + "source_commit": "40cf786ac78990108701ac2f1ec5e3b5867f4410", + "source_worktree_dirty": true, + "source_note": "Share fixed PCS query-point factors per distinct LDE height, compute one base-field point per height/query, and share nonzero denominators by (height, semantic opening-point kind) within each query. Boolean gate schemas, native Stage 2 transports/keys, Flock pins and admission defaults are unchanged. Compiled circuit, public layout and relation identities change.", + "baseline_measurements": [ + "persisted-singleton-pcs-2026-09-05.json", + "stage2-lookup-packing-2026-09-05.json", + "production-parameters-pcs-2026-09-05.json" + ], + "protocol": { + "lean_toolchain": "4.33.1", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "plonky3_revision": "3152b14a89067c83775a8076cc262ffc48a1fd7c", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "flock_config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "commitment": { + "log_blowup": 2, + "cap_height": 0 + }, + "fri": { + "query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0 + } + }, + "host": { + "cpu": "AMD Ryzen 9 7950X3D", + "mem_total_bytes": 134128111616, + "rayon_threads": 8, + "census_address_space_limit_bytes": 17179869184, + "production_parameter_proof_address_space_limit_bytes": 68719476736, + "two_query_proof_address_space_limit_bytes": 17179869184, + "timing_sample_count_per_fixture": 1 + }, + "real_aggregate_roots": [ + { + "profile": "legacy", + "fixture": "Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json", + "root_wrapper": { + "file": "root.ixon-proof", + "bytes": 8565030, + "blake3": "254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406" + }, + "aggregate_vk": { + "file": "aggr.vk", + "bytes": 181630, + "blake3": "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e" + }, + "outer_claim": { + "file": "outer-claim.bin", + "bytes": 144, + "blake3": "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713" + }, + "native_root_key_claim_bytes_unchanged": true, + "native_proofs_fresh_process_verified": true, + "before": { + "compiled": false, + "count_us": 342903, + "nu": 24, + "padded_union_witness_bytes": 824633720832, + "process_peak_rss_bytes": 446173184, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 16777216, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 6408875 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 2983568 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 12792346 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 4477770 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 668500 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "version": 1 + }, + "query_point_sharing_only": { + "nu": 23, + "table_capacity": 8388608, + "padded_union_witness_bytes": 412316860416, + "count_us": 201831, + "process_peak_rss_bytes": 370044928, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 3372375 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1796468 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 7295269 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2655870 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "compiled": false + }, + "after": { + "compiled": false, + "count_us": 175956, + "nu": 23, + "padded_union_witness_bytes": 412316860416, + "process_peak_rss_bytes": 366891008, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 8388608, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 2871375 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1629468 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 6283484 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2405370 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 104980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "version": 1 + }, + "reductions": { + "canonicality_rows": 6508862, + "canonicality_rows_percent": 50.88091, + "padded_union_witness_bytes": 412316860416, + "padded_union_witness_percent": 50 + }, + "stage3_compiled": false, + "stage3_evaluated": false, + "stage3_proven": false, + "count_command": "(ulimit -v 16777216; IX_FLOCK_TIMING=1 RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture --count Tests/Fixtures/Aggregate/singleton-2026-09-05)", + "count_admission": { + "admission_error": "Stage 3 padded union witness requires 412316860416 bytes; admission limit is 1 (PCS/compiler scratch is additional)", + "aggregate_lookup_policy": "legacy", + "compiled": false, + "schema": "ix.flock-stage3.fixture-count", + "version": 1 + } + }, + { + "profile": "min-opening-width-v1", + "fixture": "Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/fixture.json", + "root_wrapper": { + "file": "root.ixon-proof", + "bytes": 8002662, + "blake3": "635c8f79af8cf1a913cb9291fbd1aa1a3f2c4ff221e6ec1339795989c649910f" + }, + "aggregate_vk": { + "file": "aggr.vk", + "bytes": 181630, + "blake3": "3c740f5b7645b1f7cdf361b7de3e20628c18ea3bf9cfafd1bd8aac4aca2bf7ba" + }, + "outer_claim": { + "file": "outer-claim.bin", + "bytes": 144, + "blake3": "72e37e5dda38c176b1caaa377ea779ca285d8e614ca268aeb9130812fdc2216d" + }, + "native_root_key_claim_bytes_unchanged": true, + "native_proofs_fresh_process_verified": true, + "before": { + "compiled": false, + "count_us": 332532, + "nu": 24, + "padded_union_witness_bytes": 824633720832, + "process_peak_rss_bytes": 408211456, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 16777216, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 6336142 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 2916038 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 12580880 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 4376475 + }, + { + "gate": "Blake3Gate", + "rows": 158899 + }, + { + "gate": "DigestOrderGate", + "rows": 668500 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 32399 + } + ], + "version": 1 + }, + "query_point_sharing_only": { + "nu": 23, + "table_capacity": 8388608, + "padded_union_witness_bytes": 412316860416, + "count_us": 211552, + "process_peak_rss_bytes": 363098112, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 3299642 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1728938 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 7083803 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2554575 + }, + { + "gate": "Blake3Gate", + "rows": 158899 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 32399 + } + ], + "compiled": false + }, + "after": { + "compiled": false, + "count_us": 168385, + "nu": 23, + "padded_union_witness_bytes": 412316860416, + "process_peak_rss_bytes": 364548096, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 8388608, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 2798642 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 1561938 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 6072018 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 2304075 + }, + { + "gate": "Blake3Gate", + "rows": 158899 + }, + { + "gate": "DigestOrderGate", + "rows": 88700 + }, + { + "gate": "F128EqualityGate", + "rows": 104980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 32399 + } + ], + "version": 1 + }, + "reductions": { + "canonicality_rows": 6508862, + "canonicality_rows_percent": 51.736142, + "padded_union_witness_bytes": 412316860416, + "padded_union_witness_percent": 50 + }, + "stage3_compiled": false, + "stage3_evaluated": false, + "stage3_proven": false, + "count_command": "(ulimit -v 16777216; IX_FLOCK_TIMING=1 RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture --min-opening-width --count Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05)", + "count_admission": { + "admission_error": "Stage 3 padded union witness requires 412316860416 bytes; admission limit is 1 (PCS/compiler scratch is additional)", + "aggregate_lookup_policy": "min-opening-width-v1", + "compiled": false, + "schema": "ix.flock-stage3.fixture-count", + "version": 1 + } + } + ], + "count_only_limits": { + "max_table_capacity": 4294967296, + "max_union_witness_bytes": 1 + }, + "production_admission_defaults": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "default_cli_admission_errors": [ + "Stage 3 table capacity 8388608 (nu=23) exceeds admission limit 4194304", + "Stage 3 padded union witness requires 412316860416 bytes; admission limit is 34359738368 (PCS/compiler scratch is additional)" + ], + "production_parameter_toy": { + "fixture": "Three native multi-STARK circuits: one inactive, active heights 8 and 4, preprocessing and an 18-word claim lookup. Not an ix_aggr corpus root.", + "stage2_root_unchanged_from_baseline": true, + "census": { + "count_us": 2891, + "test_total_seconds": 1.84, + "circuit_digest": "4a436f834faaeb4a4bcf80b7e310ea6ceee69dc45902a0800a186a54bdb3e231", + "phase_ms": { + "count_and_admit": 3.08, + "declare_slots": 755.96, + "shared_pcs_constraints": 0.24, + "pcs_query_constraints": 19.07, + "fri_query_constraints": 8.74, + "finish_builder": 934.58, + "compile_relation": 1724 + }, + "admission_limits": { + "max_table_capacity": 65536, + "max_union_witness_bytes": 3221225472 + }, + "rejected_one_row_or_byte_below": true + }, + "proved": true, + "report": { + "advice": { + "active_circuits": 2, + "advice_bytes": 143426, + "commitment_cap_digests": 6, + "fri_merkle_siblings": 900, + "fri_rounds": 3, + "fri_sibling_extension_values": 300, + "input_merkle_siblings": 2000, + "input_rounds_per_query": 4, + "opened_base_values": 3200, + "other_extension_values": 63, + "queries": 100, + "total_circuits": 3 + }, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "limits": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + "merkle_hash": "blake3", + "process_peak_rss_bytes": 999923712, + "profile": "fast128", + "relation": { + "blake3_rows": 4470, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 65268, + "circuit_digest": "4a436f834faaeb4a4bcf80b7e310ea6ceee69dc45902a0800a186a54bdb3e231", + "digest_order_rows": 5000, + "equality_rows": 3815, + "field_sample_rows": 8, + "goldilocks_add_rows": 33661, + "goldilocks_mul_rows": 14858, + "hash_sample_rows": 52, + "lane_repack_rows": 21198, + "nu": 16, + "public_values": 14289, + "relation_inputs": 13786, + "table_capacity": 65536, + "u64_split_rows": 520 + }, + "relation_cache": "none", + "relation_digest": "84e51adf14fe15746142b64ab5f2db9648a9529612ed4e4a125b7523539a03bf", + "resources": { + "committed_union_log": 30, + "dense_witness_bytes": 99271760, + "padded_union_witness_bytes": 3221225472, + "pcs_codeword_bytes": 201326592, + "pcs_lanes": 48, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 100663296, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 1610612736, + "registry_slot": 0, + "rows": 14858, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 402653184, + "registry_slot": 1, + "rows": 4470, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 402653184, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 100663296, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 50331648, + "registry_slot": 4, + "rows": 33661, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 50331648, + "registry_slot": 5, + "rows": 5000, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 25165824, + "registry_slot": 6, + "rows": 21198, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 7, + "rows": 65268, + "useful_boolean_columns": 318 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 8, + "rows": 3815, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 9, + "rows": 52, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 12582912, + "registry_slot": 10, + "rows": 520, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 33 + }, + "schema": "ix.flock-stage3.preflight", + "specialization": { + "activation": [ + false, + true, + true + ], + "active_log_degrees": [ + 3, + 2 + ], + "fri_parameter_words": [ + 0, + 1, + 100, + 0, + 20 + ], + "typed_witness_layout_digest": "82fba0be5f9777bf92835a742a02e153ffb288dd53f1a2618ac82f8841cb5cfb" + }, + "stage2_root_digest": "9773a5442674c6679b2551d15dce94702964f25402094831d930fe010182320f", + "stage3_statement_digest": "dd65d7c9be6669e1501888dadf8d11c1d485c67ad1494cf68dbcf596a5d51ce2", + "timings": { + "compile_us": 1738175, + "evaluate_us": 10313, + "lowering_us": 1611, + "native_prepare_us": 1649, + "total_us": 1751791 + }, + "transcript": "chained-blake3", + "transcript_domain": "ix:flock-stage3:fri-verifier:v1", + "transport": { + "claim_bytes": 144, + "compact_proof_bytes": 43249, + "compact_proof_digest": "a553b3ec29ceb843daff22b36f7996e128082bb4de0b24fa034858e0cbfad053", + "verifying_key_bytes": 279, + "verifying_key_digest": "afa90f0a22bfaf47cf2d5b9fa47f466b46ae1eba7c5e9fbe63b6ce47e5402302" + }, + "version": 1 + }, + "proof_run": { + "prove_us": 2234030, + "self_verify_us": 16449, + "package_us": 272, + "total_us": 2250751, + "artifact_bytes": 481987, + "payload_bytes": 481861, + "post_proof_process_peak_rss_bytes": 5509857280, + "explicit_reuse_verify_seconds": 0.017, + "fresh_process_verify_seconds": 2.7, + "fresh_relation_warm_tables_verify_seconds": 1.609, + "test_total_seconds": 10.059, + "temporary_artifact_persisted_and_cold_verified": true, + "rejected_wrong_relation_and_corrupt_proof": true, + "temporary_artifact_removed_after_test": true + } + }, + "two_query_cross_check": { + "proved": true, + "report": { + "advice": { + "active_circuits": 2, + "advice_bytes": 3916, + "commitment_cap_digests": 6, + "fri_merkle_siblings": 12, + "fri_rounds": 3, + "fri_sibling_extension_values": 6, + "input_merkle_siblings": 32, + "input_rounds_per_query": 4, + "opened_base_values": 64, + "other_extension_values": 63, + "queries": 2, + "total_circuits": 3 + }, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "limits": { + "max_advice_bytes": 268435456, + "max_compact_proof_bytes": 67108864, + "max_fri_queries": 1024, + "max_fri_rounds": 32, + "max_profile_items": 16777216, + "max_table_capacity": 4194304, + "max_total_circuits": 65536, + "max_union_witness_bytes": 34359738368, + "max_verifying_key_bytes": 16777216 + }, + "memory_note": "Padded witness covers z/a/b; compiler, lincheck, PCS and allocator scratch are additional. RSS is the process lifetime high-water mark, not a per-root peak.", + "merkle_hash": "blake3", + "process_peak_rss_bytes": 819068928, + "profile": "fast128", + "relation": { + "blake3_rows": 119, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 3999, + "circuit_digest": "be8bbe53f1af8a04277b704875f7d3965048d7d653fc311aa23b00ec9e54b23d", + "digest_order_rows": 76, + "equality_rows": 91, + "field_sample_rows": 8, + "goldilocks_add_rows": 2173, + "goldilocks_mul_rows": 828, + "hash_sample_rows": 2, + "lane_repack_rows": 1284, + "nu": 12, + "public_values": 604, + "relation_inputs": 593, + "table_capacity": 4096, + "u64_split_rows": 8 + }, + "relation_cache": "none", + "relation_digest": "b6df063431ff94bf4d4b2fa7ae3ccd8f1cf18a79148f7af5daba4d10419010a2", + "resources": { + "committed_union_log": 26, + "dense_witness_bytes": 5440128, + "padded_union_witness_bytes": 201326592, + "pcs_codeword_bytes": 11010048, + "pcs_lanes": 42, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 5505024, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 100663296, + "registry_slot": 0, + "rows": 828, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 25165824, + "registry_slot": 1, + "rows": 119, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 25165824, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 6291456, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 3145728, + "registry_slot": 4, + "rows": 2173, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 3145728, + "registry_slot": 5, + "rows": 76, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 1572864, + "registry_slot": 6, + "rows": 1284, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 7, + "rows": 3999, + "useful_boolean_columns": 318 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 8, + "rows": 91, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 9, + "rows": 2, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 786432, + "registry_slot": 10, + "rows": 8, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 29 + }, + "schema": "ix.flock-stage3.preflight", + "specialization": { + "activation": [ + false, + true, + true + ], + "active_log_degrees": [ + 3, + 2 + ], + "fri_parameter_words": [ + 0, + 1, + 2, + 0, + 0 + ], + "typed_witness_layout_digest": "f25d2a4c87b411eac37f49c4479fada0e628c6d7f57a2c303e55af4f0e83d772" + }, + "stage2_root_digest": "e4d635cb35dee195f87c29653c58b7313d4da0ce8468b20d97d93d9bf32fff54", + "stage3_statement_digest": "c254fb22705d7e3653804d17422e8a96de9ad09e619c134b5c106882b763cf53", + "timings": { + "compile_us": 1466998, + "evaluate_us": 235, + "lowering_us": 80, + "native_prepare_us": 99, + "total_us": 1467418 + }, + "transcript": "chained-blake3", + "transcript_domain": "ix:flock-stage3:fri-verifier:v1", + "transport": { + "claim_bytes": 144, + "compact_proof_bytes": 3441, + "compact_proof_digest": "a513eb6ca3f53f0437d5e81cbb910f735e7deb1520c7ba3ebcf85e6b7ca296f2", + "verifying_key_bytes": 279, + "verifying_key_digest": "c6c140a56ee9662394c92a6ca6edfa131c523254f958b3f94de3d0f75eb7d37d" + }, + "version": 1 + }, + "proof_run": { + "prove_us": 1102599, + "self_verify_us": 13126, + "package_us": 171, + "total_us": 1115897, + "artifact_bytes": 343851, + "payload_bytes": 343725, + "post_proof_process_peak_rss_bytes": 1701462016, + "explicit_reuse_verify_seconds": 0.012, + "fresh_process_verify_seconds": 2.422, + "fresh_relation_warm_tables_verify_seconds": 1.237, + "test_total_seconds": 7.6, + "temporary_artifact_persisted_and_cold_verified": true, + "rejected_wrong_relation_and_corrupt_proof": true, + "temporary_artifact_removed_after_test": true + } + }, + "validation": { + "fast_tests": 74, + "fast_tests_passed": true, + "serial_cryptographic_tests": 13, + "serial_cryptographic_tests_passed": true, + "serial_cryptographic_test_seconds": 72.88, + "shared_query_point_oracle_and_mutations": "compiled relation; repeated/interleaved heights 0,1,2,9,16,31,32; two independent queries; changed bits/values/high lanes and non-Boolean selectors rejected without native PCS validation or public-vector comparison", + "counting_builder_real_builder_parity": true, + "denominator_inverse_quotient_and_zero_denominator_mutations": true, + "native_fixture_key_and_proof_verification": "both profiles passed in fresh processes", + "cli_admission_and_explicit_profile_regressions": "passed", + "root_and_isolated_clippy": "passed", + "root_and_isolated_formatting": "passed", + "feature_disabled_ffi_check": "passed" + }, + "notes": [ + "The real-root measurements are exact row-count diagnostics only. Neither real aggregate was wired, evaluated or proven with Flock; both still exceed production admission limits.", + "Query points use the suffix of the global transcript-derived index bits appropriate to the matrix height, then bit-reversed subgroup exponentiation with the fixed coset shift 7.", + "The denominator cache is local to one query. Its metadata key distinguishes Zeta from every ZetaNext generator log degree, even if some witness values coincide. No deduplication uses Wire identity; counting wires remain all-identical placeholders.", + "Each distinct denominator still has canonical D/inverse checks, D+x=point and D*inverse=1. Each matrix/point keeps a separately constrained weighted quotient, exact alpha offset and height-bucket contribution. Empty column sets retain nonzero-denominator enforcement.", + "Native Stage 2 fixtures, the opt-in nature of the minimum-opening-width key profile, protocol parameters and admission defaults are unchanged. Prior measurements remain immutable historical snapshots.", + "The 100-query toy has 65268 canonicality rows at capacity 65536 (268 rows headroom). Its 3 GiB padded estimate is not a total-RSS bound: the measured proving process peaked at 5509857280 bytes (5.13 GiB).", + "The 100-query artifact grew from 461635 to 481987 bytes despite fewer rows and lower memory/time. PCS lanes changed from 35 to 48; proof size is not monotonic in the padded capacity. The two-query artifact decreased from 368163 to 343851 bytes.", + "Proving and census measurements ran in separate standalone processes with fixed Rayon thread counts. Timings are single local observations, not confidence intervals or performance guarantees.", + "Toy proof tests persist an artifact and a separate trusted root transport for fresh-process verification, then remove their temporary files. No real-root Flock artifact or permanent toy artifact is produced here.", + "Independent soundness review, a broader aggregate corpus, practical aggregate memory use and explicit deployment pin regeneration remain outstanding." + ] +} diff --git a/flock-stage3/measurements/persisted-singleton-2026-09-05.json b/flock-stage3/measurements/persisted-singleton-2026-09-05.json new file mode 100644 index 00000000..77ab9ebe --- /dev/null +++ b/flock-stage3/measurements/persisted-singleton-2026-09-05.json @@ -0,0 +1,59 @@ +{ + "schema": "ix.flock-stage3.persisted-root-measurement", + "version": 1, + "date": "2026-09-05", + "source_commit": "40cf786ac78990108701ac2f1ec5e3b5867f4410", + "source_worktree_dirty": true, + "fixture": "Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json", + "fixture_kind": "production-protocol ix_aggr shape-0 wrap of one well-formed axiom declaration", + "root_wrapper_blake3": "254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406", + "root_wrapper_bytes": 8565030, + "aggregate_vk_blake3": "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e", + "outer_claim_blake3": "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713", + "native_aggregate_verified": true, + "native_aggregate_active_circuits": 175, + "native_aggregate_prove_us": 75812960, + "native_aggregate_verify_us": 121037, + "native_aggregate_sampled_peak_rss_bytes": 27118215168, + "stage3_compiled": false, + "stage3_evaluated": false, + "stage3_proven": false, + "diagnostic_address_space_limit_bytes": 17179869184, + "rayon_threads": 8, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "shape_count": { + "schema": "ix.flock-stage3.shape-count", + "version": 1, + "compiled": false, + "count_us": 1307431, + "nu": 26, + "table_capacity": 67108864, + "padded_union_witness_bytes": 3298534883328, + "process_peak_rss_bytes": 548392960, + "tables": [ + {"gate": "GoldilocksAddPairGate", "rows": 33716533}, + {"gate": "GoldilocksMulPairGate", "rows": 12027002}, + {"gate": "CanonicalGoldilocksPairGate", "rows": 62893085}, + {"gate": "GoldilocksLaneRepackGate", "rows": 19400421}, + {"gate": "Blake3Gate", "rows": 167996}, + {"gate": "DigestOrderGate", "rows": 668500}, + {"gate": "F128EqualityGate", "rows": 1836780}, + {"gate": "HashSampleGate", "rows": 52}, + {"gate": "GoldilocksSampleGate", "rows": 26}, + {"gate": "U64SplitGate", "rows": 2320}, + {"gate": "ByteWindowGate", "rows": 1759377} + ] + }, + "default_admission_error": "Stage 3 table capacity 67108864 (nu=26) exceeds admission limit 4194304", + "capacity_override_admission_error": "Stage 3 padded union witness requires 3298534883328 bytes; admission limit is 34359738368 (PCS/compiler scratch is additional)", + "capacity_override": 67108864, + "max_union_witness_bytes_unchanged": 34359738368, + "notes": [ + "The capacity-only override exposes the padded-witness rejection; no witness limit was raised and no wiring was compiled.", + "The shape count is diagnostic, not an evaluated relation, security claim, or deployment pin.", + "Count timing excludes native validation/lowering. Its RSS is the process lifetime high-water mark.", + "One minimal production-protocol root is not a representative production-sized corpus." + ] +} diff --git a/flock-stage3/measurements/persisted-singleton-pcs-2026-09-05.json b/flock-stage3/measurements/persisted-singleton-pcs-2026-09-05.json new file mode 100644 index 00000000..8fe5e5c8 --- /dev/null +++ b/flock-stage3/measurements/persisted-singleton-pcs-2026-09-05.json @@ -0,0 +1,95 @@ +{ + "schema": "ix.flock-stage3.persisted-root-measurement", + "version": 1, + "date": "2026-09-05", + "source_commit": "40cf786ac78990108701ac2f1ec5e3b5867f4410", + "source_worktree_dirty": true, + "fixture": "Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json", + "fixture_kind": "production-protocol ix_aggr shape-0 wrap of one well-formed axiom declaration", + "root_wrapper_blake3": "254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406", + "root_wrapper_bytes": 8565030, + "aggregate_vk_blake3": "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e", + "outer_claim_blake3": "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713", + "native_aggregate_verified": true, + "native_aggregate_active_circuits": 175, + "native_aggregate_prove_us": 75812960, + "native_aggregate_verify_us": 121037, + "native_aggregate_sampled_peak_rss_bytes": 27118215168, + "stage3_compiled": false, + "stage3_evaluated": false, + "stage3_proven": false, + "diagnostic_address_space_limit_bytes": 17179869184, + "rayon_threads": 8, + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "shape_count": { + "compiled": false, + "count_us": 342903, + "nu": 24, + "padded_union_witness_bytes": 824633720832, + "process_peak_rss_bytes": 446173184, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 16777216, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 6408875 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 2983568 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 12792346 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 4477770 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 668500 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "version": 1 + }, + "default_admission_error": "Stage 3 table capacity 16777216 (nu=24) exceeds admission limit 4194304", + "capacity_override_admission_error": "Stage 3 padded union witness requires 824633720832 bytes; admission limit is 34359738368 (PCS/compiler scratch is additional)", + "capacity_override": 16777216, + "max_union_witness_bytes_unchanged": 34359738368, + "notes": [ + "The capacity-only override exposes the padded-witness rejection; no witness limit was raised and no wiring was compiled.", + "The shape count is diagnostic, not an evaluated relation, security claim, or deployment pin.", + "Count timing excludes native validation/lowering. Its RSS is the process lifetime high-water mark.", + "One minimal production-protocol root is not a representative production-sized corpus.", + "The original measurement remains unchanged. Padded z/a/b fell from 3 TiB to 768 GiB (4x); canonicality rows fell from 62893085 to 12792346." + ], + "source_note": "PCS follow-up: share query-independent alpha powers, OOD sums, points and commitment bindings; reduce each matrix/point to its exact weighted quotient with a constrained nonzero denominator. Circuit and relation identities change; protocol pins and admission defaults do not. Count only, not an independently audited or compiled aggregate relation.", + "baseline_measurement": "persisted-singleton-2026-09-05.json" +} diff --git a/flock-stage3/measurements/production-parameters-2026-09-05.json b/flock-stage3/measurements/production-parameters-2026-09-05.json new file mode 100644 index 00000000..e0cd9f11 --- /dev/null +++ b/flock-stage3/measurements/production-parameters-2026-09-05.json @@ -0,0 +1,44 @@ +{ + "schema": "ix.flock-stage3.sizing-experiment", + "version": 1, + "date": "2026-09-05", + "source_base_commit": "40cf786ac789", + "source_note": "Measured during the uncommitted admission/reporting follow-up; cryptographic lowering and dependency pins unchanged. The original run extended the ordinary production-parameter fixture test; the retained census is now opt-in.", + "fixture": "Three native multi-STARK circuits: one inactive, active heights 8 and 4, preprocessing and an 18-word claim lookup. This is not a persisted ix_aggr corpus root.", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "commitment": {"log_blowup": 2, "cap_height": 0}, + "fri_parameter_words": [0, 1, 100, 0, 20], + "circuit_digest": "360efd279e048e490b56af044f74a978cb056ccea333a7e879a739456e95f98b", + "test_total_seconds": 653.60, + "proved": false, + "relation": { + "nu": 21, + "table_capacity": 2097152, + "relation_inputs": 29506, + "public_values": 30009, + "blake3_rows": 4470, + "digest_order_rows": 7300, + "goldilocks_add_rows": 135523, + "goldilocks_mul_rows": 48362, + "lane_repack_rows": 77604, + "canonical_goldilocks_rows": 248886, + "equality_rows": 9015, + "hash_sample_rows": 52, + "field_sample_rows": 8, + "u64_split_rows": 520, + "byte_window_rows": 6885 + }, + "resources": { + "virtual_union_log": 38, + "committed_union_log": 32, + "dense_witness_bytes": 316791024, + "padded_union_witness_bytes": 103079215104, + "pcs_message_bytes": 318767104, + "pcs_codeword_bytes": 637534208, + "pcs_log_batch_size": 6, + "pcs_lanes": 38, + "pcs_log_inverse_rate": 1 + }, + "memory_note": "Buffer geometry, not measured RSS. Prover buffers were not allocated. Compilation, lincheck, PCS, allocator and other scratch are additional. Default 32 GiB padded-witness admission rejects this shape before wiring compilation." +} diff --git a/flock-stage3/measurements/production-parameters-exact-2026-09-05.json b/flock-stage3/measurements/production-parameters-exact-2026-09-05.json new file mode 100644 index 00000000..e9e0e002 --- /dev/null +++ b/flock-stage3/measurements/production-parameters-exact-2026-09-05.json @@ -0,0 +1,73 @@ +{ + "schema": "ix.flock-stage3.sizing-experiment", + "version": 1, + "date": "2026-09-05", + "source_base_commit": "40cf786ac789", + "source_note": "Uncommitted exact-emission sizing and bounded arithmetic zero-assertion follow-up. This changes circuit and relation digests; dependency and configuration pins are unchanged. Census and proof were separate standalone processes.", + "fixture": "Three native multi-STARK circuits: one inactive, active heights 8 and 4, preprocessing and an 18-word claim lookup. This is not a persisted ix_aggr corpus root.", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "commitment": {"log_blowup": 2, "cap_height": 0}, + "fri_parameter_words": [0, 1, 100, 0, 20], + "circuit_digest": "7d63b3f0a9da9d04237e63914701ea0a321371f042449b4c5d7051d7177c915f", + "relation_digest": "4759dbe52ce3a0d7ca91fb4dc4f98245c40c50fc1ec20621da4884b265a7601d", + "stage2_root_digest": "9773a5442674c6679b2551d15dce94702964f25402094831d930fe010182320f", + "stage3_statement_digest": "fca63ac72f96a93faad4841fd2c46226181189e2578622ddd2d416fb7a0bdccb", + "test_total_seconds": 2.62, + "census_phase_ms": { + "count_and_admit": 8.37, + "declare_slots": 773.36, + "pcs_query_constraints": 100.16, + "fri_query_constraints": 10.64, + "finish_builder": 1468.99 + }, + "proved": true, + "proof_run": { + "prepare_us": 2420220, + "compile_us": 2391063, + "evaluate_us": 25572, + "prove_us": 5124758, + "self_verify_us": 27977, + "package_us": 352, + "proof_operation_us": 5153088, + "artifact_bytes": 489187, + "payload_bytes": 489061, + "preflight_process_peak_rss_bytes": 1533452288, + "post_proof_process_peak_rss_bytes": 16762380288, + "explicit_reuse_verify_seconds": 0.027, + "fresh_relation_warm_tables_verify_seconds": 2.226, + "fresh_process_verify_seconds": 3.536, + "artifact_persisted_and_cold_verified": true, + "rejected_wrong_relation_and_corrupt_proof": true, + "test_total_seconds": 15.82 + }, + "relation": { + "nu": 18, + "table_capacity": 262144, + "relation_inputs": 29506, + "public_values": 30009, + "blake3_rows": 4470, + "digest_order_rows": 7300, + "goldilocks_add_rows": 135523, + "goldilocks_mul_rows": 48362, + "lane_repack_rows": 77604, + "canonical_goldilocks_rows": 251484, + "equality_rows": 9015, + "hash_sample_rows": 52, + "field_sample_rows": 8, + "u64_split_rows": 520, + "byte_window_rows": 6885 + }, + "resources": { + "virtual_union_log": 35, + "committed_union_log": 32, + "dense_witness_bytes": 316915728, + "padded_union_witness_bytes": 12884901888, + "pcs_message_bytes": 318767104, + "pcs_codeword_bytes": 637534208, + "pcs_log_batch_size": 6, + "pcs_lanes": 38, + "pcs_log_inverse_rate": 1 + }, + "memory_note": "Padded z/a/b geometry is not a total RSS bound. Compiler, lincheck, PCS and allocator scratch are additional. RSS is the proving process lifetime high-water mark, not the child verifier's. This fixture passes default 32 GiB padded-witness admission without raising limits." +} diff --git a/flock-stage3/measurements/production-parameters-pcs-2026-09-05.json b/flock-stage3/measurements/production-parameters-pcs-2026-09-05.json new file mode 100644 index 00000000..4b2ec873 --- /dev/null +++ b/flock-stage3/measurements/production-parameters-pcs-2026-09-05.json @@ -0,0 +1,304 @@ +{ + "schema": "ix.flock-stage3.sizing-experiment", + "version": 1, + "date": "2026-09-05", + "source_base_commit": "40cf786ac789", + "source_note": "Uncommitted shared-PCS and exact weighted-quotient follow-up on the exact-emission compiler. Circuit, public layout and relation digests change; dependency/configuration pins and admission defaults do not. Census and proof were separate standalone processes.", + "baseline_measurement": "production-parameters-exact-2026-09-05.json", + "fixture": "Three native multi-STARK circuits: one inactive, active heights 8 and 4, preprocessing and an 18-word claim lookup. This is not a persisted ix_aggr corpus root.", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "commitment": { + "log_blowup": 2, + "cap_height": 0 + }, + "fri_parameter_words": [ + 0, + 1, + 100, + 0, + 20 + ], + "circuit_digest": "d2a1e42a479137b93c23d396268ef922611f39eea3e5f7317e487487b421c4c1", + "relation_digest": "e50dbebd84beae420c84176417a0d62b5c6b347298516a933af0a86f3c6eb7f3", + "stage2_root_digest": "9773a5442674c6679b2551d15dce94702964f25402094831d930fe010182320f", + "stage3_statement_digest": "c4964881763e86c4e3f119588133ac2e508e86c77d1958c0a41ef2899f7d2ea7", + "rayon_threads": 8, + "census_address_space_limit_bytes": 17179869184, + "proof_address_space_limit_bytes": 68719476736, + "test_total_seconds": 2.01, + "census_phase_ms": { + "count_and_admit": 4.44, + "declare_slots": 769.23, + "shared_pcs_constraints": 0.42, + "pcs_query_constraints": 39.21, + "fri_query_constraints": 9.77, + "finish_builder": 1022.95 + }, + "census_admission_boundary": { + "max_table_capacity": 131072, + "max_union_witness_bytes": 6442450944, + "rejected_one_row_or_byte_below": true + }, + "proved": true, + "two_query_cross_check": { + "fri_parameter_words": [ + 0, + 1, + 2, + 0, + 0 + ], + "relation": { + "blake3_rows": 119, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 4633, + "circuit_digest": "3a8bda222284035196bf7fba73fbf673878bd35cbdf47dc692224192dcbd5029", + "digest_order_rows": 112, + "equality_rows": 123, + "field_sample_rows": 8, + "goldilocks_add_rows": 2519, + "goldilocks_mul_rows": 946, + "hash_sample_rows": 2, + "lane_repack_rows": 1482, + "nu": 13, + "public_values": 680, + "relation_inputs": 669, + "table_capacity": 8192, + "u64_split_rows": 8 + }, + "resources": { + "committed_union_log": 26, + "dense_witness_bytes": 6200960, + "padded_union_witness_bytes": 402653184, + "pcs_codeword_bytes": 12582912, + "pcs_lanes": 48, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 6291456, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 201326592, + "registry_slot": 0, + "rows": 946, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 50331648, + "registry_slot": 1, + "rows": 119, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 50331648, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 12582912, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 6291456, + "registry_slot": 4, + "rows": 2519, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 6291456, + "registry_slot": 5, + "rows": 112, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 3145728, + "registry_slot": 6, + "rows": 1482, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 1572864, + "registry_slot": 7, + "rows": 4633, + "useful_boolean_columns": 318 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 1572864, + "registry_slot": 8, + "rows": 123, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 1572864, + "registry_slot": 9, + "rows": 2, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 1572864, + "registry_slot": 10, + "rows": 8, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 30 + }, + "prepare_us": 1532128, + "prove_us": 1121261, + "self_verify_us": 11691, + "artifact_bytes": 368163, + "payload_bytes": 368037, + "post_proof_process_peak_rss_bytes": 1883480064, + "explicit_reuse_verify_seconds": 0.011, + "fresh_relation_warm_tables_verify_seconds": 1.245, + "fresh_process_verify_seconds": 2.462, + "test_total_seconds": 7.75, + "address_space_limit_bytes": 17179869184, + "note": "Two-query integration test passed persistence, fresh-process verification and negative artifact checks. Capacity remains nu=13 / 384 MiB; artifact grew from 323267 bytes despite fewer rows because PCS geometry changed." + }, + "proof_run": { + "prepare_us": 1854401, + "compile_us": 1838683, + "evaluate_us": 12617, + "prove_us": 3178037, + "self_verify_us": 18596, + "package_us": 309, + "proof_operation_us": 3196942, + "artifact_bytes": 461635, + "payload_bytes": 461509, + "preflight_process_peak_rss_bytes": 1154314240, + "post_proof_process_peak_rss_bytes": 9046319104, + "explicit_reuse_verify_seconds": 0.018, + "fresh_relation_warm_tables_verify_seconds": 1.726, + "fresh_process_verify_seconds": 2.861, + "artifact_persisted_and_cold_verified": true, + "rejected_wrong_relation_and_corrupt_proof": true, + "test_total_seconds": 11.6, + "artifact_retention": "Test persisted and fresh-process verified its artifact and separate trusted root transport, then removed both temporary files." + }, + "relation": { + "blake3_rows": 4470, + "byte_window_rows": 153, + "canonical_goldilocks_rows": 103160, + "circuit_digest": "d2a1e42a479137b93c23d396268ef922611f39eea3e5f7317e487487b421c4c1", + "digest_order_rows": 7300, + "equality_rows": 5415, + "field_sample_rows": 8, + "goldilocks_add_rows": 54461, + "goldilocks_mul_rows": 21958, + "hash_sample_rows": 52, + "lane_repack_rows": 33198, + "nu": 17, + "public_values": 19179, + "relation_inputs": 18676, + "table_capacity": 131072, + "u64_split_rows": 520 + }, + "resources": { + "committed_union_log": 31, + "dense_witness_bytes": 145047376, + "padded_union_witness_bytes": 6442450944, + "pcs_codeword_bytes": 293601280, + "pcs_lanes": 35, + "pcs_log_batch_size": 6, + "pcs_log_inverse_rate": 1, + "pcs_message_bytes": 146800640, + "tables": [ + { + "boolean_columns": 65536, + "padded_witness_bytes": 3221225472, + "registry_slot": 0, + "rows": 21958, + "useful_boolean_columns": 42357 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 805306368, + "registry_slot": 1, + "rows": 4470, + "useful_boolean_columns": 11707 + }, + { + "boolean_columns": 16384, + "padded_witness_bytes": 805306368, + "registry_slot": 2, + "rows": 8, + "useful_boolean_columns": 5482 + }, + { + "boolean_columns": 4096, + "padded_witness_bytes": 201326592, + "registry_slot": 3, + "rows": 153, + "useful_boolean_columns": 2561 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 100663296, + "registry_slot": 4, + "rows": 54461, + "useful_boolean_columns": 1725 + }, + { + "boolean_columns": 2048, + "padded_witness_bytes": 100663296, + "registry_slot": 5, + "rows": 7300, + "useful_boolean_columns": 1409 + }, + { + "boolean_columns": 1024, + "padded_witness_bytes": 50331648, + "registry_slot": 6, + "rows": 33198, + "useful_boolean_columns": 768 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 25165824, + "registry_slot": 7, + "rows": 103160, + "useful_boolean_columns": 318 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 25165824, + "registry_slot": 8, + "rows": 5415, + "useful_boolean_columns": 385 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 25165824, + "registry_slot": 9, + "rows": 52, + "useful_boolean_columns": 256 + }, + { + "boolean_columns": 512, + "padded_witness_bytes": 25165824, + "registry_slot": 10, + "rows": 520, + "useful_boolean_columns": 385 + } + ], + "virtual_union_log": 34 + }, + "memory_note": "Padded z/a/b geometry is not a total RSS bound. Compiler, lincheck, PCS and allocator scratch are additional. RSS is the proving process lifetime high-water mark, not the child verifier's. The native real ix_aggr singleton still fails admission at 768 GiB padded z/a/b.", + "validation_note": "72 fast tests and 13 serial cryptographic vectors pass. Added columnwise-versus-grouped algebra vectors across heights/offsets and alpha edge cases, plus compiled-gate rejection of modified/noncanonical D, inverse and Q and a coordinated zero-denominator attack. These checks are not an independent security audit." +} diff --git a/flock-stage3/measurements/stage2-lookup-packing-2026-09-05.json b/flock-stage3/measurements/stage2-lookup-packing-2026-09-05.json new file mode 100644 index 00000000..9c50cdd8 --- /dev/null +++ b/flock-stage3/measurements/stage2-lookup-packing-2026-09-05.json @@ -0,0 +1,694 @@ +{ + "schema": "ix.flock-stage3.stage2-lookup-packing-measurement", + "version": 1, + "date": "2026-09-05", + "source_commit": "40cf786ac78990108701ac2f1ec5e3b5867f4410", + "source_worktree_dirty": true, + "fixture_kind": "ix_aggr shape-0 wrap of one well-formed axiom declaration; same IxVM child, CheckEnv claim and circuit heights", + "baseline_fixture": "Tests/Fixtures/Aggregate/singleton-2026-09-05/fixture.json", + "experimental_fixture": "Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05/fixture.json", + "baseline_stage3_measurement": "persisted-singleton-pcs-2026-09-05.json", + "profile": { + "name": "min-opening-width-v1", + "opt_in": true, + "production_default_changed": false, + "method": "Choose circuit-local lookup group size k=1..8 minimizing stage_2_width + extension_degree * quotient_degree, with quotient degree at most 2^log_blowup. Strict improvements only; retain the old key on ties; among improvements prefer lower degree then smaller groups.", + "changed_circuits": 55, + "changed_circuit_group_size_histogram": { + "2": 48, + "4": 7 + }, + "user_constraint_graph_changed": false, + "lookup_order_changed": false, + "main_trace_layout_changed": false, + "preprocessed_commitment_changed": false, + "aggregate_verifying_key_changed": true, + "outer_allowed_key_digest_and_claim_changed": true, + "commitment_parameters_changed": false, + "fri_parameters_changed": false + }, + "protocol": { + "lean_toolchain": "4.33.1", + "multi_stark_revision": "6ad074c1f2983ecdd7a56984d333441d6b38186a", + "plonky3_revision": "3152b14a89067c83775a8076cc262ffc48a1fd7c", + "flock_revision": "b310f35f35f68095537150a1c8c0a43caca9a29e", + "flock_config_digest": "1897ad7e36bc1a11a9dc4170552b1b48f5689b8f04ecc3c3825ce0273ecfaffc", + "commitment": { + "log_blowup": 2, + "cap_height": 0 + }, + "fri": { + "query_pow_bits": 20, + "num_queries": 100, + "max_log_arity": 1, + "log_final_poly_len": 0, + "log_blowup": 2, + "commit_pow_bits": 0 + } + }, + "host": { + "cpu": "AMD Ryzen 9 7950X3D", + "mem_total_bytes": 134128111616, + "rayon_threads": 8, + "native_generation_address_space_limit_bytes": 68719476736, + "stage3_count_address_space_limit_bytes": 17179869184, + "native_process_tree_rss_sample_interval_ms": 10, + "timing_sample_count_per_profile": 1 + }, + "unchanged_child": { + "file": "ixvm.ixon-proof", + "bytes": 4485008, + "blake3": "9381b528d31fdb23e8af24c6131bb3a3dd898f446848d295e4f8b0da24c7a618" + }, + "unchanged_inputs": [ + { + "file": "environment.ixe", + "bytes": 116, + "blake3": "bed282f9db4dee56f1746870478aad7223ce2a325f7e6baf24dacde3aa06e0e8" + }, + { + "file": "check-env.claim", + "bytes": 34, + "blake3": "ddef67870a4517be2f996e2eeb6f7fdcd3871ba527190f17d45b93308a40825f" + }, + { + "file": "subjects.tree", + "bytes": 34, + "blake3": "9014207ff8cef20d1f8f64934fef14d4114e8afe43f7a35f930dfcb70175d67e" + }, + { + "file": "ixvm.vk", + "bytes": 709741, + "blake3": "e3b0aff0da508c305da9048b113e586502d35e75dadc4f7647d89efb320fbc78" + } + ], + "baseline": { + "lookup_policy": "legacy", + "root_wrapper": { + "file": "root.ixon-proof", + "bytes": 8565030, + "blake3": "254dcab734b79f1714d6c9b372ccdf8fcbad69e01fb90b51b0007f8cb6841406" + }, + "aggregate_vk": { + "file": "aggr.vk", + "bytes": 181630, + "blake3": "75452941bc0dbe4a861c88c792067f34d7864f0460f305858a0a004ec409406e" + }, + "outer_claim": { + "file": "outer-claim.bin", + "bytes": 144, + "blake3": "6f872178af386cef9c799c1417035682c120756b3bbec883f09b1c31c703c713" + }, + "native_aggregate_verified": true, + "active_circuits": 175, + "all_circuits": 203, + "active_committed_width": 9025, + "all_circuit_committed_width": 10408, + "aggregate_execute_us": 1652316, + "aggregate_prove_us": 75812960, + "aggregate_verify_us": 121037, + "aggregate_prove_sampled_peak_rss_bytes": 27118215168, + "ixvm_predicted_peak_bytes": 228450779, + "shape_count": { + "compiled": false, + "count_us": 342903, + "nu": 24, + "padded_union_witness_bytes": 824633720832, + "process_peak_rss_bytes": 446173184, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 16777216, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 6408875 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 2983568 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 12792346 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 4477770 + }, + { + "gate": "Blake3Gate", + "rows": 167996 + }, + { + "gate": "DigestOrderGate", + "rows": 668500 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 35391 + } + ], + "version": 1 + } + }, + "experimental": { + "lookup_policy": "min-opening-width-v1", + "root_wrapper": { + "file": "root.ixon-proof", + "bytes": 8002662, + "blake3": "635c8f79af8cf1a913cb9291fbd1aa1a3f2c4ff221e6ec1339795989c649910f" + }, + "aggregate_vk": { + "file": "aggr.vk", + "bytes": 181630, + "blake3": "3c740f5b7645b1f7cdf361b7de3e20628c18ea3bf9cfafd1bd8aac4aca2bf7ba" + }, + "outer_claim": { + "file": "outer-claim.bin", + "bytes": 144, + "blake3": "72e37e5dda38c176b1caaa377ea779ca285d8e614ca268aeb9130812fdc2216d" + }, + "native_aggregate_verified": true, + "active_circuits": 175, + "all_circuits": 203, + "active_committed_width": 8367, + "all_circuit_committed_width": 9706, + "aggregate_execute_us": 1443624, + "aggregate_prove_us": 75386250, + "aggregate_verify_us": 228214, + "aggregate_prove_sampled_peak_rss_bytes": 24276983808, + "ixvm_predicted_peak_bytes": 237549820, + "shape_count": { + "compiled": false, + "count_us": 332532, + "nu": 24, + "padded_union_witness_bytes": 824633720832, + "process_peak_rss_bytes": 408211456, + "schema": "ix.flock-stage3.shape-count", + "table_capacity": 16777216, + "tables": [ + { + "gate": "GoldilocksAddPairGate", + "rows": 6336142 + }, + { + "gate": "GoldilocksMulPairGate", + "rows": 2916038 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows": 12580880 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows": 4376475 + }, + { + "gate": "Blake3Gate", + "rows": 158899 + }, + { + "gate": "DigestOrderGate", + "rows": 668500 + }, + { + "gate": "F128EqualityGate", + "rows": 271980 + }, + { + "gate": "HashSampleGate", + "rows": 52 + }, + { + "gate": "GoldilocksSampleGate", + "rows": 26 + }, + { + "gate": "U64SplitGate", + "rows": 2320 + }, + { + "gate": "ByteWindowGate", + "rows": 32399 + } + ], + "version": 1 + } + }, + "reductions": { + "active_committed_columns": 658, + "active_committed_columns_percent": 7.290859, + "native_root_wrapper_bytes": 562368, + "native_root_wrapper_bytes_percent": 6.565861, + "native_prove_sampled_peak_rss_bytes": 2841231360, + "native_prove_sampled_peak_rss_percent": 10.477206, + "stage3_canonicality_rows": 211466, + "stage3_canonicality_rows_percent": 1.653067, + "stage3_padded_union_witness_bytes": 0, + "per_gate_rows": [ + { + "gate": "GoldilocksAddPairGate", + "rows_before": 6408875, + "rows_after": 6336142, + "rows_removed": 72733 + }, + { + "gate": "GoldilocksMulPairGate", + "rows_before": 2983568, + "rows_after": 2916038, + "rows_removed": 67530 + }, + { + "gate": "CanonicalGoldilocksPairGate", + "rows_before": 12792346, + "rows_after": 12580880, + "rows_removed": 211466 + }, + { + "gate": "GoldilocksLaneRepackGate", + "rows_before": 4477770, + "rows_after": 4376475, + "rows_removed": 101295 + }, + { + "gate": "Blake3Gate", + "rows_before": 167996, + "rows_after": 158899, + "rows_removed": 9097 + }, + { + "gate": "DigestOrderGate", + "rows_before": 668500, + "rows_after": 668500, + "rows_removed": 0 + }, + { + "gate": "F128EqualityGate", + "rows_before": 271980, + "rows_after": 271980, + "rows_removed": 0 + }, + { + "gate": "HashSampleGate", + "rows_before": 52, + "rows_after": 52, + "rows_removed": 0 + }, + { + "gate": "GoldilocksSampleGate", + "rows_before": 26, + "rows_after": 26, + "rows_removed": 0 + }, + { + "gate": "U64SplitGate", + "rows_before": 2320, + "rows_after": 2320, + "rows_removed": 0 + }, + { + "gate": "ByteWindowGate", + "rows_before": 35391, + "rows_after": 32399, + "rows_removed": 2992 + } + ] + }, + "changed_circuit_widths": [ + { + "name": "blake3_compress", + "height": 332248, + "committed_width_before": 925, + "committed_width_after": 735 + }, + { + "name": "aggr_verify_child", + "height": 1, + "committed_width_before": 500, + "committed_width_after": 394 + }, + { + "name": "ood_loop", + "height": 78, + "committed_width_before": 251, + "committed_width_after": 211 + }, + { + "name": "read_sys_circuits_n", + "height": 765, + "committed_width_before": 185, + "committed_width_after": 155 + }, + { + "name": "bytes_to_block", + "height": 13089, + "committed_width_before": 265, + "committed_width_after": 237 + }, + { + "name": "ix_aggr", + "height": 1, + "committed_width_before": 202, + "committed_width_after": 174 + }, + { + "name": "verify_query", + "height": 1231, + "committed_width_before": 170, + "committed_width_after": 142 + }, + { + "name": "aggr_load_sys", + "height": 1, + "committed_width_before": 227, + "committed_width_after": 201 + }, + { + "name": "ch_sample8", + "height": 143, + "committed_width_before": 138, + "committed_width_after": 116 + }, + { + "name": "b3_rows_chunks", + "height": 13889, + "committed_width_before": 282, + "committed_width_after": 264 + }, + { + "name": "aggr_put_address", + "height": 0, + "committed_width_before": 106, + "committed_width_after": 94 + }, + { + "name": "aggr_read_address", + "height": 2, + "committed_width_before": 138, + "committed_width_after": 126 + }, + { + "name": "blake3_compress_block", + "height": 14209, + "committed_width_before": 211, + "committed_width_after": 201 + }, + { + "name": "logup_steps_fold", + "height": 678, + "committed_width_before": 126, + "committed_width_after": 116 + }, + { + "name": "accs_onto", + "height": 9679, + "committed_width_before": 68, + "committed_width_after": 60 + }, + { + "name": "frontier_level", + "height": 10798, + "committed_width_before": 70, + "committed_width_after": 62 + }, + { + "name": "select_rows_le", + "height": 48377, + "committed_width_before": 68, + "committed_width_after": 60 + }, + { + "name": "verify_input_multi", + "height": 1, + "committed_width_before": 62, + "committed_width_after": 54 + }, + { + "name": "aggr_child_check_env_digest", + "height": 1, + "committed_width_before": 89, + "committed_width_after": 83 + }, + { + "name": "aggr_discharge_choice", + "height": 0, + "committed_width_before": 78, + "committed_width_after": 72 + }, + { + "name": "aggr_pair", + "height": 0, + "committed_width_before": 137, + "committed_width_after": 131 + }, + { + "name": "aggr_pair_structural", + "height": 0, + "committed_width_before": 134, + "committed_width_after": 128 + }, + { + "name": "verify_commit_multi", + "height": 17, + "committed_width_before": 51, + "committed_width_after": 45 + }, + { + "name": "verify_one_query", + "height": 100, + "committed_width_before": 96, + "committed_width_after": 90 + }, + { + "name": "aggr_address_order", + "height": 0, + "committed_width_before": 120, + "committed_width_after": 116 + }, + { + "name": "blake3_finish", + "height": 49, + "committed_width_before": 190, + "committed_width_after": 186 + }, + { + "name": "cap_onto", + "height": 23, + "committed_width_before": 65, + "committed_width_after": 61 + }, + { + "name": "heights_max", + "height": 3, + "committed_width_before": 44, + "committed_width_after": 40 + }, + { + "name": "pcs_betas", + "height": 17, + "committed_width_before": 64, + "committed_width_after": 60 + }, + { + "name": "read_vk_cap_n", + "height": 2, + "committed_width_before": 66, + "committed_width_after": 62 + }, + { + "name": "aggr_assert_difference", + "height": 0, + "committed_width_before": 35, + "committed_width_after": 33 + }, + { + "name": "aggr_assert_structural_difference", + "height": 0, + "committed_width_before": 34, + "committed_width_after": 32 + }, + { + "name": "aggr_assert_union", + "height": 0, + "committed_width_before": 40, + "committed_width_after": 38 + }, + { + "name": "aggr_fold_path", + "height": 0, + "committed_width_before": 35, + "committed_width_after": 33 + }, + { + "name": "aggr_pair_hashes", + "height": 0, + "committed_width_before": 33, + "committed_width_after": 31 + }, + { + "name": "blake3_compress_layer", + "height": 3257, + "committed_width_before": 157, + "committed_width_after": 155 + }, + { + "name": "blake3_next_layer", + "height": 3191, + "committed_width_before": 191, + "committed_width_after": 189 + }, + { + "name": "ch_sample_bits", + "height": 101, + "committed_width_before": 69, + "committed_width_after": 67 + }, + { + "name": "frontier_leaves", + "height": 2018, + "committed_width_before": 35, + "committed_width_after": 33 + }, + { + "name": "frontier_merge", + "height": 10686, + "committed_width_before": 43, + "committed_width_after": 41 + }, + { + "name": "frontier_sort", + "height": 3465, + "committed_width_before": 40, + "committed_width_after": 38 + }, + { + "name": "frontier_split", + "height": 6774, + "committed_width_before": 37, + "committed_width_after": 35 + }, + { + "name": "open_batch_2pt", + "height": 15600, + "committed_width_before": 38, + "committed_width_after": 36 + }, + { + "name": "open_prep", + "height": 7800, + "committed_width_before": 45, + "committed_width_after": 43 + }, + { + "name": "open_prep_batch", + "height": 100, + "committed_width_before": 44, + "committed_width_after": 42 + }, + { + "name": "open_quotient", + "height": 7800, + "committed_width_before": 41, + "committed_width_after": 39 + }, + { + "name": "query_loop", + "height": 101, + "committed_width_before": 54, + "committed_width_after": 52 + }, + { + "name": "read_batch_opening_vec_n", + "height": 5, + "committed_width_before": 31, + "committed_width_after": 29 + }, + { + "name": "read_commit_phase_step_vec_n", + "height": 17, + "committed_width_before": 33, + "committed_width_after": 31 + }, + { + "name": "read_nodes_n", + "height": 124768, + "committed_width_before": 59, + "committed_width_after": 57 + }, + { + "name": "read_sys_lookups_n", + "height": 5866, + "committed_width_before": 31, + "committed_width_after": 29 + }, + { + "name": "reconstruct_ext_row", + "height": 46, + "committed_width_before": 32, + "committed_width_after": 30 + }, + { + "name": "rows_at_round", + "height": 1616, + "committed_width_before": 31, + "committed_width_after": 29 + }, + { + "name": "rows_pop", + "height": 103656, + "committed_width_before": 46, + "committed_width_after": 44 + }, + { + "name": "select_rows", + "height": 111490, + "committed_width_before": 46, + "committed_width_after": 44 + } + ], + "stage3_compiled": false, + "stage3_evaluated": false, + "stage3_proven": false, + "count_only_limits": { + "max_table_capacity": 4294967296, + "max_union_witness_bytes": 1 + }, + "count_only_admission_error": "Stage 3 padded union witness requires 824633720832 bytes; admission limit is 1 (PCS/compiler scratch is additional)", + "count_only_command": "(ulimit -v 16777216; IX_FLOCK_TIMING=1 RAYON_NUM_THREADS=8 .lake/build/bin/bench-flock-root-fixture --min-opening-width --count Tests/Fixtures/Aggregate/singleton-min-opening-2026-09-05)", + "validation": { + "native_fixture_fresh_process_verification": "both legacy and explicitly selected experimental fixtures passed", + "default_cli_rejects_experimental_key": true, + "harness_rejects_implicit_profile_selection": true, + "grouped_native_proof_serialized_key_and_wrong_key_claim_tests": "passed", + "recursive_verifier_interpreter_codegen_and_mutation_tests": 8, + "stage3_fast_tests": 73, + "stage3_standard_serial_cryptographic_tests": 13, + "grouped_four_message_stage3_compiled_relation_evaluated": true, + "default_100_query_stage3_census_unchanged": true, + "default_100_query_stage3_circuit_digest": "d2a1e42a479137b93c23d396268ef922611f39eea3e5f7317e487487b421c4c1" + }, + "notes": [ + "Active committed width is the sum of circuit widths with nonzero heights (main + lookup accumulator + quotient columns); it is not a height-weighted witness size or a prediction of Stage 3 cost.", + "The aggregate root/key/outer claim intentionally differ; the IxVM child, environment, CheckEnv claim, subject tree and active circuit heights are byte-identical or unchanged. Original fixtures and measurements remain untouched.", + "Stage 3 counted 211466 fewer canonicality rows (1.653067%), but both roots still require nu=24 and 768 GiB of padded z/a/b alone. Neither real aggregate relation was wired, evaluated, or proven with Flock.", + "The one-byte witness guard deliberately stops the diagnostic before wiring compilation; count-only mode exits zero only after this expected refusal. Production admission defaults are unchanged.", + "Native proving was approximately flat in these single runs (75.813 vs 75.386 s); native verification was slower (0.121 vs 0.228 s). Timings/RSS are local diagnostics, not confidence intervals or guarantees.", + "The native RSS model now takes extension degree 2 from the field instead of inferring it from grouped column width; this changes the child model prediction but not the child key or proof. The historical model remains approximate and the OS cap remains required.", + "Broader corpus measurements and independent soundness review remain necessary. This experiment is not automatic adoption of a new key/profile; adoption also requires deliberate relation/pin regeneration." + ] +} diff --git a/lakefile.lean b/lakefile.lean index 758b208d..634b0992 100644 --- a/lakefile.lean +++ b/lakefile.lean @@ -60,12 +60,16 @@ def cargoArgs (testFfi : Bool := false) (net : Bool := false) : IO (Array String -- IX_NO_PAR=1 disables parallel; IX_CUDA=1/true/yes enables CUDA. let ixNoPar ← IO.getEnv "IX_NO_PAR" let ixCuda ← IO.getEnv "IX_CUDA" + -- IX_FLOCK=1 builds ix flock-root against the isolated Flock workspace. + -- Default builds retain a descriptive FFI stub. + let ixFlock ← IO.getEnv "IX_FLOCK" let mut features : Array String := #[] if ixNoPar != some "1" then features := features.push "parallel" if ixCuda == some "1" || ixCuda == some "true" || ixCuda == some "yes" then features := features.push "cuda" if net && !System.Platform.isOSX then features := features.push "net" if testFfi then features := features.push "test-ffi" + if ixFlock == some "1" then features := features.push "flock" IO.println s!"Ix Rust features: {if features.isEmpty then "none" else ",".intercalate features.toList}" let buildArgs := #["build", "--release", "-p", "ix-ffi"] if features.isEmpty then return buildArgs @@ -77,8 +81,14 @@ arguments, so changing `IX_CUDA` cannot silently reuse a differently-featured archive from a previous invocation. -/ def buildRustStatic (pkg : Package) (args : Array String) (tag : String) : SpawnM (Job FilePath) := do - let sources ← inputDir (pkg.dir / "crates") true fun path => + let coreSources ← inputDir (pkg.dir / "crates") true fun path => path.extension == some "rs" || path.fileName == "Cargo.toml" + -- Trace the optional path dependency so feature changes cannot reuse a stale + -- static archive. + let flockSources ← inputDir (pkg.dir / "flock-stage3") true fun path => + path.extension == some "rs" || path.fileName == "Cargo.toml" || + path.fileName == "Cargo.lock" + let sources := coreSources.zipWith (fun core flock => core ++ flock) flockSources let manifests := Job.collectArray #[ ← inputTextFile (pkg.dir / "Cargo.toml"), ← inputTextFile (pkg.dir / "Cargo.lock") @@ -194,6 +204,11 @@ lean_exe «bench-aggregate-policy» where -- symbols are then resolved from ix_ffi and not pulled twice. moreLinkObjs := #[ix_rs] +lean_exe «bench-flock-root-fixture» where + root := `Benchmarks.FlockRootFixture + supportInterpreter := true + moreLinkObjs := #[ix_rs] + /- The lean4lean replay machinery as an importable lib: the `bench-lean4lean` exe root and the ignored `lean4lean` test runner both import `Benchmarks.Lean4Lean`, and modules under `Benchmarks/` belong to