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
219 changes: 195 additions & 24 deletions fuzz/fuzz_targets/api_surface.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
#![no_main]

use std::collections::{BTreeMap, HashMap, HashSet};
mod reference;

use std::{
collections::{BTreeMap, HashMap, HashSet},
path::{Path, PathBuf},
};

use arbitrary::Arbitrary;
use bitvec::view::BitView;
use libfuzzer_sys::fuzz_target;

use nomt::{
hasher::{Blake3Hasher, ValueHasher},
trie::{KeyPath, ValueHash},
KeyReadWrite, Nomt, Options, SessionParams, Value, WitnessMode,
proof::{verify_update, PathUpdate},
trie::{KeyPath, LeafData, Node, ValueHash},
KeyReadWrite, Nomt, Options, SessionParams, Value, Witness, WitnessMode,
};
use nomt_core::update::build_trie;

fuzz_target!(|run: Run| {
let db = open_db(run.commit_concurrency);
let tempdir = tempfile::tempdir().unwrap();
let path = tempdir.path().join("db");
let mut db = Nomt::<Blake3Hasher>::open(run.open.options(&path)).unwrap();
let mut model = BTreeMap::<KeyPath, ValueHash>::new();

for call in run.calls.calls {
match call {
NomtCall::Reopen => {
drop(db);
db = Nomt::open(run.open.options(&path)).unwrap();
assert_eq!(db.root().into_inner(), reference_root(&model));
for (key_path, value_hash) in &model {
let value = db.read(*key_path).unwrap().unwrap();
assert_eq!(Blake3Hasher::hash_value(&value), *value_hash);
}
}
NomtCall::BeginSession { session_calls } => {
let session = db.begin_session(
SessionParams::default().witness_mode(WitnessMode::read_write()),
Expand All @@ -35,7 +53,33 @@ fuzz_target!(|run: Run| {
SessionCall::TentativeWrite { key_path } => {
session.warm_up(key_path);
}
SessionCall::Prove { key_path } => {
let verified = session
.prove(key_path)
.unwrap()
.verify::<Blake3Hasher>(
key_path.view_bits(),
session.prev_root().into_inner(),
)
.unwrap();
match model.get(&key_path) {
Some(value_hash) => assert!(verified
.confirm_value(&LeafData {
key_path,
value_hash: *value_hash,
})
.unwrap()),
None => {
assert!(verified.confirm_nonexistence(&key_path).unwrap())
}
}
}
SessionCall::CommitAndProve { keys } => {
let prev_root = db.root().into_inner();
let mut finished = session.finish(keys.clone()).unwrap();
let witness = finished.take_witness().unwrap();
check_witness(witness, &keys, &model, prev_root, finished.root());

for (key_path, key_rw) in &keys {
match key_rw {
KeyReadWrite::Write(Some(value))
Expand All @@ -49,11 +93,15 @@ fuzz_target!(|run: Run| {
KeyReadWrite::Read(_) => {}
}
}
let _ = session.finish(keys).unwrap().commit(&db).unwrap();
let expected_root = build_trie::<Blake3Hasher>(
0,
model.iter().map(|(k, v)| (*k, *v)),
|_| {},
finished.commit(&db).unwrap();
let expected_root = reference_root(&model);
assert_eq!(
build_trie::<Blake3Hasher>(
0,
model.iter().map(|(k, v)| (*k, *v)),
|_| {},
),
expected_root
);
assert_eq!(db.root().into_inner(), expected_root);
break;
Expand All @@ -69,10 +117,95 @@ fuzz_target!(|run: Run| {
}
});

fn reference_root(model: &BTreeMap<KeyPath, ValueHash>) -> Node {
let leaves = model.iter().map(|(k, v)| (*k, *v)).collect::<Vec<_>>();
reference::node(&leaves, 0)
}

/// Checks that the witness proves exactly the session's reads and writes against `prev_root`, with
/// the values in `model`, and that applying its writes yields `new_root`.
fn check_witness(
witness: Witness,
keys: &[(KeyPath, KeyReadWrite)],
model: &BTreeMap<KeyPath, ValueHash>,
prev_root: Node,
new_root: nomt::Root,
) {
let verified = witness
.path_proofs
.iter()
.map(|p| {
p.inner
.verify::<Blake3Hasher>(p.path.path(), prev_root)
.unwrap()
})
.collect::<Vec<_>>();

let mut reads = Vec::new();
for read in &witness.operations.reads {
assert_eq!(read.value, model.get(&read.key).copied());
let proof = &verified[read.path_index];
match read.value {
Some(value_hash) => assert!(proof
.confirm_value(&LeafData {
key_path: read.key,
value_hash,
})
.unwrap()),
None => assert!(proof.confirm_nonexistence(&read.key).unwrap()),
}
reads.push(read.key);
}
reads.sort_unstable();
let expected_reads = keys
.iter()
.filter(|(_, rw)| matches!(rw, KeyReadWrite::Read(_) | KeyReadWrite::ReadThenWrite(..)))
.map(|(k, _)| *k)
.collect::<Vec<_>>();
assert_eq!(reads, expected_reads);

let mut writes = Vec::new();
let mut ops_by_path = BTreeMap::<usize, Vec<(KeyPath, Option<ValueHash>)>>::new();
for write in &witness.operations.writes {
writes.push((write.key, write.value));
ops_by_path
.entry(write.path_index)
.or_default()
.push((write.key, write.value));
}
writes.sort_unstable();
let expected_writes = keys
.iter()
.filter_map(|(k, rw)| match rw {
KeyReadWrite::Write(v) | KeyReadWrite::ReadThenWrite(_, v) => {
Some((*k, v.as_ref().map(|v| Blake3Hasher::hash_value(v))))
}
KeyReadWrite::Read(_) => None,
})
.collect::<Vec<_>>();
assert_eq!(writes, expected_writes);

let mut updates = ops_by_path
.into_iter()
.map(|(i, ops)| PathUpdate {
inner: verified[i].clone(),
ops,
})
.collect::<Vec<_>>();
// Witness path proofs may be emitted in worker order rather than trie path order.
updates.sort_by(|a, b| a.inner.path().cmp(b.inner.path()));
assert_eq!(
verify_update::<Blake3Hasher>(prev_root, &updates).unwrap(),
new_root.into_inner()
);
}

struct Context {
/// All the key paths that have been created, irregardless of whether they have been committed
/// or not.
key_paths: HashSet<KeyPath>,
/// The same key paths in creation order, so that picking one is deterministic.
key_path_list: Vec<KeyPath>,
/// The key value pairs that have been committed into the store.
///
/// The u32 is the index of the key path in `key_paths`.
Expand All @@ -86,9 +219,9 @@ impl Context {
) -> arbitrary::Result<Option<KeyPath>> {
let mut attempts = 0;
loop {
let mut key_path: [u8; 32] = [0; 32];
input.fill_buffer(&mut key_path)?;
let key_path = reference::new_key(input, &self.key_path_list)?;
if self.key_paths.insert(key_path) {
self.key_path_list.push(key_path);
return Ok(Some(key_path));
}
attempts += 1;
Expand Down Expand Up @@ -125,23 +258,56 @@ impl Context {

#[derive(Debug)]
struct Run {
commit_concurrency: usize,
open: OpenParams,
calls: NomtCalls,
}

impl<'a> Arbitrary<'a> for Run {
fn arbitrary(input: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let run = Run {
commit_concurrency: input.int_in_range(1..=4)?,
open: OpenParams {
commit_concurrency: input.int_in_range(1..=4)?,
page_cache_size: input.int_in_range(1..=32)?,
leaf_cache_size: input.int_in_range(1..=32)?,
page_cache_upper_levels: input.int_in_range(0..=2)?,
prepopulate_page_cache: input.arbitrary()?,
},
calls: input.arbitrary()?,
};
Ok(run)
}
}

#[derive(Debug)]
struct OpenParams {
commit_concurrency: usize,
page_cache_size: usize,
leaf_cache_size: usize,
page_cache_upper_levels: usize,
prepopulate_page_cache: bool,
}

impl OpenParams {
fn options(&self, path: &Path) -> Options {
let mut options = Options::new();
options.path(PathBuf::from(path));
options.commit_concurrency(self.commit_concurrency);
options.page_cache_size(self.page_cache_size);
options.leaf_cache_size(self.leaf_cache_size);
options.page_cache_upper_levels(self.page_cache_upper_levels);
options.prepopulate_page_cache(self.prepopulate_page_cache);
options.preallocate_ht(false);
options
}
}

#[derive(Debug)]
enum NomtCall {
BeginSession { session_calls: Vec<SessionCall> },
BeginSession {
session_calls: Vec<SessionCall>,
},
/// Close and reopen the database, then check every committed value.
Reopen,
}

#[derive(Debug)]
Expand All @@ -153,7 +319,11 @@ enum SessionCall {
TentativeWrite {
key_path: KeyPath,
},
/// Commit and prove the given keys.
/// Prove the key against the session's base root.
Prove {
key_path: KeyPath,
},
/// Commit the given keys and check the witness.
CommitAndProve {
keys: Vec<(KeyPath, KeyReadWrite)>,
},
Expand All @@ -173,10 +343,15 @@ impl<'a> Arbitrary<'a> for NomtCalls {

let mut cx = Context {
key_paths: HashSet::new(),
key_path_list: Vec::new(),
committed: HashMap::new(),
};

for _ in 0..call_cnt {
if input.ratio(1, 8)? {
calls.push(NomtCall::Reopen);
}

let session_sz = input.int_in_range(0..=1000)?;

// Collect all the available keys into a vector and destroy the non-determinism
Expand Down Expand Up @@ -230,6 +405,11 @@ impl<'a> Arbitrary<'a> for NomtCalls {
// api.
let mut session_calls = Vec::new();
for (key_path, key_rw) in &session {
if input.ratio(1, 8)? {
session_calls.push(SessionCall::Prove {
key_path: *key_path,
});
}
match key_rw {
KeyReadWrite::ReadThenWrite(v, _) | KeyReadWrite::Read(v) => {
session_calls.push(SessionCall::TentativeRead {
Expand Down Expand Up @@ -271,12 +451,3 @@ impl<'a> Arbitrary<'a> for NomtCalls {
Ok(NomtCalls { calls })
}
}

fn open_db(commit_concurrency: usize) -> Nomt<Blake3Hasher> {
let tempfile = tempfile::tempdir().unwrap();
let db_path = tempfile.path().join("db");
let mut options = Options::new();
options.path(db_path);
options.commit_concurrency(commit_concurrency);
Nomt::open(options).unwrap()
}
44 changes: 6 additions & 38 deletions fuzz/fuzz_targets/proof_verify.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
#![no_main]

mod reference;

use std::collections::{BTreeMap, BTreeSet};

use arbitrary::{Arbitrary, Unstructured};
use bitvec::{order::Msb0, view::BitView};
use bitvec::view::BitView;
use libfuzzer_sys::fuzz_target;
use nomt_core::{
hasher::{Blake3Hasher, NodeHasher},
hasher::Blake3Hasher,
proof::{
verify_multi_proof, verify_multi_proof_update, verify_update, KeyOutOfScope, MultiProof,
PathProof, PathProofTerminal, PathUpdate,
},
trie::{InternalData, KeyPath, LeafData, Node, ValueHash, TERMINATOR},
trie::{KeyPath, LeafData, Node, ValueHash},
trie_pos::TriePosition,
update::build_trie,
};
use reference::{bit, new_key, node};

type H = Blake3Hasher;

Expand Down Expand Up @@ -170,28 +173,6 @@ fn check_claims(
}
}

fn bit(key: &KeyPath, depth: usize) -> bool {
key.view_bits::<Msb0>()[depth]
}

/// Reference hash of the sub-trie holding `leaves`: sorted, distinct, sharing the first `depth` bits.
fn node(leaves: &[(KeyPath, ValueHash)], depth: usize) -> Node {
match leaves {
[] => TERMINATOR,
[(key_path, value_hash)] => H::hash_leaf(&LeafData {
key_path: *key_path,
value_hash: *value_hash,
}),
_ => {
let mid = leaves.partition_point(|(k, _)| !bit(k, depth));
H::hash_internal(&InternalData {
left: node(&leaves[..mid], depth + 1),
right: node(&leaves[mid..], depth + 1),
})
}
}
}

fn path_proof(mut leaves: &[(KeyPath, ValueHash)], key: &KeyPath) -> PathProof {
let mut siblings = Vec::new();
let mut depth = 0;
Expand Down Expand Up @@ -269,19 +250,6 @@ impl<'a> Arbitrary<'a> for Run {
}
}

/// Mostly derive keys from existing ones, diverging at a chosen bit, so the trie gets deep.
fn new_key(input: &mut Unstructured<'_>, existing: &[KeyPath]) -> arbitrary::Result<KeyPath> {
let mut key: KeyPath = input.arbitrary()?;
if !existing.is_empty() && input.ratio(3, 4)? {
let base = existing[input.choose_index(existing.len())?];
let diverge_at = input.int_in_range(0..=255)?;
let bits = key.view_bits_mut::<Msb0>();
bits[..diverge_at].copy_from_bitslice(&base.view_bits::<Msb0>()[..diverge_at]);
bits.set(diverge_at, !bit(&base, diverge_at));
}
Ok(key)
}

#[derive(Debug, Arbitrary)]
enum Mutation {
FlipSiblingBit { index: usize, bit: u8 },
Expand Down
Loading
Loading