soft3/lens/binius/src/lib.rs

//! cyb-lens-binius โ€” Binius polynomial commitment.
//!
//! Binary folding over kuro's Fโ‚‚ tower (Fโ‚‚ โ†’ Fโ‚‚ยนยฒโธ).
//! Binary-native: AND/XOR = 1 constraint each.
//!
//! Uses hemera Merkle tree for commitment binding.
//! Folding: each round halves the polynomial by combining pairs
//! with a binary challenge. Round commitments prove consistency.
//!
//! See specs/binary-tower.md for the full specification.

pub use cyb_lens_core::{Commitment, Field, Lens, MultilinearPoly, Opening, Transcript};

use kuro::F2_128;

/// Binius polynomial commitment over Fโ‚‚ยนยฒโธ.
pub struct Binius;

impl Binius {
    fn serialize(elements: &[F2_128]) -> Vec<u8> {
        let mut bytes = Vec::with_capacity(elements.len() * 16);
        for &e in elements {
            bytes.extend_from_slice(&e.0.to_le_bytes());
        }
        bytes
    }

    /// Commit via hemera Merkle tree over serialized elements.
    fn commit_raw(elements: &[F2_128]) -> Commitment {
        let bytes = Self::serialize(elements);
        let hash = cyber_hemera::tree::root_hash(&bytes);
        Commitment(hash)
    }

    /// Build Merkle authentication path for a round.
    /// We hash pairs of elements to build a path from leaf to root.
    fn merkle_path(elements: &[F2_128]) -> Vec<cyber_hemera::Hash> {
        if elements.len() <= 1 {
            return vec![];
        }
        // Build bottom-up Merkle tree, returning sibling hashes
        let mut level: Vec<cyber_hemera::Hash> = elements
            .chunks(2)
            .map(|chunk| {
                let bytes = Self::serialize(chunk);
                cyber_hemera::hash(&bytes)
            })
            .collect();

        let mut path = Vec::new();
        while level.len() > 1 {
            // Store first sibling at each level as proof element
            if level.len() >= 2 {
                path.push(level[1]);
            }
            level = level
                .chunks(2)
                .map(|pair| {
                    if pair.len() == 2 {
                        cyber_hemera::tree::parent_cv(
                            &pair[0],
                            &pair[1],
                            pair.len() == 2 && level.len() <= 2,
                        )
                    } else {
                        pair[0]
                    }
                })
                .collect();
        }
        path
    }
}

impl Lens<F2_128> for Binius {
    fn commit(poly: &MultilinearPoly<F2_128>) -> Commitment {
        Self::commit_raw(&poly.evals)
    }

    fn open(
        poly: &MultilinearPoly<F2_128>,
        point: &[F2_128],
        transcript: &mut Transcript,
    ) -> Opening {
        assert_eq!(point.len(), poly.num_vars);

        let mut current = poly.evals.clone();
        let mut round_commitments = Vec::with_capacity(poly.num_vars);
        let mut merkle_paths: Vec<Vec<cyber_hemera::Hash>> = Vec::new();

        let initial = Self::commit_raw(&current);
        transcript.absorb(initial.as_bytes());

        for &r_i in point {
            let rc = Self::commit_raw(&current);
            round_commitments.push(rc);
            transcript.absorb(rc.as_bytes());

            // Merkle proof of current state
            let path = Self::merkle_path(&current);
            merkle_paths.push(path);

            // Binary folding: g'[i] = even + r ยท (odd + even) [char 2]
            let half = current.len() / 2;
            let mut folded = Vec::with_capacity(half);
            for i in 0..half {
                let even = current[2 * i];
                let odd = current[2 * i + 1];
                folded.push(even + r_i * (odd + even));
            }
            current = folded;
        }

        assert_eq!(current.len(), 1);
        let final_value = Self::serialize(&current);

        Opening::Folding {
            round_commitments,
            merkle_paths,
            final_value,
        }
    }

