Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,896 changes: 3,611 additions & 285 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,10 +10,10 @@ members = [
"crates/ixon",
"crates/kernel",
]
# `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 `sp1-compress/` are their own Cargo workspaces built
# via their respective zkVM toolchains; excluded so host workspace ops don't
# pick them up.
exclude = ["zisk", "sp1", "sp1-compress", "multi-stark"]
resolver = "2"

[profile.dev]
Expand Down
10 changes: 10 additions & 0 deletions Ix/Aiur/Protocol.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -286,6 +286,16 @@ abbrev functionChannel : G := .ofNat 0
def buildClaim (funIdx : Bytecode.FunIdx) (input output : Array G) :=
#[functionChannel, .ofNat funIdx] ++ input ++ output

/-- Verify one Aiur recursion proof inside the SP1 aggregate-root guest and
run the selected SP1 terminal stage. The public statement is the
domain-separated recursion-vk digest, FRI parameters, and exact 18-word outer
claim. `output` receives the SDK proof container; `onchainOutput` receives raw
Groth16/Plonk bytes. Without the Cargo `sp1` feature this binding returns a
descriptive error while remaining linkable. -/
@[extern "rs_sp1_compress_aggregate_root"]
opaque sp1CompressAggregateRoot : @& ByteArray → @& ByteArray → @& ByteArray →
@& FriParameters → @& String → @& String → @& String → Except String Unit

end Aiur

end
112 changes: 112 additions & 0 deletions Ix/Cli/CompressRootCmd.lean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
/-
`ix compress-root ROOT_ADDRESS` turns one closed, persisted `ix_aggr` root
into an SP1 proof and, by default, a final Groth16 SNARK.

The command rebuilds the deterministic recursion backend, reconstructs the
uniform 18-word outer claim from the wrapper's `CheckEnv`, and passes exactly
that key/claim/proof triple to the SP1 guest. Open roots are rejected for every
proof-producing mode. Execute-only profiling may opt into one with
`--allow-open-root` so a small retained-subtree fixture can exercise the guest.
-/
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.Ixon
public import Ix.MultiStark
public import Ix.Store
public import Ix.Unsigned

public section

namespace Ix.Cli.CompressRootCmd

private def addrOfHex! (label : String) (s : String) : IO Address := do
match Address.fromString s with
| some a => pure a
| none =>
throw <| IO.userError
s!"error: {label}: expected 64-char hex (32-byte address), got {s.length}-char {s}"

/-- Canonical guest claim encoding: one little-endian u64 per Goldilocks word. -/
def outerClaimBytes (claim : Array Aiur.G) : ByteArray :=
claim.foldl (init := .empty) fun bytes value => bytes ++ value.val.toLEBytes

/-- Final compression accepts only closed `CheckEnv` roots. The explicit open
escape hatch is intentionally execute-only: it exists for cycle profiling and
cannot produce a misleading terminal proof. -/
def validateBundledClaim (claim : Ix.Claim) (mode : String)
(allowOpenRoot : Bool) : Except String Unit := do
let .checkEnv _ assumptions := claim
| throw "aggregate root wrapper does not contain a CheckEnv claim"
if assumptions.isSome then
if mode == "execute" && allowOpenRoot then pure ()
else throw "aggregate root retains assumptions; final compression requires a closed root"
else if allowOpenRoot && mode != "execute" then
throw "--allow-open-root is restricted to --mode execute"

def runCompressRootCmd (p : Cli.Parsed) : IO UInt32 := do
let roots := (p.variableArgsAs! String).toList
let rootHex ← match roots with
| [root] => pure root
| [] => p.printError "error: expected one aggregate root address"; return 1
| _ => p.printError "error: expected exactly one aggregate root address"; return 1
let mode := (p.flag? "mode").map (·.as! String) |>.getD "groth16"
let allowOpenRoot := p.hasFlag "allow-open-root"
let output := (p.flag? "output").map (·.as! String) |>.getD ""
let onchainOutput := (p.flag? "onchain-output").map (·.as! String) |>.getD ""
let rootAddress ← addrOfHex! "aggregate root" rootHex
let wrapper ← match Ixon.Proof.de (← StoreIO.toIO (Store.read rootAddress)) with
| .ok wrapper => pure wrapper
| .error error =>
IO.eprintln s!"error: aggregate wrapper {rootAddress} does not decode: {error}"
return 1
match validateBundledClaim wrapper.claim mode allowOpenRoot with
| .ok () => pure ()
| .error error => IO.eprintln s!"error: {error}"; return 1

