Skip to content

Commit 6e3d111

Browse files
committed
Poseidon - enforce input length equal T - 1
1 parent 2959369 commit 6e3d111

File tree

6 files changed

+31
-54
lines changed

6 files changed

+31
-54
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ let hash2 = sponge.compute_hash(&inputs2);
8282

8383
## Limitations / Future Work
8484

85-
1. **Multi-round absorption**: Currently, inputs must fit within a single rate (i.e., `inputs.len() <= T - 1`). Future versions will support absorbing inputs larger than the state size across multiple permutation rounds.
85+
1. **Multi-round absorption**: Currently, for Poseidon, inputs must exactly fill the rate (i.e., `inputs.len() == T - 1`), matching circom's behavior where `nInputs` determines `T = nInputs + 1`. Poseidon2 requires inputs to fit within a single rate (i.e., `inputs.len() <= T - 1`). Future versions will support absorbing inputs larger than the state size across multiple permutation rounds.
8686

8787
2. **Persistent parameters**: Make `PoseidonParams` / `Poseidon2Params` a `#[contracttype]` so they can be stored as contract data and reduce the contract size.
8888

src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl Field for BlsScalar {
6262
///
6363
/// # Type Parameters
6464
///
65-
/// - `T`: State size. Must be ≥ `inputs.len() + 1` (rate = T-1, capacity = 1).
65+
/// - `T`: State size. Must equal `inputs.len() + 1` (rate = T-1, capacity = 1).
6666
/// - `F`: Field type. Use [`BnScalar`] for BN254 or [`BlsScalar`] for BLS12-381.
6767
///
6868
/// # Supported Configurations
@@ -72,8 +72,7 @@ impl Field for BlsScalar {
7272
///
7373
/// # Panics
7474
///
75-
/// - if `inputs.is_empty()`
76-
/// - if `inputs.len() > T - 1` (rate exceeded)
75+
/// - if `inputs.len() != T - 1`
7776
/// - if any input value ≥ the field modulus (inputs must be valid field elements)
7877
///
7978
/// # Example

src/poseidon/sponge.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ where
217217
}
218218

219219
pub(crate) fn absorb(&mut self, inputs: &Vec<U256>) {
220-
assert!(inputs.len() <= Self::RATE);
220+
assert!(
221+
inputs.len() == Self::RATE,
222+
"Poseidon: inputs.len() must equal rate (T - 1)"
223+
);
221224
let modulus = F::modulus(&self.env);
222225
for i in 0..inputs.len() {
223226
let v = inputs.get_unchecked(i);
@@ -243,15 +246,13 @@ where
243246
/// implementation](https://github.com/iden3/circomlib/blob/master/circuits/poseidon.circom).
244247
///
245248
/// # Panics
246-
/// - if `inputs.is_empty()`. Empty inputs are not allowed because
247-
/// `hash([])` would collide with `hash([0])`. Circom also disallows empty
248-
/// inputs.
249-
/// - if `inputs.len() > RATE` (i.e., `T - 1`). For larger inputs,
250-
/// multi-round absorption would be needed (not yet implemented).
249+
/// - if `inputs.len() != RATE` (i.e., must equal `T - 1` exactly).
250+
/// This matches circom's Poseidon where `nInputs` determines
251+
/// `T = nInputs + 1`, ensuring the rate is always fully used and
252+
/// preventing suffix-zero collisions from implicit zero-padding.
251253
/// - if any input value is greater than or equal to the field modulus.
252254
/// All inputs must be valid field elements (i.e., less than the modulus).
253255
pub fn compute_hash(&mut self, inputs: &Vec<U256>) -> U256 {
254-
assert!(!inputs.is_empty(), "Poseidon: inputs cannot be empty");
255256
self.reset_state();
256257
self.absorb(inputs);
257258
self.squeeze()

src/poseidon2/sponge.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,13 @@ where
174174
}
175175

176176
pub(crate) fn absorb(&mut self, inputs: &Vec<U256>) {
177-
// Absorb into rate portion of state (positions 0..RATE)
178-
assert!(inputs.len() <= Self::RATE);
177+
// <= is safe here because IV = input_len << 64 provides domain
178+
// separation for different-length inputs. This differs from Poseidon V1
179+
// (which uses IV=0 and therefore requires == RATE).
180+
assert!(
181+
inputs.len() <= Self::RATE,
182+
"Poseidon2: inputs.len() must not exceed rate (T - 1)"
183+
);
179184
let modulus = F::modulus(&self.env);
180185
for i in 0..inputs.len() {
181186
let v = inputs.get_unchecked(i);

src/tests/poseidon.rs

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -711,53 +711,31 @@ fn test_poseidon_sponge_matches_hash_function() {
711711
}
712712

713713
// ============================================================================
714-
// Partial rate tests (inputs.len() < RATE)
714+
// Partial rate rejection tests
715715
// ============================================================================
716716

717-
// Note: Circom's Poseidon always uses T = inputs.len() + 1 (full rate), so there are
718-
// no reference test vectors for partial rate scenarios. These tests verify that:
719-
// 1. Our implementation handles partial rate correctly (zero-padding)
720-
// 2. Results are deterministic
721-
// 3. Different T values produce different results (as expected)
717+
// Poseidon V1 requires inputs.len() == RATE (full rate), matching circom's
718+
// behavior where nInputs determines T = nInputs + 1. Partial-rate inputs are
719+
// rejected to prevent suffix-zero collisions from implicit zero-padding.
722720

723-
// Test hashing 1 input with T=3 (rate=2) - partial rate usage
724-
// This verifies zero-padding works correctly when inputs don't fill the rate
725721
#[test]
726-
fn test_poseidon_bn254_partial_rate_t3_1_input() {
722+
#[should_panic(expected = "inputs.len() must equal rate")]
723+
fn test_poseidon_bn254_rejects_partial_rate() {
727724
let env = Env::default();
728725

729-
// 1 input with T=3 (rate=2) - only half the rate is used
730-
let inputs = vec![
731-
&env,
732-
U256::from_be_bytes(
733-
&env,
734-
&bytesn!(
735-
&env,
736-
0x0000000000000000000000000000000000000000000000000000000000000001
737-
)
738-
.into(),
739-
),
740-
];
726+
// 1 input with T=3 (rate=2) - partial rate must be rejected
727+
let inputs = vec![&env, U256::from_u32(&env, 1)];
741728

742729
let mut sponge = PoseidonSponge::<3, BnScalar>::new(&env);
743-
let result = sponge.compute_hash(&inputs);
744-
745-
// Result should be deterministic
746-
let result2 = sponge.compute_hash(&inputs);
747-
assert_eq!(result, result2);
748-
749-
// Verify it's different from using T=2 (full rate with 1 input)
750-
let mut sponge_t2 = PoseidonSponge::<2, BnScalar>::new(&env);
751-
let result_t2 = sponge_t2.compute_hash(&inputs);
752-
assert_ne!(result, result_t2);
730+
sponge.compute_hash(&inputs); // should panic
753731
}
754732

755733
// ============================================================================
756734
// Failure mode tests
757735
// ============================================================================
758736

759737
#[test]
760-
#[should_panic(expected = "assertion failed")]
738+
#[should_panic(expected = "inputs.len() must equal rate")]
761739
fn test_poseidon_sponge_inputs_exceed_rate() {
762740
let env = Env::default();
763741

@@ -896,15 +874,9 @@ fn test_poseidon_bls12_381_input_below_modulus_accepted() {
896874
let _ = sponge.compute_hash(&inputs);
897875
}
898876

899-
// Empty inputs are explicitly rejected in Poseidon because:
900-
// 1. Circom rejects them
901-
// 2. With IV=0, hash([]) would collide with hash([0]) since both result in
902-
// permuting state [0, 0, ...]
903-
//
904-
// This differs from Poseidon2, which uses IV = `input_len << 64`, making
905-
// hash([]) and hash([0]) produce different outputs.
877+
// Empty inputs are rejected because inputs.len() must equal RATE (>= 1).
906878
#[test]
907-
#[should_panic(expected = "Poseidon: inputs cannot be empty")]
879+
#[should_panic(expected = "inputs.len() must equal rate")]
908880
fn test_poseidon_bn254_empty_inputs_rejected() {
909881
let env = Env::default();
910882

src/tests/poseidon2.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ fn test_poseidon2_bn254_partial_rate_t4_2_inputs() {
852852
// ============================================================================
853853

854854
#[test]
855-
#[should_panic(expected = "assertion failed")]
855+
#[should_panic(expected = "Poseidon2: inputs.len() must not exceed rate (T - 1)")]
856856
fn test_poseidon2_sponge_inputs_exceed_rate_t4() {
857857
let env = Env::default();
858858

0 commit comments

Comments
 (0)