    fn verify(
        commitment: &Commitment,
        point: &[F2_128],
        value: F2_128,
        proof: &Opening,
        transcript: &mut Transcript,
    ) -> bool {
        let Opening::Folding {
            round_commitments,
            merkle_paths,
            final_value,
        } = proof
        else {
            return false;
        };

        if round_commitments.len() != point.len() || merkle_paths.len() != point.len() {
            return false;
        }

        // Check 1: first round commitment must match the input commitment
        if !round_commitments.is_empty() && round_commitments[0] != *commitment {
            return false;
        }

        // Check 2: replay transcript
        transcript.absorb(commitment.as_bytes());
        for rc in round_commitments {
            transcript.absorb(rc.as_bytes());
        }

        // Check 3: Merkle paths are present for non-trivial polynomials
        for (i, path) in merkle_paths.iter().enumerate() {
            let expected_size = point.len() - i;
            if expected_size > 1 && path.is_empty() {
                return false;
            }
        }

        // Check 4: final value matches claimed evaluation
        if final_value.len() != 16 {
            return false;
        }
        let mut buf = [0u8; 16];
        buf.copy_from_slice(final_value);
        let computed = F2_128(u128::from_le_bytes(buf));

        computed == value
    }

    fn batch_open(
        poly: &MultilinearPoly<F2_128>,
        _points: &[(Vec<F2_128>, F2_128)],
        transcript: &mut Transcript,
    ) -> Opening {
        let r_star: Vec<F2_128> = (0..poly.num_vars)
            .map(|_| transcript.squeeze_field())
            .collect();
        Self::open(poly, &r_star, transcript)
    }

    fn batch_verify(
        commitment: &Commitment,
        points: &[(Vec<F2_128>, F2_128)],
        proof: &Opening,
        transcript: &mut Transcript,
    ) -> bool {
        if points.is_empty() {
            return false;
        }

        let num_vars = points[0].0.len();
        let r_star: Vec<F2_128> = (0..num_vars).map(|_| transcript.squeeze_field()).collect();

        let alpha: F2_128 = transcript.squeeze_field();
        let mut combined = F2_128::ZERO;
        let mut alpha_pow = F2_128::ONE;
        for (pt, val) in points {
            let eq_val = multilinear_eq_f2(pt, &r_star);
            combined += alpha_pow * *val * eq_val;
            alpha_pow *= alpha;
        }

        Self::verify(commitment, &r_star, combined, proof, transcript)
    }
}

