-
Notifications
You must be signed in to change notification settings - Fork 79
test: cross-check RPO test vectors with Python reference implementation #822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
bobbinth
merged 2 commits into
0xMiden:next
from
Farukest:feat/verify-rpo-test-vectors-768
Feb 14, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
288 changes: 288 additions & 0 deletions
288
miden-crypto/src/hash/algebraic_sponge/rescue/rpo/generate_test_vectors.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a nice verification script. Since the Python script isn't part of CI, a future change to RPO could break consistency without detection. Would be worth considering either adding this to CI or generating test vectors from Python as the source of truth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't spend effort on this right now as with v0.21 VM, we no longer use RPO/RPX and we may be dropping RPO/RPX from this repo in the near future.