Skip to main content

trident/field/
proof.rs

1//! Universal STARK proof estimation and claim structure.
2//!
3//! These formulas apply to all FRI-based STARK provers regardless of
4//! target VM or field. Warriors call these functions for cost reporting
5//! and proof parameter computation.
6
7// ─── Claim ─────────────────────────────────────────────────────────
8
9/// Universal proof claim: what any STARK/SNARK proof asserts.
10///
11/// This is the public data shared between prover and verifier.
12/// The proof itself is warrior-specific (opaque bytes); the claim
13/// is universal.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct Claim {
16    /// Hash of the compiled program (VM-native digest).
17    pub program_hash: Vec<u64>,
18    /// Public input field elements.
19    pub public_input: Vec<u64>,
20    /// Public output field elements.
21    pub public_output: Vec<u64>,
22}
23
24// ─── Trace Geometry ────────────────────────────────────────────────
25
26/// Padded trace height: next power of two above the tallest table.
27///
28/// Universal to all STARKs — the execution trace is padded to a power
29/// of two for NTT (Number Theoretic Transform) efficiency.
30pub fn padded_height(max_table_rows: u64) -> u64 {
31    if max_table_rows == 0 {
32        return 1;
33    }
34    max_table_rows.next_power_of_two()
35}
36
37/// Merkle tree depth for a trace of the given padded height.
38pub fn merkle_depth(padded_height: u64) -> u32 {
39    if padded_height <= 1 {
40        return 0;
41    }
42    64 - (padded_height - 1).leading_zeros()
43}
44
45/// NTT (Number Theoretic Transform) domain size.
46///
47/// The evaluation domain is `padded_height * blowup_factor`. Larger
48/// blowup factors give stronger soundness per FRI query but increase
49/// prover work linearly.
50pub fn ntt_domain_size(padded_height: u64, blowup_factor: u64) -> u64 {
51    padded_height.saturating_mul(blowup_factor)
52}
53
54// ─── FRI Parameters ────────────────────────────────────────────────
55
56/// Estimate the number of FRI queries needed for a target security level.
57///
58/// Each FRI query provides approximately `log2(blowup_factor)` bits of
59/// security against a dishonest prover. The total number of queries is
60/// `ceil(security_bits / log2(blowup_factor))`.
61///
62/// Typical parameters: security_bits=128, blowup_factor=4 → 64 queries.
63pub fn fri_query_count(security_bits: u32, blowup_factor: u64) -> u32 {
64    if blowup_factor <= 1 {
65        return security_bits;
66    }
67    // Integer log2 of blowup_factor
68    let log2_blowup = 63 - blowup_factor.leading_zeros();
69    if log2_blowup == 0 {
70        return security_bits;
71    }
72    (security_bits + log2_blowup - 1) / log2_blowup
73}
74
75// ─── Proof Size Estimation ─────────────────────────────────────────
76
77/// Estimate proof size in bytes.
78///
79/// A STARK proof contains:
80/// - FRI merkle authentication paths (one per query per column)
81/// - Polynomial evaluations at query points
82/// - FRI folding commitments
83///
84/// This gives a rough lower bound. Actual proofs include additional
85/// metadata, boundary constraints, and zero-knowledge blinding.
86pub fn estimate_proof_size(
87    padded_height: u64,
88    column_count: u64,
89    field_bytes: u64,
90    blowup_factor: u64,
91    security_bits: u32,
92) -> u64 {
93    let queries = fri_query_count(security_bits, blowup_factor) as u64;
94    let depth = merkle_depth(padded_height) as u64;
95    // Each query: column_count evaluations + Merkle path (depth * hash_size)
96    let hash_size: u64 = 32; // typical: 256-bit hash nodes
97    let per_query = column_count * field_bytes + depth * hash_size;
98    queries * per_query
99}
100
101/// Estimate proving time in nanoseconds.
102///
103/// Rough universal estimate based on NTT-dominated proving:
104/// `padded_height * column_count * log2(padded_height) * ns_per_field_op`.
105///
106/// The constant 3 ns/op is a conservative estimate for modern CPUs
107/// performing 64-bit field multiplication.
108pub fn estimate_proving_ns(padded_height: u64, column_count: u64) -> u64 {
109    if padded_height == 0 || column_count == 0 {
110        return 0;
111    }
112    let log_h = 64 - padded_height.leading_zeros() as u64;
113    padded_height
114        .saturating_mul(column_count)
115        .saturating_mul(log_h)
116        .saturating_mul(3)
117}
118
119// ─── Tests ─────────────────────────────────────────────────────────
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn padded_height_powers_of_two() {
127        assert_eq!(padded_height(0), 1);
128        assert_eq!(padded_height(1), 1);
129        assert_eq!(padded_height(2), 2);
130        assert_eq!(padded_height(3), 4);
131        assert_eq!(padded_height(5), 8);
132        assert_eq!(padded_height(1024), 1024);
133        assert_eq!(padded_height(1025), 2048);
134    }
135
136    #[test]
137    fn merkle_depth_correct() {
138        assert_eq!(merkle_depth(1), 0);
139        assert_eq!(merkle_depth(2), 1);
140        assert_eq!(merkle_depth(4), 2);
141        assert_eq!(merkle_depth(1024), 10);
142        assert_eq!(merkle_depth(1 << 20), 20);
143    }
144
145    #[test]
146    fn fri_queries_typical() {
147        // blowup=4 → log2=2 → 128/2 = 64 queries
148        assert_eq!(fri_query_count(128, 4), 64);
149        // blowup=8 → log2=3 → ceil(128/3) = 43 queries
150        assert_eq!(fri_query_count(128, 8), 43);
151        // blowup=2 → log2=1 → 128 queries
152        assert_eq!(fri_query_count(128, 2), 128);
153        // blowup=1 → degenerate
154        assert_eq!(fri_query_count(128, 1), 128);
155    }
156
157    #[test]
158    fn proof_size_reasonable() {
159        // Typical Triton: 2^20 padded, 200 columns, 8-byte field, blowup=4, 128-bit security
160        let size = estimate_proof_size(1 << 20, 200, 8, 4, 128);
161        // Should be in the range of hundreds of KB to low MB
162        assert!(size > 100_000, "proof too small: {}", size);
163        assert!(size < 100_000_000, "proof too large: {}", size);
164    }
165
166    #[test]
167    fn proving_time_nonzero() {
168        let ns = estimate_proving_ns(1 << 20, 200);
169        assert!(ns > 0);
170        // Should be in the range of seconds (billions of ns)
171        assert!(ns > 1_000_000_000, "estimate too fast: {} ns", ns);
172    }
173
174    #[test]
175    fn proving_time_zero_inputs() {
176        assert_eq!(estimate_proving_ns(0, 100), 0);
177        assert_eq!(estimate_proving_ns(100, 0), 0);
178    }
179}