Skip to content

Commit cad588c

Browse files
authored
[fortuna] Add subsampling of hash chains (#1624)
* add hash chain test * add sampling * works * works * ok this works * cleanup * parallelize * gr * spawn
1 parent 6d71605 commit cad588c

File tree

9 files changed

+142
-28
lines changed

9 files changed

+142
-28
lines changed

apps/fortuna/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/fortuna/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fortuna"
3-
version = "6.0.1"
3+
version = "6.1.0"
44
edition = "2021"
55

66
[dependencies]

apps/fortuna/config.sample.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ chains:
1818
provider:
1919
uri: http://localhost:8080/
2020
chain_length: 100000
21+
chain_sample_interval: 10
2122

2223
# An ethereum wallet address and private key. Generate with `cast wallet new`
2324
address: 0xADDRESS

apps/fortuna/src/api.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@ mod test {
233233
// initialize the BlockchainStates below, but they aren't cloneable (nor do they need to be cloned).
234234
static ref ETH_CHAIN: Arc<HashChainState> = Arc::new(HashChainState::from_chain_at_offset(
235235
0,
236-
PebbleHashChain::new([0u8; 32], 1000),
236+
PebbleHashChain::new([0u8; 32], 1000, 1),
237237
));
238238
static ref AVAX_CHAIN: Arc<HashChainState> = Arc::new(HashChainState::from_chain_at_offset(
239239
100,
240-
PebbleHashChain::new([1u8; 32], 1000),
240+
PebbleHashChain::new([1u8; 32], 1000, 1),
241241
));
242242
}
243243

apps/fortuna/src/command/register_provider.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,21 @@ pub async fn register_provider_from_config(
6565
.ok_or(anyhow!("Please specify a provider secret in the config"))?;
6666

6767
let commitment_length = provider_config.chain_length;
68-
let mut chain = PebbleHashChain::from_config(
68+
tracing::info!("Generating hash chain");
69+
let chain = PebbleHashChain::from_config(
6970
&secret,
7071
&chain_id,
7172
&private_key_string.parse::<LocalWallet>()?.address(),
7273
&chain_config.contract_addr,
7374
&random,
7475
commitment_length,
76+
provider_config.chain_sample_interval,
7577
)?;
78+
tracing::info!("Done generating hash chain");
7679

7780
// Arguments to the contract to register our new provider.
7881
let fee_in_wei = chain_config.fee;
79-
let commitment = chain.reveal()?;
82+
let commitment = chain.reveal_ith(0)?;
8083
// Store the random seed and chain length in the metadata field so that we can regenerate the hash
8184
// chain at-will. (This is secure because you can't generate the chain unless you also have the secret)
8285
let commitment_metadata = CommitmentMetadata {

apps/fortuna/src/command/run.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use {
3636
BlockNumber,
3737
},
3838
},
39+
futures::future::join_all,
3940
prometheus_client::{
4041
encoding::EncodeLabelSet,
4142
metrics::{
@@ -157,10 +158,29 @@ pub async fn run(opts: &RunOptions) -> Result<()> {
157158
))?;
158159
let (tx_exit, rx_exit) = watch::channel(false);
159160

161+
let mut tasks = Vec::new();
162+
for (chain_id, chain_config) in config.chains.clone() {
163+
let secret_copy = secret.clone();
164+
165+
tasks.push(spawn(async move {
166+
let state = setup_chain_state(
167+
&config.provider.address,
168+
&secret_copy,
169+
config.provider.chain_sample_interval,
170+
&chain_id,
171+
&chain_config,
172+
)
173+
.await;
174+
175+
(chain_id, state)
176+
}));
177+
}
178+
let states = join_all(tasks).await;
179+
160180
let mut chains: HashMap<ChainId, BlockchainState> = HashMap::new();
161-
for (chain_id, chain_config) in &config.chains {
162-
let state =
163-
setup_chain_state(&config.provider.address, &secret, chain_id, chain_config).await;
181+
for result in states {
182+
let (chain_id, state) = result?;
183+
164184
match state {
165185
Ok(state) => {
166186
chains.insert(chain_id.clone(), state);
@@ -211,6 +231,7 @@ pub async fn run(opts: &RunOptions) -> Result<()> {
211231
async fn setup_chain_state(
212232
provider: &Address,
213233
secret: &String,
234+
chain_sample_interval: u64,
214235
chain_id: &ChainId,
215236
chain_config: &EthereumConfig,
216237
) -> Result<BlockchainState> {
@@ -267,6 +288,7 @@ async fn setup_chain_state(
267288
&chain_config.contract_addr,
268289
&commitment.seed,
269290
commitment.chain_length,
291+
chain_sample_interval,
270292
)?;
271293
hash_chains.push(pebble_hash_chain);
272294
}

apps/fortuna/src/command/setup_provider.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ async fn setup_chain_provider(
106106
&chain_config.contract_addr,
107107
&metadata.seed,
108108
provider_config.chain_length,
109+
provider_config.chain_sample_interval,
109110
)?;
110111
let chain_state = HashChainState {
111112
offsets: vec![provider_info

apps/fortuna/src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ pub struct ProviderConfig {
180180

181181
/// The length of the hash chain to generate.
182182
pub chain_length: u64,
183+
184+
/// How frequently the hash chain is sampled -- increase this value to tradeoff more
185+
/// compute per request for less RAM use.
186+
#[serde(default = "default_chain_sample_interval")]
187+
pub chain_sample_interval: u64,
188+
}
189+
190+
fn default_chain_sample_interval() -> u64 {
191+
1
183192
}
184193

185194
/// Configuration values for the keeper service that are shared across chains.

apps/fortuna/src/state.rs

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,52 @@ use {
1111
},
1212
};
1313

14-
/// A HashChain.
14+
/// A hash chain of a specific length. The hash chain has the property that
15+
/// hash(chain.reveal_ith(i)) == chain.reveal_ith(i - 1)
16+
///
17+
/// The implementation subsamples the elements of the chain such that it uses less memory
18+
/// to keep the chain around.
1519
#[derive(Clone)]
1620
pub struct PebbleHashChain {
17-
hash: Vec<[u8; 32]>,
18-
next: usize,
21+
hash: Vec<[u8; 32]>,
22+
sample_interval: usize,
23+
length: usize,
1924
}
2025

2126
impl PebbleHashChain {
2227
// Given a secret, we hash it with Keccak256 len times to get the final hash, this is an S/KEY
2328
// like protocol in which revealing the hashes in reverse proves knowledge.
24-
pub fn new(secret: [u8; 32], length: usize) -> Self {
29+
pub fn new(secret: [u8; 32], length: usize, sample_interval: usize) -> Self {
30+
assert!(sample_interval > 0, "Sample interval must be positive");
2531
let mut hash = Vec::<[u8; 32]>::with_capacity(length);
26-
hash.push(Keccak256::digest(secret).into());
27-
for _ in 1..length {
28-
hash.push(Keccak256::digest(&hash[hash.len() - 1]).into());
32+
let mut current: [u8; 32] = Keccak256::digest(secret).into();
33+
34+
hash.push(current.clone());
35+
for i in 1..length {
36+
current = Keccak256::digest(&current).into();
37+
if i % sample_interval == 0 {
38+
hash.push(current);
39+
}
2940
}
3041

3142
hash.reverse();
3243

33-
Self { hash, next: 0 }
44+
Self {
45+
hash,
46+
sample_interval,
47+
length,
48+
}
3449
}
3550

51+
3652
pub fn from_config(
3753
secret: &str,
3854
chain_id: &ChainId,
3955
provider_address: &Address,
4056
contract_address: &Address,
4157
random: &[u8; 32],
4258
chain_length: u64,
59+
sample_interval: u64,
4360
) -> Result<Self> {
4461
let mut input: Vec<u8> = vec![];
4562
input.extend_from_slice(&hex::decode(secret.trim())?);
@@ -49,24 +66,32 @@ impl PebbleHashChain {
4966
input.extend_from_slice(random);
5067

5168
let secret: [u8; 32] = Keccak256::digest(input).into();
52-
Ok(Self::new(secret, chain_length.try_into()?))
53-
}
54-
55-
/// Reveal the next hash in the chain using the previous proof.
56-
pub fn reveal(&mut self) -> Result<[u8; 32]> {
57-
ensure!(self.next < self.len(), "no more hashes in the chain");
58-
let next = self.hash[self.next].clone();
59-
self.next += 1;
60-
Ok(next)
69+
Ok(Self::new(
70+
secret,
71+
chain_length.try_into()?,
72+
sample_interval.try_into()?,
73+
))
6174
}
6275

6376
pub fn reveal_ith(&self, i: usize) -> Result<[u8; 32]> {
6477
ensure!(i < self.len(), "index not in range");
65-
Ok(self.hash[i].clone())
78+
79+
// Note that subsample_interval may not perfectly divide length, in which case the uneven segment is
80+
// actually at the *front* of the list. Thus, it's easier to compute indexes from the end of the list.
81+
let index_from_end_of_subsampled_list = ((self.len() - 1) - i) / self.sample_interval;
82+
let mut i_index = self.len() - 1 - index_from_end_of_subsampled_list * self.sample_interval;
83+
let mut val = self.hash[self.hash.len() - 1 - index_from_end_of_subsampled_list].clone();
84+
85+
while i_index > i {
86+
val = Keccak256::digest(val).into();
87+
i_index -= 1;
88+
}
89+
90+
Ok(val)
6691
}
6792

6893
pub fn len(&self) -> usize {
69-
self.hash.len()
94+
self.length
7095
}
7196
}
7297

@@ -99,3 +124,56 @@ impl HashChainState {
99124
self.hash_chains[chain_index].reveal_ith(sequence_number - self.offsets[chain_index])
100125
}
101126
}
127+
128+
#[cfg(test)]
129+
mod test {
130+
use {
131+
crate::state::PebbleHashChain,
132+
sha3::{
133+
Digest,
134+
Keccak256,
135+
},
136+
};
137+
138+
fn run_hash_chain_test(secret: [u8; 32], length: usize, sample_interval: usize) {
139+
// Calculate the hash chain the naive way as a comparison point to the subsampled implementation.
140+
let mut basic_chain = Vec::<[u8; 32]>::with_capacity(length);
141+
let mut current: [u8; 32] = Keccak256::digest(secret).into();
142+
basic_chain.push(current.clone());
143+
for _ in 1..length {
144+
current = Keccak256::digest(&current).into();
145+
basic_chain.push(current);
146+
}
147+
148+
basic_chain.reverse();
149+
150+
let chain = PebbleHashChain::new(secret, length, sample_interval);
151+
152+
let mut last_val = chain.reveal_ith(0).unwrap();
153+
for i in 1..length {
154+
let cur_val = chain.reveal_ith(i).unwrap();
155+
println!("{}", i);
156+
assert_eq!(basic_chain[i], cur_val);
157+
158+
let expected_last_val: [u8; 32] = Keccak256::digest(cur_val).into();
159+
assert_eq!(expected_last_val, last_val);
160+
last_val = cur_val;
161+
}
162+
}
163+
164+
#[test]
165+
fn test_hash_chain() {
166+
run_hash_chain_test([0u8; 32], 10, 1);
167+
run_hash_chain_test([0u8; 32], 10, 2);
168+
run_hash_chain_test([0u8; 32], 10, 3);
169+
run_hash_chain_test([1u8; 32], 10, 1);
170+
run_hash_chain_test([1u8; 32], 10, 2);
171+
run_hash_chain_test([1u8; 32], 10, 3);
172+
run_hash_chain_test([0u8; 32], 100, 1);
173+
run_hash_chain_test([0u8; 32], 100, 2);
174+
run_hash_chain_test([0u8; 32], 100, 3);
175+
run_hash_chain_test([0u8; 32], 100, 7);
176+
run_hash_chain_test([0u8; 32], 100, 50);
177+
run_hash_chain_test([0u8; 32], 100, 55);
178+
}
179+
}

0 commit comments

Comments
 (0)