Skip to content

Commit 567debb

Browse files
Poseidon2 - add sponge function and padding support (#743)
Co-authored-by: LeonHibnik <[email protected]>
1 parent 33d586e commit 567debb

File tree

6 files changed

+592
-148
lines changed

6 files changed

+592
-148
lines changed

docs/docs/icicle/primitives/hash.md

+15-15
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,27 @@ The optional `domain_tag` pointer parameter enables domain separation, allowing
5858
### Poseidon2
5959

6060
[Poseidon2](https://eprint.iacr.org/2023/323.pdf) is a cryptographic hash function designed specifically for field elements.
61-
It is an improved version of the original [Poseidon](https://eprint.iacr.org/2019/458) hash, offering better performance on modern hardware. Poseidon2 is optimized for use with elliptic curve cryptography and finite fields, making it ideal for decentralized systems like blockchain. Its main advantage is balancing strong security with efficient computation, which is crucial for applications that require fast, reliable hashing.
61+
It is an improved version of the original [Poseidon](https://eprint.iacr.org/2019/458) hash, offering better performance on modern hardware.
62+
Poseidon2 is optimized for use with elliptic curve cryptography and finite fields, making it ideal for decentralized systems like blockchain.
63+
Its main advantage is balancing strong security with efficient computation, which is crucial for applications that require fast, reliable hashing.
6264

6365
The optional `domain_tag` pointer parameter enables domain separation, allowing isolation of hash outputs across different contexts or applications.
6466

65-
:::info
67+
The supported values of state size ***t*** as defined in [eprint 2023/323](https://eprint.iacr.org/2023/323.pdf) are 2, 3, 4, 8, 12, 16, 20 and 24.
68+
Note that ***t*** sizes 8, 12, 16, 20 and 24 are supported only for small fields (babybear and m31).
6669

67-
The supported values of state size ***t*** as defined in [eprint 2023/323](https://eprint.iacr.org/2023/323.pdf) are 2, 3, 4, 8, 12, 16, 20 and 24. Note that ***t*** sizes 8, 12, 16, 20 and 24 are supported only for small fields (babybear and m31).
70+
The S box power alpha, number of full rounds and partial rounds, rounds constants, MDS matrix, and partial matrix for each field and ***t*** can be
71+
found in this [folder](https://github.com/ingonyama-zk/icicle/tree/9b1506cda9eab30fc6a8d0a338e2cfab877402f7/icicle/include/icicle/hash/poseidon2_constants/constants).
6872

69-
:::
73+
There are two modes for using the Poseidon2 hash - sponge function and non-sponge (merkle tree) function. The key difference between these
74+
modes is their execution pattern. The sponge function is inherently serial (each hash must wait for the previous hash to complete before
75+
starting its own process), while the non-sponge function (which consists of multiple independent hashes that don't share inputs) runs in
76+
parallel using GPU threads, with the number of threads equal to config.batch.
77+
Another difference between two modes is that currently padding is supported for the sponge function and is not supported for the non-sponge.
78+
For the sponge function the config.batch should be equal one.
7079

71-
:::info
72-
73-
The S box power alpha, number of full rounds and partial rounds, rounds constants, MDS matrix, and partial matrix for each field and ***t*** can be found in this [folder](https://github.com/ingonyama-zk/icicle/tree/9b1506cda9eab30fc6a8d0a338e2cfab877402f7/icicle/include/icicle/hash/poseidon2_constants/constants).
74-
75-
:::
76-
77-
In the current version the padding is not supported and should be performed by the user.
80+
The hash function automatically chooses between these modes based on the input size. It runs in sponge mode if the input size (including the
81+
domain_tag if present) is greater than the single hash width (in this case, config.batch should be set to one). Otherwise, it uses the non-sponge mode.
7882

7983
## Using Hash API
8084

@@ -177,10 +181,6 @@ eIcicleErr err = keccak256.hash(input.data(), input.size() / config.batch, confi
177181

178182
Currently the poseidon sponge mode (sponge function description could be found in Sec 2.1 of [eprint 2019/458](https://eprint.iacr.org/2019/458.pdf)) isn't implemented.
179183

180-
### 5. Poseidon2 sponge function
181-
182-
Currently the poseidon2 is implemented in compression mode, the sponge mode discussed in [eprint 2023/323](https://eprint.iacr.org/2023/323.pdf) is not implemented.
183-
184184
### Supported Bindings
185185

186186
- [Rust](../rust-bindings/hash)

docs/versioned_docs/version-2.8.0/icicle/primitives/msm.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ The MSM supports batch mode - running multiple MSMs in parallel. It's always bet
7979
struct MSMConfig {
8080
device_context::DeviceContext ctx; /**< Details related to the device such as its id and stream id. */
8181
int points_size; /**< Number of points in the MSM. If a batch of MSMs needs to be computed, this should be
82-
* a number of different points. So, if each MSM re-uses the same set of points, this
82+
* a number of different points. So, if each MSM reuses the same set of points, this
8383
* variable is set equal to the MSM size. And if every MSM uses a distinct set of
8484
* points, it should be set to the product of MSM size and [batch_size](@ref
8585
* batch_size). Default value: 0 (meaning it's equal to the MSM size). */

examples/c++/polynomial-multiplication/example.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ void incremental_values(scalar_t* res, uint32_t count)
2323
}
2424
}
2525

26-
// calcaulting polynomial multiplication A*B via NTT,pointwise-multiplication and INTT
26+
// calculating polynomial multiplication A*B via NTT,pointwise-multiplication and INTT
2727
// (1) allocate A,B on HOST. Randomize first half, zero second half
2828
// (2) allocate A,B,Res on device
2929
// (3) calc NTT for A and for B from host to device
@@ -99,4 +99,4 @@ int main(int argc, char** argv)
9999
ntt_release_domain<scalar_t>();
100100

101101
return 0;
102-
}
102+
}

icicle/backend/cpu/src/hash/cpu_poseidon2.cpp

+172-50
Original file line numberDiff line numberDiff line change
@@ -196,33 +196,176 @@ namespace icicle {
196196

197197
// For merkle tree size should be equal to the arity of a single hasher multiplier by sizeof(S).
198198
// For sponge function it could be any number.
199+
// Size parameter here is in bytes.
199200
eIcicleError hash(const std::byte* input, uint64_t size, const HashConfig& config, std::byte* output) const override
200201
{
201-
unsigned int arity = m_use_domain_tag ? m_t - 1 : m_t;
202+
const unsigned arity = m_use_domain_tag ? m_t - 1 : m_t;
203+
bool is_sponge = false;
204+
int input_size_in_scalars = size / sizeof(S);
205+
if ((config.batch == 1) && (input_size_in_scalars != (m_use_domain_tag ? m_t - 1 : m_t))) { // Check if sponge
206+
// function.
207+
is_sponge = true;
208+
if (config.batch != 1) {
209+
ICICLE_LOG_ERROR << "The only supported value of config.batch for sponge functions is 1.\n";
210+
return eIcicleError::INVALID_ARGUMENT;
211+
}
212+
} // sponge function
213+
else { // Non-sponge function.
214+
if ((m_use_domain_tag ? input_size_in_scalars : input_size_in_scalars - 1) % (m_t - 1) != 0) {
215+
ICICLE_LOG_ERROR << "Padding isn't supported for non-sponge function hash. The following should be true: "
216+
"((m_use_domain_tag ? size : size-1) % (m_t-1) != 0).\n";
217+
return eIcicleError::INVALID_ARGUMENT;
218+
}
219+
} // Non-sponge function.
202220

203-
// Currently sponge and padding functionalities are not supported.
204-
if (size != arity * sizeof(S)) {
205-
ICICLE_LOG_ERROR
206-
<< "Sponge function still isn't supported. The following should be true: (size == T) but it is not.\n";
207-
return eIcicleError::INVALID_ARGUMENT;
221+
const unsigned int T = m_t;
222+
bool is_unsupported_T_for_this_field = poseidon2_constants[T].nof_upper_full_rounds == 0;
223+
if (is_unsupported_T_for_this_field) {
224+
ICICLE_LOG_ERROR << "Unsupported poseidon width (t = " << T << ") for this field! Planned for next version";
225+
return eIcicleError::API_NOT_IMPLEMENTED;
208226
}
209-
// Call hash_single config.batch times.
210-
for (int batch_hash_idx = 0; batch_hash_idx < config.batch; batch_hash_idx++) {
211-
eIcicleError err = hash_single(input, output);
227+
228+
int alpha = poseidon2_constants[T].alpha;
229+
int nof_upper_full_rounds = poseidon2_constants[T].nof_upper_full_rounds;
230+
int nof_partial_rounds = poseidon2_constants[T].nof_partial_rounds;
231+
int nof_bottom_full_rounds = poseidon2_constants[T].nof_bottom_full_rounds;
232+
S* rounds_constants = poseidon2_constants[T].rounds_constants;
233+
S* mds_matrix = poseidon2_constants[T].mds_matrix;
234+
S* partial_matrix_diagonal_m1 = poseidon2_constants[T].partial_matrix_diagonal_m1;
235+
236+
// Allocate temporary memory for intermediate calcs and in order not to change the input.
237+
// int sponge_nof_hashers = m_use_domain_tag ? (input_size_in_scalars / arity) : ((input_size_in_scalars - 1) /
238+
// (arity - 1)); int tmp_fields_nof_scalars = is_sponge ? (T * sponge_nof_hashers) : (T * config.batch); S*
239+
// tmp_fields = new S[tmp_fields_nof_scalars];
240+
S* tmp_fields;
241+
S* tmp_fields_init_ptr; // This pointer to keep initial tmp_fields value to perform a easy rollback when needed.
242+
int sponge_nof_hashers;
243+
const S* in_fields = (S*)(input);
244+
int padding_size = 0;
245+
S* padding;
246+
if (is_sponge) {
247+
if (input_size_in_scalars < T) { // Single hasher in the chain.
248+
sponge_nof_hashers = 1;
249+
padding_size = T - (input_size_in_scalars + (m_use_domain_tag == true));
250+
} else if (input_size_in_scalars >= T) { // More than a single hasher in the chain.
251+
sponge_nof_hashers = (input_size_in_scalars - !(m_use_domain_tag == true) + (T - 2)) / (T - 1);
252+
bool is_padding_needed = (input_size_in_scalars - !(m_use_domain_tag == true)) % (T - 1);
253+
if (is_padding_needed) {
254+
padding_size = (T - 1) - ((input_size_in_scalars - !(m_use_domain_tag == true)) % (T - 1));
255+
}
256+
}
257+
if (padding_size > 0) { // Fill padding array with 1,0,0,...
258+
padding = new S[padding_size];
259+
padding[0] = S::from(1);
260+
for (int i = 1; i < padding_size; i++) {
261+
padding[i] = S::from(0);
262+
}
263+
}
264+
tmp_fields = new S[T * sponge_nof_hashers];
265+
tmp_fields_init_ptr = tmp_fields;
266+
// Take care of hasher 0. It's done separately of the rest of the hashers because of the domain tag.
267+
if (m_use_domain_tag) {
268+
// Domain tag exists only for the first hasher. For the rest of the hashers this
269+
// input is undefined at this stage and its value will be set later.
270+
// tmp_fields = {{dt, in0}, {undef, in1}, {undef, in2}, etc.}
271+
memcpy(tmp_fields, &m_domain_tag, sizeof(S));
272+
} else {
273+
// tmp_fields = {{in0 (T inputs)}, {undef, in1 (T-1 inputs)}, {under, in2 (T-1 inputs)}, etc.}
274+
memcpy(tmp_fields, &in_fields[0], sizeof(S));
275+
in_fields += 1;
276+
}
277+
tmp_fields += 1;
278+
// Take care of rest of the hashers (T-1 scalar to each hasher).
279+
for (int hasher_idx = 0; hasher_idx < sponge_nof_hashers; hasher_idx++) {
280+
if (hasher_idx == sponge_nof_hashers - 1 && padding_size > 0) {
281+
// Last hasher in the chain. Take care of padding if needed.
282+
memcpy(tmp_fields, in_fields, (T - padding_size - 1) * sizeof(S));
283+
memcpy(tmp_fields + T - padding_size - 1, padding, padding_size * sizeof(S));
284+
} else { // Not a last hasher in the chain. There is no padding.
285+
memcpy(tmp_fields, in_fields, (T - 1) * sizeof(S));
286+
}
287+
in_fields += (T - 1);
288+
tmp_fields += T;
289+
}
290+
tmp_fields = tmp_fields_init_ptr; // Rollback to initial value.
291+
} // if (is_sponge) {
292+
else { // Not a sponge function. The is no padding.
293+
// Input of each hash should have domain tag at its input.
294+
// tmp_fields = {{dt, in0 (T-1 inputs)}, {dt, in1 (T-1 inputs)}, {dt, in2 (T-1 inputs)}, etc.}
295+
tmp_fields = new S[T * config.batch];
296+
tmp_fields_init_ptr = tmp_fields; // Keep tmp_fields pointer for delete.
297+
if (m_use_domain_tag) {
298+
for (int batch_idx = 0; batch_idx < config.batch; batch_idx++) {
299+
memcpy(tmp_fields, &m_domain_tag, sizeof(S));
300+
memcpy(tmp_fields + 1, in_fields, (T - 1) * sizeof(S));
301+
in_fields += (T - 1);
302+
tmp_fields += T;
303+
}
304+
tmp_fields = tmp_fields_init_ptr; // Rollback to initial value.
305+
} else {
306+
// tmp_fields = {{in0 (T inputs)}, {in1 (T inputs)}, {in2 (T inputs)}, etc.}
307+
memcpy(tmp_fields, in_fields, T * config.batch * sizeof(S));
308+
}
309+
}
310+
311+
// Hashes processing.
312+
if (is_sponge) {
313+
// Call hash_single for hasher[0]
314+
eIcicleError err = hash_single(
315+
tmp_fields /* input */, tmp_fields /* output */, alpha, nof_upper_full_rounds, nof_partial_rounds,
316+
nof_bottom_full_rounds, rounds_constants, mds_matrix, partial_matrix_diagonal_m1);
317+
S* tmp_fields_tmp_ptr = tmp_fields; // Save current pointer in order to access prev output.
212318
if (err != eIcicleError::SUCCESS) return err;
213-
input += arity * sizeof(S);
214-
output += sizeof(S);
319+
if (sponge_nof_hashers != 1) {
320+
tmp_fields[T] = tmp_fields[0]; // Current first output is an input to the next hasher.
321+
}
322+
tmp_fields += T;
323+
// Process rest of the hashers.
324+
for (int hasher_idx = 1; hasher_idx < sponge_nof_hashers; hasher_idx++) {
325+
// The first output of the prev hasher is the first input of the current hasher.
326+
// The T-1 new inputs of the current hasher should be added to the T-1 outputs of the
327+
// prev hasher (starting from index 1).
328+
for (int i = 1; i < T; i++) {
329+
tmp_fields[i] = tmp_fields_tmp_ptr[i] + tmp_fields[i];
330+
}
331+
eIcicleError err = hash_single(
332+
tmp_fields /* input */, tmp_fields /* output */, alpha, nof_upper_full_rounds, nof_partial_rounds,
333+
nof_bottom_full_rounds, rounds_constants, mds_matrix, partial_matrix_diagonal_m1);
334+
tmp_fields_tmp_ptr = tmp_fields; // Save current pointer in order to access prev output.
335+
if (err != eIcicleError::SUCCESS) return err;
336+
if (hasher_idx != sponge_nof_hashers - 1) // Not to do in the last loop to prevent mem leak.
337+
tmp_fields[T] = tmp_fields[0]; // Fill first scalar of the input to the next hasher.
338+
tmp_fields += T; // Now tmp_fields points to input of the next hasher before the addition.
339+
} // for (int hasher_idx = 1; hasher_idx < sponge_nof_hashers; hasher_idx++) {
340+
tmp_fields -= T; // Rollback to the last hasher output.
341+
memcpy(output, (std::byte*)(&tmp_fields[1]), sizeof(S));
342+
tmp_fields = tmp_fields_init_ptr; // Rollback to initial value.
343+
} else { // Not a sponge function.
344+
for (int batch_hash_idx = 0; batch_hash_idx < config.batch; batch_hash_idx++) {
345+
eIcicleError err = hash_single(
346+
tmp_fields /* input */, tmp_fields /* output */, alpha, nof_upper_full_rounds, nof_partial_rounds,
347+
nof_bottom_full_rounds, rounds_constants, mds_matrix, partial_matrix_diagonal_m1);
348+
if (err != eIcicleError::SUCCESS) return err;
349+
memcpy(output, (std::byte*)(&tmp_fields[1]), sizeof(S));
350+
tmp_fields += T;
351+
output += sizeof(S);
352+
}
353+
tmp_fields = tmp_fields_init_ptr; // Rollback to initial value.
215354
}
216355

356+
delete[] tmp_fields;
357+
if (padding_size != 0) delete[] padding;
358+
tmp_fields = nullptr;
359+
217360
return eIcicleError::SUCCESS;
218-
}
361+
} // eIcicleError hash(const std::byte* input, uint64_t size, const HashConfig& config, std::byte* output) const
362+
// override
219363

220364
private:
221365
// // DEBUG start. Do not remove!!!
222-
// void print_state(std::string str, S* state_to_print) const {
366+
// void print_state(std::string str, const S* state_to_print, int count) const {
223367
// std::cout << str << std::endl;
224-
// unsigned int T = m_t;
225-
// for (int state_idx = 0; state_idx < T; state_idx++) { // Columns of matrix.
368+
// for (int state_idx = 0; state_idx < count; state_idx++) { // Columns of matrix.
226369
// std::cout << std::hex << state_to_print[state_idx] << std::endl;
227370
// }
228371
// }
@@ -236,40 +379,21 @@ namespace icicle {
236379
// // DEBUG end
237380

238381
// This function performs a single hash according to parameters in the poseidon2_constants[] struct.
239-
eIcicleError hash_single(const std::byte* input, std::byte* output) const
382+
// eIcicleError hash_single(const std::byte* input, std::byte* output) const
383+
eIcicleError hash_single(
384+
S* tmp_fields,
385+
S* hasher_output,
386+
int alpha,
387+
int nof_upper_full_rounds,
388+
int nof_partial_rounds,
389+
int nof_bottom_full_rounds,
390+
S* rounds_constants,
391+
S* mds_matrix,
392+
S* partial_matrix_diagonal_m1) const
240393
{
241394
const unsigned int T = m_t;
242-
bool is_unsupported_T_for_this_field = poseidon2_constants[T].nof_upper_full_rounds == 0;
243-
if (is_unsupported_T_for_this_field) {
244-
ICICLE_LOG_ERROR << "Unsupported poseidon width (t=" << T << ") for this field! Planned for next version";
245-
return eIcicleError::API_NOT_IMPLEMENTED;
246-
}
247395

248-
unsigned int alpha = poseidon2_constants[T].alpha;
249-
unsigned int nof_upper_full_rounds = poseidon2_constants[T].nof_upper_full_rounds;
250-
unsigned int nof_partial_rounds = poseidon2_constants[T].nof_partial_rounds;
251-
unsigned int nof_bottom_full_rounds = poseidon2_constants[T].nof_bottom_full_rounds;
252-
// S* rounds_constants = poseidon2_constants[T].rounds_constants;
253-
S* rounds_constants = poseidon2_constants[T].rounds_constants;
254-
S* mds_matrix = poseidon2_constants[T].mds_matrix;
255-
// S* partial_matrix_diagonal = poseidon2_constants[T].partial_matrix_diagonal;
256-
S* partial_matrix_diagonal_m1 = poseidon2_constants[T].partial_matrix_diagonal_m1;
257-
// Allocate temporary memory for intermediate calcs.
258-
S* tmp_fields = new S[T];
259-
// Casting from bytes to scalar.
260-
const S* in_fields = (S*)(input);
261-
// Copy input scalar to the output (as a temp storage) to be used in the rounds.
262-
// *tmp_fields are used as a temp storage during the calculations in this function.
263-
if (m_use_domain_tag) {
264-
// in that case we hash [domain_tag, t-1 field elements]
265-
memcpy(tmp_fields, &m_domain_tag, sizeof(S));
266-
memcpy(tmp_fields + 1, in_fields, (T - 1) * sizeof(S));
267-
} else {
268-
// in that case we hash [t field elements]
269-
memcpy(tmp_fields, in_fields, T * sizeof(S));
270-
}
271-
272-
// Pre-rounds full maatrix multiplication.
396+
// Pre-rounds full matrix multiplication.
273397
full_matrix_mul_by_vector(tmp_fields, mds_matrix, tmp_fields);
274398

275399
// Upper full rounds.
@@ -288,10 +412,8 @@ namespace icicle {
288412
// Bottom full rounds.
289413
full_rounds(nof_bottom_full_rounds, tmp_fields, rounds_constants);
290414

291-
memcpy(output, (std::byte*)(&tmp_fields[1]), sizeof(S));
292-
293-
delete[] tmp_fields;
294-
tmp_fields = nullptr;
415+
memcpy(hasher_output, (std::byte*)(tmp_fields), T * sizeof(S));
416+
// memcpy(output, (std::byte*)(&tmp_fields[1]), sizeof(S));
295417

296418
return eIcicleError::SUCCESS;
297419
} // eIcicleError hash_single(const std::byte* input, std::byte* output) const

0 commit comments

Comments
 (0)