|
|
@@ -11,28 +11,44 @@ use {
|
|
|
},
|
|
|
};
|
|
|
|
|
|
-/// A HashChain.
|
|
|
+/// A hash chain of a specific length. The hash chain has the property that
|
|
|
+/// hash(chain.reveal_ith(i)) == chain.reveal_ith(i - 1)
|
|
|
+///
|
|
|
+/// The implementation subsamples the elements of the chain such that it uses less memory
|
|
|
+/// to keep the chain around.
|
|
|
#[derive(Clone)]
|
|
|
pub struct PebbleHashChain {
|
|
|
- hash: Vec<[u8; 32]>,
|
|
|
- next: usize,
|
|
|
+ hash: Vec<[u8; 32]>,
|
|
|
+ sample_interval: usize,
|
|
|
+ length: usize,
|
|
|
}
|
|
|
|
|
|
impl PebbleHashChain {
|
|
|
// Given a secret, we hash it with Keccak256 len times to get the final hash, this is an S/KEY
|
|
|
// like protocol in which revealing the hashes in reverse proves knowledge.
|
|
|
- pub fn new(secret: [u8; 32], length: usize) -> Self {
|
|
|
+ pub fn new(secret: [u8; 32], length: usize, sample_interval: usize) -> Self {
|
|
|
+ assert!(sample_interval > 0, "Sample interval must be positive");
|
|
|
let mut hash = Vec::<[u8; 32]>::with_capacity(length);
|
|
|
- hash.push(Keccak256::digest(secret).into());
|
|
|
- for _ in 1..length {
|
|
|
- hash.push(Keccak256::digest(&hash[hash.len() - 1]).into());
|
|
|
+ let mut current: [u8; 32] = Keccak256::digest(secret).into();
|
|
|
+
|
|
|
+ hash.push(current.clone());
|
|
|
+ for i in 1..length {
|
|
|
+ current = Keccak256::digest(¤t).into();
|
|
|
+ if i % sample_interval == 0 {
|
|
|
+ hash.push(current);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
hash.reverse();
|
|
|
|
|
|
- Self { hash, next: 0 }
|
|
|
+ Self {
|
|
|
+ hash,
|
|
|
+ sample_interval,
|
|
|
+ length,
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+
|
|
|
pub fn from_config(
|
|
|
secret: &str,
|
|
|
chain_id: &ChainId,
|
|
|
@@ -40,6 +56,7 @@ impl PebbleHashChain {
|
|
|
contract_address: &Address,
|
|
|
random: &[u8; 32],
|
|
|
chain_length: u64,
|
|
|
+ sample_interval: u64,
|
|
|
) -> Result<Self> {
|
|
|
let mut input: Vec<u8> = vec![];
|
|
|
input.extend_from_slice(&hex::decode(secret.trim())?);
|
|
|
@@ -49,24 +66,32 @@ impl PebbleHashChain {
|
|
|
input.extend_from_slice(random);
|
|
|
|
|
|
let secret: [u8; 32] = Keccak256::digest(input).into();
|
|
|
- Ok(Self::new(secret, chain_length.try_into()?))
|
|
|
- }
|
|
|
-
|
|
|
- /// Reveal the next hash in the chain using the previous proof.
|
|
|
- pub fn reveal(&mut self) -> Result<[u8; 32]> {
|
|
|
- ensure!(self.next < self.len(), "no more hashes in the chain");
|
|
|
- let next = self.hash[self.next].clone();
|
|
|
- self.next += 1;
|
|
|
- Ok(next)
|
|
|
+ Ok(Self::new(
|
|
|
+ secret,
|
|
|
+ chain_length.try_into()?,
|
|
|
+ sample_interval.try_into()?,
|
|
|
+ ))
|
|
|
}
|
|
|
|
|
|
pub fn reveal_ith(&self, i: usize) -> Result<[u8; 32]> {
|
|
|
ensure!(i < self.len(), "index not in range");
|
|
|
- Ok(self.hash[i].clone())
|
|
|
+
|
|
|
+ // Note that subsample_interval may not perfectly divide length, in which case the uneven segment is
|
|
|
+ // actually at the *front* of the list. Thus, it's easier to compute indexes from the end of the list.
|
|
|
+ let index_from_end_of_subsampled_list = ((self.len() - 1) - i) / self.sample_interval;
|
|
|
+ let mut i_index = self.len() - 1 - index_from_end_of_subsampled_list * self.sample_interval;
|
|
|
+ let mut val = self.hash[self.hash.len() - 1 - index_from_end_of_subsampled_list].clone();
|
|
|
+
|
|
|
+ while i_index > i {
|
|
|
+ val = Keccak256::digest(val).into();
|
|
|
+ i_index -= 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(val)
|
|
|
}
|
|
|
|
|
|
pub fn len(&self) -> usize {
|
|
|
- self.hash.len()
|
|
|
+ self.length
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -99,3 +124,56 @@ impl HashChainState {
|
|
|
self.hash_chains[chain_index].reveal_ith(sequence_number - self.offsets[chain_index])
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod test {
|
|
|
+ use {
|
|
|
+ crate::state::PebbleHashChain,
|
|
|
+ sha3::{
|
|
|
+ Digest,
|
|
|
+ Keccak256,
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ fn run_hash_chain_test(secret: [u8; 32], length: usize, sample_interval: usize) {
|
|
|
+ // Calculate the hash chain the naive way as a comparison point to the subsampled implementation.
|
|
|
+ let mut basic_chain = Vec::<[u8; 32]>::with_capacity(length);
|
|
|
+ let mut current: [u8; 32] = Keccak256::digest(secret).into();
|
|
|
+ basic_chain.push(current.clone());
|
|
|
+ for _ in 1..length {
|
|
|
+ current = Keccak256::digest(¤t).into();
|
|
|
+ basic_chain.push(current);
|
|
|
+ }
|
|
|
+
|
|
|
+ basic_chain.reverse();
|
|
|
+
|
|
|
+ let chain = PebbleHashChain::new(secret, length, sample_interval);
|
|
|
+
|
|
|
+ let mut last_val = chain.reveal_ith(0).unwrap();
|
|
|
+ for i in 1..length {
|
|
|
+ let cur_val = chain.reveal_ith(i).unwrap();
|
|
|
+ println!("{}", i);
|
|
|
+ assert_eq!(basic_chain[i], cur_val);
|
|
|
+
|
|
|
+ let expected_last_val: [u8; 32] = Keccak256::digest(cur_val).into();
|
|
|
+ assert_eq!(expected_last_val, last_val);
|
|
|
+ last_val = cur_val;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[test]
|
|
|
+ fn test_hash_chain() {
|
|
|
+ run_hash_chain_test([0u8; 32], 10, 1);
|
|
|
+ run_hash_chain_test([0u8; 32], 10, 2);
|
|
|
+ run_hash_chain_test([0u8; 32], 10, 3);
|
|
|
+ run_hash_chain_test([1u8; 32], 10, 1);
|
|
|
+ run_hash_chain_test([1u8; 32], 10, 2);
|
|
|
+ run_hash_chain_test([1u8; 32], 10, 3);
|
|
|
+ run_hash_chain_test([0u8; 32], 100, 1);
|
|
|
+ run_hash_chain_test([0u8; 32], 100, 2);
|
|
|
+ run_hash_chain_test([0u8; 32], 100, 3);
|
|
|
+ run_hash_chain_test([0u8; 32], 100, 7);
|
|
|
+ run_hash_chain_test([0u8; 32], 100, 50);
|
|
|
+ run_hash_chain_test([0u8; 32], 100, 55);
|
|
|
+ }
|
|
|
+}
|