Skip to main content

zync_core/
prover.rs

1//! Ligerito proof generation for header chain traces.
2//!
3//! Uses SHA256 Fiat-Shamir transcript for browser WASM verification compatibility.
4//! The transcript choice is load-bearing: verifier MUST use the same transcript.
5//!
6//! ## Proof format
7//!
8//! ```text
9//! [public_outputs_len: u32 LE]
10//! [public_outputs: bincode-serialized ProofPublicOutputs]
11//! [log_size: u8]
12//! [ligerito_proof: bincode-serialized FinalizedLigeritoProof]
13//! ```
14//!
15//! Public outputs are bound to the Fiat-Shamir transcript before proving,
16//! so swapping outputs after proof generation invalidates the proof.
17//! However, the Ligerito proximity test does NOT constrain the public
18//! outputs to match the polynomial — an honest prover is assumed.
19//!
20//! ## Public outputs
21//!
22//! Extracted from fixed positions in the committed trace polynomial by
23//! the (honest) prover. Transcript-bound, not evaluation-proven.
24//!
25//! - `start_hash`, `tip_hash`: first and last block hashes
26//! - `start_prev_hash`, `tip_prev_hash`: chain continuity linkage
27//! - `cumulative_difficulty`: total chain work
28//! - `final_commitment`: running header hash chain
29//! - `final_state_commitment`: running state root chain
30//! - `tip_tree_root`, `tip_nullifier_root`: NOMT roots at tip
31//! - `final_actions_commitment`: running actions commitment chain
32
33use ligerito::transcript::{FiatShamir, Transcript};
34use ligerito::{data_structures::FinalizedLigeritoProof, prove_with_transcript, ProverConfig};
35use ligerito_binary_fields::{BinaryElem128, BinaryElem32, BinaryFieldElement};
36use serde::{Deserialize, Serialize};
37
38use crate::error::ZyncError;
39use crate::trace::{HeaderChainTrace, FIELDS_PER_HEADER, TIP_SENTINEL_SIZE};
40
41/// Public outputs claimed by the prover.
42///
43/// These values are extracted from the committed trace at fixed offsets
44/// and bound to the Fiat-Shamir transcript before proving, so they cannot
45/// be swapped after proof generation. However, the Ligerito proof itself
46/// does NOT constrain these values — a malicious prover can claim arbitrary
47/// outputs for any valid polynomial commitment.
48///
49/// # Security model
50///
51/// Under the honest-prover assumption (zidecar), the values are correct
52/// because the prover honestly extracts them from the trace. The cross-
53/// verification layer (BFT majority against independent lightwalletd nodes)
54/// detects a malicious prover claiming forged outputs.
55///
56/// For sound proof composition, evaluation opening proofs binding these
57/// values to specific polynomial positions are required (not yet implemented).
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct ProofPublicOutputs {
60    pub start_height: u32,
61    pub end_height: u32,
62    /// First block hash (known checkpoint).
63    pub start_hash: [u8; 32],
64    /// prev_hash of first block. Links to previous proof's tip_hash.
65    pub start_prev_hash: [u8; 32],
66    /// Last block hash. Verify against external checkpoint.
67    pub tip_hash: [u8; 32],
68    pub tip_prev_hash: [u8; 32],
69    pub cumulative_difficulty: u64,
70    /// Running header hash chain result.
71    pub final_commitment: [u8; 32],
72    /// Running state root chain result.
73    pub final_state_commitment: [u8; 32],
74    pub num_headers: u32,
75    /// Orchard commitment tree root at tip.
76    pub tip_tree_root: [u8; 32],
77    /// Nullifier root (NOMT) at tip.
78    pub tip_nullifier_root: [u8; 32],
79    /// Running actions commitment chain result.
80    pub final_actions_commitment: [u8; 32],
81}
82
83/// Header chain proof with public outputs and serialized ligerito proof.
84pub struct HeaderChainProof {
85    pub proof_bytes: Vec<u8>,
86    pub public_outputs: ProofPublicOutputs,
87    pub trace_log_size: u32,
88}
89
90impl HeaderChainProof {
91    /// Generate a proof from a trace with explicit config.
92    ///
93    /// Uses SHA256 transcript for browser WASM verification.
94    /// Binds public outputs to Fiat-Shamir transcript before proving.
95    ///
96    /// Note: the transcript binding prevents post-hoc output tampering
97    /// but does NOT prove the outputs match the polynomial. See
98    /// [`ProofPublicOutputs`] for the full security model.
99    pub fn prove(
100        config: &ProverConfig<BinaryElem32, BinaryElem128>,
101        trace: &HeaderChainTrace,
102    ) -> Result<Self, ZyncError> {
103        let public_outputs = Self::extract_public_outputs(trace)?;
104
105        let mut transcript = FiatShamir::new_sha256(0);
106
107        // Bind public outputs to Fiat-Shamir transcript BEFORE proving.
108        let public_bytes = bincode::serialize(&public_outputs)
109            .map_err(|e| ZyncError::Serialization(format!("bincode public outputs: {}", e)))?;
110        transcript.absorb_bytes(b"public_outputs", &public_bytes);
111
112        let proof = prove_with_transcript(config, &trace.trace, transcript)
113            .map_err(|e| ZyncError::Ligerito(format!("{:?}", e)))?;
114
115        let trace_log_size = (trace.trace.len() as f64).log2().ceil() as u32;
116        let proof_bytes = Self::serialize_proof_with_config(&proof, trace_log_size as u8)?;
117
118        Ok(Self {
119            proof_bytes,
120            public_outputs,
121            trace_log_size,
122        })
123    }
124
125    /// Generate proof with auto-selected config based on trace size.
126    /// Pads trace to required power-of-2 size if needed.
127    pub fn prove_auto(trace: &mut HeaderChainTrace) -> Result<Self, ZyncError> {
128        let (config, required_size) = crate::prover_config_for_size(trace.trace.len());
129
130        if trace.trace.len() < required_size {
131            trace.trace.resize(required_size, BinaryElem32::zero());
132        }
133
134        Self::prove(&config, trace)
135    }
136
137    /// Extract public outputs from the committed trace.
138    ///
139    /// Reads values from fixed positions in the trace polynomial.
140    /// These values are honest extractions — not proven by the Ligerito
141    /// proximity test. See [`ProofPublicOutputs`] security model.
142    fn extract_public_outputs(trace: &HeaderChainTrace) -> Result<ProofPublicOutputs, ZyncError> {
143        if trace.num_headers == 0 {
144            return Err(ZyncError::InvalidData("empty trace".into()));
145        }
146
147        let extract_hash = |base_offset: usize, field_start: usize| -> [u8; 32] {
148            let mut hash = [0u8; 32];
149            for j in 0..8 {
150                let field_val = trace.trace[base_offset + field_start + j].poly().value();
151                hash[j * 4..(j + 1) * 4].copy_from_slice(&field_val.to_le_bytes());
152            }
153            hash
154        };
155
156        let first_offset = 0;
157        let start_hash = extract_hash(first_offset, 1);
158        let start_prev_hash = extract_hash(first_offset, 9);
159
160        let last_offset = (trace.num_headers - 1) * FIELDS_PER_HEADER;
161        let tip_hash = extract_hash(last_offset, 1);
162        let tip_prev_hash = extract_hash(last_offset, 9);
163
164        let sentinel_offset = trace.num_headers * FIELDS_PER_HEADER;
165        let tip_tree_root = extract_hash(sentinel_offset, 0);
166        let tip_nullifier_root = extract_hash(sentinel_offset, 8);
167        let final_actions_commitment = extract_hash(sentinel_offset, 16);
168
169        Ok(ProofPublicOutputs {
170            start_height: trace.start_height,
171            end_height: trace.end_height,
172            start_hash,
173            start_prev_hash,
174            tip_hash,
175            tip_prev_hash,
176            cumulative_difficulty: trace.cumulative_difficulty,
177            final_commitment: trace.final_commitment,
178            final_state_commitment: trace.final_state_commitment,
179            num_headers: trace.num_headers as u32,
180            tip_tree_root,
181            tip_nullifier_root,
182            final_actions_commitment,
183        })
184    }
185
186    /// Serialize the full proof (public outputs + ligerito proof).
187    pub fn serialize_full(&self) -> Result<Vec<u8>, ZyncError> {
188        let public_bytes = bincode::serialize(&self.public_outputs)
189            .map_err(|e| ZyncError::Serialization(format!("bincode: {}", e)))?;
190
191        let mut result = Vec::with_capacity(4 + public_bytes.len() + self.proof_bytes.len());
192        result.extend_from_slice(&(public_bytes.len() as u32).to_le_bytes());
193        result.extend(public_bytes);
194        result.extend(&self.proof_bytes);
195        Ok(result)
196    }
197
198    /// Deserialize full proof. Returns (public_outputs, proof_bytes, log_size).
199    pub fn deserialize_full(
200        bytes: &[u8],
201    ) -> Result<(ProofPublicOutputs, Vec<u8>, u8), ZyncError> {
202        if bytes.len() < 5 {
203            return Err(ZyncError::Serialization("proof too short".into()));
204        }
205
206        let public_len =
207            u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
208        if bytes.len() < 4 + public_len + 1 {
209            return Err(ZyncError::Serialization("proof truncated".into()));
210        }
211
212        let public_outputs: ProofPublicOutputs =
213            bincode::deserialize(&bytes[4..4 + public_len])
214                .map_err(|e| ZyncError::Serialization(format!("bincode: {}", e)))?;
215
216        let proof_bytes = bytes[4 + public_len..].to_vec();
217        let log_size = if !proof_bytes.is_empty() {
218            proof_bytes[0]
219        } else {
220            0
221        };
222
223        Ok((public_outputs, proof_bytes, log_size))
224    }
225
226    /// Serialize ligerito proof with config size prefix.
227    fn serialize_proof_with_config(
228        proof: &FinalizedLigeritoProof<BinaryElem32, BinaryElem128>,
229        log_size: u8,
230    ) -> Result<Vec<u8>, ZyncError> {
231        let proof_bytes = bincode::serialize(proof)
232            .map_err(|e| ZyncError::Serialization(format!("bincode: {}", e)))?;
233
234        let mut result = Vec::with_capacity(1 + proof_bytes.len());
235        result.push(log_size);
236        result.extend(proof_bytes);
237        Ok(result)
238    }
239
240    /// Deserialize ligerito proof with config prefix.
241    pub fn deserialize_proof_with_config(
242        bytes: &[u8],
243    ) -> Result<(FinalizedLigeritoProof<BinaryElem32, BinaryElem128>, u8), ZyncError> {
244        if bytes.is_empty() {
245            return Err(ZyncError::Serialization("empty proof bytes".into()));
246        }
247        let log_size = bytes[0];
248        let proof = bincode::deserialize(&bytes[1..])
249            .map_err(|e| ZyncError::Serialization(format!("bincode: {}", e)))?;
250        Ok((proof, log_size))
251    }
252}