let recursionParameters := MultiStark.defaultRecursionParameters
let backend ← match ← Ix.Cli.VerifyCmd.buildAggregateBackend recursionParameters with
| .ok backend => pure backend
| .error error => IO.eprintln s!"error: {error}"; return 1
let outerClaim := Ix.Cli.AggregateCmd.aggregateOuterClaim
backend.allowed backend.aggrIdx wrapper.claim
if outerClaim.size != 18 then
IO.eprintln s!"error: internal ix_aggr claim width is {outerClaim.size}, expected 18"
return 1

IO.println s!"Compressing aggregate root {rootAddress} with SP1 ({mode})"
IO.println s!" bundled claim: {wrapper.claim}"
IO.println s!" recursion vk: {Address.blake3 backend.system.vkBytes}"
(← IO.getStdout).flush
match Aiur.sp1CompressAggregateRoot backend.system.vkBytes
(outerClaimBytes outerClaim) wrapper.proof recursionParameters.fri
mode output onchainOutput with
| .ok () =>
IO.println s!"ok: SP1 {mode} accepted aggregate root {rootAddress}"
return 0
| .error error =>
IO.eprintln s!"error: SP1 root compression failed: {error}"
return 1

end Ix.Cli.CompressRootCmd

open Ix.Cli.CompressRootCmd in
def compressRootCmd : Cli.Cmd := `[Cli|
"compress-root" VIA runCompressRootCmd;
"Compress one closed ix_aggr root through SP1 to a final SNARK (build with IX_SP1=1)"

FLAGS:
"mode" : String; "SP1 stage: execute | core | compressed | groth16 | plonk (default: groth16)."
"output" : String; "Save the verified SP1 SDK proof container at this path."
"onchain-output" : String; "For groth16/plonk, save the raw onchain proof bytes at this path."
"allow-open-root"; "Allow a root retaining assumptions for execute-only guest profiling; never permits proof generation."

ARGS:
...root : String; "Exactly one 32-byte store address of a persisted aggregate root."
]

end
2 changes: 1 addition & 1 deletion Ix/Cli/VerifyCmd.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -96,7 +96,7 @@ structure ExpectedAggregate where

/-- 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
Expand Down
2 changes: 2 additions & 0 deletions Main.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,6 +11,7 @@ import Ix.Cli.ValidateLeanCmd
import Ix.Cli.ClaimCmd
import Ix.Cli.CatalogCmd
import Ix.Cli.CompileCmd
import Ix.Cli.CompressRootCmd
import Ix.Cli.DecompileCmd
import Ix.Cli.DiffCmd
import Ix.Cli.IngressCmd
Expand DownExpand Up@@ -52,6 +53,7 @@ def ixCmd : Cli.Cmd := `[Cli|
treeCmd;
profileCmd;
proveCmd;
compressRootCmd;
shardCmd;
codegenCmd;
verifyCmd;
Expand Down
19 changes: 19 additions & 0 deletions Tests/Aggr.lean
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ public import Ix.Aggr
public import Ix.Claim
public import Ix.AssumptionTree
public import Ix.Cli.AggregateCmd
public import Ix.Cli.CompressRootCmd
public import Tests.MultiStark

/-!
Expand DownExpand Up@@ -531,6 +532,24 @@ def smokeSuite : IO UInt32 := do
shapeWeightsBounded,
test "driver specs use uniform aggregate claims and cache version 2"
uniformDriverClaims,
test "final compression accepts a closed CheckEnv root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" false).isOk),
test "final compression rejects a root retaining assumptions"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "groth16" false).isOk),
test "execute profiling requires an explicit open-root opt-in"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" false).isOk),
test "execute profiling may opt into an open root"
((Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a (some b)) "execute" true).isOk),
test "open-root opt-in cannot be used by a proof-producing mode"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.checkEnv a none) "groth16" true).isOk),
test "final compression rejects non-CheckEnv claims"
(!(Ix.Cli.CompressRootCmd.validateBundledClaim
(.check a none) "execute" false).isOk),
expectOk "wrap of an IxVM child accepts" wrapIxvm,
expectOk "wrap of a self child accepts" wrapSelf,
expectOk "pair (IxVM, IxVM) accepts" pairII,
Expand Down
5 changes: 3 additions & 2 deletions crates/aiur/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,12 +13,13 @@ num-bigint = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
tracing = { workspace = true }
tracing-texray = { workspace = true }
tracing-texray = { workspace = true, optional = true }

