Skip to content

feat: add Poseidon transcript#1173

Open
akoidefi wants to merge 6 commits intoa16z:mainfrom
defi-wonderland:feat/poseidon-hash
Open

feat: add Poseidon transcript#1173
akoidefi wants to merge 6 commits intoa16z:mainfrom
defi-wonderland:feat/poseidon-hash

Conversation

@akoidefi
Copy link

Adds PoseidonTranscript using light_poseidon over BN254.

Uses width-3 Poseidon to include n_rounds in every hash call for domain separation, same as Blake2b/Keccak. Chunks large inputs into 32-byte pieces since Poseidon has fixed-width inputs.

Gated behind transcript-poseidon feature flag.

@akoidefi akoidefi marked this pull request as draft December 18, 2025 14:21
@akoidefi akoidefi marked this pull request as ready for review December 18, 2025 14:24
@markosg04 markosg04 self-requested a review December 18, 2025 15:07
Copy link
Collaborator

@markosg04 markosg04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent job! Just have one question about the usage of Fr.

Comment on lines 47 to 49
fn hasher() -> Poseidon<Fr> {
Poseidon::<Fr>::new_circom(POSEIDON_WIDTH).expect("Failed to initialize Poseidon")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

light-poseidon seems to rely on trait PrimeField from arkworks.

I think it is fine to couple to arkworks for now, especially if this is the best poseidon lib. Ideal world is that this would be generic over F: JoltField, which I don't think we can achieve. Maybe if it was over generic like F: PrimeField then it's easier to swap to Fq or BLS fields later.

Do you foresee any problems with fixing Fr in a snark composition context? In snark composition, sum-checks are over Fq (BN254 base field), and in this implementation we interpret all bytes as arkworks Fr. My hunch is that it's fine, but I wonder if there could be some weird attacks since Fq modulus is larger than Fr (a tiny number of collisions). Probably this fact is not really exploitable, but maybe worth a bit of thought.

if we make generic over F: PrimeField we could probably avoid this by just using Fq when the sum-check is working with Fq.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch. I'll make it generic on F: PrimeField.

@akoidefi akoidefi force-pushed the feat/poseidon-hash branch 2 times, most recently from b46cfbb to 34e0e30 Compare January 8, 2026 00:53
@0xParti 0xParti force-pushed the feat/poseidon-hash branch from 854b65f to 6338e06 Compare February 2, 2026 14:27
Comment on lines 195 to 201
self.n_rounds as usize <= expected_state_history.len(),
"Fiat-Shamir transcript mismatch: n_rounds {} exceeds expected history length {}",
self.n_rounds,
expected_state_history.len()
);
assert!(
new_state == expected_state_history[self.n_rounds as usize],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-by-one error causing array index out of bounds panic in test mode. After incrementing n_rounds at line 190, the code checks self.n_rounds as usize <= expected_state_history.len() at line 195, then accesses expected_state_history[self.n_rounds as usize] at line 201. When n_rounds equals len(), this accesses an invalid index (arrays are 0-indexed, so valid indices are 0 to len-1).

// Fix: Change <= to <
assert!(
    self.n_rounds as usize < expected_state_history.len(),
    "Fiat-Shamir transcript mismatch: n_rounds {} exceeds expected history length {}",
    self.n_rounds,
    expected_state_history.len()
);
assert!(
    new_state == expected_state_history[self.n_rounds as usize - 1],
    "Fiat-Shamir transcript mismatch at round {}",
    self.n_rounds
);

Or alternatively access at [self.n_rounds as usize - 1] since n_rounds was just incremented.

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

akoidefi and others added 6 commits February 3, 2026 12:59
Adds PoseidonTranscript using light_poseidon over BN254.

Uses width-3 Poseidon to include n_rounds in every hash call for domain
separation, same as Blake2b/Keccak. Chunks large inputs into 32-byte
pieces since Poseidon has fixed-width inputs.

Gated behind transcript-poseidon feature flag.
- Add PoseidonParams<F> trait for type-level parameter configuration
- Implement FrParams (uses new_circom) and FqParams (generated params)
- Add poseidon_fq_params.rs with BN254 Fq parameters (128-bit security)
- Create type aliases PoseidonTranscriptFr and PoseidonTranscriptFq
- Fq params generated with poseidon-paramgen v0.4.0 (audited by NCC Group)

This enables SNARK composition where sumchecks operate over Fq.
Integrates the poseidon-paramgen library (arkworks 0.5 compatible fork) to enable
verification that hardcoded BN254 Fq Poseidon parameters match the audited output
from poseidon-paramgen v0.4.0 (NCC Group, Summer 2022).

This adds transparency and verifiability to the Fq parameters needed for SNARK
recursion, where the verifier operates in the base field rather than the scalar field.

Changes:
- Add workspace deps for defi-wonderland/poseidon377 fork (arkworks-0.5 branch)
- Add poseidon-paramgen and poseidon-parameters as optional deps in jolt-core
- Create poseidon_param_gen.rs with generate_fq_params() and verification tests
- Update poseidon_fq_params.rs header with verification instructions

Tests verify:
- Round counts (8 full, 56 partial)
- Alpha/S-box exponent (5)
- MDS matrix (4x4, 16 elements)
- Round constants (256 values)
- Integration with light-poseidon hasher
- Transcript determinism
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants