Skip to content
Merged
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
31 changes: 31 additions & 0 deletions src/compiler/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1561,4 +1561,35 @@ mod tests {
Err(Error::BytesError(dusk_bytes::Error::InvalidData))
));
}

#[test]
fn prover_try_from_bytes_rejects_truncated_segments_without_panicking() {
let mut rng = StdRng::seed_from_u64(57);
let pp = PublicParameters::setup(1 << 5, &mut rng)
.expect("public parameters should build");
let (prover, _) =
Compiler::compile::<MinimalCircuit>(&pp, b"truncated-segments")
.expect("circuit should compile");
let bytes = prover.to_bytes();

// The label and the three keys follow the 56-byte header, so a cut
// inside them leaves fewer bytes than the header declares.
let segments: usize = (1..5)
.map(|i| {
let word = &bytes[i * u64::SIZE..(i + 1) * u64::SIZE];
u64::from_be_bytes(word.try_into().expect("header word"))
as usize
})
.sum();
for len in [56, 56 + segments / 2, 56 + segments - 1] {
let result = std::panic::catch_unwind(|| {
Prover::try_from_bytes(&bytes[..len])
});
assert!(result.is_ok(), "deserializer should never panic");
assert!(matches!(
result.expect("checked above"),
Err(Error::NotEnoughBytes)
));
}
}
}
24 changes: 24 additions & 0 deletions src/composer/compress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,30 @@ mod tests {
}
}

/// A compressed circuit stores a scalar of the Hades dictionary as its
/// index, so every compressed circuit depends on the dictionary's values
/// and on the index each one gets, not only on its size, which is all
/// `compressed_circuit_is_pinned` sees.
#[test]
fn hades_dictionary_is_pinned() {
const DICTIONARY_BLAKE2B: &str = "2c3591e597fee57e204f8e23404e45e8\
7dbeceb115ede2fd440fb0c14b72c9865da40c0a98ccfc4f39b3cde5b936af60\
2cc63a64c2ddef23ddc36eb63870a215";

// Lay the dictionary out by index, as `from_bytes` rebuilds it.
let map = scalar_map(true);
let mut dictionary = vec![BlsScalar::zero(); map.len()];
map.into_iter()
.for_each(|(scalar, i)| dictionary[i] = scalar);

let mut state = blake2b_simd::State::new();
dictionary.iter().for_each(|scalar| {
state.update(&scalar.to_bytes());
});

assert_eq!(state.finalize().to_hex().as_str(), DICTIONARY_BLAKE2B);
}
Comment thread
HDauven marked this conversation as resolved.
Resolved

#[derive(Default)]
struct PinnedCircuit;

Expand Down
21 changes: 21 additions & 0 deletions src/composer/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

use dusk_bls12_381::BlsScalar;

use super::{Composer, Constraint, WireData};

mod soundness;

#[test]
Expand All @@ -26,3 +28,22 @@ fn minus_one_keeps_its_previous_limbs() {
assert_eq!(super::MINUS_ONE.internal_repr(), &PREVIOUS);
assert_eq!(super::MINUS_ONE, -BlsScalar::one());
}

/// Both gates that read a witness must have their wires linked into one cycle
/// of the copy permutation. The permutation argument enforces that cycle, so
/// without it the two gates could read different values.
#[test]
fn gates_reading_one_witness_share_a_permutation_cycle() {
let mut composer = Composer::initialized();
let x = composer.append_witness(BlsScalar::from(7u64));

let first = composer.constraints();
composer.append_gate(Constraint::new().a(x));
let second = composer.constraints();
composer.append_gate(Constraint::new().b(x));

let n = composer.constraints();
let [left, right, _, _] = composer.perm.compute_sigma_permutations(n);
assert_eq!(left[first], WireData::Right(second));
assert_eq!(right[second], WireData::Left(first));
}
1 change: 1 addition & 0 deletions src/composer/tests/soundness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ mod fixed_base;
mod logic;
mod point;
mod range;
mod select;
mod support;
mod truncate;
107 changes: 95 additions & 12 deletions src/composer/tests/soundness/decomposition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
//
// Copyright (c) DUSK NETWORK. All rights reserved.

//! Soundness regression for `component_decomposition`'s canonical guard: from
//! 255 bits up, bit strings of `x + r` also recompose to `x` in the field.
//! Soundness regressions for `component_decomposition`: its bits must be
//! boolean, and from 255 bits up its canonical guard must reject the bit
//! strings of `x + r`, which also recompose to `x` in the field.

use alloc::vec::Vec;

use dusk_bls12_381::BlsScalar;
use rand::SeedableRng;
use rand::rngs::StdRng;

use super::support::{assert_rejected, assert_verifies};
use super::support::{assert_rejected, assert_verifies, gate_digest};
use crate::composer::bits::recompose_bits;
use crate::composer::{Composer, Witness};
use crate::prelude::{Circuit, Compiler, Error, PublicParameters};
Expand All @@ -33,30 +36,50 @@ impl<const N: usize> Circuit for Decomposition<N> {
}
}

