Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
Copy link
Contributor

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.

Copy link
Contributor

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.

"""
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)
14 changes: 11 additions & 3 deletions miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -279,6 +278,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 <https://github.com/ASDiscreteMathematics/rpo>. 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),
Expand Down