Skip to content
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

Hash arbitrary number of elements in masm #750

Merged
merged 11 commits into from
Jul 2, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- Created `get_serial_number` procedure to get the serial num of the currently processed note (#760).
- [BREAKING] Added support for input notes with delayed verification of inclusion proofs (#724, #732, #759, #770, #772).
- [BREAKING] Added support for conversion from `Nullifier` to `InputNoteCommitment`, commitment header return reference (#774).
- Added `compute_inputs_hash` procedure for hash computation of the arbitrary number of note inouts (#750).

## 0.3.0 (2024-05-14)

Expand Down
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/architecture/transactions/procedures.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ To import the note procedures, set `use.miden::note` at the beginning of the fil
| `get_assets` | `[dest_ptr]` | `[num_assets, dest_ptr]` | note | <ul> <li>Writes the assets of the currently executing note into memory starting at the specified address `dest_ptr `. </li><li> `num_assets` is the number of assets in the currently executing note.</li> </ul> |
| `get_inputs` | `[dest_ptr]` | `[dest_ptr]` | note | <ul> <li>Writes the inputs of the currently executed note into memory starting at the specified address, `dest_ptr`. </li> </ul> |
| `get_sender` | `[]` | `[sender]` | note | <ul> <li>Returns the `sender` of the note currently being processed. Panics if a note is not being processed. </li> </ul> |
| `compute_inputs_hash` | `[inputs_ptr, num_inputs]` | `[HASH]` | note | <ul> <li>Computes hash of note inputs starting at the specified memory address.</li> </ul> |


### Tx
Expand Down
166 changes: 165 additions & 1 deletion miden-lib/asm/miden/note.masm
bobbinth marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,168 @@ export.get_serial_number

syscall.get_note_serial_number
# => [SERIAL_NUMBER]
end
end

#! Computes hash of note inputs starting at the specified memory address.
#!
#! This procedure divides the hashing process into two parts: hashing pairs of words using
#! `hash_memory_even` procedure and hashing the remaining values using the `hperm` instruction.
#!
#! If the number if inputs is 0, procedure returns the empty word: [0, 0, 0, 0].
#!
#! Inputs: [inputs_ptr, num_inputs]
#! Outputs: [HASH]
#! Cycles:
#! - If number of elements divides by 8: 56 cycles + 3 * words
#! - Else: 189 cycles + 3 * words
#!
#! Panics if num_inputs is greater than 128.
export.compute_inputs_hash
# check that number of inputs is less than 128
dup.1 push.128 u32assert2 u32lte assert

# move number of inputs to the top of the stack
swap
# => [num_inputs, inputs_ptr]

# get the number of double words
u32divmod.8 swap
# => [num_inputs/8, num_inputs%8, inputs_ptr]

# get the end_addr for hash_memory_even procedure (end address for pairs of words)
mul.2 dup.2 add movup.2
# => [inputs_ptr, end_addr, num_inputs%8]

bobbinth marked this conversation as resolved.
Show resolved Hide resolved
# get the padding flag to add it to the capacity part
dup.2 eq.0 not
# => [pad_flag, inputs_ptr, end_addr, num_inputs%8]

# prepare hasher state for RPO permutation
push.0.0.0 padw padw
# => [C, B, A, inputs_ptr, end_addr, num_inputs%8]

# hash every pair of words
exec.native::hash_memory_even
# => [C', B', A', inputs_ptr', end_addr, num_inputs%8] where inputs_ptr' = end_addr

# hash remaining input values if there are any left
# if num_inputs%8 is ZERO and there are no elements to hash
dup.14 eq.0
if.true
# clean the stack
exec.native::state_to_digest
swapw drop drop drop movdn.4
# => [B']
else
# load the remaining double word
mem_stream
# => [E, D, A', inputs_ptr'+2, end_addr, num_inputs%8]

# clean the stack
movup.12 drop movup.12 drop
# => [E, D, A', num_inputs%8]

# get the number of elements we need to drop
# notice that drop_counter could be any number from 1 to 7
push.8 movup.13 sub movdn.12
# => [E, D, A', drop_counter]

### 0th value ########################################################

# if current value is the last value to drop ("cycle" number equals to the number of values
# to drop), push 1 instead of 0 to the stack
dup.12 eq.1 swap

# we need to drop first value anyway, since number of values is not divisible by 8
drop movdn.6
# => [e_2, e_1, e_0, d_3, d_2, d_1, 0/1, d_0, A', drop_counter]

### 1st value ########################################################

# prepare the second element of the E Word for cdrop instruction
# if current value is the last value to drop ("cycle" number equals to the number of values
# to drop), push 1 instead of 0 to the stack
dup.12 eq.2 swap
# => [e_2, 0, e_1, e_0, d_3, d_2, d_1, 0/1, d_0, A', drop_counter]

# push latch variable onto the stack; this will be the control for the cdrop instruction
push.0
# => [latch = 0, e_2, 0, e_1, e_0, d_3, d_2, d_1, 0, d_0, A', drop_counter]

# get the flag whether the drop counter is equal 1
dup.14 eq.1
# => [drop_counter == 1, latch = 0, e_2, 0, e_1, e_0, d_3, d_2, d_1, 0, d_0, A', drop_counter]

# update the latch: if drop_counter == 1, latch will become 1
or
# => [latch', e_2, 0, e_1, e_0, d_3, d_2, d_1, 0, d_0, A', drop_counter]

# save the latch value
dup movdn.14
# => [latch', e_2, 0, e_1, e_0, d_3, d_2, d_1, 0, d_0, A', latch', drop_counter]

# if latch == 1, drop 0; otherwise drop e_1
cdrop
# => [e_2_or_0, e_1, e_0, d_3, d_2, d_1, 0, d_0, A', latch', drop_counter]

# move the calculated value down the stack
movdn.6
# => [e_1, e_0, d_3, d_2, d_1, 0, e_2_or_0, d_0, A', latch', drop_counter]

### 2nd value ########################################################

# repeat the above process but now compare drop_counter to 2
dup.13 eq.3 swap
movup.13 dup.14 eq.2 or
dup movdn.14
cdrop movdn.6
# => [e_0, d_3, d_2, d_1, 0, e_2_or_0, e_1_or_0, d_0, A', latch', drop_counter]

### 3rd value ########################################################

# repeat the above process but now compare drop_counter to 3
dup.13 eq.4 swap
movup.13 dup.14 eq.3 or
dup movdn.14
cdrop movdn.6
# => [d_3, d_2, d_1, 0, e_2_or_0, e_1_or_0, e_0_or_0, d_0, A', latch', drop_counter]

### 4th value ########################################################

# repeat the above process but now compare drop_counter to 4
dup.13 eq.5 swap
movup.13 dup.14 eq.4 or
dup movdn.14
cdrop movdn.6
# => [d_2, d_1, 0, e_2_or_0, e_1_or_0, e_0_or_0, d_3_or_0, d_0, A', latch', drop_counter]

### 5th value ########################################################

# repeat the above process but now compare drop_counter to 5
dup.13 eq.6 swap
movup.13 dup.14 eq.5 or
dup movdn.14
cdrop movdn.6
# => [d_1, 0, e_2_or_0, e_1_or_0, e_0_or_0, d_3_or_0, d_2_or_0, d_0, A', latch', drop_counter]

### 6th value ########################################################

# repeat the above process but now compare drop_counter to 6
dup.13 eq.7 swap
movup.13 movup.14 eq.6 or
cdrop movdn.6
# => [0, e_2_or_0, e_1_or_0, e_0_or_0, d_3_or_0, d_2_or_0, d_1_or_0, d_0, A']
# or in other words
# => [C, B, A', ... ]
# notice that we don't need to check the d_0 value: entering the else branch means that
# we have number of elements not divisible by 8, so we will have at least one element to
# hash here (which turns out to be d_0)

hperm
# => [F, E, D]

exec.native::state_to_digest
# => [E]
end
end

100 changes: 98 additions & 2 deletions miden-tx/src/tests/kernel_tests/test_note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ use miden_objects::{
notes::Note,
testing::{notes::AssetPreservationStatus, prepare_word},
transaction::TransactionArgs,
WORD_SIZE,
Hasher, WORD_SIZE,
};
use vm_processor::{EMPTY_WORD, ONE};
use vm_processor::{ProcessState, EMPTY_WORD, ONE};

use super::{Felt, Process, ZERO};
use crate::{
Expand Down Expand Up @@ -429,3 +429,99 @@ fn test_get_note_serial_number() {
let serial_number = tx_context.input_notes().get_note(0).note().serial_num();
assert_eq!(process.stack.get_word(0), serial_number);
}

#[test]
fn test_get_inputs_hash() {
let tx_context = TransactionContextBuilder::with_standard_account(
ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN,
ONE,
)
.with_mock_notes(AssetPreservationStatus::Preserved)
.build();

let code = "
use.miden::note

begin
# put the values that will be hashed into the memory
push.1.2.3.4.1000 mem_storew dropw
push.5.6.7.8.1001 mem_storew dropw
push.9.10.11.12.1002 mem_storew dropw
push.13.14.15.16.1003 mem_storew dropw

# push the number of values and pointer to the inputs on the stack
push.5.1000
# execute the `compute_inputs_hash` procedure for 5 values
exec.note::compute_inputs_hash
# => [HASH_5]

push.8.1000
# execute the `compute_inputs_hash` procedure for 8 values
exec.note::compute_inputs_hash
# => [HASH_8, HASH_5]

push.15.1000
# execute the `compute_inputs_hash` procedure for 15 values
exec.note::compute_inputs_hash
# => [HASH_15, HASH_8, HASH_5]

push.0.1000
# check that calling `compute_inputs_hash` procedure with 0 elements will result in an
# empty word
exec.note::compute_inputs_hash
# => [0, 0, 0, 0, HASH_15, HASH_8, HASH_5]
end
";

let process = tx_context.execute_code(code).unwrap();

let mut expected_5 = Hasher::hash_elements(&[
Felt::new(1),
Felt::new(2),
Felt::new(3),
Felt::new(4),
Felt::new(5),
])
.to_vec();
expected_5.reverse();

let mut expected_8 = Hasher::hash_elements(&[
Felt::new(1),
Felt::new(2),
Felt::new(3),
Felt::new(4),
Felt::new(5),
Felt::new(6),
Felt::new(7),
Felt::new(8),
])
.to_vec();
expected_8.reverse();

let mut expected_15 = Hasher::hash_elements(&[
Felt::new(1),
Felt::new(2),
Felt::new(3),
Felt::new(4),
Felt::new(5),
Felt::new(6),
Felt::new(7),
Felt::new(8),
Felt::new(9),
Felt::new(10),
Felt::new(11),
Felt::new(12),
Felt::new(13),
Felt::new(14),
Felt::new(15),
])
.to_vec();
expected_15.reverse();

let mut expected_stack = vec![Felt::new(0), Felt::new(0), Felt::new(0), Felt::new(0)];
expected_stack.extend_from_slice(&expected_15);
expected_stack.extend_from_slice(&expected_8);
expected_stack.extend_from_slice(&expected_5);

assert_eq!(process.get_stack_state()[0..16], expected_stack);
}
Loading