From 6af0ca8a01d67d397f38807195b4245e840a1fd5 Mon Sep 17 00:00:00 2001 From: Farukest Date: Wed, 11 Feb 2026 13:44:32 +0300 Subject: [PATCH 1/2] test: cross-check RPO test vectors with reference implementation Add a Python script that implements the RPO hash function with the current state layout [RATE0, RATE1, CAPACITY] and verifies all 19 test vectors match the Rust implementation. This addresses the concern raised in issue #768 that test vectors were not cross-checked with the reference implementation after the state layout change in PR #755. Closes #768 --- .../rescue/rpo/generate_test_vectors.py | 288 ++++++++++++++++++ .../hash/algebraic_sponge/rescue/rpo/tests.rs | 9 + 2 files changed, 297 insertions(+) create mode 100644 miden-crypto/src/hash/algebraic_sponge/rescue/rpo/generate_test_vectors.py diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/generate_test_vectors.py b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/generate_test_vectors.py new file mode 100644 index 000000000..c26266680 --- /dev/null +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/generate_test_vectors.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +""" +RPO (Rescue Prime Optimized) test vector generator. + +This script generates test vectors for the RPO hash function using the state layout +where rate elements are at positions 0..7 and capacity elements are at positions 8..11. +This corresponds to the layout used in the Rust implementation after the state remapping +in PR #755 (issue #673). + +The reference implementation at https://github.com/ASDiscreteMathematics/rpo uses the +original layout where capacity is at positions 0..3 and rate is at positions 4..11. +This script adapts that reference to verify consistency with the new layout. + +Usage: + python3 generate_test_vectors.py + +Parameters: + - Field: Goldilocks (p = 2^64 - 2^32 + 1) + - State width: 12 field elements + - Rate: 8 elements (positions 0..7) + - Capacity: 4 elements (positions 8..11) + - Digest: 4 elements (positions 0..3) + - Rounds: 7 + - S-Box exponent: 7 +""" + +# Goldilocks prime field: p = 2^64 - 2^32 + 1 +P = (1 << 64) - (1 << 32) + 1 + +# State layout constants +STATE_WIDTH = 12 +RATE_START = 0 +RATE_END = 8 +RATE_WIDTH = RATE_END - RATE_START +CAPACITY_START = 8 +CAPACITY_END = 12 +DIGEST_START = 0 +DIGEST_END = 4 + +# Number of rounds +NUM_ROUNDS = 7 + +# S-Box exponent and its inverse mod (p - 1) +ALPHA = 7 +# INV_ALPHA = inverse of 7 mod (p - 1) +INV_ALPHA = 10540996611094048183 + +# MDS matrix (first row of the circulant matrix) +MDS_ROW = [7, 23, 8, 26, 13, 10, 9, 7, 6, 22, 21, 8] + +# Round constants ARK1 (first half of each round) +ARK1 = [ + [ + 5789762306288267392, 6522564764413701783, 17809893479458208203, 107145243989736508, + 6388978042437517382, 15844067734406016715, 9975000513555218239, 3344984123768313364, + 9959189626657347191, 12960773468763563665, 9602914297752488475, 16657542370200465908, + ], + [ + 12987190162843096997, 653957632802705281, 4441654670647621225, 4038207883745915761, + 5613464648874830118, 13222989726778338773, 3037761201230264149, 16683759727265180203, + 8337364536491240715, 3227397518293416448, 8110510111539674682, 2872078294163232137, + ], + [ + 18072785500942327487, 6200974112677013481, 17682092219085884187, 10599526828986756440, + 975003873302957338, 8264241093196931281, 10065763900435475170, 2181131744534710197, + 6317303992309418647, 1401440938888741532, 8884468225181997494, 13066900325715521532, + ], + [ + 5674685213610121970, 5759084860419474071, 13943282657648897737, 1352748651966375394, + 17110913224029905221, 1003883795902368422, 4141870621881018291, 8121410972417424656, + 14300518605864919529, 13712227150607670181, 17021852944633065291, 6252096473787587650, + ], + [ + 4887609836208846458, 3027115137917284492, 9595098600469470675, 10528569829048484079, + 7864689113198939815, 17533723827845969040, 5781638039037710951, 17024078752430719006, + 109659393484013511, 7158933660534805869, 2955076958026921730, 7433723648458773977, + ], + [ + 16308865189192447297, 11977192855656444890, 12532242556065780287, 14594890931430968898, + 7291784239689209784, 5514718540551361949, 10025733853830934803, 7293794580341021693, + 6728552937464861756, 6332385040983343262, 13277683694236792804, 2600778905124452676, + ], + [ + 7123075680859040534, 1034205548717903090, 7717824418247931797, 3019070937878604058, + 11403792746066867460, 10280580802233112374, 337153209462421218, 13333398568519923717, + 3596153696935337464, 8104208463525993784, 14345062289456085693, 17036731477169661256, + ], +] + +# Round constants ARK2 (second half of each round) +ARK2 = [ + [ + 6077062762357204287, 15277620170502011191, 5358738125714196705, 14233283787297595718, + 13792579614346651365, 11614812331536767105, 14871063686742261166, 10148237148793043499, + 4457428952329675767, 15590786458219172475, 10063319113072092615, 14200078843431360086, + ], + [ + 6202948458916099932, 17690140365333231091, 3595001575307484651, 373995945117666487, + 1235734395091296013, 14172757457833931602, 707573103686350224, 15453217512188187135, + 219777875004506018, 17876696346199469008, 17731621626449383378, 2897136237748376248, + ], + [ + 8023374565629191455, 15013690343205953430, 4485500052507912973, 12489737547229155153, + 9500452585969030576, 2054001340201038870, 12420704059284934186, 355990932618543755, + 9071225051243523860, 12766199826003448536, 9045979173463556963, 12934431667190679898, + ], + [ + 18389244934624494276, 16731736864863925227, 4440209734760478192, 17208448209698888938, + 8739495587021565984, 17000774922218161967, 13533282547195532087, 525402848358706231, + 16987541523062161972, 5466806524462797102, 14512769585918244983, 10973956031244051118, + ], + [ + 6982293561042362913, 14065426295947720331, 16451845770444974180, 7139138592091306727, + 9012006439959783127, 14619614108529063361, 1394813199588124371, 4635111139507788575, + 16217473952264203365, 10782018226466330683, 6844229992533662050, 7446486531695178711, + ], + [ + 3736792340494631448, 577852220195055341, 6689998335515779805, 13886063479078013492, + 14358505101923202168, 7744142531772274164, 16135070735728404443, 12290902521256031137, + 12059913662657709804, 16456018495793751911, 4571485474751953524, 17200392109565783176, + ], + [ + 17130398059294018733, 519782857322261988, 9625384390925085478, 1664893052631119222, + 7629576092524553570, 3485239601103661425, 9755891797164033838, 15218148195153269027, + 16460604813734957368, 9643968136937729763, 3611348709641382851, 18256379591337759196, + ], +] + + +def mod_p(x): + """Reduce x modulo P.""" + return x % P + + +def field_add(a, b): + """Add two field elements.""" + return mod_p(a + b) + + +def field_mul(a, b): + """Multiply two field elements.""" + return mod_p(a * b) + + +def field_pow(base, exp): + """Exponentiate a field element.""" + return pow(base, exp, P) + + +def apply_mds(state): + """Apply MDS matrix multiplication (circulant matrix).""" + result = [0] * STATE_WIDTH + for i in range(STATE_WIDTH): + acc = 0 + for j in range(STATE_WIDTH): + # circulant: row i has MDS_ROW shifted by i positions + acc += state[j] * MDS_ROW[(j - i) % STATE_WIDTH] + result[i] = mod_p(acc) + return result + + +def apply_sbox(state): + """Apply S-Box: x -> x^7.""" + return [field_pow(s, ALPHA) for s in state] + + +def apply_inv_sbox(state): + """Apply inverse S-Box: x -> x^{1/7}.""" + return [field_pow(s, INV_ALPHA) for s in state] + + +def add_constants(state, constants): + """Add round constants to the state.""" + return [field_add(state[i], constants[i]) for i in range(STATE_WIDTH)] + + +def apply_permutation(state): + """Apply the RPO permutation (7 rounds).""" + for r in range(NUM_ROUNDS): + # First half of the round + state = apply_mds(state) + state = add_constants(state, ARK1[r]) + state = apply_sbox(state) + + # Second half of the round + state = apply_mds(state) + state = add_constants(state, ARK2[r]) + state = apply_inv_sbox(state) + + return state + + +def rpo_hash_elements(elements): + """ + Hash a sequence of field elements using RPO with the new state layout. + + State layout: [RATE0(0..3), RATE1(4..7), CAPACITY(8..11)] + Digest: state[0..3] + """ + if len(elements) == 0: + return [0, 0, 0, 0] + + # Initialize state to all zeros + state = [0] * STATE_WIDTH + + # Set the first capacity element to len(elements) % RATE_WIDTH + # This serves as domain separation + state[CAPACITY_START] = len(elements) % RATE_WIDTH + + # Absorb elements into the rate portion + i = 0 + for elem in elements: + state[RATE_START + i] = elem + i += 1 + if i == RATE_WIDTH: + state = apply_permutation(state) + i = 0 + + # If there are remaining elements that haven't been permuted, pad with zeros and permute + if i > 0: + while i < RATE_WIDTH: + state[RATE_START + i] = 0 + i += 1 + state = apply_permutation(state) + + # Return the digest (first 4 elements of the rate) + return state[DIGEST_START:DIGEST_END] + + +def generate_test_vectors(): + """Generate 19 test vectors matching the Rust test: hash([0], [0,1], ..., [0..18]).""" + print("=" * 80) + print("RPO Test Vectors (new state layout: [RATE0, RATE1, CAPACITY])") + print("=" * 80) + print() + + # Expected values from the Rust implementation + expected = [ + [8563248028282119176, 14757918088501470722, 14042820149444308297, 7607140247535155355], + [8762449007102993687, 4386081033660325954, 5000814629424193749, 8171580292230495897], + [16710087681096729759, 10808706421914121430, 14661356949236585983, 5683478730832134441], + [5309818427047650994, 17172251659920546244, 8288476618870804357, 18080473279382182941], + [3647545403045515695, 3358383208908083302, 8797161010298072910, 2412100201132087248], + [8409780526028662686, 214479528340808320, 13626616722984122219, 13991752159726061594], + [4800410126693035096, 8293686005479024958, 16849389505608627981, 12129312715917897796], + [5421234586123900205, 9738602082989433872, 7017816005734536787, 8635896173743411073], + [11707446879505873182, 7588005580730590001, 4664404372972250366, 17613162115550587316], + [6991094187713033844, 10140064581418506488, 1235093741254112241, 16755357411831959519], + [18007834547781860956, 5262789089508245576, 4752286606024269423, 15626544383301396533], + [5419895278045886802, 10747737918518643252, 14861255521757514163, 3291029997369465426], + [16916426112258580265, 8714377345140065340, 14207246102129706649, 6226142825442954311], + [7320977330193495928, 15630435616748408136, 10194509925259146809, 15938750299626487367], + [9872217233988117092, 5336302253150565952, 9650742686075483437, 8725445618118634861], + [12539853708112793207, 10831674032088582545, 11090804155187202889, 105068293543772992], + [7287113073032114129, 6373434548664566745, 8097061424355177769, 14780666619112596652], + [17147873541222871127, 17350918081193545524, 5785390176806607444, 12480094913955467088], + [17273934282489765074, 8007352780590012415, 16690624932024962846, 8137543572359747206], + ] + + all_match = True + for i in range(19): + elements = list(range(i + 1)) # [0], [0,1], [0,1,2], ..., [0..18] + digest = rpo_hash_elements(elements) + + match = digest == expected[i] + status = "OK" if match else "MISMATCH" + + if not match: + all_match = False + + print(f"hash([0..{i}]) = {digest}") + print(f" expected = {expected[i]}") + print(f" status = {status}") + print() + + print("=" * 80) + if all_match: + print("ALL 19 TEST VECTORS MATCH the Rust implementation.") + else: + print("SOME TEST VECTORS DO NOT MATCH!") + print("=" * 80) + + return all_match + + +if __name__ == "__main__": + success = generate_test_vectors() + exit(0 if success else 1) diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs index eecb4f666..84ce36224 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs @@ -279,6 +279,15 @@ proptest! { } } +/// Expected hash outputs for RPO with the state layout `[RATE0, RATE1, CAPACITY]`. +/// +/// These test vectors have been cross-checked against a Python reference implementation adapted +/// from the original specification at . The +/// reference uses the same permutation (MDS matrix, round constants, S-Box) but with the +/// original layout `[CAPACITY, RATE0, RATE1]`. This script adapts it to use the current layout +/// `[RATE0, RATE1, CAPACITY]` and verifies all 19 vectors match. +/// +/// The verification script is located at `generate_test_vectors.py` in this directory. const EXPECTED: [Word; 19] = [ Word::new([ Felt::new(8563248028282119176), From 0bf6ccbf2c2998dc80423da54dffcda1ad5862f2 Mon Sep 17 00:00:00 2001 From: Farukest Date: Wed, 11 Feb 2026 14:17:59 +0300 Subject: [PATCH 2/2] chore: add CHANGELOG entry for RPO test vector verification --- CHANGELOG.md | 1 + miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a9057ea..33c53fdd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [BREAKING] Removed `hashbrown` dependency and `hashmaps` feature; `Map`/`Set` type aliases are now tied to the `std` feature ([#813](https://github.com/0xMiden/crypto/pull/813)). - [BREAKING] Renamed `NodeIndex::value()` to `NodeIndex::position()`, `NodeIndex::is_value_odd()` to `NodeIndex::is_position_odd()`, and `LeafIndex::value()` to `LeafIndex::position()` ([#814](https://github.com/0xMiden/crypto/pull/814)). - Fixed tuple `min_serialized_size()` to exclude alignment padding, fixing `BudgetedReader` rejecting valid data ([#827](https://github.com/0xMiden/crypto/pull/827)). +- Cross-checked RPO test vectors against the Python reference implementation after state layout change ([#822](https://github.com/0xMiden/crypto/pull/822)). ## 0.22.2 (2026-02-01) diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs index 84ce36224..84e30a4c3 100644 --- a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs @@ -235,10 +235,9 @@ fn hash_test_vectors() { Felt::new(18), ]; - for i in 0..elements.len() { - let expected = EXPECTED[i]; + for (i, expected) in EXPECTED.iter().enumerate() { let result = Rpo256::hash_elements(&elements[..(i + 1)]); - assert_eq!(result, expected); + assert_eq!(result, *expected); } }