Skip to content

Commit 16cb19d

Browse files
authored
Adding test vector generation example (#445)
1 parent 54608f9 commit 16cb19d

File tree

7 files changed

+351
-2
lines changed

7 files changed

+351
-2
lines changed

examples/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ cargo run -p examples -- fixture-generator \
6666
This will automatically write the new fixtures to the appropriate files under `examples/src/fixture_generator/examples/`, and
6767
the tests should now pass.
6868

69+
### Test Vectors
70+
71+
Similarly to the fixture generator, this is also used for testing and compatibility purposes, but specifically for generating test
72+
vectors that can be matched against on a separate client implementation. Note that the serialization of these structs is done
73+
through protobuf, so this can be used to double-check that a compatible client implementation can indeed parse the proof bytes
74+
that are generated by the server-side API. The resulting output files under `examples/src/test_vectors/`
75+
contain hex-encoded values for the inputs to lookup and history proof verification.
76+
77+
The test vector generation code can be run with the following command:
78+
```
79+
cargo run -p examples --release -- test-vectors \
80+
--out examples/src/test_vectors
81+
```
82+
6983
### WASM Client
7084

7185
This example, unlike the others, is not executable and is mainly intended to demonstrate how an application can build the WASM bindings

examples/src/fixture_generator/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mod examples;
2121
mod generator;
2222
mod parser;
2323
mod reader;
24-
mod writer;
24+
pub mod writer;
2525

2626
pub(crate) use parser::Args;
2727

examples/src/fixture_generator/writer/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use serde::Serialize;
1212

1313
/// Interface for writing output generated by the tool.
14-
pub(crate) trait Writer {
14+
pub trait Writer {
1515
/// Writes a serde serializable object.
1616
fn write_object(&mut self, object: impl Serialize);
1717

examples/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
mod fixture_generator;
1111
mod mysql_demo;
12+
mod test_vectors;
1213
mod wasm_client;
1314
mod whatsapp_kt_auditor;
1415

@@ -32,6 +33,8 @@ enum ExampleType {
3233
MysqlDemo(mysql_demo::CliArgs),
3334
/// Fixture Generator
3435
FixtureGenerator(fixture_generator::Args),
36+
/// Test vectors generator
37+
TestVectors(test_vectors::Args),
3538
}
3639

3740
// MAIN //
@@ -43,6 +46,7 @@ async fn main() -> Result<()> {
4346
ExampleType::WhatsappKtAuditor(args) => whatsapp_kt_auditor::render_cli(args).await?,
4447
ExampleType::MysqlDemo(args) => mysql_demo::render_cli(args).await?,
4548
ExampleType::FixtureGenerator(args) => fixture_generator::run(args).await,
49+
ExampleType::TestVectors(args) => test_vectors::run(args).await,
4650
}
4751

4852
Ok(())

examples/src/test_vectors/experimental.yaml

Lines changed: 41 additions & 0 deletions
Large diffs are not rendered by default.

examples/src/test_vectors/mod.rs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
//
3+
// This source code is dual-licensed under either the MIT license found in the
4+
// LICENSE-MIT file in the root directory of this source tree or the Apache
5+
// License, Version 2.0 found in the LICENSE-APACHE file in the root directory
6+
// of this source tree. You may select, at your option, one of the above-listed licenses.
7+
8+
//! Produces test vectors for various structs that can be used to verify operations
9+
//! in the client against what the server produces.
10+
11+
use crate::fixture_generator::writer::yaml::YamlWriter;
12+
use crate::fixture_generator::writer::Writer;
13+
use akd::directory::Directory;
14+
use akd::ecvrf::HardCodedAkdVRF;
15+
use akd::hash::DIGEST_BYTES;
16+
use akd::storage::memory::AsyncInMemoryDatabase;
17+
use akd::storage::StorageManager;
18+
use akd::verify::{key_history_verify, lookup_verify};
19+
use akd::{
20+
AkdLabel, AkdValue, DomainLabel, HistoryParams, HistoryVerificationParams, NamedConfiguration,
21+
};
22+
use anyhow::Result;
23+
use clap::Parser;
24+
use protobuf::Message;
25+
use serde::{Deserialize, Serialize};
26+
use std::fs::File;
27+
use std::io::Write;
28+
29+
// "@" has to be separated from "generated" or linters might ignore this file
30+
const HEADER_COMMENT: &str = concat!(
31+
"@",
32+
"generated This file was automatically generated by \n\
33+
the test vectors tool with the following command:\n\n\
34+
cargo run -p examples -- test-vectors \\"
35+
);
36+
const METADATA_COMMENT: &str = "Metadata";
37+
38+
/// Metadata about the output, including arguments passed to this tool and
39+
/// the tool version.
40+
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
41+
pub struct Metadata {
42+
pub args: Args,
43+
pub version: String,
44+
pub configuration: String,
45+
pub domain_label: String,
46+
}
47+
48+
#[derive(Parser, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49+
pub(crate) struct Args {
50+
/// Name of output path.
51+
/// If omitted, output will be printed to stdout.
52+
#[arg(long = "out", short = 'o')]
53+
out: Option<String>,
54+
}
55+
56+
pub async fn run(args: Args) {
57+
// NOTE(new_config): Add new configurations here
58+
type L = akd::ExampleLabel;
59+
generate::<akd::WhatsAppV1Configuration, L>(&args)
60+
.await
61+
.unwrap();
62+
generate::<akd::ExperimentalConfiguration<L>, L>(&args)
63+
.await
64+
.unwrap();
65+
}
66+
67+
pub(crate) async fn generate<TC: NamedConfiguration, L: DomainLabel>(args: &Args) -> Result<()> {
68+
// initialize writer
69+
let buffer: Box<dyn Write> = if let Some(ref file_path) = args.out {
70+
Box::new(File::create(format!("{}/{}.yaml", file_path, TC::name())).unwrap())
71+
} else {
72+
Box::new(std::io::stdout())
73+
};
74+
let mut writer = YamlWriter::new(buffer);
75+
76+
// write raw args as comment
77+
let raw_args = format!(
78+
" {}",
79+
std::env::args().skip(1).collect::<Vec<String>>().join(" ")
80+
);
81+
writer.write_comment(HEADER_COMMENT);
82+
raw_args
83+
.split(" -")
84+
.skip(1)
85+
.map(|arg| format!(" -{arg} \\"))
86+
.for_each(|comment| writer.write_comment(&comment));
87+
88+
// write fixture metadata
89+
let comment = METADATA_COMMENT.to_string();
90+
let metadata = Metadata {
91+
args: args.clone(),
92+
version: env!("CARGO_PKG_VERSION").to_string(),
93+
configuration: TC::name().to_string(),
94+
domain_label: String::from_utf8(L::domain_label().to_vec()).unwrap(),
95+
};
96+
writer.write_line();
97+
writer.write_comment(&comment);
98+
writer.write_object(metadata);
99+
100+
let db = AsyncInMemoryDatabase::new();
101+
let storage_manager = StorageManager::new_no_cache(db);
102+
let vrf = HardCodedAkdVRF {};
103+
// epoch 0
104+
let akd = Directory::<TC, _, _>::new(storage_manager, vrf).await?;
105+
let vrf_pk = akd.get_public_key().await?;
106+
107+
let num_labels = 5;
108+
let num_epochs = 50;
109+
110+
let label_to_write = num_labels - 1;
111+
let epoch_to_write = num_epochs - 1;
112+
113+
let mut previous_hash = [0u8; DIGEST_BYTES];
114+
for epoch in 1..num_epochs {
115+
let mut to_insert = vec![];
116+
for i in 0..num_labels {
117+
let index = 1 << i;
118+
let label = AkdLabel::from(format!("{index}").as_str());
119+
let value = AkdValue::from(format!("{index},{epoch}").as_str());
120+
if epoch % index == 0 {
121+
to_insert.push((label, value));
122+
}
123+
}
124+
let epoch_hash = akd.publish(to_insert).await?;
125+
126+
if epoch > 1 {
127+
let audit_proof = akd
128+
.audit(epoch_hash.epoch() - 1, epoch_hash.epoch())
129+
.await?;
130+
akd::auditor::audit_verify::<TC>(vec![previous_hash, epoch_hash.hash()], audit_proof)
131+
.await?;
132+
}
133+
134+
previous_hash = epoch_hash.hash();
135+
136+
for i in 0..num_labels {
137+
let index = 1 << i;
138+
if epoch < index {
139+
// Cannot produce proofs if there are no versions added yet for that user
140+
continue;
141+
}
142+
let latest_added_epoch = epoch_hash.epoch() - (epoch_hash.epoch() % index);
143+
let label = AkdLabel::from(format!("{index}").as_str());
144+
let lookup_value = AkdValue::from(format!("{index},{latest_added_epoch}").as_str());
145+
146+
let (lookup_proof, epoch_hash_from_lookup) = akd.lookup(label.clone()).await?;
147+
assert_eq!(epoch_hash, epoch_hash_from_lookup);
148+
let lookup_verify_result = lookup_verify::<TC>(
149+
vrf_pk.as_bytes(),
150+
epoch_hash.hash(),
151+
epoch_hash.epoch(),
152+
label.clone(),
153+
lookup_proof.clone(),
154+
)
155+
.unwrap();
156+
assert_eq!(lookup_verify_result.epoch, latest_added_epoch);
157+
assert_eq!(lookup_verify_result.value, lookup_value);
158+
assert_eq!(lookup_verify_result.version, epoch / index);
159+
160+
let (history_proof_complete, epoch_hash_from_history_complete) =
161+
akd.key_history(&label, HistoryParams::Complete).await?;
162+
assert_eq!(epoch_hash, epoch_hash_from_history_complete);
163+
164+
let history_results_complete = key_history_verify::<TC>(
165+
vrf_pk.as_bytes(),
166+
epoch_hash.hash(),
167+
epoch_hash.epoch(),
168+
label.clone(),
169+
history_proof_complete.clone(),
170+
HistoryVerificationParams::default(),
171+
)
172+
.unwrap();
173+
for (j, res) in history_results_complete.iter().enumerate() {
174+
let added_in_epoch =
175+
epoch_hash.epoch() - (epoch_hash.epoch() % index) - (j as u64) * index;
176+
let history_value = AkdValue::from(format!("{index},{added_in_epoch}").as_str());
177+
assert_eq!(res.epoch, added_in_epoch);
178+
assert_eq!(res.value, history_value);
179+
assert_eq!(res.version, epoch / index - j as u64);
180+
}
181+
182+
let (history_proof_partial, epoch_hash_from_history_partial) = akd
183+
.key_history(&label, HistoryParams::MostRecent(1))
184+
.await?;
185+
assert_eq!(epoch_hash, epoch_hash_from_history_partial);
186+
187+
let history_results_partial = key_history_verify::<TC>(
188+
vrf_pk.as_bytes(),
189+
epoch_hash.hash(),
190+
epoch_hash.epoch(),
191+
label.clone(),
192+
history_proof_partial.clone(),
193+
HistoryVerificationParams::Default {
194+
history_params: HistoryParams::MostRecent(1),
195+
},
196+
)
197+
.unwrap();
198+
assert_eq!(history_results_partial.len(), 1);
199+
for (j, res) in history_results_partial.iter().enumerate() {
200+
let added_in_epoch =
201+
epoch_hash.epoch() - (epoch_hash.epoch() % index) - (j as u64) * index;
202+
let history_value = AkdValue::from(format!("{index},{added_in_epoch}").as_str());
203+
assert_eq!(res.epoch, added_in_epoch);
204+
assert_eq!(res.value, history_value);
205+
assert_eq!(res.version, epoch / index - j as u64);
206+
}
207+
208+
if (i, epoch) == (label_to_write, epoch_to_write) {
209+
writer.write_line();
210+
writer.write_comment("Public Key");
211+
writer.write_object(hex::encode(vrf_pk.as_bytes()));
212+
writer.write_line();
213+
writer.write_comment("Epoch Hash");
214+
writer.write_object(hex::encode(epoch_hash.hash()));
215+
writer.write_line();
216+
writer.write_comment("Epoch");
217+
writer.write_object(epoch_hash.epoch());
218+
writer.write_line();
219+
writer.write_comment("Label");
220+
writer.write_object(hex::encode(&label.clone().0));
221+
writer.write_line();
222+
writer.write_comment("Lookup Proof");
223+
writer.write_object(hex::encode(
224+
akd_core::proto::specs::types::LookupProof::from(&lookup_proof)
225+
.write_to_bytes()?,
226+
));
227+
writer.write_line();
228+
writer.write_comment("History Proof (HistoryParams::MostRecent(1))");
229+
writer.write_object(hex::encode(
230+
akd_core::proto::specs::types::HistoryProof::from(&history_proof_partial)
231+
.write_to_bytes()?,
232+
));
233+
writer.write_line();
234+
writer.write_comment(&format!(
235+
"History Proof (HistoryParams::Complete with {} versions)",
236+
history_results_complete.len()
237+
));
238+
writer.write_object(hex::encode(
239+
akd_core::proto::specs::types::HistoryProof::from(&history_proof_complete)
240+
.write_to_bytes()?,
241+
));
242+
}
243+
}
244+
}
245+
246+
// flush writer and exit
247+
writer.flush();
248+
Ok(())
249+
}

0 commit comments

Comments
 (0)