[features]
default = []
default = ["texray"]
parallel = ["multi-stark/parallel"]
cuda = ["multi-stark/cuda"]
texray = ["dep:tracing-texray"]

[lints]
workspace = true
19 changes: 19 additions & 0 deletions crates/aiur/src/synthesis.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -313,6 +313,7 @@ impl AiurSystem {
input: &[G],
io_buffer: &mut IOBuffer,
) -> (Vec<G>, AiurProof) {
#[cfg(feature = "texray")]
tracing_texray::examine_current();

// Execute the Aiur bytecode.
Expand DownExpand Up@@ -393,6 +394,7 @@ impl AiurSystem {
&mut IOBuffer,
) -> Result<(QueryRecord, Vec<G>), ExecError>,
{
#[cfg(feature = "texray")]
tracing_texray::examine_current();
let _g = tracing::info_span!("aiur/execute_ixvm").entered();
let (query_record, output) =
Expand DownExpand Up@@ -573,6 +575,23 @@ mod tests {
]
);
system.verify(&claim, &proof).expect("xor split outputs must verify");

// The terminal zkVM 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 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"
);
}

/// Hand-build a toplevel exercising the two migrated integration paths that
Expand Down
47 changes: 46 additions & 1 deletion crates/aiur/src/vk_codec.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ use multi_stark::{
lookup::{Lookup, WidthBinding},
p3_field::{PrimeCharacteristicRing, PrimeField64},
system::{Circuit, System},
types::{Commitment, CommitmentParameters, FriParameters, Val},
types::{Commitment, CommitmentParameters, FriParameters, PcsError, Val},
};

use crate::synthesis::{AiurConfig, AiurSystem};
Expand DownExpand Up@@ -502,6 +502,51 @@ 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<AiurConfig>,
commitment_parameters: CommitmentParameters,
fri_parameters: FriParameters,
}

impl AiurVerifyingKey {
/// Decode a verifying key and require full input consumption.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
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<u8> {
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()
}

pub fn verify(
&self,
claim: &[Val],
proof: &crate::synthesis::AiurProof,
) -> Result<(), multi_stark::verifier::VerificationError<PcsError>> {
self.system.verify(claim, proof)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions crates/ffi/Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,10 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-texray = { workspace = true }

# Optional SP1 terminal connector. The default build keeps a linkable error
# stub; enabling this dependency builds the aggregate-verifier guest ELF.
sp1-compress-host = { path = "../../sp1-compress/host", optional = true }

# Iroh dependencies
bytes = { version = "1.10.1", optional = true }
tokio = { version = "1.44.1", optional = true }
Expand All@@ -51,6 +55,8 @@ parallel = ["aiur/parallel"]
cuda = ["aiur/cuda"]
test-ffi = []
net = ["bytes", "tokio", "iroh", "iroh-base", "n0-error", "getrandom", "bincode", "serde"]
sp1 = ["dep:sp1-compress-host"]
sp1-cuda = ["sp1", "sp1-compress-host/cuda"]

[lints]
workspace = true
Loading