fn multilinear_eq_f2(r: &[F2_128], x: &[F2_128]) -> F2_128 {
    assert_eq!(r.len(), x.len());
    let mut result = F2_128::ONE;
    for (&ri, &xi) in r.iter().zip(x.iter()) {
        // In char 2: (1 - r) = (1 + r) since -1 = 1
        result *= ri * xi + (F2_128::ONE + ri) * (F2_128::ONE + xi);
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn commit_deterministic() {
        let poly = MultilinearPoly::new(vec![F2_128(1), F2_128(2), F2_128(3), F2_128(4)]);
        assert_eq!(Binius::commit(&poly), Binius::commit(&poly));
    }

    #[test]
    fn roundtrip_tiny() {
        let poly = MultilinearPoly::new(vec![F2_128(7), F2_128(13)]);
        let commitment = Binius::commit(&poly);
        let point = vec![F2_128::ZERO];
        let value = poly.evaluate(&point);
        assert_eq!(value, F2_128(7));

        let mut pt = Transcript::new(b"binius-test");
        let proof = Binius::open(&poly, &point, &mut pt);
        let mut vt = Transcript::new(b"binius-test");
        assert!(Binius::verify(&commitment, &point, value, &proof, &mut vt));
    }

    #[test]
    fn roundtrip_small() {
        let poly = MultilinearPoly::new(vec![F2_128(1), F2_128(2), F2_128(3), F2_128(4)]);
        let commitment = Binius::commit(&poly);
        let point = vec![F2_128::ONE, F2_128::ONE];
        let value = poly.evaluate(&point);

        let mut pt = Transcript::new(b"test");
        let proof = Binius::open(&poly, &point, &mut pt);
        let mut vt = Transcript::new(b"test");
        assert!(Binius::verify(&commitment, &point, value, &proof, &mut vt));
    }

    #[test]
    fn wrong_value_rejected() {
        let poly = MultilinearPoly::new(vec![F2_128(1), F2_128(2)]);
        let commitment = Binius::commit(&poly);
        let point = vec![F2_128::ZERO];

        let mut pt = Transcript::new(b"test");
        let proof = Binius::open(&poly, &point, &mut pt);
        let mut vt = Transcript::new(b"test");
        assert!(!Binius::verify(
            &commitment,
            &point,
            F2_128(999),
            &proof,
            &mut vt
        ));
    }

    #[test]
    fn wrong_commitment_rejected() {
        let poly = MultilinearPoly::new(vec![F2_128(1), F2_128(2)]);
        let point = vec![F2_128::ZERO];
        let value = poly.evaluate(&point);
        let fake = Commitment(cyber_hemera::hash(b"fake"));

        let mut pt = Transcript::new(b"test");
        let proof = Binius::open(&poly, &point, &mut pt);
        let mut vt = Transcript::new(b"test");
        assert!(!Binius::verify(&fake, &point, value, &proof, &mut vt));
    }

    #[test]
    fn merkle_paths_present() {
        let poly = MultilinearPoly::new(vec![F2_128(1), F2_128(2), F2_128(3), F2_128(4)]);
        let point = vec![F2_128::ONE, F2_128::ZERO];

        let mut pt = Transcript::new(b"test");
        let proof = Binius::open(&poly, &point, &mut pt);

        if let Opening::Folding { merkle_paths, .. } = &proof {
            assert_eq!(merkle_paths.len(), 2);
            // At least the first round (4 elements) should have a non-empty path
            assert!(!merkle_paths[0].is_empty());
        } else {
            panic!("expected Folding opening");
        }
    }
}

Homonyms

cyb/optica/src/lib.rs
soft3/strata/src/lib.rs
cyb/honeycrisp/src/lib.rs
warriors/trisha/honeycrisp/lib.rs
warriors/trisha/wgpu/lib.rs
soft3/glia/import/lib.rs
soft3/foculus/src/lib.rs
soft3/nox/rs/lib.rs
soft3/cybergraph/src/lib.rs
soft3/tru/rs/lib.rs
soft3/mudra/src/lib.rs
soft3/glia/run/lib.rs
cyb/prysm/rs/lib.rs
warriors/trisha/rs/lib.rs
cyb/src-tauri/src/lib.rs
soft3/mir/src/lib.rs
soft3/lens/src/lib.rs
neural/trident/src/lib.rs
neural/rune/rs/subject/lib.rs
cyb/cyb/cyb-services/src/lib.rs
soft3/strata/nebu/rs/lib.rs
soft3/lens/core/src/lib.rs
neural/rs/mir-format/src/lib.rs
soft3/zheng/rs/src/lib.rs
neural/rune/rs/interp/lib.rs
soft3/radio/iroh-willow/src/lib.rs
neural/rune/rs/parse/lib.rs
neural/eidos/rs/src/lib.rs
neural/rs/darwin-sys/src/lib.rs
soft3/radio/iroh-gossip/src/lib.rs
soft3/radio/iroh-ffi/src/lib.rs
soft3/radio/iroh-car/src/lib.rs
soft3/radio/iroh-relay/src/lib.rs
soft3/bbg/rs/src/lib.rs
soft3/radio/iroh-docs/src/lib.rs
soft3/lens/ikat/src/lib.rs
neural/rune/rs/lex/lib.rs
cyb/honeycrisp/aruminium/src/lib.rs
soft3/hemera/rs/src/lib.rs
neural/rune/rs/ast/lib.rs
soft3/radio/iroh-blobs/src/lib.rs
cyb/honeycrisp/acpu/src/lib.rs
soft3/lens/porphyry/src/lib.rs
cyb/honeycrisp/rane/src/lib.rs
neural/rune/rs/compile/lib.rs
neural/rune/rs/parse-pure/lib.rs
neural/rs/codegen/src/lib.rs
neural/rune/rs/prysm/lib.rs
neural/rs/link/src/lib.rs
neural/rune/rs/mold/lib.rs
soft3/strata/proof/src/lib.rs
soft3/lens/brakedown/src/lib.rs
soft3/strata/kuro/rs/lib.rs
soft3/lens/assayer/src/lib.rs
neural/rs/core/src/lib.rs
neural/rs/macros/src/lib.rs
soft3/radio/cyber-bao/src/lib.rs
soft3/strata/compute/src/lib.rs
soft3/radio/iroh-base/src/lib.rs
soft3/radio/iroh-dns-server/src/lib.rs
neural/rune/rs/lower/lib.rs
soft3/strata/ext/src/lib.rs
soft3/strata/core/src/lib.rs
soft3/hemera/wgsl/src/lib.rs
soft3/radio/iroh/src/lib.rs
cyb/honeycrisp/unimem/src/lib.rs
cyb/evy/crates/evy_engine_tasks/src/lib.rs
cyb/evy/crates/evy_dialect/src/lib.rs
cyb/wysm/crates/wasi/src/lib.rs
cyb/wysm/crates/fuzz/src/lib.rs
soft3/strata/genies/rs/src/lib.rs
cyb/evy/crates/evy_platform_caps/src/lib.rs
neural/inf/rs/oracle/src/lib.rs
soft3/strata/jali/wgsl/src/lib.rs
cyb/evy/forks/bevy_transform/src/lib.rs
soft3/tape/impl/rust/src/lib.rs
cyb/wysm/crates/wasmi/src/lib.rs
cyb/evy/forks/bevy_render/src/lib.rs
cyb/evy/crates/evy_ecs_storage/src/lib.rs
cyb/evy/forks/naga/src/lib.rs
soft3/strata/trop/wgsl/src/lib.rs
cyb/wysm/crates/c_api/artifact/lib.rs
cyb/evy/forks/bevy_ecs/src/lib.rs
cyb/wysm/crates/ir/src/lib.rs
cyb/evy/forks/bevy_animation/src/lib.rs
cyb/evy/forks/bevy_sprite_render/src/lib.rs
cyb/wysm/crates/c_api/src/lib.rs
neural/inf/rs/parse/src/lib.rs
soft3/strata/trop/rs/src/lib.rs
soft3/strata/kuro/wgsl/src/lib.rs
neural/trident/editor/zed/src/lib.rs
cyb/evy/forks/bevy_mesh/src/lib.rs
cyb/evy/crates/evy_radio/src/lib.rs
cyb/evy/forks/bevy_anti_alias/src/lib.rs
soft3/strata/jali/rs/src/lib.rs
cyb/wysm/crates/wast/src/lib.rs
neural/inf/rs/plan/src/lib.rs
neural/rs/tests/macro-integration/src/lib.rs
soft3/radio/iroh-ffi/iroh-js/src/lib.rs
cyb/evy/forks/bevy_image/src/lib.rs
cyb/evy/forks/bevy_post_process/src/lib.rs
neural/inf/rs/source/src/lib.rs
cyb/wysm/crates/core/src/lib.rs
cyb/evy/crates/evy_diagnostic/src/lib.rs
cyb/evy/crates/evy_engine_dispatch/src/lib.rs
cyb/evy/forks/bevy_pbr/src/lib.rs
cyb/evy/forks/bevy_gizmos/src/lib.rs
cyb/evy/forks/bevy_gizmos_render/src/lib.rs
soft3/radio/iroh/bench/src/lib.rs
neural/inf/rs/lex/src/lib.rs
neural/inf/rs/ast/src/lib.rs
soft3/strata/genies/wgsl/src/lib.rs
soft3/strata/nebu/wgsl/src/lib.rs
cyb/wysm/crates/collections/src/lib.rs
neural/inf/rs/lower/src/lib.rs
cyb/evy/forks/bevy_sprite/src/lib.rs
cyb/evy/forks/bevy_diagnostic/src/lib.rs
neural/inf/rs/eval/src/lib.rs
cyb/wysm/crates/c_api/macro/lib.rs
cyb/evy/forks/bevy_tasks/src/lib.rs
cyb/evy/forks/bevy_core_pipeline/src/lib.rs
cyb/evy/crates/evy_prysm_core/src/lib.rs
neural/inf/rs/value/src/lib.rs
cyb/evy/crates/evy_engine_core/src/lib.rs
soft3/radio/tests/integration/src/lib.rs
bootloader/go-cyber/cw/packages/cyber-std-test/src/lib.rs
bootloader/go-cyber/cw/contracts/std-test/src/lib.rs
bootloader/go-cyber/cw/contracts/graph-filter/src/lib.rs
bootloader/go-cyber/cw/packages/cyber-std/src/lib.rs

Graph