Skip to content
Open
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
3 changes: 3 additions & 0 deletions sinsemilla/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to Rust's notion of

## [Unreleased]

- Added `UncheckedFixedLengthHashDomain::hash_with_first_word` for messages
whose first Sinsemilla word is already decoded and whose suffix remains a
bit iterator.
- Prepared the `1.0.0-rc.4` release.
- Replaced `lazy_static` (`spin_no_std`) with `once_cell` plus an
exactly-once initialization wrapper for the affine generator table,
Expand Down
67 changes: 67 additions & 0 deletions sinsemilla/src/weighted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,28 @@ impl<const N: usize> UncheckedFixedLengthHashDomain<N> {
extract(self.hash_words_to_point(words))
}

/// Evaluates a pre-decoded first word followed by a bit-encoded suffix.
///
/// `first_word` is used directly as the message's first complete [`K`]-bit
/// Sinsemilla word. `remaining_bits` are then decoded into zero-padded
/// [`K`]-bit words. This avoids making callers encode an already-decoded
/// prefix back into bits when the rest of their message is naturally
/// represented as a bit iterator.
///
/// # Panics
///
/// Panics if `first_word` is not a valid [`K`]-bit Sinsemilla word, or if
/// the complete message does not contain exactly `N` words after padding.
pub fn hash_with_first_word(
&self,
first_word: u16,
remaining_bits: impl Iterator<Item = bool>,
) -> pallas::Base {
let remaining_words = MessageWords::new(remaining_bits)
.map(|word| u16::try_from(word).expect("a Sinsemilla word fits into u16"));
extract(self.evaluate(core::iter::once(first_word).chain(remaining_words)))
}

/// Evaluates a batch of `N`-word messages position-first and returns their
/// extracted Sinsemilla hashes.
///
Expand Down Expand Up @@ -745,10 +767,55 @@ mod tests {
assert!(bool::from(expected.is_some()));
let expected = expected.unwrap();
assert_eq!(expected, weighted.hash_words(&words));
assert_eq!(
expected,
weighted.hash_with_first_word(words[0], words_to_bits(&words[1..]).into_iter())
);
assert_eq!(expected, weighted.hash(bits.iter().copied()));
}
}

#[test]
fn first_word_hash_zero_pads_the_remaining_bits() {
let domain = HashDomain::new(MERKLE_DOMAIN);
let weighted = UncheckedFixedLengthHashDomain::<2>::new(&domain);
let first_word = 7;
let second_word = 5;
let remaining_bits = [true, false, true];

assert_eq!(
weighted.hash_with_first_word(first_word, remaining_bits.into_iter()),
weighted.hash_words(&[first_word, second_word]),
);
}

#[test]
#[should_panic(expected = "invalid Sinsemilla word")]
fn first_word_hash_rejects_invalid_first_word() {
let domain = HashDomain::new(MERKLE_DOMAIN);
let weighted = UncheckedFixedLengthHashDomain::<1>::new(&domain);

weighted.hash_with_first_word(GENERATOR_COUNT as u16, core::iter::empty());
}

#[test]
#[should_panic(expected = "unexpected Sinsemilla word count")]
fn first_word_hash_rejects_too_few_suffix_words() {
let domain = HashDomain::new(MERKLE_DOMAIN);
let weighted = UncheckedFixedLengthHashDomain::<2>::new(&domain);

weighted.hash_with_first_word(0, core::iter::empty());
}

#[test]
#[should_panic(expected = "unexpected Sinsemilla word count")]
fn first_word_hash_rejects_too_many_suffix_words() {
let domain = HashDomain::new(MERKLE_DOMAIN);
let weighted = UncheckedFixedLengthHashDomain::<2>::new(&domain);

weighted.hash_with_first_word(0, core::iter::repeat_n(false, 2 * K));
}

#[test]
fn batch_matches_individual_word_evaluation() {
let domain = HashDomain::new(MERKLE_DOMAIN);
Expand Down
Loading