/// Proves the honest decomposition of `-1` at width `N` and rejects each
/// forged bit string.
fn assert_canonical<const N: usize>(forgeries: &[(&str, [u8; 256])]) {
/// Proves the honest decomposition of `input` at width `N` and rejects each
/// forgery: an input claimed with the bit values it is decomposed into.
fn assert_forgeries_rejected<const N: usize>(
input: BlsScalar,
forgeries: &[(&str, BlsScalar, [u8; 256])],
) {
let mut rng = StdRng::seed_from_u64(0xdec0);
let pp = PublicParameters::setup(1 << 11, &mut rng).expect("setup");
let (prover, verifier) =
Compiler::compile::<Decomposition<N>>(&pp, b"decomposition")
.expect("compile");

let honest = Decomposition::<N> {
input: -BlsScalar::one(),
bits: None,
};
let honest = Decomposition::<N> { input, bits: None };
assert_verifies(&prover, &verifier, &mut rng, &honest);

for (case, bits) in forgeries {
for (case, input, bits) in forgeries {
let forged = Decomposition::<N> {
input: recompose_bits(bits, 0, 256),
input: *input,
bits: Some(*bits),
};
assert_rejected(&prover, &mut rng, &honest, &forged, case);
}
}

/// Proves the honest decomposition of `-1` at width `N` and rejects each
/// forged bit string.
fn assert_canonical<const N: usize>(forgeries: &[(&str, [u8; 256])]) {
let forgeries: Vec<_> = forgeries
.iter()
.map(|(case, bits)| (*case, recompose_bits(bits, 0, 256), *bits))
.collect();
assert_forgeries_rejected::<N>(-BlsScalar::one(), &forgeries);
}

/// `sum bits[i] * 2^i` in the field, for any bit values: unlike
/// [`recompose_bits`], a value other than one still counts.
fn weighted_sum(bits: &[u8; 256]) -> BlsScalar {
bits.iter()
.enumerate()
.fold(BlsScalar::zero(), |sum, (i, bit)| {
sum + BlsScalar::from(*bit as u64) * BlsScalar::pow_of_2(i as u64)
})
}

#[test]
fn decomposition_rejects_noncanonical_bits() {
// `2^255 - 1 > r` in bits `0..255`, caught by the `< r` guard at both
Expand All @@ -68,3 +91,63 @@ fn decomposition_rejects_noncanonical_bits() {
assert_canonical::<255>(&[("2^255 - 1", low_ones)]);
assert_canonical::<256>(&[("2^255 - 1", low_ones), ("2^255", top_bit)]);
}

#[test]
fn decomposition_rejects_non_boolean_bits() {
// `3 * 2^0` and `2 * 2^7` recompose to `3` and `2^8`, but each carries a
// bit that is not boolean, so only the boolean constraint on every bit
// rejects them. The second claims an input that does not fit in 8 bits.
let mut three = [0u8; 256];
three[0] = 3;
let mut two_at_seven = [0u8; 256];
two_at_seven[7] = 2;
assert_forgeries_rejected::<8>(
BlsScalar::from(3u64),
&[
("3 * 2^0", weighted_sum(&three), three),
("2 * 2^7", weighted_sum(&two_at_seven), two_at_seven),
],
);
}

/// Pins the gate layout below the canonical guard and at both widths that
/// carry it. A guard or accumulator gate dropped from the emission leaves its
/// witness free, which no forgery through the bit seam can reach, so the
/// layout is what catches it.
#[test]
fn component_decomposition_layout_matches_golden() {
// `gate_digest`s of `component_decomposition::<N>(-1)` as released in
// `v0.24.0`.
const GOLDEN_254: [u8; 32] = [
106, 2, 178, 84, 169, 10, 63, 87, 193, 154, 96, 30, 43, 142, 230, 193,
160, 108, 88, 48, 165, 131, 135, 61, 30, 186, 140, 99, 63, 90, 177, 16,
];
const GOLDEN_255: [u8; 32] = [
151, 108, 105, 123, 39, 103, 1, 180, 34, 249, 236, 153, 34, 239, 34,
17, 146, 11, 51, 14, 200, 88, 76, 46, 238, 92, 211, 237, 204, 53, 227,
25,
];
const GOLDEN_256: [u8; 32] = [
234, 137, 191, 47, 204, 22, 91, 217, 203, 214, 5, 7, 61, 127, 205, 117,
177, 40, 150, 17, 191, 237, 16, 225, 73, 120, 62, 74, 71, 78, 51, 3,
];

macro_rules! assert_golden_eq {
($n:literal, $expected:expr) => {{
let mut composer = Composer::initialized();
let input = composer.append_witness(-BlsScalar::one());
let _: [Witness; $n] = composer.component_decomposition(input);
assert_eq!(
gate_digest(&composer.constraints),
$expected,
"component_decomposition::<{}>'s gate layout drifted from the \
pinned one",
$n,
);
}};
}

assert_golden_eq!(254, GOLDEN_254);
assert_golden_eq!(255, GOLDEN_255);
assert_golden_eq!(256, GOLDEN_256);
}
27 changes: 27 additions & 0 deletions src/composer/tests/soundness/fixed_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,33 @@ fn fixed_base_requires_a_canonical_jubjub_scalar_under_v3() {
);
}

#[test]
fn fixed_base_rejects_a_scalar_above_its_bit_width() {
let mut rng = StdRng::seed_from_u64(0x000c_a100_2ca1);
let pp = PublicParameters::setup(1 << 10, &mut rng).expect("setup");
let (prover, verifier) = compile(&pp);

let max = FixedBaseCircuit::honest(-JubJubScalar::one());
assert_verifies(&prover, &verifier, &mut rng, &max);

// The field's `-1` is above `2^252`, yet `(r - 1) - (-1) = r` is below it,
// and the single digit `-1` reaches it with the leading rows at zero. Only
// the `< 2^252` range check on the scalar itself rejects it.
let mut digits = [0i8; FIXED_BASE_SIGNED_DIGIT_ROUNDS];
digits[0] = -1;
assert_eq!(signed_digit_endpoint(&digits), -BlsScalar::one());
assert_eq!(leading_endpoint(&digits), BlsScalar::zero());

let wrapped = FixedBaseCircuit::forged(-BlsScalar::one(), digits);
assert_rejected(
&prover,
&mut rng,
&max,
&wrapped,
"the field's -1 is noncanonical",
);
}

// ---------------------------------------------------------------------------
// Helper-wire binding.
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading