Skip to content

Commit 2b2c9a0

Browse files
authored
feat: update proof types to be generic over mutable or immutable collections (#1121)
In a future PR, there will be several cleanups that take advantage of proof being able to switch back and forth from immutable and mutable containers. Particularly in testing, but a few other cases as well. See #1117 for a raw view. The `EmptyProofCollection` is primarily for tests.
1 parent 3750f3d commit 2b2c9a0

File tree

7 files changed

+179
-36
lines changed

7 files changed

+179
-36
lines changed

firewood/src/db.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
)]
88

99
use crate::merkle::Merkle;
10-
use crate::proof::{Proof, ProofNode};
11-
use crate::range_proof::RangeProof;
1210
use crate::stream::MerkleKeyValueStream;
13-
use crate::v2::api::{self, KeyType, ValueType};
11+
use crate::v2::api::{self, FrozenProof, FrozenRangeProof, KeyType, ValueType};
1412
pub use crate::v2::api::{Batch, BatchOp};
1513

1614
use crate::manager::{RevisionManager, RevisionManagerConfig};
@@ -107,10 +105,7 @@ impl api::DbView for HistoricalRev {
107105
Ok(merkle.get_value(key.as_ref())?)
108106
}
109107

110-
async fn single_key_proof<K: api::KeyType>(
111-
&self,
112-
key: K,
113-
) -> Result<Proof<ProofNode>, api::Error> {
108+
async fn single_key_proof<K: api::KeyType>(&self, key: K) -> Result<FrozenProof, api::Error> {
114109
let merkle = Merkle::from(self);
115110
merkle.prove(key.as_ref()).map_err(api::Error::from)
116111
}
@@ -120,7 +115,7 @@ impl api::DbView for HistoricalRev {
120115
_first_key: Option<K>,
121116
_last_key: Option<K>,
122117
_limit: Option<usize>,
123-
) -> Result<Option<RangeProof<Box<[u8]>, Box<[u8]>, ProofNode>>, api::Error> {
118+
) -> Result<FrozenRangeProof, api::Error> {
124119
todo!()
125120
}
126121

@@ -382,7 +377,7 @@ impl api::DbView for Proposal<'_> {
382377
merkle.get_value(key.as_ref()).map_err(api::Error::from)
383378
}
384379

385-
async fn single_key_proof<K: KeyType>(&self, key: K) -> Result<Proof<ProofNode>, api::Error> {
380+
async fn single_key_proof<K: KeyType>(&self, key: K) -> Result<FrozenProof, api::Error> {
386381
let merkle = Merkle::from(self.nodestore.clone());
387382
merkle.prove(key.as_ref()).map_err(api::Error::from)
388383
}
@@ -392,7 +387,7 @@ impl api::DbView for Proposal<'_> {
392387
_first_key: Option<K>,
393388
_last_key: Option<K>,
394389
_limit: Option<usize>,
395-
) -> Result<Option<api::RangeProof<Box<[u8]>, Box<[u8]>, ProofNode>>, api::Error> {
390+
) -> Result<FrozenRangeProof, api::Error> {
396391
todo!()
397392
}
398393

firewood/src/merkle.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ use crate::range_proof::RangeProof;
2020
#[cfg(test)]
2121
use crate::stream::MerkleKeyValueStream;
2222
use crate::stream::PathIterator;
23+
use crate::v2::api::FrozenProof;
2324
#[cfg(test)]
24-
use crate::v2::api;
25+
use crate::v2::api::{self, FrozenRangeProof};
2526
use firewood_storage::{
2627
BranchNode, Child, FileIoError, HashType, Hashable, HashedNodeReader, ImmutableProposal,
2728
IntoHashType, LeafNode, MaybePersistedNode, MutableProposal, NibblesIterator, Node, NodeStore,
@@ -178,7 +179,7 @@ impl<T: TrieReader> Merkle<T> {
178179

179180
/// Returns a proof that the given key has a certain value,
180181
/// or that the key isn't in the trie.
181-
pub fn prove(&self, key: &[u8]) -> Result<Proof<ProofNode>, ProofError> {
182+
pub fn prove(&self, key: &[u8]) -> Result<FrozenProof, ProofError> {
182183
let Some(root) = self.root() else {
183184
return Err(ProofError::Empty);
184185
};
@@ -216,7 +217,7 @@ impl<T: TrieReader> Merkle<T> {
216217
});
217218
}
218219

219-
Ok(Proof(proof.into_boxed_slice()))
220+
Ok(Proof::new(proof.into_boxed_slice()))
220221
}
221222

222223
/// Verify a proof that a key has a certain value, or that the key isn't in the trie.
@@ -258,7 +259,7 @@ impl<T: TrieReader> Merkle<T> {
258259
start_key: Option<&[u8]>,
259260
end_key: Option<&[u8]>,
260261
limit: Option<NonZeroUsize>,
261-
) -> Result<RangeProof<Box<[u8]>, Box<[u8]>, ProofNode>, api::Error> {
262+
) -> Result<FrozenRangeProof, api::Error> {
262263
if let (Some(k1), Some(k2)) = (&start_key, &end_key) {
263264
if k1 > k2 {
264265
return Err(api::Error::InvalidRange {

firewood/src/proof.rs

Lines changed: 153 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,10 @@ impl From<PathIterItem> for ProofNode {
146146
}
147147

148148
/// A proof that a given key-value pair either exists or does not exist in a trie.
149-
#[derive(Clone, Debug)]
150-
pub struct Proof<T: Hashable>(pub Box<[T]>);
149+
#[derive(Clone, Debug, PartialEq, Eq, Default)]
150+
pub struct Proof<T: ?Sized>(T);
151151

152-
impl<T: Hashable> Proof<T> {
152+
impl<T: ProofCollection + ?Sized> Proof<T> {
153153
/// Verify a proof
154154
pub fn verify<K: AsRef<[u8]>, V: AsRef<[u8]>>(
155155
&self,
@@ -164,20 +164,20 @@ impl<T: Hashable> Proof<T> {
164164
/// with the given `root_hash`. If the key does not exist in the trie, returns `None`.
165165
/// Returns an error if the proof is invalid or doesn't prove the key for the
166166
/// given revision.
167-
fn value_digest<K: AsRef<[u8]>>(
167+
pub fn value_digest<K: AsRef<[u8]>>(
168168
&self,
169169
key: K,
170170
root_hash: &TrieHash,
171171
) -> Result<Option<ValueDigest<&[u8]>>, ProofError> {
172172
let key: Box<[u8]> = NibblesIterator::new(key.as_ref()).collect();
173173

174-
let Some(last_node) = self.0.last() else {
174+
let Some(last_node) = self.0.as_ref().last() else {
175175
return Err(ProofError::Empty);
176176
};
177177

178178
let mut expected_hash = root_hash.clone().into_hash_type();
179179

180-
let mut iter = self.0.iter().peekable();
180+
let mut iter = self.0.as_ref().iter().peekable();
181181
while let Some(node) = iter.next() {
182182
if node.to_hash() != expected_hash {
183183
return Err(ProofError::UnexpectedHash);
@@ -224,6 +224,153 @@ impl<T: Hashable> Proof<T> {
224224
// This is an exclusion proof.
225225
Ok(None)
226226
}
227+
228+
/// Returns the length of the proof.
229+
#[must_use]
230+
pub fn len(&self) -> usize {
231+
self.0.as_ref().len()
232+
}
233+
234+
/// Returns true if the proof is empty.
235+
#[must_use]
236+
pub fn is_empty(&self) -> bool {
237+
self.0.as_ref().is_empty()
238+
}
239+
}
240+
241+
impl<T: ProofCollection + ?Sized> std::ops::Deref for Proof<T> {
242+
type Target = T;
243+
244+
#[inline]
245+
fn deref(&self) -> &Self::Target {
246+
&self.0
247+
}
248+
}
249+
250+
impl<T: ProofCollection + ?Sized> std::ops::DerefMut for Proof<T> {
251+
#[inline]
252+
fn deref_mut(&mut self) -> &mut Self::Target {
253+
&mut self.0
254+
}
255+
}
256+
257+
impl<T: ProofCollection> Proof<T> {
258+
/// Constructs a new proof from a collection of proof nodes.
259+
#[inline]
260+
#[must_use]
261+
pub const fn new(proof: T) -> Self {
262+
Self(proof)
263+
}
264+
}
265+
266+
impl Proof<EmptyProofCollection> {
267+
/// Constructs a new empty proof.
268+
#[inline]
269+
#[must_use]
270+
pub const fn empty() -> Self {
271+
Self::new(EmptyProofCollection)
272+
}
273+
274+
/// Converts an empty immutable proof into an empty mutable proof.
275+
#[inline]
276+
#[must_use]
277+
pub const fn into_mutable<T: Hashable>(self) -> Proof<Vec<T>> {
278+
Proof::new(Vec::new())
279+
}
280+
}
281+
282+
impl<T: Hashable> Proof<Box<[T]>> {
283+
/// Converts an immutable proof into a mutable proof.
284+
#[inline]
285+
#[must_use]
286+
pub fn into_mutable(self) -> Proof<Vec<T>> {
287+
Proof::new(self.0.into_vec())
288+
}
289+
}
290+
291+
impl<T: Hashable> Proof<Vec<T>> {
292+
/// Converts a mutable proof into an immutable proof.
293+
#[inline]
294+
#[must_use]
295+
pub fn into_immutable(self) -> Proof<Box<[T]>> {
296+
Proof::new(self.0.into_boxed_slice())
297+
}
298+
}
299+
300+
impl<T, V> Proof<V>
301+
where
302+
T: Hashable,
303+
V: ProofCollection<Node = T> + IntoIterator<Item = T> + FromIterator<T>,
304+
{
305+
/// Joins two proofs into one.
306+
#[inline]
307+
#[must_use]
308+
pub fn join<O: ProofCollection<Node = T> + IntoIterator<Item = T>>(
309+
self,
310+
other: Proof<O>,
311+
) -> Proof<V> {
312+
self.into_iter().chain(other).collect()
313+
}
314+
}
315+
316+
impl<V: ProofCollection + FromIterator<V::Node>> FromIterator<V::Node> for Proof<V> {
317+
#[inline]
318+
fn from_iter<I: IntoIterator<Item = V::Node>>(iter: I) -> Self {
319+
Proof(iter.into_iter().collect())
320+
}
321+
}
322+
323+
impl<V: ProofCollection + Extend<V::Node>> Extend<V::Node> for Proof<V> {
324+
#[inline]
325+
fn extend<I: IntoIterator<Item = V::Node>>(&mut self, iter: I) {
326+
self.0.extend(iter);
327+
}
328+
}
329+
330+
impl<V: ProofCollection + IntoIterator<Item = V::Node>> IntoIterator for Proof<V> {
331+
type Item = V::Node;
332+
type IntoIter = V::IntoIter;
333+
334+
#[inline]
335+
fn into_iter(self) -> Self::IntoIter {
336+
self.0.into_iter()
337+
}
338+
}
339+
340+
/// A trait representing a collection of proof nodes.
341+
///
342+
/// This allows [`Proof`] to be generic over different types of collections such
343+
/// a `Box<[T]>` or `Vec<T>`, where `T` implements the `Hashable` trait.
344+
pub trait ProofCollection: AsRef<[Self::Node]> {
345+
/// The type of nodes in the proof collection.
346+
type Node: Hashable;
347+
}
348+
349+
impl<T: Hashable> ProofCollection for [T] {
350+
type Node = T;
351+
}
352+
353+
impl<T: Hashable> ProofCollection for Box<[T]> {
354+
type Node = T;
355+
}
356+
357+
impl<T: Hashable> ProofCollection for Vec<T> {
358+
type Node = T;
359+
}
360+
361+
/// A zero-sized type to represent an empty proof collection.
362+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
363+
pub struct EmptyProofCollection;
364+
365+
impl AsRef<[ProofNode]> for EmptyProofCollection {
366+
#[inline]
367+
fn as_ref(&self) -> &[ProofNode] {
368+
&[]
369+
}
370+
}
371+
372+
impl ProofCollection for EmptyProofCollection {
373+
type Node = ProofNode;
227374
}
228375

229376
/// Returns the next nibble in `c` after `b`.

firewood/src/range_proof.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
22
// See the file LICENSE.md for licensing terms.
33

4-
use firewood_storage::Hashable;
5-
64
use crate::proof::Proof;
75

86
/// A range proof proves that a given set of key-value pairs
97
/// are in the trie with a given root hash.
108
#[derive(Debug)]
11-
pub struct RangeProof<K: AsRef<[u8]>, V: AsRef<[u8]>, H: Hashable> {
9+
pub struct RangeProof<K: AsRef<[u8]>, V: AsRef<[u8]>, H> {
1210
#[expect(dead_code)]
1311
pub(crate) start_proof: Option<Proof<H>>,
1412
#[expect(dead_code)]

firewood/src/v2/api.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ impl<T> ValueType for T where T: AsRef<[u8]> + Send + Sync + Debug {}
4444
/// proof
4545
pub type HashKey = firewood_storage::TrieHash;
4646

47+
/// A frozen proof is a proof that is stored in immutable memory.
48+
pub type FrozenRangeProof = RangeProof<Box<[u8]>, Box<[u8]>, Box<[ProofNode]>>;
49+
50+
/// A frozen proof uses an immutable collection of proof nodes.
51+
pub type FrozenProof = Proof<Box<[ProofNode]>>;
52+
4753
/// A key/value pair operation. Only put (upsert) and delete are
4854
/// supported
4955
#[derive(Debug)]
@@ -244,7 +250,7 @@ pub trait DbView {
244250
async fn val<K: KeyType>(&self, key: K) -> Result<Option<Box<[u8]>>, Error>;
245251

246252
/// Obtain a proof for a single key
247-
async fn single_key_proof<K: KeyType>(&self, key: K) -> Result<Proof<ProofNode>, Error>;
253+
async fn single_key_proof<K: KeyType>(&self, key: K) -> Result<FrozenProof, Error>;
248254

249255
/// Obtain a range proof over a set of keys
250256
///
@@ -259,7 +265,7 @@ pub trait DbView {
259265
first_key: Option<K>,
260266
last_key: Option<K>,
261267
limit: Option<usize>,
262-
) -> Result<Option<RangeProof<Box<[u8]>, Box<[u8]>, ProofNode>>, Error>;
268+
) -> Result<FrozenRangeProof, Error>;
263269

264270
/// Obtain a stream over the keys/values of this view, using an optional starting point
265271
///

firewood/src/v2/emptydb.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
22
// See the file LICENSE.md for licensing terms.
33

4-
use crate::proof::{Proof, ProofNode};
5-
use crate::range_proof::RangeProof;
6-
74
use super::api::{Batch, Db, DbView, Error, HashKey, KeyType, ValueType};
85
use super::propose::{Proposal, ProposalBase};
6+
use crate::v2::api::{FrozenProof, FrozenRangeProof};
97
use async_trait::async_trait;
108
use futures::Stream;
119
use std::sync::Arc;
@@ -66,7 +64,7 @@ impl DbView for HistoricalImpl {
6664
Ok(None)
6765
}
6866

69-
async fn single_key_proof<K: KeyType>(&self, _key: K) -> Result<Proof<ProofNode>, Error> {
67+
async fn single_key_proof<K: KeyType>(&self, _key: K) -> Result<FrozenProof, Error> {
7068
Err(Error::RangeProofOnEmptyTrie)
7169
}
7270

@@ -75,8 +73,8 @@ impl DbView for HistoricalImpl {
7573
_first_key: Option<K>,
7674
_last_key: Option<K>,
7775
_limit: Option<usize>,
78-
) -> Result<Option<RangeProof<Box<[u8]>, Box<[u8]>, ProofNode>>, Error> {
79-
Ok(None)
76+
) -> Result<FrozenRangeProof, Error> {
77+
Err(Error::RangeProofOnEmptyTrie)
8078
}
8179

8280
fn iter_option<K: KeyType>(&self, _first_key: Option<K>) -> Result<EmptyStreamer, Error> {

firewood/src/v2/propose.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ use async_trait::async_trait;
99
use futures::stream::Empty;
1010

1111
use super::api::{KeyType, ValueType};
12-
use crate::proof::{Proof, ProofNode};
13-
use crate::range_proof::RangeProof;
14-
use crate::v2::api;
12+
use crate::v2::api::{self, FrozenProof, FrozenRangeProof};
1513

1614
#[derive(Clone, Debug)]
1715
pub(crate) enum KeyOp<V: ValueType> {
@@ -137,7 +135,7 @@ impl<T: api::DbView + Send + Sync> api::DbView for Proposal<T> {
137135
}
138136
}
139137

140-
async fn single_key_proof<K: KeyType>(&self, _key: K) -> Result<Proof<ProofNode>, api::Error> {
138+
async fn single_key_proof<K: KeyType>(&self, _key: K) -> Result<FrozenProof, api::Error> {
141139
todo!();
142140
}
143141

@@ -146,7 +144,7 @@ impl<T: api::DbView + Send + Sync> api::DbView for Proposal<T> {
146144
_first_key: Option<KT>,
147145
_last_key: Option<KT>,
148146
_limit: Option<usize>,
149-
) -> Result<Option<RangeProof<Box<[u8]>, Box<[u8]>, ProofNode>>, api::Error> {
147+
) -> Result<FrozenRangeProof, api::Error> {
150148
todo!();
151149
}
152150

0 commit comments

Comments
 (0)