4670 lines
163 KiB
Rust
4670 lines
163 KiB
Rust
use plonky2::hash::poseidon2::P2Permuter;
|
|
use qp_plonky2_field::goldilocks_field::GoldilocksField;
|
|
use qp_plonky2_field::types::{Field, Field64, PrimeField64};
|
|
|
|
use qp_poseidon_core::serialization::{p2_backend::GF as P2Goldilocks, GoldiCompat};
|
|
use rand::{Rng, SeedableRng};
|
|
use wgpu::util::DeviceExt;
|
|
|
|
// A simple struct to hold a test case for gf_mul
|
|
#[derive(Debug, Clone)]
|
|
struct GfMulTestCase {
|
|
a: GoldilocksField,
|
|
b: GoldilocksField,
|
|
expected: GoldilocksField,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct GfFromConstTestCase {
|
|
a: u32, // low 32 bits
|
|
b: u32, // high 32 bits
|
|
expected: GoldilocksField,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct MdsTestCase {
|
|
input: [GoldilocksField; 4],
|
|
expected: [GoldilocksField; 4],
|
|
}
|
|
|
|
// A simple struct to hold a test case for sbox
|
|
#[derive(Debug, Clone)]
|
|
struct SboxTestCase {
|
|
input: GoldilocksField,
|
|
expected: GoldilocksField,
|
|
}
|
|
|
|
// A simple struct to hold a test case for internal linear layer
|
|
#[derive(Debug, Clone)]
|
|
struct InternalLinearLayerTestCase {
|
|
input: [GoldilocksField; 12],
|
|
expected: [GoldilocksField; 12],
|
|
}
|
|
|
|
// A simple struct to hold a test case for external linear layer
|
|
#[derive(Debug, Clone)]
|
|
struct ExternalLinearLayerTestCase {
|
|
input: [GoldilocksField; 12],
|
|
expected: [GoldilocksField; 12],
|
|
}
|
|
|
|
// Represents the GoldilocksField in a WGSL-compatible format (four u16 limbs stored as u32s)
|
|
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
|
#[repr(C)]
|
|
struct GfWgls {
|
|
limb0: u32,
|
|
limb1: u32,
|
|
limb2: u32,
|
|
limb3: u32,
|
|
}
|
|
|
|
impl From<GoldilocksField> for GfWgls {
|
|
fn from(gf: GoldilocksField) -> Self {
|
|
let val = gf.0;
|
|
Self {
|
|
limb0: (val & 0xFFFF) as u32,
|
|
limb1: ((val >> 16) & 0xFFFF) as u32,
|
|
limb2: ((val >> 32) & 0xFFFF) as u32,
|
|
limb3: ((val >> 48) & 0xFFFF) as u32,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<GfWgls> for GoldilocksField {
|
|
fn from(gf: GfWgls) -> Self {
|
|
let val = (gf.limb0 as u64)
|
|
| ((gf.limb1 as u64) << 16)
|
|
| ((gf.limb2 as u64) << 32)
|
|
| ((gf.limb3 as u64) << 48);
|
|
GoldilocksField::from_noncanonical_u64(val)
|
|
}
|
|
}
|
|
|
|
fn generate_gf_from_const_test_vectors() -> Vec<GfFromConstTestCase> {
|
|
let mut vectors = Vec::new();
|
|
|
|
// Test zero
|
|
vectors.push(GfFromConstTestCase {
|
|
a: 0,
|
|
b: 0,
|
|
expected: GoldilocksField::from_canonical_u64(0),
|
|
});
|
|
|
|
// Test one
|
|
vectors.push(GfFromConstTestCase {
|
|
a: 1,
|
|
b: 0,
|
|
expected: GoldilocksField::from_canonical_u64(1),
|
|
});
|
|
|
|
// Test small values
|
|
for val in [2, 7, 42, 255, 1024, 65535] {
|
|
vectors.push(GfFromConstTestCase {
|
|
a: val,
|
|
b: 0,
|
|
expected: GoldilocksField::from_canonical_u64(val as u64),
|
|
});
|
|
}
|
|
|
|
// Test values that use high 32 bits
|
|
for high in [1, 2, 1000, 65535] {
|
|
let val = (high as u64) << 32;
|
|
vectors.push(GfFromConstTestCase {
|
|
a: 0,
|
|
b: high,
|
|
expected: GoldilocksField::from_canonical_u64(val),
|
|
});
|
|
}
|
|
|
|
// Test combined low and high bits
|
|
for (low, high) in [(123, 456), (65535, 32767), (4294967295, 65535)] {
|
|
let val = (high as u64) << 32 | (low as u64);
|
|
vectors.push(GfFromConstTestCase {
|
|
a: low,
|
|
b: high,
|
|
expected: GoldilocksField::from_canonical_u64(val),
|
|
});
|
|
}
|
|
|
|
// Test actual constants from INITIAL_EXTERNAL_CONSTANTS
|
|
let actual_constants = [
|
|
(2539329031u32, 3221415792u32),
|
|
(1633409695u32, 3565876427u32),
|
|
(2143952946u32, 1246797945u32),
|
|
];
|
|
|
|
for (low, high) in actual_constants {
|
|
let val = (high as u64) << 32 | (low as u64);
|
|
vectors.push(GfFromConstTestCase {
|
|
a: low,
|
|
b: high,
|
|
expected: GoldilocksField::from_canonical_u64(val),
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_gf_mul_test_vectors() -> Vec<GfMulTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(12345);
|
|
|
|
// Test case 1: 0x19a071bdddc16e57 * 0x9f34cc3172d67f12
|
|
let a1 = GoldilocksField::from_noncanonical_u64(0x19a071bdddc16e57);
|
|
let b1 = GoldilocksField::from_noncanonical_u64(0x9f34cc3172d67f12);
|
|
vectors.push(GfMulTestCase {
|
|
a: a1,
|
|
b: b1,
|
|
expected: a1 * b1,
|
|
});
|
|
|
|
// Test case 2: 0x7e92900e25221b88 * 0x967d2999df3fa192
|
|
let a2 = GoldilocksField::from_noncanonical_u64(0x7e92900e25221b88);
|
|
let b2 = GoldilocksField::from_noncanonical_u64(0x967d2999df3fa192);
|
|
vectors.push(GfMulTestCase {
|
|
a: a2,
|
|
b: b2,
|
|
expected: a2 * b2,
|
|
});
|
|
|
|
// Test case 3: 0xd258f723b9f7cbc2 * 0x5ddb844a50f2bd61
|
|
let a3 = GoldilocksField::from_noncanonical_u64(0xd258f723b9f7cbc2);
|
|
let b3 = GoldilocksField::from_noncanonical_u64(0x5ddb844a50f2bd61);
|
|
vectors.push(GfMulTestCase {
|
|
a: a3,
|
|
b: b3,
|
|
expected: a3 * b3,
|
|
});
|
|
|
|
// Test case 4: 0x679ad356dc569f8a * 0x7a0ef9078a5b4fc5
|
|
let a4 = GoldilocksField::from_noncanonical_u64(0x679ad356dc569f8a);
|
|
let b4 = GoldilocksField::from_noncanonical_u64(0x7a0ef9078a5b4fc5);
|
|
vectors.push(GfMulTestCase {
|
|
a: a4,
|
|
b: b4,
|
|
expected: a4 * b4,
|
|
});
|
|
|
|
// Zero cases
|
|
vectors.push(GfMulTestCase {
|
|
a: GoldilocksField::ZERO,
|
|
b: GoldilocksField::from_canonical_u64(123456789),
|
|
expected: GoldilocksField::ZERO,
|
|
});
|
|
|
|
// One cases
|
|
let val = GoldilocksField::from_canonical_u64(987654321);
|
|
vectors.push(GfMulTestCase {
|
|
a: GoldilocksField::ONE,
|
|
b: val,
|
|
expected: val,
|
|
});
|
|
|
|
// Small values
|
|
let small_a = GoldilocksField::from_canonical_u64(u32::MAX as u64);
|
|
let small_b = GoldilocksField::from_canonical_u64(u32::MAX as u64);
|
|
vectors.push(GfMulTestCase {
|
|
a: small_a,
|
|
b: small_b,
|
|
expected: small_a * small_b,
|
|
});
|
|
|
|
// Random small values (both operands < 2^32)
|
|
for _ in 0..50 {
|
|
let a = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
|
let b = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
|
vectors.push(GfMulTestCase {
|
|
a,
|
|
b,
|
|
expected: a * b,
|
|
});
|
|
}
|
|
|
|
// Random mixed cases (one small, one large)
|
|
for _ in 0..80 {
|
|
let a = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
|
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>());
|
|
vectors.push(GfMulTestCase {
|
|
a,
|
|
b,
|
|
expected: a * b,
|
|
});
|
|
}
|
|
|
|
// Random large values (both operands >= 2^32)
|
|
for _ in 0..100 {
|
|
let a = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
|
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
|
vectors.push(GfMulTestCase {
|
|
a,
|
|
b,
|
|
expected: a * b,
|
|
});
|
|
}
|
|
|
|
// Random values near the field modulus
|
|
for _ in 0..30 {
|
|
let offset = rng.gen::<u32>() as u64;
|
|
let a = GoldilocksField::from_canonical_u64(GoldilocksField::ORDER - offset);
|
|
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>());
|
|
vectors.push(GfMulTestCase {
|
|
a,
|
|
b,
|
|
expected: a * b,
|
|
});
|
|
}
|
|
|
|
// Add more specific high-limb test cases to detect patterns
|
|
for i in 20..40 {
|
|
let val1 = 1u64 << i; // Powers of 2 from 2^20 to 2^39
|
|
let val2 = rng.gen::<u32>() as u64 | (1u64 << 32); // Random value with high bit set
|
|
let a = GoldilocksField::from_canonical_u64(val1);
|
|
let b = GoldilocksField::from_canonical_u64(val2);
|
|
vectors.push(GfMulTestCase {
|
|
a,
|
|
b,
|
|
expected: a * b,
|
|
});
|
|
}
|
|
|
|
// Test values with specific limb patterns
|
|
for _ in 0..20 {
|
|
let limb2_val = (rng.gen::<u16>() as u64) << 32;
|
|
let limb3_val = (rng.gen::<u16>() as u64) << 48;
|
|
let a = GoldilocksField::from_canonical_u64(limb2_val | limb3_val);
|
|
let b = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
|
vectors.push(GfMulTestCase {
|
|
a,
|
|
b,
|
|
expected: a * b,
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_sbox_test_vectors() -> Vec<SboxTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(12345);
|
|
|
|
// Zero case
|
|
vectors.push(SboxTestCase {
|
|
input: GoldilocksField::ZERO,
|
|
expected: GoldilocksField::ZERO, // 0^7 = 0
|
|
});
|
|
|
|
// One case
|
|
vectors.push(SboxTestCase {
|
|
input: GoldilocksField::ONE,
|
|
expected: GoldilocksField::ONE, // 1^7 = 1
|
|
});
|
|
|
|
// Two case
|
|
let two = GoldilocksField::from_canonical_u64(2);
|
|
let two_to_7 = two.exp_u64(7); // 2^7 = 128
|
|
vectors.push(SboxTestCase {
|
|
input: two,
|
|
expected: two_to_7,
|
|
});
|
|
|
|
// Small powers of 2
|
|
for i in 0..8 {
|
|
let base = GoldilocksField::from_canonical_u64(1u64 << i);
|
|
let expected = base.exp_u64(7);
|
|
vectors.push(SboxTestCase {
|
|
input: base,
|
|
expected,
|
|
});
|
|
}
|
|
|
|
// Small consecutive values
|
|
for i in 3..32 {
|
|
let base = GoldilocksField::from_canonical_u64(i);
|
|
let expected = base.exp_u64(7);
|
|
vectors.push(SboxTestCase {
|
|
input: base,
|
|
expected,
|
|
});
|
|
}
|
|
|
|
// Larger powers of 2
|
|
for i in 8..32 {
|
|
let base = GoldilocksField::from_canonical_u64(1u64 << i);
|
|
let expected = base.exp_u64(7);
|
|
vectors.push(SboxTestCase {
|
|
input: base,
|
|
expected,
|
|
});
|
|
}
|
|
|
|
// Random small values (< 2^32)
|
|
for _ in 0..20 {
|
|
let input = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
|
let expected = input.exp_u64(7);
|
|
vectors.push(SboxTestCase { input, expected });
|
|
}
|
|
|
|
// Random large values (>= 2^32)
|
|
for _ in 0..30 {
|
|
let input = GoldilocksField::from_canonical_u64(rng.gen::<u64>() | 0x100000000u64);
|
|
let expected = input.exp_u64(7);
|
|
vectors.push(SboxTestCase { input, expected });
|
|
}
|
|
|
|
// Values near the field modulus
|
|
for _ in 0..10 {
|
|
let offset = rng.gen::<u32>() as u64;
|
|
let input = GoldilocksField::from_canonical_u64(GoldilocksField::ORDER - offset);
|
|
let expected = input.exp_u64(7);
|
|
vectors.push(SboxTestCase { input, expected });
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_external_linear_layer_test_vectors() -> Vec<ExternalLinearLayerTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(67890);
|
|
|
|
// Zero state
|
|
let zero_input = [GoldilocksField::ZERO; 12];
|
|
let zero_expected = apply_full_external_linear_layer_to_state(&zero_input);
|
|
vectors.push(ExternalLinearLayerTestCase {
|
|
input: zero_input,
|
|
expected: zero_expected,
|
|
});
|
|
|
|
// Sequential values 1, 2, 3, ..., 12
|
|
let sequential_input: [GoldilocksField; 12] =
|
|
core::array::from_fn(|i| GoldilocksField::from_canonical_u64((i + 1) as u64));
|
|
let sequential_expected = apply_full_external_linear_layer_to_state(&sequential_input);
|
|
vectors.push(ExternalLinearLayerTestCase {
|
|
input: sequential_input,
|
|
expected: sequential_expected,
|
|
});
|
|
|
|
// Unit vectors (one element is 1, rest are 0)
|
|
for i in 0..12 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
input[i] = GoldilocksField::ONE;
|
|
let expected = apply_full_external_linear_layer_to_state(&input);
|
|
vectors.push(ExternalLinearLayerTestCase { input, expected });
|
|
}
|
|
|
|
// Random small values (all elements < 2^16)
|
|
for _ in 0..15 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = rng.gen::<u16>() as u64;
|
|
input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let expected = apply_full_external_linear_layer_to_state(&input);
|
|
vectors.push(ExternalLinearLayerTestCase { input, expected });
|
|
}
|
|
|
|
// Random medium values (all elements < 2^32)
|
|
for _ in 0..10 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = rng.gen::<u32>() as u64;
|
|
input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let expected = apply_full_external_linear_layer_to_state(&input);
|
|
vectors.push(ExternalLinearLayerTestCase { input, expected });
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_internal_linear_layer_test_vectors() -> Vec<InternalLinearLayerTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(67890);
|
|
|
|
// Zero vector
|
|
let zero_input = [GoldilocksField::ZERO; 12];
|
|
let zero_expected = apply_internal_linear_layer_to_state(&zero_input);
|
|
vectors.push(InternalLinearLayerTestCase {
|
|
input: zero_input,
|
|
expected: zero_expected,
|
|
});
|
|
|
|
// Unit vectors (one element is 1, rest are 0)
|
|
for i in 0..12 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
input[i] = GoldilocksField::ONE;
|
|
let expected = apply_internal_linear_layer_to_state(&input);
|
|
vectors.push(InternalLinearLayerTestCase { input, expected });
|
|
}
|
|
|
|
// Small values
|
|
for _ in 0..10 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
input[j] = GoldilocksField::from_canonical_u64(rng.gen_range(0..100));
|
|
}
|
|
let expected = apply_internal_linear_layer_to_state(&input);
|
|
vectors.push(InternalLinearLayerTestCase { input, expected });
|
|
}
|
|
|
|
// Random large values
|
|
for _ in 0..20 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
input[j] = GoldilocksField::from_noncanonical_u64(rng.gen());
|
|
}
|
|
let expected = apply_internal_linear_layer_to_state(&input);
|
|
vectors.push(InternalLinearLayerTestCase { input, expected });
|
|
}
|
|
|
|
println!(
|
|
"Generated {} internal linear layer test vectors",
|
|
vectors.len()
|
|
);
|
|
vectors
|
|
}
|
|
|
|
fn generate_mds_test_vectors() -> Vec<MdsTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(54321);
|
|
|
|
// Zero vector
|
|
let zero_input = [GoldilocksField::ZERO; 4];
|
|
let zero_expected = apply_external_linear_layer_to_chunk(&zero_input);
|
|
vectors.push(MdsTestCase {
|
|
input: zero_input,
|
|
expected: zero_expected,
|
|
});
|
|
|
|
// Unit vectors
|
|
for i in 0..4 {
|
|
let mut input = [GoldilocksField::ZERO; 4];
|
|
input[i] = GoldilocksField::ONE;
|
|
let expected = apply_external_linear_layer_to_chunk(&input);
|
|
vectors.push(MdsTestCase { input, expected });
|
|
}
|
|
|
|
// All ones vector
|
|
let ones_input = [GoldilocksField::ONE; 4];
|
|
let ones_expected = apply_external_linear_layer_to_chunk(&ones_input);
|
|
vectors.push(MdsTestCase {
|
|
input: ones_input,
|
|
expected: ones_expected,
|
|
});
|
|
|
|
// Random small values (all elements < 2^16)
|
|
for _ in 0..50 {
|
|
let mut input = [GoldilocksField::ZERO; 4];
|
|
for j in 0..4 {
|
|
input[j] = GoldilocksField::from_canonical_u64(rng.gen::<u16>() as u64);
|
|
}
|
|
let expected = apply_external_linear_layer_to_chunk(&input);
|
|
vectors.push(MdsTestCase { input, expected });
|
|
}
|
|
|
|
// Random medium values (all elements < 2^32)
|
|
for _ in 0..30 {
|
|
let mut input = [GoldilocksField::ZERO; 4];
|
|
for j in 0..4 {
|
|
input[j] = GoldilocksField::from_canonical_u64(rng.gen::<u32>() as u64);
|
|
}
|
|
let expected = apply_external_linear_layer_to_chunk(&input);
|
|
vectors.push(MdsTestCase { input, expected });
|
|
}
|
|
|
|
// Random large values (using full 64-bit range)
|
|
for _ in 0..30 {
|
|
let mut input = [GoldilocksField::ZERO; 4];
|
|
for j in 0..4 {
|
|
input[j] = GoldilocksField::from_canonical_u64(rng.gen::<u64>());
|
|
}
|
|
let expected = apply_external_linear_layer_to_chunk(&input);
|
|
vectors.push(MdsTestCase { input, expected });
|
|
}
|
|
|
|
// Edge cases near field modulus
|
|
for _ in 0..10 {
|
|
let mut input = [GoldilocksField::ZERO; 4];
|
|
for j in 0..4 {
|
|
let offset = rng.gen::<u32>() as u64;
|
|
input[j] = GoldilocksField::from_canonical_u64(GoldilocksField::ORDER - offset);
|
|
}
|
|
let expected = apply_external_linear_layer_to_chunk(&input);
|
|
vectors.push(MdsTestCase { input, expected });
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
// Helper function to apply the external linear layer to a 4-element chunk
|
|
// This implements the exact same MDS matrix as used in p3_poseidon2 for Goldilocks field
|
|
// Apply full external linear layer to a 12-element state (CPU reference implementation)
|
|
fn apply_full_external_linear_layer_to_state(
|
|
state: &[GoldilocksField; 12],
|
|
) -> [GoldilocksField; 12] {
|
|
let mut result = *state;
|
|
|
|
// First apply the 4x4 MDS matrix to each consecutive 4 elements
|
|
for chunk_idx in 0..3 {
|
|
let offset = chunk_idx * 4;
|
|
let chunk = [
|
|
result[offset],
|
|
result[offset + 1],
|
|
result[offset + 2],
|
|
result[offset + 3],
|
|
];
|
|
let transformed_chunk = apply_external_linear_layer_to_chunk(&chunk);
|
|
|
|
result[offset] = transformed_chunk[0];
|
|
result[offset + 1] = transformed_chunk[1];
|
|
result[offset + 2] = transformed_chunk[2];
|
|
result[offset + 3] = transformed_chunk[3];
|
|
}
|
|
|
|
// Now apply the circulant matrix part
|
|
// Precompute the four sums of every four elements
|
|
let mut sums = [GoldilocksField::ZERO; 4];
|
|
for k in 0..4 {
|
|
for j in (k..12).step_by(4) {
|
|
sums[k] += result[j];
|
|
}
|
|
}
|
|
|
|
// Add the appropriate sum to each element
|
|
for i in 0..12 {
|
|
result[i] += sums[i % 4];
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
fn apply_external_linear_layer_to_chunk(chunk: &[GoldilocksField; 4]) -> [GoldilocksField; 4] {
|
|
// Implement P3's apply_mat4 algorithm using plonky2's Goldilocks
|
|
// This is the exact algorithm from Plonky3's poseidon2/src/external.rs apply_mat4
|
|
//
|
|
// The 4x4 MDS matrix is:
|
|
// [[2, 3, 1, 1],
|
|
// [1, 2, 3, 1],
|
|
// [1, 1, 2, 3],
|
|
// [3, 1, 1, 2]]
|
|
//
|
|
// P3's optimized algorithm computes this using only 7 additions and 2 doubles
|
|
// instead of 16 multiplications + 12 additions for naive matrix multiplication.
|
|
//
|
|
// The key insight is computing intermediate sums:
|
|
// - t01 = x[0] + x[1]
|
|
// - t23 = x[2] + x[3]
|
|
// - t0123 = t01 + t23 = x[0] + x[1] + x[2] + x[3]
|
|
// - t01123 = t0123 + x[1] = 2*x[1] + x[0] + x[2] + x[3]
|
|
// - t01233 = t0123 + x[3] = x[0] + x[1] + x[2] + 2*x[3]
|
|
//
|
|
// Then the output is:
|
|
// - x[0] = t01123 + t01 = 2*x[0] + 3*x[1] + x[2] + x[3]
|
|
// - x[1] = t01123 + 2*x[2] = x[0] + 2*x[1] + 3*x[2] + x[3]
|
|
// - x[2] = t01233 + t23 = x[0] + x[1] + 2*x[2] + 3*x[3]
|
|
// - x[3] = t01233 + 2*x[0] = 3*x[0] + x[1] + x[2] + 2*x[3]
|
|
|
|
let mut x = *chunk;
|
|
|
|
let t01 = x[0] + x[1];
|
|
let t23 = x[2] + x[3];
|
|
let t0123 = t01 + t23;
|
|
let t01123 = t0123 + x[1];
|
|
let t01233 = t0123 + x[3];
|
|
|
|
// The order here is important. Need to overwrite x[0] and x[2] after x[1] and x[3].
|
|
x[3] = t01233 + x[0].double(); // 3*x[0] + x[1] + x[2] + 2*x[3]
|
|
x[1] = t01123 + x[2].double(); // x[0] + 2*x[1] + 3*x[2] + x[3]
|
|
x[0] = t01123 + t01; // 2*x[0] + 3*x[1] + x[2] + x[3]
|
|
x[2] = t01233 + t23; // x[0] + x[1] + 2*x[2] + 3*x[3]
|
|
|
|
x
|
|
}
|
|
|
|
// Apply internal linear layer to a 12-element state (CPU reference implementation)
|
|
fn apply_internal_linear_layer_to_state(state: &[GoldilocksField; 12]) -> [GoldilocksField; 12] {
|
|
let mut result = *state;
|
|
|
|
// Compute sum of all elements first
|
|
let mut sum = GoldilocksField::ZERO;
|
|
for &element in state {
|
|
sum += element;
|
|
}
|
|
|
|
// Apply internal linear layer: result[i] = state[i] * diag[i] + sum
|
|
// Use the same diagonal constants as in WGSL (MATRIX_DIAG_12_GOLDILOCKS from Plonky3)
|
|
let diag_constants = [
|
|
0xc3b6c08e23ba9300u64,
|
|
0xd84b5de94a324fb6u64,
|
|
0x0d0c371c5b35b84fu64,
|
|
0x7964f570e7188037u64,
|
|
0x5daf18bbd996604bu64,
|
|
0x6743bc47b9595257u64,
|
|
0x5528b9362c59bb70u64,
|
|
0xac45e25b7127b68bu64,
|
|
0xa2077d7dfbb606b5u64,
|
|
0xf3faac6faee378aeu64,
|
|
0x0c6388b51545e883u64,
|
|
0xd27dbb6944917b60u64,
|
|
];
|
|
|
|
for i in 0..12 {
|
|
let diag_value = GoldilocksField::from_noncanonical_u64(diag_constants[i]);
|
|
result[i] = state[i] * diag_value + sum;
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Poseidon2TestCase {
|
|
input: [GoldilocksField; 12],
|
|
expected: [GoldilocksField; 12],
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Poseidon2SqueezeTwiceTestCase {
|
|
input_bytes: Vec<u8>, // Should be exactly 96 bytes for our GPU implementation
|
|
expected_hash: [u8; 32], // First 32 bytes of the 64-byte squeeze_twice output
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct BytesToFieldTestCase {
|
|
input_bytes: [u8; 96],
|
|
expected_felts: Vec<GoldilocksField>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct FieldToBytesTestCase {
|
|
input_felts: [GoldilocksField; 4],
|
|
expected_bytes: [u8; 32],
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct InternalRoundsTestCase {
|
|
input: [GoldilocksField; 12],
|
|
expected: [GoldilocksField; 12],
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct DoubleHashTestCase {
|
|
input_96_bytes: [u8; 96], // Header (32) + Nonce (64) bytes
|
|
expected_hash: [u8; 64], // Full 64-byte double hash output
|
|
description: String,
|
|
}
|
|
|
|
fn generate_poseidon2_test_vectors() -> Vec<Poseidon2TestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(12345);
|
|
|
|
// Zero state
|
|
let zero_input = [GoldilocksField::ZERO; 12];
|
|
let zero_expected = <GoldilocksField as P2Permuter>::permute(zero_input);
|
|
vectors.push(Poseidon2TestCase {
|
|
input: zero_input,
|
|
expected: zero_expected,
|
|
});
|
|
|
|
// One state (first element = 1, rest = 0)
|
|
let mut one_input = [GoldilocksField::ZERO; 12];
|
|
one_input[0] = GoldilocksField::ONE;
|
|
let one_expected = <GoldilocksField as P2Permuter>::permute(one_input);
|
|
vectors.push(Poseidon2TestCase {
|
|
input: one_input,
|
|
expected: one_expected,
|
|
});
|
|
|
|
// Small random values (0-255)
|
|
for _ in 0..30 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = rng.gen::<u8>() as u64;
|
|
input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
|
vectors.push(Poseidon2TestCase { input, expected });
|
|
}
|
|
|
|
// Medium random values (0-65535)
|
|
for _ in 0..20 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = rng.gen::<u16>() as u64;
|
|
input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
|
vectors.push(Poseidon2TestCase { input, expected });
|
|
}
|
|
|
|
// Large random values (full u32 range)
|
|
for _ in 0..15 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = rng.gen::<u32>() as u64;
|
|
input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
|
vectors.push(Poseidon2TestCase { input, expected });
|
|
}
|
|
|
|
// Very large random values (full field range)
|
|
for _ in 0..10 {
|
|
let mut input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = rng.gen::<u64>();
|
|
input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let expected = <GoldilocksField as P2Permuter>::permute(input);
|
|
vectors.push(Poseidon2TestCase { input, expected });
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_field_to_bytes_test_vectors() -> Vec<FieldToBytesTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0x13579BDF);
|
|
|
|
// Test 1: All zeros
|
|
let zero_input = [GoldilocksField::ZERO; 4];
|
|
let mut zero_expected = [0u8; 32];
|
|
for (i, &felt) in zero_input.iter().enumerate() {
|
|
let u64_val = felt.to_canonical_u64();
|
|
zero_expected[i * 8..(i + 1) * 8].copy_from_slice(&u64_val.to_le_bytes());
|
|
}
|
|
vectors.push(FieldToBytesTestCase {
|
|
input_felts: zero_input,
|
|
expected_bytes: zero_expected,
|
|
});
|
|
|
|
// Test 2: All ones
|
|
let ones_input = [GoldilocksField::ONE; 4];
|
|
let mut ones_expected = [0u8; 32];
|
|
for (i, &felt) in ones_input.iter().enumerate() {
|
|
let u64_val = felt.to_canonical_u64();
|
|
ones_expected[i * 8..(i + 1) * 8].copy_from_slice(&u64_val.to_le_bytes());
|
|
}
|
|
vectors.push(FieldToBytesTestCase {
|
|
input_felts: ones_input,
|
|
expected_bytes: ones_expected,
|
|
});
|
|
|
|
// Test 3: Sequential values
|
|
let seq_input = [
|
|
GoldilocksField::from_canonical_u64(0x0123456789ABCDEF),
|
|
GoldilocksField::from_canonical_u64(0xFEDCBA9876543210),
|
|
GoldilocksField::from_canonical_u64(0x1111111111111111),
|
|
GoldilocksField::from_canonical_u64(0x2222222222222222),
|
|
];
|
|
let mut seq_expected = [0u8; 32];
|
|
for (i, &felt) in seq_input.iter().enumerate() {
|
|
let u64_val = felt.to_canonical_u64();
|
|
seq_expected[i * 8..(i + 1) * 8].copy_from_slice(&u64_val.to_le_bytes());
|
|
}
|
|
vectors.push(FieldToBytesTestCase {
|
|
input_felts: seq_input,
|
|
expected_bytes: seq_expected,
|
|
});
|
|
|
|
// Test 4: Random values
|
|
for _ in 0..10 {
|
|
let input = [
|
|
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
|
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
|
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
|
GoldilocksField::from_canonical_u64(rng.gen::<u64>() % GoldilocksField::ORDER),
|
|
];
|
|
let mut expected = [0u8; 32];
|
|
for (i, &felt) in input.iter().enumerate() {
|
|
let u64_val = felt.to_canonical_u64();
|
|
expected[i * 8..(i + 1) * 8].copy_from_slice(&u64_val.to_le_bytes());
|
|
}
|
|
vectors.push(FieldToBytesTestCase {
|
|
input_felts: input,
|
|
expected_bytes: expected,
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_poseidon2_squeeze_twice_test_vectors() -> Vec<Poseidon2SqueezeTwiceTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42424242);
|
|
|
|
// Our GPU function expects exactly 96 bytes and produces 32 bytes (first half of 64-byte output)
|
|
// Test with 96-byte inputs using qp_poseidon_core::hash_squeeze_twice for reference
|
|
|
|
// Zero input (96 bytes of zeros)
|
|
let zero_input = vec![0u8; 96];
|
|
let zero_expected_64 = qp_poseidon_core::hash_squeeze_twice(&zero_input);
|
|
let mut zero_expected_32 = [0u8; 32];
|
|
zero_expected_32.copy_from_slice(&zero_expected_64[0..32]); // Take first 32 bytes
|
|
vectors.push(Poseidon2SqueezeTwiceTestCase {
|
|
input_bytes: zero_input,
|
|
expected_hash: zero_expected_32,
|
|
});
|
|
|
|
// All 0x01 input
|
|
let ones_input = vec![1u8; 96];
|
|
let ones_expected_64 = qp_poseidon_core::hash_squeeze_twice(&ones_input);
|
|
let mut ones_expected_32 = [0u8; 32];
|
|
ones_expected_32.copy_from_slice(&ones_expected_64[0..32]);
|
|
vectors.push(Poseidon2SqueezeTwiceTestCase {
|
|
input_bytes: ones_input,
|
|
expected_hash: ones_expected_32,
|
|
});
|
|
|
|
// All 0xFF input
|
|
let ff_input = vec![0xFFu8; 96];
|
|
let ff_expected_64 = qp_poseidon_core::hash_squeeze_twice(&ff_input);
|
|
let mut ff_expected_32 = [0u8; 32];
|
|
ff_expected_32.copy_from_slice(&ff_expected_64[0..32]);
|
|
vectors.push(Poseidon2SqueezeTwiceTestCase {
|
|
input_bytes: ff_input,
|
|
expected_hash: ff_expected_32,
|
|
});
|
|
|
|
// Sequential pattern input
|
|
let seq_input: Vec<u8> = (0u8..96).collect();
|
|
let seq_expected_64 = qp_poseidon_core::hash_squeeze_twice(&seq_input);
|
|
let mut seq_expected_32 = [0u8; 32];
|
|
seq_expected_32.copy_from_slice(&seq_expected_64[0..32]);
|
|
vectors.push(Poseidon2SqueezeTwiceTestCase {
|
|
input_bytes: seq_input,
|
|
expected_hash: seq_expected_32,
|
|
});
|
|
|
|
// Random inputs (96 bytes each)
|
|
for _ in 0..20 {
|
|
let mut input = vec![0u8; 96];
|
|
rng.fill(&mut input[..]);
|
|
let expected_64 = qp_poseidon_core::hash_squeeze_twice(&input);
|
|
let mut expected_32 = [0u8; 32];
|
|
expected_32.copy_from_slice(&expected_64[0..32]);
|
|
vectors.push(Poseidon2SqueezeTwiceTestCase {
|
|
input_bytes: input,
|
|
expected_hash: expected_32,
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Poseidon2StandardHashTestCase {
|
|
input_bytes: Vec<u8>, // Variable length input
|
|
expected_hash: [u8; 32], // Standard 32-byte output
|
|
}
|
|
|
|
fn generate_poseidon2_standard_hash_test_vectors() -> Vec<Poseidon2StandardHashTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0xFEEDBEEF);
|
|
|
|
// Test 1: Empty input
|
|
let empty_input = vec![];
|
|
let empty_expected = qp_poseidon_core::hash_variable_length_bytes(&empty_input);
|
|
vectors.push(Poseidon2StandardHashTestCase {
|
|
input_bytes: empty_input,
|
|
expected_hash: empty_expected,
|
|
});
|
|
|
|
// Test 2: Single byte inputs
|
|
for byte_val in [0u8, 1u8, 255u8, 42u8] {
|
|
let input = vec![byte_val];
|
|
let expected = qp_poseidon_core::hash_variable_length_bytes(&input);
|
|
vectors.push(Poseidon2StandardHashTestCase {
|
|
input_bytes: input,
|
|
expected_hash: expected,
|
|
});
|
|
}
|
|
|
|
// Test 3: Small inputs (1-32 bytes)
|
|
for len in [1, 4, 8, 16, 32] {
|
|
let mut input = vec![0u8; len];
|
|
rng.fill(&mut input[..]);
|
|
let expected = qp_poseidon_core::hash_variable_length_bytes(&input);
|
|
vectors.push(Poseidon2StandardHashTestCase {
|
|
input_bytes: input,
|
|
expected_hash: expected,
|
|
});
|
|
}
|
|
|
|
// Test 4: String inputs
|
|
let strings = ["", "a", "hello", "test"];
|
|
for s in strings {
|
|
let input = s.as_bytes().to_vec();
|
|
let expected = qp_poseidon_core::hash_variable_length_bytes(&input);
|
|
vectors.push(Poseidon2StandardHashTestCase {
|
|
input_bytes: input,
|
|
expected_hash: expected,
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_bytes_to_field_test_vectors() -> Vec<BytesToFieldTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0x98765432);
|
|
|
|
// Test 1: All zeros
|
|
let zero_input = [0u8; 96];
|
|
let zero_expected =
|
|
qp_poseidon_core::serialization::injective_bytes_to_felts::<P2Goldilocks>(&zero_input)
|
|
.into_iter()
|
|
.map(|g| GoldilocksField::from_noncanonical_u64(g.to_u64()))
|
|
.collect();
|
|
vectors.push(BytesToFieldTestCase {
|
|
input_bytes: zero_input,
|
|
expected_felts: zero_expected,
|
|
});
|
|
|
|
// Test 2: All 0xFF
|
|
let ff_input = [0xFFu8; 96];
|
|
let ff_expected =
|
|
qp_poseidon_core::serialization::injective_bytes_to_felts::<P2Goldilocks>(&ff_input)
|
|
.into_iter()
|
|
.map(|g| GoldilocksField::from_noncanonical_u64(g.to_u64()))
|
|
.collect();
|
|
vectors.push(BytesToFieldTestCase {
|
|
input_bytes: ff_input,
|
|
expected_felts: ff_expected,
|
|
});
|
|
|
|
// Test 3: Sequential bytes
|
|
let mut seq_input = [0u8; 96];
|
|
for (i, byte) in seq_input.iter_mut().enumerate() {
|
|
*byte = (i % 256) as u8;
|
|
}
|
|
let seq_expected =
|
|
qp_poseidon_core::serialization::injective_bytes_to_felts::<P2Goldilocks>(&seq_input)
|
|
.into_iter()
|
|
.map(|g| GoldilocksField::from_noncanonical_u64(g.to_u64()))
|
|
.collect();
|
|
vectors.push(BytesToFieldTestCase {
|
|
input_bytes: seq_input,
|
|
expected_felts: seq_expected,
|
|
});
|
|
|
|
// Test 4: Random inputs
|
|
for _ in 0..10 {
|
|
let mut input = [0u8; 96];
|
|
rng.fill(&mut input[..]);
|
|
let expected =
|
|
qp_poseidon_core::serialization::injective_bytes_to_felts::<P2Goldilocks>(&input)
|
|
.into_iter()
|
|
.map(|g| GoldilocksField::from_noncanonical_u64(g.to_u64()))
|
|
.collect();
|
|
vectors.push(BytesToFieldTestCase {
|
|
input_bytes: input,
|
|
expected_felts: expected,
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
// Standardized test reporting functions
|
|
struct TestResults {
|
|
name: String,
|
|
passed: usize,
|
|
failed: Vec<usize>,
|
|
total: usize,
|
|
}
|
|
|
|
impl TestResults {
|
|
fn new(name: String, total: usize) -> Self {
|
|
Self {
|
|
name,
|
|
passed: 0,
|
|
failed: Vec::new(),
|
|
total,
|
|
}
|
|
}
|
|
|
|
fn add_pass(&mut self) {
|
|
self.passed += 1;
|
|
}
|
|
|
|
fn add_failure(&mut self, test_index: usize) {
|
|
self.failed.push(test_index);
|
|
}
|
|
|
|
fn print_summary(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("\n=== {} TEST SUMMARY ===", self.name.to_uppercase());
|
|
println!("Total tests: {}", self.total);
|
|
println!(
|
|
"Passed: {} ({:.1}%)",
|
|
self.passed,
|
|
self.passed as f32 / self.total as f32 * 100.0
|
|
);
|
|
|
|
if self.failed.is_empty() {
|
|
println!("🎉 All {} tests PASSED!", self.name.to_lowercase());
|
|
Ok(())
|
|
} else {
|
|
println!(
|
|
"Failed: {} ({:.1}%)",
|
|
self.failed.len(),
|
|
self.failed.len() as f32 / self.total as f32 * 100.0
|
|
);
|
|
if self.failed.len() > 10 {
|
|
println!(" (showing first 10 failures above)");
|
|
}
|
|
Err(format!(
|
|
"{} out of {} {} tests failed",
|
|
self.failed.len(),
|
|
self.total,
|
|
self.name.to_lowercase()
|
|
)
|
|
.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn print_test_failure<T: std::fmt::Display>(
|
|
test_name: &str,
|
|
test_index: usize,
|
|
expected: &[T],
|
|
actual: &[T],
|
|
max_elements: usize,
|
|
) {
|
|
println!("\n{} test case {} FAILED:", test_name, test_index);
|
|
for j in 0..max_elements.min(expected.len()).min(actual.len()) {
|
|
println!(" Expected [{}]: {}", j, expected[j]);
|
|
println!(" GPU [{}]: {}", j, actual[j]);
|
|
}
|
|
}
|
|
|
|
fn print_test_failure_hex(
|
|
test_name: &str,
|
|
test_index: usize,
|
|
expected: &[GoldilocksField],
|
|
actual: &[GoldilocksField],
|
|
max_elements: usize,
|
|
) {
|
|
println!("\n{} test case {} FAILED:", test_name, test_index);
|
|
for j in 0..max_elements.min(expected.len()).min(actual.len()) {
|
|
println!(
|
|
" Expected [{}]: 0x{:016x}",
|
|
j,
|
|
expected[j].to_canonical_u64()
|
|
);
|
|
println!(
|
|
" GPU [{}]: 0x{:016x}",
|
|
j,
|
|
actual[j].to_canonical_u64()
|
|
);
|
|
}
|
|
}
|
|
|
|
fn generate_double_hash_test_vectors() -> Vec<DoubleHashTestCase> {
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0xDEADBEEF);
|
|
|
|
// Test 1-250: Random header and nonce combinations (25 tests)
|
|
for i in 0..250 {
|
|
let mut input = [0u8; 96];
|
|
|
|
rng.fill(&mut input[..]);
|
|
|
|
let expected = {
|
|
let first_hash = qp_poseidon_core::hash_squeeze_twice(&input);
|
|
qp_poseidon_core::hash_squeeze_twice(&first_hash)
|
|
};
|
|
|
|
vectors.push(DoubleHashTestCase {
|
|
input_96_bytes: input,
|
|
expected_hash: expected,
|
|
description: format!("random_case_{}", i + 1),
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
fn generate_true_internal_only_test_vectors() -> Vec<InternalRoundsTestCase> {
|
|
// This function generates test vectors for INTERNAL ROUNDS ONLY
|
|
// Input: state after initial external rounds
|
|
// Expected: state after internal rounds (before terminal external)
|
|
let mut vectors = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(54321);
|
|
use qp_poseidon_constants::{
|
|
POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW, POSEIDON2_INTERNAL_CONSTANTS_RAW,
|
|
};
|
|
|
|
// Helper to apply initial external rounds
|
|
let apply_initial_external = |mut state: [GoldilocksField; 12]| -> [GoldilocksField; 12] {
|
|
for round in 0..4 {
|
|
// Add constants
|
|
for i in 0..12 {
|
|
state[i] += GoldilocksField::from_canonical_u64(
|
|
POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW[round][i],
|
|
);
|
|
}
|
|
// S-box all elements
|
|
for i in 0..12 {
|
|
state[i] = state[i].exp_u64(7);
|
|
}
|
|
// External linear layer
|
|
state = apply_full_external_linear_layer_to_state(&state);
|
|
}
|
|
state
|
|
};
|
|
|
|
// Helper to apply internal rounds only
|
|
let apply_internal_only = |mut state: [GoldilocksField; 12]| -> [GoldilocksField; 12] {
|
|
for round in 0..22 {
|
|
state[0] +=
|
|
GoldilocksField::from_canonical_u64(POSEIDON2_INTERNAL_CONSTANTS_RAW[round]);
|
|
state[0] = state[0].exp_u64(7);
|
|
state = apply_internal_linear_layer_to_state(&state);
|
|
}
|
|
state
|
|
};
|
|
|
|
// Zero state
|
|
let zero_input = [GoldilocksField::ZERO; 12];
|
|
let after_initial_external = apply_initial_external(zero_input);
|
|
let after_internal = apply_internal_only(after_initial_external);
|
|
vectors.push(InternalRoundsTestCase {
|
|
input: after_initial_external,
|
|
expected: after_internal,
|
|
});
|
|
|
|
// Unit vectors (one element = 1, rest = 0)
|
|
for i in 0..12 {
|
|
let mut unit_input = [GoldilocksField::ZERO; 12];
|
|
unit_input[i] = GoldilocksField::ONE;
|
|
let after_initial_external = apply_initial_external(unit_input);
|
|
let after_internal = apply_internal_only(after_initial_external);
|
|
vectors.push(InternalRoundsTestCase {
|
|
input: after_initial_external,
|
|
expected: after_internal,
|
|
});
|
|
}
|
|
|
|
// All ones
|
|
let ones_input = [GoldilocksField::ONE; 12];
|
|
let after_initial_external = apply_initial_external(ones_input);
|
|
let after_internal = apply_internal_only(after_initial_external);
|
|
vectors.push(InternalRoundsTestCase {
|
|
input: after_initial_external,
|
|
expected: after_internal,
|
|
});
|
|
|
|
// Sequential [1,2,3,...,12]
|
|
let sequential_input =
|
|
core::array::from_fn(|i| GoldilocksField::from_canonical_u64((i + 1) as u64));
|
|
let after_initial_external = apply_initial_external(sequential_input);
|
|
let after_internal = apply_internal_only(after_initial_external);
|
|
vectors.push(InternalRoundsTestCase {
|
|
input: after_initial_external,
|
|
expected: after_internal,
|
|
});
|
|
|
|
// Random small values (all elements < 2^8)
|
|
for _ in 0..15 {
|
|
let mut random_input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = (rng.gen::<u8>()) as u64;
|
|
random_input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let after_initial_external = apply_initial_external(random_input);
|
|
let after_internal = apply_internal_only(after_initial_external);
|
|
vectors.push(InternalRoundsTestCase {
|
|
input: after_initial_external,
|
|
expected: after_internal,
|
|
});
|
|
}
|
|
|
|
// Random medium values (all elements < 2^16)
|
|
for _ in 0..10 {
|
|
let mut random_input = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = (rng.gen::<u16>()) as u64;
|
|
random_input[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
let after_initial_external = apply_initial_external(random_input);
|
|
let after_internal = apply_internal_only(after_initial_external);
|
|
vectors.push(InternalRoundsTestCase {
|
|
input: after_initial_external,
|
|
expected: after_internal,
|
|
});
|
|
}
|
|
|
|
vectors
|
|
}
|
|
|
|
pub async fn test_poseidon2_permutation(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_poseidon2_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
// Use the same pattern as other successful tests - include mining.wgsl and define our own bindings
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Simple test entry point for Poseidon2 permutation
|
|
@group(0) @binding(0) var<storage, read> input_state: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_state: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let offset = test_id * 12u;
|
|
|
|
// Copy input to local state
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = input_state[offset + i];
|
|
}}
|
|
|
|
// Apply Poseidon2 permutation
|
|
poseidon2_permute(&state);
|
|
|
|
// Write result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_state[offset + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Poseidon2 Permutation Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
// Create pipeline
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Poseidon2 Permutation Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 16;
|
|
for (batch_idx, chunk) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
// Prepare input data (flatten 12-element arrays and convert to GfWgls)
|
|
let mut input_data = Vec::new();
|
|
for test_case in chunk {
|
|
for &field_elem in &test_case.input {
|
|
input_data.push(GfWgls::from(field_elem));
|
|
}
|
|
}
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (input_data.len() * std::mem::size_of::<GfWgls>()) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results for the chunk
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
// Convert results back to GoldilocksField arrays using proper conversion
|
|
let gpu_result: [GoldilocksField; 12] = [
|
|
results[i * 12].into(),
|
|
results[i * 12 + 1].into(),
|
|
results[i * 12 + 2].into(),
|
|
results[i * 12 + 3].into(),
|
|
results[i * 12 + 4].into(),
|
|
results[i * 12 + 5].into(),
|
|
results[i * 12 + 6].into(),
|
|
results[i * 12 + 7].into(),
|
|
results[i * 12 + 8].into(),
|
|
results[i * 12 + 9].into(),
|
|
results[i * 12 + 10].into(),
|
|
results[i * 12 + 11].into(),
|
|
];
|
|
|
|
// Compare all 12 elements
|
|
let mut all_match = true;
|
|
for j in 0..12 {
|
|
if gpu_result[j].to_canonical_u64() != test_case.expected[j].to_canonical_u64() {
|
|
all_match = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if all_match {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(batch_idx * BATCH_SIZE + i + 1);
|
|
failure_details.push((test_case.clone(), gpu_result));
|
|
if failure_details.len() <= 3 {
|
|
println!("❌ FAILED: Poseidon2 permutation test");
|
|
println!("Input state:");
|
|
for (j, &val) in test_case.input.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, val.to_canonical_u64());
|
|
}
|
|
println!("Expected state:");
|
|
for (j, &val) in test_case.expected.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, val.to_canonical_u64());
|
|
}
|
|
println!("GPU result:");
|
|
for (j, &val) in gpu_result.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, val.to_canonical_u64());
|
|
}
|
|
println!("Element-by-element comparison:");
|
|
for j in 0..12 {
|
|
let expected = test_case.expected[j].to_canonical_u64();
|
|
let got = gpu_result[j].to_canonical_u64();
|
|
let status = if expected == got { "✓" } else { "✗" };
|
|
println!(
|
|
" [{}] {} Expected: 0x{:016x}, Got: 0x{:016x}",
|
|
j, status, expected, got
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("poseidon2 permutation".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_initial_external_rounds(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Test just the initial 4 external rounds
|
|
let test_vectors = generate_gf_from_const_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_state: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_state: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let offset = test_id * 12u;
|
|
|
|
// Copy input to local state
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = input_state[offset + i];
|
|
}}
|
|
|
|
// Apply ONLY the 4 initial external rounds
|
|
for (var round = 0u; round < 4u; round++) {{
|
|
// Add round constants
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = gf_add(state[i], gf_from_const(INITIAL_EXTERNAL_CONSTANTS[round][i]));
|
|
}}
|
|
// S-box on all elements
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = sbox(state[i]);
|
|
}}
|
|
// External linear layer
|
|
external_linear_layer(&state);
|
|
}}
|
|
|
|
// Write result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_state[offset + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Initial External Rounds Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Initial External Rounds Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Generate comprehensive test cases
|
|
let mut test_cases = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
|
|
|
|
// Zero state
|
|
test_cases.push([GoldilocksField::ZERO; 12]);
|
|
|
|
// Unit vectors (one element = 1, rest = 0)
|
|
for i in 0..12 {
|
|
let mut state = [GoldilocksField::ZERO; 12];
|
|
state[i] = GoldilocksField::ONE;
|
|
test_cases.push(state);
|
|
}
|
|
|
|
// All ones state
|
|
test_cases.push([GoldilocksField::ONE; 12]);
|
|
|
|
// Sequential state [1,2,3,...,12]
|
|
let sequential_state: [GoldilocksField; 12] =
|
|
core::array::from_fn(|i| GoldilocksField::from_canonical_u64((i + 1) as u64));
|
|
test_cases.push(sequential_state);
|
|
|
|
// Random small values (all elements < 2^8)
|
|
for _ in 0..15 {
|
|
let mut state = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = (rng.gen::<u8>()) as u64;
|
|
state[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
test_cases.push(state);
|
|
}
|
|
|
|
// Random medium values (all elements < 2^16)
|
|
for _ in 0..10 {
|
|
let mut state = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = (rng.gen::<u16>()) as u64;
|
|
state[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
test_cases.push(state);
|
|
}
|
|
|
|
for (test_idx, input_state) in test_cases.iter().enumerate() {
|
|
let input_data: Vec<GfWgls> = input_state.iter().map(|&f| GfWgls::from(f)).collect();
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Initial External Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Initial External Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 12) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Initial External Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Initial External Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Initial External Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Initial External Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let gpu_result: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Compute CPU reference
|
|
let mut cpu_state = *input_state;
|
|
use qp_poseidon_constants::POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW;
|
|
|
|
for round in 0..4 {
|
|
// Add constants
|
|
for i in 0..12 {
|
|
let raw_constant = POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW[round][i];
|
|
cpu_state[i] += GoldilocksField::from_canonical_u64(raw_constant);
|
|
}
|
|
// S-box
|
|
for i in 0..12 {
|
|
cpu_state[i] = cpu_state[i].exp_u64(7);
|
|
}
|
|
// External linear layer (simplified CPU version)
|
|
// This is a basic implementation - if this test fails we'll know it's here
|
|
let two = GoldilocksField::from_canonical_u64(2);
|
|
let three = GoldilocksField::from_canonical_u64(3);
|
|
|
|
// 4x4 MDS matrix on chunks
|
|
for chunk in 0..3 {
|
|
let offset = chunk * 4;
|
|
let mut chunk_state = [GoldilocksField::ZERO; 4];
|
|
for i in 0..4 {
|
|
chunk_state[i] = cpu_state[offset + i];
|
|
}
|
|
let new_0 =
|
|
two * chunk_state[0] + three * chunk_state[1] + chunk_state[2] + chunk_state[3];
|
|
let new_1 =
|
|
chunk_state[0] + two * chunk_state[1] + three * chunk_state[2] + chunk_state[3];
|
|
let new_2 =
|
|
chunk_state[0] + chunk_state[1] + two * chunk_state[2] + three * chunk_state[3];
|
|
let new_3 =
|
|
three * chunk_state[0] + chunk_state[1] + chunk_state[2] + two * chunk_state[3];
|
|
cpu_state[offset] = new_0;
|
|
cpu_state[offset + 1] = new_1;
|
|
cpu_state[offset + 2] = new_2;
|
|
cpu_state[offset + 3] = new_3;
|
|
}
|
|
// Circulant matrix
|
|
let mut sums = [GoldilocksField::ZERO; 4];
|
|
for k in 0..4 {
|
|
for j in (k..12).step_by(4) {
|
|
sums[k] += cpu_state[j];
|
|
}
|
|
}
|
|
for i in 0..12 {
|
|
cpu_state[i] += sums[i % 4];
|
|
}
|
|
}
|
|
|
|
// Compare
|
|
let mut test_passed = true;
|
|
for i in 0..12 {
|
|
let gpu_gf: GoldilocksField = gpu_result[i].into();
|
|
if gpu_gf.to_canonical_u64() != cpu_state[i].to_canonical_u64() {
|
|
test_passed = false;
|
|
if failed_tests.is_empty() {
|
|
println!("❌ Initial external rounds test {} failed:", test_idx + 1);
|
|
for j in 0..3 {
|
|
println!(
|
|
" [{}] Expected: 0x{:016x}, Got: 0x{:016x}",
|
|
j,
|
|
cpu_state[j].to_canonical_u64(),
|
|
gpu_gf.to_canonical_u64()
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if test_passed {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(test_idx + 1);
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("initial external rounds".to_string(), test_cases.len());
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_terminal_external_rounds(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Test just the terminal 4 external rounds
|
|
let total_tests = 2;
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_state: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_state: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let offset = test_id * 12u;
|
|
|
|
// Copy input to local state
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = input_state[offset + i];
|
|
}}
|
|
|
|
// Apply ONLY the 4 terminal external rounds
|
|
for (var round = 0u; round < 4u; round++) {{
|
|
// Add round constants
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = gf_add(state[i], gf_from_const(TERMINAL_EXTERNAL_CONSTANTS[round][i]));
|
|
}}
|
|
// S-box on all elements
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = sbox(state[i]);
|
|
}}
|
|
// External linear layer
|
|
external_linear_layer(&state);
|
|
}}
|
|
|
|
// Write result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_state[offset + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Terminal External Rounds Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Terminal External Rounds Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Generate comprehensive test cases
|
|
let mut test_cases = Vec::new();
|
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
|
|
|
|
// Zero state
|
|
test_cases.push([GoldilocksField::ZERO; 12]);
|
|
|
|
// Unit vectors (one element = 1, rest = 0)
|
|
for i in 0..12 {
|
|
let mut state = [GoldilocksField::ZERO; 12];
|
|
state[i] = GoldilocksField::ONE;
|
|
test_cases.push(state);
|
|
}
|
|
|
|
// All ones state
|
|
test_cases.push([GoldilocksField::ONE; 12]);
|
|
|
|
// Sequential state [1,2,3,...,12]
|
|
let sequential_state: [GoldilocksField; 12] =
|
|
core::array::from_fn(|i| GoldilocksField::from_canonical_u64((i + 1) as u64));
|
|
test_cases.push(sequential_state);
|
|
|
|
// Random small values (all elements < 2^8)
|
|
for _ in 0..15 {
|
|
let mut state = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = (rng.gen::<u8>()) as u64;
|
|
state[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
test_cases.push(state);
|
|
}
|
|
|
|
// Random medium values (all elements < 2^16)
|
|
for _ in 0..10 {
|
|
let mut state = [GoldilocksField::ZERO; 12];
|
|
for j in 0..12 {
|
|
let val = (rng.gen::<u16>()) as u64;
|
|
state[j] = GoldilocksField::from_canonical_u64(val);
|
|
}
|
|
test_cases.push(state);
|
|
}
|
|
|
|
for (test_idx, input_state) in test_cases.iter().enumerate() {
|
|
let input_data: Vec<GfWgls> = input_state.iter().map(|&f| GfWgls::from(f)).collect();
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Terminal External Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Terminal External Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 12) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Terminal External Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Terminal External Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Terminal External Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Terminal External Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let gpu_result: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Compute CPU reference
|
|
let mut cpu_state = *input_state;
|
|
use qp_poseidon_constants::POSEIDON2_TERMINAL_EXTERNAL_CONSTANTS_RAW;
|
|
|
|
for round in 0..4 {
|
|
// Add constants
|
|
for i in 0..12 {
|
|
let raw_constant = POSEIDON2_TERMINAL_EXTERNAL_CONSTANTS_RAW[round][i];
|
|
cpu_state[i] += GoldilocksField::from_canonical_u64(raw_constant);
|
|
}
|
|
// S-box
|
|
for i in 0..12 {
|
|
cpu_state[i] = cpu_state[i].exp_u64(7);
|
|
}
|
|
// External linear layer (same as initial external)
|
|
let two = GoldilocksField::from_canonical_u64(2);
|
|
let three = GoldilocksField::from_canonical_u64(3);
|
|
|
|
// 4x4 MDS matrix on chunks
|
|
for chunk in 0..3 {
|
|
let offset = chunk * 4;
|
|
let mut chunk_state = [GoldilocksField::ZERO; 4];
|
|
for i in 0..4 {
|
|
chunk_state[i] = cpu_state[offset + i];
|
|
}
|
|
let new_0 =
|
|
two * chunk_state[0] + three * chunk_state[1] + chunk_state[2] + chunk_state[3];
|
|
let new_1 =
|
|
chunk_state[0] + two * chunk_state[1] + three * chunk_state[2] + chunk_state[3];
|
|
let new_2 =
|
|
chunk_state[0] + chunk_state[1] + two * chunk_state[2] + three * chunk_state[3];
|
|
let new_3 =
|
|
three * chunk_state[0] + chunk_state[1] + chunk_state[2] + two * chunk_state[3];
|
|
cpu_state[offset] = new_0;
|
|
cpu_state[offset + 1] = new_1;
|
|
cpu_state[offset + 2] = new_2;
|
|
cpu_state[offset + 3] = new_3;
|
|
}
|
|
// Circulant matrix
|
|
let mut sums = [GoldilocksField::ZERO; 4];
|
|
for k in 0..4 {
|
|
for j in (k..12).step_by(4) {
|
|
sums[k] += cpu_state[j];
|
|
}
|
|
}
|
|
for i in 0..12 {
|
|
cpu_state[i] += sums[i % 4];
|
|
}
|
|
}
|
|
|
|
// Compare
|
|
let mut test_passed = true;
|
|
for i in 0..12 {
|
|
let gpu_gf: GoldilocksField = gpu_result[i].into();
|
|
if gpu_gf.to_canonical_u64() != cpu_state[i].to_canonical_u64() {
|
|
test_passed = false;
|
|
if failed_tests.is_empty() {
|
|
println!("❌ Terminal external rounds test {} failed:", test_idx + 1);
|
|
for j in 0..3 {
|
|
println!(
|
|
" [{}] Expected: 0x{:016x}, Got: 0x{:016x}",
|
|
j,
|
|
cpu_state[j].to_canonical_u64(),
|
|
gpu_gf.to_canonical_u64()
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if test_passed {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(test_idx + 1);
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("terminal external rounds".to_string(), test_cases.len());
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_gf_from_const(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Test the gf_from_const function that converts constant arrays to GoldilocksField
|
|
let test_vectors = generate_gf_from_const_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_constants: array<array<u32, 2>>;
|
|
@group(0) @binding(1) var<storage, read_write> output_fields: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let index = global_id.x;
|
|
output_fields[index] = gf_from_const(input_constants[index]);
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("gf_from_const Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("gf_from_const Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
const BATCH_SIZE: usize = 32;
|
|
for (batch_idx, chunk) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
let input_data: Vec<[u32; 2]> = chunk.iter().map(|tc| [tc.a, tc.b]).collect();
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("gf_from_const Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("gf_from_const Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * chunk.len()) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("gf_from_const Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("gf_from_const Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("gf_from_const Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("gf_from_const Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
let gpu_result = GoldilocksField::from(results[i]);
|
|
if gpu_result.to_canonical_u64() == test_case.expected.to_canonical_u64() {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(batch_idx * BATCH_SIZE + i + 1);
|
|
if failed_tests.len() <= 5 {
|
|
println!(
|
|
"❌ gf_from_const test failed: input=[{}, {}], expected=0x{:016x}, got=0x{:016x}",
|
|
test_case.a, test_case.b,
|
|
test_case.expected.to_canonical_u64(),
|
|
gpu_result.to_canonical_u64()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("gf_from_const".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_constants_verification(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let total_tests = 24;
|
|
|
|
// Create shader that just outputs the constants
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Constants verification entry point
|
|
@group(0) @binding(0) var<storage, read_write> output_constants: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
// Output first round constants (we know these work)
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_constants[i] = gf_from_const(INITIAL_EXTERNAL_CONSTANTS[0][i]);
|
|
}}
|
|
|
|
// Output second round constants (these seem to have issues)
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_constants[i + 12u] = gf_from_const(INITIAL_EXTERNAL_CONSTANTS[1][i]);
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Constants Verification Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Constants Verification Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Constants Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 24) as u64, // 2 rounds * 12 elements
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Constants Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: output_buffer.as_entire_binding(),
|
|
}],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Constants Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Constants Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Constants Staging Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 24) as u64,
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(
|
|
&output_buffer,
|
|
0,
|
|
&staging_buffer,
|
|
0,
|
|
(std::mem::size_of::<GfWgls>() * 24) as u64,
|
|
);
|
|
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_constants: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
use qp_poseidon_constants::POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW;
|
|
|
|
for i in 0..12 {
|
|
let gpu_gf: GoldilocksField = gpu_constants[i].into();
|
|
let expected = POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW[0][i];
|
|
if gpu_gf.to_canonical_u64() == expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(i + 1);
|
|
}
|
|
}
|
|
|
|
for i in 0..12 {
|
|
let gpu_gf: GoldilocksField = gpu_constants[12 + i].into();
|
|
let expected = POSEIDON2_INITIAL_EXTERNAL_CONSTANTS_RAW[1][i];
|
|
if gpu_gf.to_canonical_u64() == expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(12 + i + 1);
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
|
|
// Print final summary
|
|
let mut results = TestResults::new("constants verification".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_internal_rounds_only(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
// Test ONLY the internal rounds (22 rounds) without initial/terminal external rounds
|
|
let test_vectors = generate_true_internal_only_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut results = TestResults::new("internal rounds only".to_string(), total_tests);
|
|
|
|
for (test_idx, test_case) in test_vectors.iter().enumerate() {
|
|
// test_case.input is already the state after initial external rounds
|
|
// test_case.expected is the state after internal rounds only
|
|
let input_data: Vec<GfWgls> = test_case.input.map(GfWgls::from).to_vec();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_data: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_data: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let index = global_id.x;
|
|
if (index >= 1u) {{
|
|
return;
|
|
}}
|
|
|
|
let base_idx = index * 12u;
|
|
|
|
// Copy input to local state
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = input_data[base_idx + i];
|
|
}}
|
|
|
|
// Apply ONLY internal rounds (22 rounds)
|
|
for (var round = 0u; round < 22u; round++) {{
|
|
// Add round constant to first element only
|
|
state[0] = gf_add(state[0], gf_from_const(INTERNAL_CONSTANTS[round]));
|
|
// S-box on first element only
|
|
state[0] = sbox(state[0]);
|
|
// Internal linear layer (diagonal matrix)
|
|
internal_linear_layer(&state);
|
|
}}
|
|
|
|
// Store result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_data[base_idx + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Internal Rounds Only Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Internal Rounds Only Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Internal Rounds Only Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Internal Rounds Only Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 12) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Internal Rounds Only Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Internal Rounds Only Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Internal Rounds Only Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Internal Rounds Only Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_result: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// The expected result is already computed in the test vector
|
|
let cpu_after_internal = test_case.expected;
|
|
|
|
// Compare GPU vs CPU results
|
|
let gpu_state: [GoldilocksField; 12] = core::array::from_fn(|j| {
|
|
let wgls_result = gpu_result[j];
|
|
GoldilocksField::from(wgls_result)
|
|
});
|
|
|
|
let mut test_passed = true;
|
|
for j in 0..12 {
|
|
if gpu_state[j].to_canonical_u64() != cpu_after_internal[j].to_canonical_u64() {
|
|
test_passed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if test_passed {
|
|
results.add_pass();
|
|
} else {
|
|
results.add_failure(test_idx + 1);
|
|
if results.failed.len() <= 5 {
|
|
let expected_strings: Vec<String> = cpu_after_internal
|
|
.iter()
|
|
.take(3)
|
|
.map(|x| format!("0x{:016x}", x.to_canonical_u64()))
|
|
.collect();
|
|
let actual_strings: Vec<String> = gpu_state
|
|
.iter()
|
|
.take(3)
|
|
.map(|x| format!("0x{:016x}", x.to_canonical_u64()))
|
|
.collect();
|
|
print_test_failure(
|
|
"Internal rounds only",
|
|
test_idx + 1,
|
|
&expected_strings,
|
|
&actual_strings,
|
|
3,
|
|
);
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
// Print final summary
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_internal_constants_verification(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let total_tests = 22;
|
|
|
|
// Create shader that just outputs the internal constants
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Internal constants verification entry point
|
|
@group(0) @binding(0) var<storage, read_write> output_constants: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
// Output all 22 internal constants
|
|
for (var i = 0u; i < 22u; i++) {{
|
|
output_constants[i] = gf_from_const(INTERNAL_CONSTANTS[i]);
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Internal Constants Verification Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Internal Constants Verification Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Internal Constants Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 22) as u64, // 22 internal constants
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Internal Constants Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: output_buffer.as_entire_binding(),
|
|
}],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Internal Constants Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Internal Constants Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Internal Constants Staging Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 22) as u64,
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(
|
|
&output_buffer,
|
|
0,
|
|
&staging_buffer,
|
|
0,
|
|
(std::mem::size_of::<GfWgls>() * 22) as u64,
|
|
);
|
|
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_constants: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
use qp_poseidon_constants::POSEIDON2_INTERNAL_CONSTANTS_RAW;
|
|
|
|
for i in 0..22 {
|
|
let gpu_gf: GoldilocksField = gpu_constants[i].into();
|
|
let expected = POSEIDON2_INTERNAL_CONSTANTS_RAW[i];
|
|
if gpu_gf.to_canonical_u64() == expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(i + 1);
|
|
}
|
|
}
|
|
|
|
let mut results = TestResults::new("internal constants".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_terminal_external_rounds_issue(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("🔍 Testing terminal external rounds issue for sequential input...");
|
|
|
|
// Create shader that tests terminal external rounds specifically
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Debug entry point for terminal external rounds
|
|
@group(0) @binding(0) var<storage, read> state_before_terminal: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> after_terminal: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let offset = test_id * 12u;
|
|
|
|
// Copy the state after initial+internal rounds
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = state_before_terminal[offset + i];
|
|
}}
|
|
|
|
// Apply terminal external rounds
|
|
for (var round = 0u; round < 4u; round++) {{
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = gf_add(state[i], gf_from_const(TERMINAL_EXTERNAL_CONSTANTS[round][i]));
|
|
}}
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = sbox(state[i]);
|
|
}}
|
|
external_linear_layer(&state);
|
|
}}
|
|
|
|
// Save final result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
after_terminal[offset + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Terminal External Rounds Issue Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Terminal External Rounds Issue Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Use the state after initial+internal for sequential input: 0x5f14ee0d55368aa5, etc.
|
|
let state_after_internal = [
|
|
GoldilocksField::from_canonical_u64(0x5f14ee0d55368aa5),
|
|
GoldilocksField::from_canonical_u64(0xd858a148e6ba3c15),
|
|
GoldilocksField::from_canonical_u64(0xa63e197036ba7747),
|
|
// We need to get the full state - let me create a more complete test
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
GoldilocksField::ZERO,
|
|
];
|
|
|
|
let mut input_data = Vec::new();
|
|
for &field_elem in &state_after_internal {
|
|
input_data.push(GfWgls::from(field_elem));
|
|
}
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Terminal Issue Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Terminal Issue Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 12) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Terminal Issue Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Terminal Issue Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Terminal Issue Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Terminal Issue Staging Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 12) as u64,
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(
|
|
&output_buffer,
|
|
0,
|
|
&staging_buffer,
|
|
0,
|
|
(std::mem::size_of::<GfWgls>() * 12) as u64,
|
|
);
|
|
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
println!("🔍 GPU result after terminal external rounds:");
|
|
for i in 0..3 {
|
|
let gf: GoldilocksField = gpu_results[i].into();
|
|
println!(" [{}] = 0x{:016x}", i, gf.to_canonical_u64());
|
|
}
|
|
|
|
println!("Expected final result for sequential [1,2,3...12]: 0x7e9574e2a3d6c48b");
|
|
println!("This test helps isolate if terminal external constants are wrong");
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn test_poseidon2_terminal_external_constants_verification(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let total_tests = 24;
|
|
|
|
// Create shader that just outputs the terminal external constants
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Terminal constants verification entry point
|
|
@group(0) @binding(0) var<storage, read_write> output_constants: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
// Output first round terminal constants
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_constants[i] = gf_from_const(TERMINAL_EXTERNAL_CONSTANTS[0][i]);
|
|
}}
|
|
|
|
// Output second round terminal constants
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_constants[i + 12u] = gf_from_const(TERMINAL_EXTERNAL_CONSTANTS[1][i]);
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Terminal Constants Verification Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Terminal Constants Verification Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Terminal Constants Output Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 24) as u64, // 2 rounds * 12 elements
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Terminal Constants Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: output_buffer.as_entire_binding(),
|
|
}],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Terminal Constants Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Terminal Constants Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Terminal Constants Staging Buffer"),
|
|
size: (std::mem::size_of::<GfWgls>() * 24) as u64,
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(
|
|
&output_buffer,
|
|
0,
|
|
&staging_buffer,
|
|
0,
|
|
(std::mem::size_of::<GfWgls>() * 24) as u64,
|
|
);
|
|
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
buffer_slice.map_async(wgpu::MapMode::Read, |_| {});
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_constants: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
use qp_poseidon_constants::POSEIDON2_TERMINAL_EXTERNAL_CONSTANTS_RAW;
|
|
|
|
for i in 0..12 {
|
|
let gpu_gf: GoldilocksField = gpu_constants[i].into();
|
|
let expected = POSEIDON2_TERMINAL_EXTERNAL_CONSTANTS_RAW[0][i];
|
|
if gpu_gf.to_canonical_u64() == expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(i + 1);
|
|
}
|
|
}
|
|
|
|
for i in 0..12 {
|
|
let gpu_gf: GoldilocksField = gpu_constants[12 + i].into();
|
|
let expected = POSEIDON2_TERMINAL_EXTERNAL_CONSTANTS_RAW[1][i];
|
|
if gpu_gf.to_canonical_u64() == expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(12 + i + 1);
|
|
}
|
|
}
|
|
|
|
// Print final summary
|
|
let mut results = TestResults::new("terminal external constants".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_gf_mul(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_gf_mul_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
|
|
// Use the mining shader but create a simple test wrapper
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Simple test entry point that just multiplies two field elements
|
|
@group(0) @binding(0) var<storage, read> input_a: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read> input_b: array<GoldilocksField>;
|
|
@group(0) @binding(2) var<storage, read_write> output: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let i = global_id.x;
|
|
output[i] = gf_mul(input_a[i], input_b[i]);
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("gf_mul test shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
// Create pipeline
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("gf_mul test pipeline"),
|
|
layout: None, // Let wgpu infer the layout
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
for (i, vector) in test_vectors.iter().enumerate() {
|
|
let a_wgls: GfWgls = vector.a.into();
|
|
let b_wgls: GfWgls = vector.b.into();
|
|
|
|
let input_a_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input A Buffer"),
|
|
contents: bytemuck::cast_slice(&[a_wgls]),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let input_b_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input B Buffer"),
|
|
contents: bytemuck::cast_slice(&[b_wgls]),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: std::mem::size_of::<GfWgls>() as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_a_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: input_b_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 2,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
device
|
|
.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
})
|
|
.unwrap();
|
|
|
|
let data = slice.get_mapped_range();
|
|
let result_wgls: GfWgls = bytemuck::from_bytes::<GfWgls>(&data).clone();
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
|
|
let gpu_result_u64 = (result_wgls.limb0 as u64)
|
|
| ((result_wgls.limb1 as u64) << 16)
|
|
| ((result_wgls.limb2 as u64) << 32)
|
|
| ((result_wgls.limb3 as u64) << 48);
|
|
let gpu_result = GoldilocksField(gpu_result_u64);
|
|
|
|
let expected_wgls: GfWgls = vector.expected.into();
|
|
|
|
// The GPU result might not be canonical, so we need to canonicalize it before comparing.
|
|
if gpu_result.to_canonical_u64() == vector.expected.to_canonical_u64() {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(i + 1);
|
|
|
|
// Only show detailed output for failures
|
|
println!(
|
|
"\nTest case {} FAILED: 0x{:016x} * 0x{:016x}",
|
|
i + 1,
|
|
vector.a.0,
|
|
vector.b.0
|
|
);
|
|
|
|
// Analyze the operands
|
|
let a_limbs = [
|
|
(vector.a.0 & 0xFFFF) as u32,
|
|
((vector.a.0 >> 16) & 0xFFFF) as u32,
|
|
((vector.a.0 >> 32) & 0xFFFF) as u32,
|
|
((vector.a.0 >> 48) & 0xFFFF) as u32,
|
|
];
|
|
let b_limbs = [
|
|
(vector.b.0 & 0xFFFF) as u32,
|
|
((vector.b.0 >> 16) & 0xFFFF) as u32,
|
|
((vector.b.0 >> 32) & 0xFFFF) as u32,
|
|
((vector.b.0 >> 48) & 0xFFFF) as u32,
|
|
];
|
|
|
|
println!(
|
|
" a: [{}, {}, {}, {}]",
|
|
a_limbs[0], a_limbs[1], a_limbs[2], a_limbs[3]
|
|
);
|
|
println!(
|
|
" b: [{}, {}, {}, {}]",
|
|
b_limbs[0], b_limbs[1], b_limbs[2], b_limbs[3]
|
|
);
|
|
|
|
// Determine which multiplication path this should take
|
|
let a_high_nonzero = a_limbs[2] != 0 || a_limbs[3] != 0;
|
|
let b_high_nonzero = b_limbs[2] != 0 || b_limbs[3] != 0;
|
|
let path = if !a_high_nonzero && !b_high_nonzero {
|
|
"Step 1: Both small (high limbs=0)"
|
|
} else if !a_high_nonzero || !b_high_nonzero {
|
|
"Step 2: Mixed case (one has high limbs=0)"
|
|
} else {
|
|
"Step 3: Both large (both have high limbs≠0)"
|
|
};
|
|
println!(" Expected path: {}", path);
|
|
|
|
println!(
|
|
" CPU expected: 0x{:016x} [{}, {}, {}, {}]",
|
|
vector.expected.0,
|
|
expected_wgls.limb0,
|
|
expected_wgls.limb1,
|
|
expected_wgls.limb2,
|
|
expected_wgls.limb3
|
|
);
|
|
println!(
|
|
" GPU result: 0x{:016x} [{}, {}, {}, {}]",
|
|
gpu_result.0,
|
|
result_wgls.limb0,
|
|
result_wgls.limb1,
|
|
result_wgls.limb2,
|
|
result_wgls.limb3
|
|
);
|
|
|
|
// Show the exact differences
|
|
let diff_limb0 = result_wgls.limb0 as i64 - expected_wgls.limb0 as i64;
|
|
let diff_limb1 = result_wgls.limb1 as i64 - expected_wgls.limb1 as i64;
|
|
let diff_limb2 = result_wgls.limb2 as i64 - expected_wgls.limb2 as i64;
|
|
let diff_limb3 = result_wgls.limb3 as i64 - expected_wgls.limb3 as i64;
|
|
println!(
|
|
" Differences: limb0={:+}, limb1={:+}, limb2={:+}, limb3={:+}",
|
|
diff_limb0, diff_limb1, diff_limb2, diff_limb3
|
|
);
|
|
}
|
|
}
|
|
|
|
// Print final summary
|
|
let mut results = TestResults::new("gf_mul".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_mds_matrix(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_mds_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
// Read the mining shader source and create a test wrapper
|
|
let mining_shader_source = include_str!("mining.wgsl");
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// MDS matrix test entry point
|
|
@group(0) @binding(0) var<storage, read> input_data: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_data: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let i = global_id.x;
|
|
let base_idx = i * 4u;
|
|
|
|
// Read 4 input elements
|
|
var chunk_state: array<GoldilocksField, 4>;
|
|
chunk_state[0] = input_data[base_idx + 0u];
|
|
chunk_state[1] = input_data[base_idx + 1u];
|
|
chunk_state[2] = input_data[base_idx + 2u];
|
|
chunk_state[3] = input_data[base_idx + 3u];
|
|
|
|
// Apply 4x4 MDS matrix transformation: [[2, 3, 1, 1], [1, 2, 3, 1], [1, 1, 2, 3], [3, 1, 1, 2]]
|
|
let two = gf_from_limbs(2u, 0u, 0u, 0u);
|
|
let three = gf_from_limbs(3u, 0u, 0u, 0u);
|
|
|
|
// new[0] = 2*x[0] + 3*x[1] + 1*x[2] + 1*x[3]
|
|
let new_0 = gf_add(gf_add(gf_add(
|
|
gf_mul(chunk_state[0], two),
|
|
gf_mul(chunk_state[1], three)),
|
|
chunk_state[2]),
|
|
chunk_state[3]);
|
|
|
|
// new[1] = 1*x[0] + 2*x[1] + 3*x[2] + 1*x[3]
|
|
let new_1 = gf_add(gf_add(gf_add(
|
|
chunk_state[0],
|
|
gf_mul(chunk_state[1], two)),
|
|
gf_mul(chunk_state[2], three)),
|
|
chunk_state[3]);
|
|
|
|
// new[2] = 1*x[0] + 1*x[1] + 2*x[2] + 3*x[3]
|
|
let new_2 = gf_add(gf_add(gf_add(
|
|
chunk_state[0],
|
|
chunk_state[1]),
|
|
gf_mul(chunk_state[2], two)),
|
|
gf_mul(chunk_state[3], three));
|
|
|
|
// new[3] = 3*x[0] + 1*x[1] + 1*x[2] + 2*x[3]
|
|
let new_3 = gf_add(gf_add(gf_add(
|
|
gf_mul(chunk_state[0], three),
|
|
chunk_state[1]),
|
|
chunk_state[2]),
|
|
gf_mul(chunk_state[3], two));
|
|
|
|
// Write results
|
|
output_data[base_idx + 0u] = new_0;
|
|
output_data[base_idx + 1u] = new_1;
|
|
output_data[base_idx + 2u] = new_2;
|
|
output_data[base_idx + 3u] = new_3;
|
|
}}
|
|
",
|
|
mining_shader_source
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("MDS test shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
// Create explicit bind group layout for MDS test
|
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("MDS Test Bind Group Layout"),
|
|
entries: &[
|
|
// binding 0: input_data buffer
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: wgpu::ShaderStages::COMPUTE,
|
|
ty: wgpu::BindingType::Buffer {
|
|
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: None,
|
|
},
|
|
count: None,
|
|
},
|
|
// binding 1: output_data buffer
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 1,
|
|
visibility: wgpu::ShaderStages::COMPUTE,
|
|
ty: wgpu::BindingType::Buffer {
|
|
ty: wgpu::BufferBindingType::Storage { read_only: false },
|
|
has_dynamic_offset: false,
|
|
min_binding_size: None,
|
|
},
|
|
count: None,
|
|
},
|
|
],
|
|
});
|
|
|
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
label: Some("MDS Test Pipeline Layout"),
|
|
bind_group_layouts: &[&bind_group_layout],
|
|
push_constant_ranges: &[],
|
|
});
|
|
|
|
// Create compute pipeline with explicit layout
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("MDS test pipeline"),
|
|
layout: Some(&pipeline_layout),
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 64;
|
|
for chunk in test_vectors.chunks(BATCH_SIZE) {
|
|
// Prepare input data (flatten 4-element arrays and convert to GfWgls)
|
|
let mut input_data = Vec::new();
|
|
for test_case in chunk {
|
|
for &elem in &test_case.input {
|
|
let u64_val = elem.to_canonical_u64();
|
|
input_data.push(GfWgls {
|
|
limb0: (u64_val & 0xFFFF) as u32,
|
|
limb1: ((u64_val >> 16) & 0xFFFF) as u32,
|
|
limb2: ((u64_val >> 32) & 0xFFFF) as u32,
|
|
limb3: ((u64_val >> 48) & 0xFFFF) as u32,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Create input buffer
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("MDS Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
|
|
// Create output buffer
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("MDS Output Buffer"),
|
|
size: (input_data.len() * std::mem::size_of::<GfWgls>()) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
// Create bind group
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("MDS Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
// Run compute shader
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("MDS Test Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("MDS Test Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
// Create staging buffer and copy results
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("MDS Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
// Read results
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
let (sender, receiver) = futures::channel::oneshot::channel();
|
|
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
|
sender.send(result).unwrap();
|
|
});
|
|
|
|
device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
receiver.await??;
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
let gpu_result = [
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(gpu_results[i * 4].limb0 as u64 & 0xFFFF)
|
|
| (((gpu_results[i * 4].limb1 as u64) & 0xFFFF) << 16)
|
|
| (((gpu_results[i * 4].limb2 as u64) & 0xFFFF) << 32)
|
|
| (((gpu_results[i * 4].limb3 as u64) & 0xFFFF) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(gpu_results[i * 4 + 1].limb0 as u64 & 0xFFFF)
|
|
| (((gpu_results[i * 4 + 1].limb1 as u64) & 0xFFFF) << 16)
|
|
| (((gpu_results[i * 4 + 1].limb2 as u64) & 0xFFFF) << 32)
|
|
| (((gpu_results[i * 4 + 1].limb3 as u64) & 0xFFFF) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(gpu_results[i * 4 + 2].limb0 as u64 & 0xFFFF)
|
|
| (((gpu_results[i * 4 + 2].limb1 as u64) & 0xFFFF) << 16)
|
|
| (((gpu_results[i * 4 + 2].limb2 as u64) & 0xFFFF) << 32)
|
|
| (((gpu_results[i * 4 + 2].limb3 as u64) & 0xFFFF) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(gpu_results[i * 4 + 3].limb0 as u64 & 0xFFFF)
|
|
| (((gpu_results[i * 4 + 3].limb1 as u64) & 0xFFFF) << 16)
|
|
| (((gpu_results[i * 4 + 3].limb2 as u64) & 0xFFFF) << 32)
|
|
| (((gpu_results[i * 4 + 3].limb3 as u64) & 0xFFFF) << 48),
|
|
),
|
|
];
|
|
|
|
if gpu_result == test_case.expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
let test_index = i + 1;
|
|
failed_tests.push(test_index);
|
|
failure_details.push((test_case.clone(), gpu_result));
|
|
if failure_details.len() <= 5 {
|
|
println!(
|
|
"❌ FAILED: Input=[{}, {}, {}, {}], Expected=[{}, {}, {}, {}], Got=[{}, {}, {}, {}]",
|
|
test_case.input[0].to_canonical_u64(),
|
|
test_case.input[1].to_canonical_u64(),
|
|
test_case.input[2].to_canonical_u64(),
|
|
test_case.input[3].to_canonical_u64(),
|
|
test_case.expected[0].to_canonical_u64(),
|
|
test_case.expected[1].to_canonical_u64(),
|
|
test_case.expected[2].to_canonical_u64(),
|
|
test_case.expected[3].to_canonical_u64(),
|
|
gpu_result[0].to_canonical_u64(),
|
|
gpu_result[1].to_canonical_u64(),
|
|
gpu_result[2].to_canonical_u64(),
|
|
gpu_result[3].to_canonical_u64(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("mds matrix".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_internal_linear_layer(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_internal_linear_layer_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
// Read the mining shader source and create a test wrapper
|
|
let mining_shader_source = include_str!("mining.wgsl");
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Test entry point for internal linear layer
|
|
@group(0) @binding(0) var<storage, read> input_state: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_state: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let offset = test_id * 12u;
|
|
|
|
// Copy input to local state
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = input_state[offset + i];
|
|
}}
|
|
|
|
// Apply internal linear layer
|
|
internal_linear_layer(&state);
|
|
|
|
// Write result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_state[offset + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
mining_shader_source
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Internal Linear Layer Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
// Create pipeline
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Internal Linear Layer Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 32;
|
|
for chunk in test_vectors.chunks(BATCH_SIZE) {
|
|
// Prepare input data (flatten 12-element arrays and convert to GfWgls)
|
|
let mut input_data = Vec::new();
|
|
for test_case in chunk {
|
|
for &field_elem in &test_case.input {
|
|
input_data.push(GfWgls::from(field_elem));
|
|
}
|
|
}
|
|
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (input_data.len() * std::mem::size_of::<GfWgls>()) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
device
|
|
.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
})
|
|
.unwrap();
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
let gpu_result = [
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12].limb0 as u64)
|
|
| ((results[i * 12].limb1 as u64) << 16)
|
|
| ((results[i * 12].limb2 as u64) << 32)
|
|
| ((results[i * 12].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 1].limb0 as u64)
|
|
| ((results[i * 12 + 1].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 1].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 1].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 2].limb0 as u64)
|
|
| ((results[i * 12 + 2].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 2].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 2].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 3].limb0 as u64)
|
|
| ((results[i * 12 + 3].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 3].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 3].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 4].limb0 as u64)
|
|
| ((results[i * 12 + 4].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 4].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 4].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 5].limb0 as u64)
|
|
| ((results[i * 12 + 5].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 5].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 5].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 6].limb0 as u64)
|
|
| ((results[i * 12 + 6].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 6].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 6].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 7].limb0 as u64)
|
|
| ((results[i * 12 + 7].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 7].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 7].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 8].limb0 as u64)
|
|
| ((results[i * 12 + 8].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 8].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 8].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 9].limb0 as u64)
|
|
| ((results[i * 12 + 9].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 9].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 9].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 10].limb0 as u64)
|
|
| ((results[i * 12 + 10].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 10].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 10].limb3 as u64) << 48),
|
|
),
|
|
GoldilocksField::from_noncanonical_u64(
|
|
(results[i * 12 + 11].limb0 as u64)
|
|
| ((results[i * 12 + 11].limb1 as u64) << 16)
|
|
| ((results[i * 12 + 11].limb2 as u64) << 32)
|
|
| ((results[i * 12 + 11].limb3 as u64) << 48),
|
|
),
|
|
];
|
|
|
|
// Compare all 12 elements
|
|
let mut all_match = true;
|
|
for j in 0..12 {
|
|
if gpu_result[j].to_canonical_u64() != test_case.expected[j].to_canonical_u64() {
|
|
all_match = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if all_match {
|
|
passed_tests += 1;
|
|
} else {
|
|
let test_index = i + 1;
|
|
failed_tests.push(test_index);
|
|
failure_details.push((test_case.clone(), gpu_result));
|
|
if failure_details.len() <= 3 {
|
|
println!("❌ FAILED: Internal linear layer test");
|
|
println!("Input state:");
|
|
for (j, &val) in test_case.input.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, val.to_canonical_u64());
|
|
}
|
|
println!("Expected state:");
|
|
for (j, &val) in test_case.expected.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, val.to_canonical_u64());
|
|
}
|
|
println!("GPU result:");
|
|
for (j, &val) in gpu_result.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, val.to_canonical_u64());
|
|
}
|
|
println!("Element-by-element comparison:");
|
|
for j in 0..12 {
|
|
let expected = test_case.expected[j].to_canonical_u64();
|
|
let got = gpu_result[j].to_canonical_u64();
|
|
let status = if expected == got { "✓" } else { "✗" };
|
|
println!(
|
|
" [{}] {} Expected: 0x{:016x}, Got: 0x{:016x}",
|
|
j, status, expected, got
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("internal linear layer".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_external_linear_layer(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_external_linear_layer_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
|
|
const BATCH_SIZE: usize = 20;
|
|
|
|
// Use the same pattern as other successful tests - include mining.wgsl and define our own bindings
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
// Simple test entry point for external linear layer
|
|
@group(0) @binding(0) var<storage, read> input_state: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_state: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let offset = test_id * 12u;
|
|
|
|
// Copy input to local state
|
|
var state: array<GoldilocksField, 12>;
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
state[i] = input_state[offset + i];
|
|
}}
|
|
|
|
// Apply external linear layer
|
|
external_linear_layer(&state);
|
|
|
|
// Write result
|
|
for (var i = 0u; i < 12u; i++) {{
|
|
output_state[offset + i] = state[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("External Linear Layer Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
// Create pipeline
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("External Linear Layer Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
for (batch_start, batch) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
let batch_size = batch.len();
|
|
|
|
// Prepare input data
|
|
let mut input_data = Vec::with_capacity(batch_size * 12);
|
|
for vector in batch {
|
|
for &element in &vector.input {
|
|
let gf_wgls: GfWgls = element.into();
|
|
input_data.push(gf_wgls);
|
|
}
|
|
}
|
|
|
|
// Create buffers
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("External Linear Layer Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("External Linear Layer Output Buffer"),
|
|
size: (batch_size * 12 * std::mem::size_of::<GfWgls>()) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let readback_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("External Linear Layer Readback Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
// Create bind group
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
label: Some("External Linear Layer Test Bind Group"),
|
|
});
|
|
|
|
// Execute
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("External Linear Layer Test Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("External Linear Layer Test Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
|
|
compute_pass.set_pipeline(&pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(batch_size as u32, 1, 1);
|
|
}
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &readback_buffer, 0, output_buffer.size());
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
// Read results
|
|
let buffer_slice = readback_buffer.slice(..);
|
|
let (sender, receiver) = futures::channel::oneshot::channel();
|
|
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
|
sender.send(result).unwrap();
|
|
});
|
|
device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
receiver.await.unwrap().unwrap();
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let gpu_results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Verify results
|
|
for (i, vector) in batch.iter().enumerate() {
|
|
let offset = i * 12;
|
|
let mut all_match = true;
|
|
|
|
for j in 0..12 {
|
|
let gpu_result: GoldilocksField = gpu_results[offset + j].into();
|
|
let expected = vector.expected[j];
|
|
if gpu_result != expected {
|
|
all_match = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if all_match {
|
|
passed_tests += 1;
|
|
} else {
|
|
let test_index = batch_start * BATCH_SIZE + i;
|
|
failed_tests.push(test_index);
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
readback_buffer.unmap();
|
|
}
|
|
|
|
// Print results summary
|
|
let mut results = TestResults::new("external linear layer".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_sbox(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_sbox_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
const BATCH_SIZE: usize = 64;
|
|
let batches = test_vectors.chunks(BATCH_SIZE);
|
|
|
|
for (_batch_idx, batch) in batches.enumerate() {
|
|
// Convert test vectors to WGSL format
|
|
let input_data: Vec<GfWgls> = batch
|
|
.iter()
|
|
.map(|test_case| GfWgls::from(test_case.input))
|
|
.collect();
|
|
|
|
// Create compute shader for S-box testing using the same pattern as other tests
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_data: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_data: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let index = global_id.x;
|
|
if (index >= {}u) {{
|
|
return;
|
|
}}
|
|
|
|
let input_val = input_data[index];
|
|
let result = sbox(input_val);
|
|
output_data[index] = result;
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl"),
|
|
batch.len()
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("S-box Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("S-box Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
// Create buffers
|
|
let input_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (batch.len() * std::mem::size_of::<GfWgls>()) as u64,
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: (batch.len() * std::mem::size_of::<GfWgls>()) as u64,
|
|
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
// Create bind group
|
|
let bind_group_layout = compute_pipeline.get_bind_group_layout(0);
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
// Execute compute shader
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Compute Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
compute_pass.set_pipeline(&compute_pipeline);
|
|
compute_pass.set_bind_group(0, &bind_group, &[]);
|
|
compute_pass.dispatch_workgroups(batch.len() as u32, 1, 1);
|
|
}
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, staging_buffer.size());
|
|
queue.submit(std::iter::once(encoder.finish()));
|
|
|
|
// Read results
|
|
let buffer_slice = staging_buffer.slice(..);
|
|
let (sender, receiver) = futures::channel::oneshot::channel();
|
|
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
|
sender.send(result).unwrap();
|
|
});
|
|
|
|
device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
receiver.await??;
|
|
|
|
let data = buffer_slice.get_mapped_range();
|
|
let results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results
|
|
for (i, (test_case, &gpu_result_raw)) in batch.iter().zip(results.iter()).enumerate() {
|
|
let gpu_result = GoldilocksField::from(gpu_result_raw);
|
|
|
|
if gpu_result == test_case.expected {
|
|
passed_tests += 1;
|
|
} else {
|
|
let test_index = i + 1;
|
|
failed_tests.push(test_index);
|
|
failure_details.push((test_case.clone(), gpu_result));
|
|
if failure_details.len() <= 10 {
|
|
println!(
|
|
"❌ FAILED: sbox(0x{:016x}) = 0x{:016x}, expected 0x{:016x}",
|
|
test_case.input.to_canonical_u64(),
|
|
gpu_result.to_canonical_u64(),
|
|
test_case.expected.to_canonical_u64()
|
|
);
|
|
|
|
// Debug intermediate calculations for failing cases
|
|
let x = test_case.input;
|
|
let x2_cpu = x * x;
|
|
let x4_cpu = x2_cpu * x2_cpu;
|
|
let x6_cpu = x4_cpu * x2_cpu;
|
|
let x7_cpu = x6_cpu * x;
|
|
|
|
println!(" CPU step-by-step:");
|
|
println!(" x = 0x{:016x}", x.to_canonical_u64());
|
|
println!(" x^2 = 0x{:016x}", x2_cpu.to_canonical_u64());
|
|
println!(" x^4 = 0x{:016x}", x4_cpu.to_canonical_u64());
|
|
println!(" x^6 = 0x{:016x}", x6_cpu.to_canonical_u64());
|
|
println!(" x^7 = 0x{:016x}", x7_cpu.to_canonical_u64());
|
|
|
|
// Check if GPU is returning input unchanged
|
|
if gpu_result == test_case.input {
|
|
println!(
|
|
" ⚠️ GPU returned input unchanged - possible multiplication bug"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("s-box".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_poseidon2_squeeze_twice(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_poseidon2_squeeze_twice_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_data: array<u32>;
|
|
@group(0) @binding(1) var<storage, read_write> output_hashes: array<u32>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let input_offset = test_id * 24u; // 24 u32s = 96 bytes per test
|
|
|
|
// Copy input data to array (exactly 96 bytes = 24 u32s)
|
|
var input_array: array<u32, 24>;
|
|
for (var i = 0u; i < 24u; i++) {{
|
|
input_array[i] = input_data[input_offset + i];
|
|
}}
|
|
|
|
// Hash the input (96 bytes -> 64 bytes)
|
|
let hash_result = poseidon2_hash_squeeze_twice(input_array);
|
|
|
|
// Write first 32 bytes (8 u32s) of result as the hash
|
|
let output_offset = test_id * 8u;
|
|
for (var i = 0u; i < 8u; i++) {{
|
|
output_hashes[output_offset + i] = hash_result[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Poseidon2 Squeeze Twice Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Poseidon2 Squeeze Twice Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 16;
|
|
for (batch_idx, chunk) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
// Prepare input data - all inputs are exactly 96 bytes
|
|
let mut input_data = Vec::new();
|
|
|
|
for test_case in chunk {
|
|
// Ensure input is exactly 96 bytes
|
|
assert_eq!(
|
|
test_case.input_bytes.len(),
|
|
96,
|
|
"All test inputs must be exactly 96 bytes"
|
|
);
|
|
|
|
// Convert bytes to u32s (96 bytes = 24 u32s)
|
|
for chunk_4 in test_case.input_bytes.chunks(4) {
|
|
let u32_val = u32::from_le_bytes([chunk_4[0], chunk_4[1], chunk_4[2], chunk_4[3]]);
|
|
input_data.push(u32_val);
|
|
}
|
|
}
|
|
|
|
let input_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Data Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (chunk.len() * 8 * 4) as u64, // 8 u32s per hash * 4 bytes per u32
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_data_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[u32] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results for the chunk
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
// Get GPU result (first 32 bytes = 8 u32s)
|
|
let gpu_hash_u32s = &results[i * 8..(i + 1) * 8];
|
|
let mut gpu_hash = [0u8; 32];
|
|
for (j, &u32_val) in gpu_hash_u32s.iter().enumerate() {
|
|
let bytes = u32_val.to_le_bytes();
|
|
gpu_hash[j * 4..(j + 1) * 4].copy_from_slice(&bytes);
|
|
}
|
|
|
|
if gpu_hash == test_case.expected_hash {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(batch_idx * BATCH_SIZE + i + 1);
|
|
failure_details.push((test_case.clone(), gpu_hash));
|
|
|
|
if failure_details.len() <= 3 {
|
|
println!("❌ FAILED: Poseidon2 squeeze-twice test (96 bytes input)");
|
|
println!(
|
|
"Input bytes (first 32): {:02x?}",
|
|
&test_case.input_bytes[..32]
|
|
);
|
|
println!("Expected hash: {:02x?}", &test_case.expected_hash[..]);
|
|
println!("GPU hash: {:02x?}", &gpu_hash[..]);
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("poseidon2 squeeze-twice".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_bytes_to_field_elements(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_bytes_to_field_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_data: array<u32>;
|
|
@group(0) @binding(1) var<storage, read_write> output_felts: array<GoldilocksField>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let input_offset = test_id * 24u; // 24 u32s = 96 bytes per test
|
|
|
|
// Copy input data to array (exactly 96 bytes = 24 u32s)
|
|
var input_array: array<u32, 24>;
|
|
for (var i = 0u; i < 24u; i++) {{
|
|
input_array[i] = input_data[input_offset + i];
|
|
}}
|
|
|
|
// Convert to field elements
|
|
let felts = bytes_to_field_elements(input_array);
|
|
|
|
// Write result (25 field elements per test)
|
|
let output_offset = test_id * 25u;
|
|
for (var i = 0u; i < 25u; i++) {{
|
|
output_felts[output_offset + i] = felts[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Bytes to Field Elements Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Bytes to Field Elements Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 8;
|
|
for (batch_idx, chunk) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
// Prepare input data
|
|
let mut input_data = Vec::new();
|
|
for test_case in chunk {
|
|
// Convert bytes to u32s (96 bytes = 24 u32s)
|
|
for chunk_4 in test_case.input_bytes.chunks(4) {
|
|
let u32_val = u32::from_le_bytes([chunk_4[0], chunk_4[1], chunk_4[2], chunk_4[3]]);
|
|
input_data.push(u32_val);
|
|
}
|
|
}
|
|
|
|
let input_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Data Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (chunk.len() * 25 * std::mem::size_of::<GfWgls>()) as u64, // 25 felts per test
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_data_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[GfWgls] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results for the chunk
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
let mut all_match = true;
|
|
let mut gpu_felts = Vec::new();
|
|
|
|
// Convert GPU results back to GoldilocksField
|
|
for j in 0..25 {
|
|
let gpu_felt: GoldilocksField = results[i * 25 + j].into();
|
|
gpu_felts.push(gpu_felt);
|
|
|
|
if j < test_case.expected_felts.len() {
|
|
if gpu_felt.to_canonical_u64() != test_case.expected_felts[j].to_canonical_u64()
|
|
{
|
|
all_match = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if all_match && gpu_felts.len() == test_case.expected_felts.len() {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(batch_idx * BATCH_SIZE + i + 1);
|
|
failure_details.push((test_case.clone(), gpu_felts.clone()));
|
|
|
|
if failure_details.len() <= 3 {
|
|
println!("❌ FAILED: Bytes to field elements test");
|
|
println!(
|
|
"Input bytes (first 16): {:02x?}",
|
|
&test_case.input_bytes[..16]
|
|
);
|
|
println!("Expected {} elements:", test_case.expected_felts.len());
|
|
for (j, &expected) in test_case.expected_felts.iter().enumerate().take(5) {
|
|
println!(" [{}] = 0x{:016x}", j, expected.to_canonical_u64());
|
|
}
|
|
println!("GPU produced {} elements:", gpu_felts.len());
|
|
for (j, &gpu) in gpu_felts.iter().enumerate().take(5) {
|
|
println!(" [{}] = 0x{:016x}", j, gpu.to_canonical_u64());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("bytes to field elements".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_field_elements_to_bytes(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_field_to_bytes_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_felts: array<GoldilocksField>;
|
|
@group(0) @binding(1) var<storage, read_write> output_bytes: array<u32>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let input_offset = test_id * 4u; // 4 field elements per test
|
|
|
|
// Copy input field elements
|
|
var felts: array<GoldilocksField, 4>;
|
|
for (var i = 0u; i < 4u; i++) {{
|
|
felts[i] = input_felts[input_offset + i];
|
|
}}
|
|
|
|
// Convert to bytes
|
|
let bytes_result = field_elements_to_bytes(felts);
|
|
|
|
// Write result (8 u32s = 32 bytes per test)
|
|
let output_offset = test_id * 8u;
|
|
for (var i = 0u; i < 8u; i++) {{
|
|
output_bytes[output_offset + i] = bytes_result[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Field Elements to Bytes Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Field Elements to Bytes Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 8;
|
|
for (batch_idx, chunk) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
// Prepare input data
|
|
let mut input_data = Vec::new();
|
|
for test_case in chunk {
|
|
for &felt in &test_case.input_felts {
|
|
input_data.push(GfWgls::from(felt));
|
|
}
|
|
}
|
|
|
|
let input_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Data Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (chunk.len() * 8 * 4) as u64, // 8 u32s per test * 4 bytes per u32
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_data_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[u32] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results for the chunk
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
// Get GPU result (8 u32s = 32 bytes)
|
|
let gpu_bytes_u32s = &results[i * 8..(i + 1) * 8];
|
|
let mut gpu_bytes = [0u8; 32];
|
|
for (j, &u32_val) in gpu_bytes_u32s.iter().enumerate() {
|
|
let bytes = u32_val.to_le_bytes();
|
|
gpu_bytes[j * 4..(j + 1) * 4].copy_from_slice(&bytes);
|
|
}
|
|
|
|
if gpu_bytes == test_case.expected_bytes {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(batch_idx * BATCH_SIZE + i + 1);
|
|
failure_details.push((test_case.clone(), gpu_bytes));
|
|
|
|
if failure_details.len() <= 3 {
|
|
println!("❌ FAILED: Field elements to bytes test");
|
|
println!("Input felts:");
|
|
for (j, &felt) in test_case.input_felts.iter().enumerate() {
|
|
println!(" [{}] = 0x{:016x}", j, felt.to_canonical_u64());
|
|
}
|
|
println!("Expected bytes: {:02x?}", &test_case.expected_bytes[..]);
|
|
println!("GPU bytes: {:02x?}", &gpu_bytes[..]);
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("field elements to bytes".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|
|
|
|
pub async fn test_double_hash(
|
|
device: &wgpu::Device,
|
|
queue: &wgpu::Queue,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let test_vectors = generate_double_hash_test_vectors();
|
|
let total_tests = test_vectors.len();
|
|
let mut passed_tests = 0;
|
|
let mut failed_tests = Vec::new();
|
|
let mut failure_details = Vec::new();
|
|
|
|
let shader_source = format!(
|
|
"
|
|
{}
|
|
|
|
@group(0) @binding(0) var<storage, read> input_data: array<u32>;
|
|
@group(0) @binding(1) var<storage, read_write> output_hashes: array<u32>;
|
|
|
|
@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {{
|
|
let test_id = global_id.x;
|
|
let input_offset = test_id * 24u; // 24 u32s = 96 bytes per test
|
|
|
|
// Copy input data to array (exactly 96 bytes = 24 u32s)
|
|
var input_array: array<u32, 24>;
|
|
for (var i = 0u; i < 24u; i++) {{
|
|
input_array[i] = input_data[input_offset + i];
|
|
}}
|
|
|
|
// Hash the input using double hash (96 bytes -> 64 bytes)
|
|
let hash_result = double_hash(input_array);
|
|
|
|
// Write full 64 bytes (16 u32s) of result as the hash
|
|
let output_offset = test_id * 16u;
|
|
for (var i = 0u; i < 16u; i++) {{
|
|
output_hashes[output_offset + i] = hash_result[i];
|
|
}}
|
|
}}
|
|
",
|
|
include_str!("mining.wgsl")
|
|
);
|
|
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("Double Hash Test Shader"),
|
|
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
label: Some("Double Hash Test Pipeline"),
|
|
layout: None,
|
|
module: &shader,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
|
|
|
// Process tests in batches
|
|
const BATCH_SIZE: usize = 8;
|
|
for (batch_idx, chunk) in test_vectors.chunks(BATCH_SIZE).enumerate() {
|
|
// Prepare input data - all inputs are exactly 96 bytes
|
|
let mut input_data = Vec::new();
|
|
|
|
for test_case in chunk {
|
|
// Convert bytes to u32s (96 bytes = 24 u32s)
|
|
for chunk_4 in test_case.input_96_bytes.chunks(4) {
|
|
let u32_val = u32::from_le_bytes([chunk_4[0], chunk_4[1], chunk_4[2], chunk_4[3]]);
|
|
input_data.push(u32_val);
|
|
}
|
|
}
|
|
|
|
let input_data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
|
label: Some("Input Data Buffer"),
|
|
contents: bytemuck::cast_slice(&input_data),
|
|
usage: wgpu::BufferUsages::STORAGE,
|
|
});
|
|
|
|
let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Output Buffer"),
|
|
size: (chunk.len() * 16 * 4) as u64, // 16 u32s per hash * 4 bytes per u32
|
|
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("Test Bind Group"),
|
|
layout: &bind_group_layout,
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: input_data_buffer.as_entire_binding(),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: output_buffer.as_entire_binding(),
|
|
},
|
|
],
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("Command Encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
label: Some("Compute Pass"),
|
|
timestamp_writes: None,
|
|
});
|
|
pass.set_pipeline(&pipeline);
|
|
pass.set_bind_group(0, &bind_group, &[]);
|
|
pass.dispatch_workgroups(chunk.len() as u32, 1, 1);
|
|
}
|
|
|
|
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("Staging Buffer"),
|
|
size: output_buffer.size(),
|
|
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
|
|
encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_buffer.size());
|
|
queue.submit(Some(encoder.finish()));
|
|
|
|
let slice = staging_buffer.slice(..);
|
|
slice.map_async(wgpu::MapMode::Read, |_| ());
|
|
let _ = device.poll(wgpu::PollType::Wait {
|
|
submission_index: None,
|
|
timeout: None,
|
|
});
|
|
|
|
let data = slice.get_mapped_range();
|
|
let results: &[u32] = bytemuck::cast_slice(&data);
|
|
|
|
// Check results for the chunk
|
|
for (i, test_case) in chunk.iter().enumerate() {
|
|
// Get GPU result (full 64 bytes = 16 u32s)
|
|
let gpu_hash_u32s = &results[i * 16..(i + 1) * 16];
|
|
let mut gpu_hash = [0u8; 64];
|
|
for (j, &u32_val) in gpu_hash_u32s.iter().enumerate() {
|
|
let bytes = u32_val.to_le_bytes();
|
|
gpu_hash[j * 4..(j + 1) * 4].copy_from_slice(&bytes);
|
|
}
|
|
|
|
if gpu_hash == test_case.expected_hash {
|
|
passed_tests += 1;
|
|
} else {
|
|
failed_tests.push(batch_idx * BATCH_SIZE + i + 1);
|
|
failure_details.push((test_case.clone(), gpu_hash));
|
|
|
|
if failure_details.len() <= 3 {
|
|
println!("❌ FAILED: Double hash test ({})", test_case.description);
|
|
println!(
|
|
"Input (first 16 bytes): {:02x?}",
|
|
&test_case.input_96_bytes[..16]
|
|
);
|
|
println!("Expected hash: {:02x?}", &test_case.expected_hash[..]);
|
|
println!("GPU hash: {:02x?}", &gpu_hash[..]);
|
|
|
|
// Show first difference
|
|
for k in 0..64 {
|
|
if gpu_hash[k] != test_case.expected_hash[k] {
|
|
println!(
|
|
"First difference at byte {}: Expected={:02x}, GPU={:02x}",
|
|
k, test_case.expected_hash[k], gpu_hash[k]
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(data);
|
|
staging_buffer.unmap();
|
|
}
|
|
|
|
let mut results = TestResults::new("double hash".to_string(), total_tests);
|
|
results.passed = passed_tests;
|
|
results.failed = failed_tests;
|
|
results.print_summary()
|
|
}
|