Skip to content

Commit f5c42dc

Browse files
committed
Merge branch 'main' into rkuris/simple-range-proof-verification
2 parents 9292db6 + c5fffea commit f5c42dc

File tree

10 files changed

+523
-41
lines changed

10 files changed

+523
-41
lines changed

ffi/firewood.h

Lines changed: 20 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ffi/proofs.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,10 @@ func getChangeProofFromChangeProofResult(result C.ChangeProofResult) (*ChangePro
579579
switch result.tag {
580580
case C.ChangeProofResult_NullHandlePointer:
581581
return nil, errDBClosed
582+
case C.ChangeProofResult_StartRevisionNotFound:
583+
return nil, ErrStartRevisionNotFound
584+
case C.ChangeProofResult_EndRevisionNotFound:
585+
return nil, ErrEndRevisionNotFound
582586
case C.ChangeProofResult_Ok:
583587
ptr := *(**C.ChangeProofContext)(unsafe.Pointer(&result.anon0))
584588
return &ChangeProof{handle: ptr}, nil

ffi/proofs_test.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
const (
15-
rangeProofLenUnbounded = 0
16-
rangeProofLenTruncated = 10
15+
rangeProofLenUnbounded = 0
16+
rangeProofLenTruncated = 10
17+
changeProofLenUnbounded = 0
1718
)
1819

1920
type maybe struct {
@@ -385,3 +386,29 @@ func TestRangeProofFinalizerCleanup(t *testing.T) {
385386

386387
r.NoError(db.Close(t.Context()), "Database should be closeable after proof is garbage collected")
387388
}
389+
390+
func TestChangeProofEmptyDB(t *testing.T) {
391+
r := require.New(t)
392+
db := newTestDatabase(t)
393+
394+
proof, err := db.ChangeProof(EmptyRoot, EmptyRoot, nothing(), nothing(), changeProofLenUnbounded)
395+
r.ErrorIs(err, ErrEndRevisionNotFound)
396+
r.Nil(proof)
397+
}
398+
399+
func TestChangeProofCreation(t *testing.T) {
400+
r := require.New(t)
401+
db := newTestDatabase(t)
402+
403+
// Insert first half of data in the first batch
404+
_, _, batch := kvForTest(10000)
405+
root1, err := db.Update(batch[:5000])
406+
r.NoError(err)
407+
408+
// Insert the rest in the second batch
409+
root2, err := db.Update(batch[5000:])
410+
r.NoError(err)
411+
412+
_, err = db.ChangeProof(root1, root2, nothing(), nothing(), changeProofLenUnbounded)
413+
r.NoError(err)
414+
}

ffi/revision.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import (
1616
)
1717

1818
var (
19-
ErrDroppedRevision = errors.New("revision already dropped")
20-
errRevisionNotFound = errors.New("revision not found")
19+
ErrDroppedRevision = errors.New("revision already dropped")
20+
errRevisionNotFound = errors.New("revision not found")
21+
ErrEndRevisionNotFound = errors.New("end revision not found")
22+
ErrStartRevisionNotFound = errors.New("start revision not found")
2123
)
2224

2325
// Revision is an immutable view over the state at a specific root hash.

ffi/src/handle.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
22
// See the file LICENSE.md for licensing terms.
33

4+
use std::num::NonZeroUsize;
5+
46
use firewood::{
57
db::{Db, DbConfig},
68
manager::RevisionManagerConfig,
7-
v2::api::{self, ArcDynDbView, Db as _, DbView, HashKey, HashKeyExt, IntoBatchIter, KeyType},
9+
merkle::Merkle,
10+
v2::api::{
11+
self, ArcDynDbView, Db as _, DbView, FrozenChangeProof, HashKey, HashKeyExt, IntoBatchIter,
12+
KeyType,
13+
},
814
};
915

1016
use crate::{BatchOp, BorrowedBytes, CView, CreateProposalResult, arc_cache::ArcCache};
@@ -274,6 +280,56 @@ impl DatabaseHandle {
274280
})
275281
}
276282

283+
/// Create a Change Proof between two revisions specified by the start and end hash.
284+
///
285+
/// # Errors
286+
///
287+
/// * `api::Error::StartRevisionNotFound` - If the revision for `start_hash` cannot
288+
/// be found. Note that only an `EndRevisionNotFound` is returned when both
289+
/// `start_hash` and `end_hash` cannot be found.
290+
///
291+
/// * `api::Error::EndRevisionNotFound` - If the revision for `end_hash` cannot be
292+
/// found. Note that only an `EndRevisionNotFound` is returned when both
293+
/// `start_hash` and `end_hash` cannot be found.
294+
///
295+
/// * `api::Error::InvalidRange` - If `start_key` > `end_key` when both are provided.
296+
/// This ensures the range bounds are logically consistent.
297+
///
298+
/// * `api::Error` - Various other errors can occur during proof generation, such as:
299+
/// - I/O errors when reading nodes from storage
300+
/// - Corrupted trie structure
301+
/// - Invalid node references
302+
pub(crate) fn change_proof(
303+
&self,
304+
start_hash: HashKey,
305+
end_hash: HashKey,
306+
start_key: Option<&[u8]>,
307+
end_key: Option<&[u8]>,
308+
limit: Option<NonZeroUsize>,
309+
) -> Result<FrozenChangeProof, api::Error> {
310+
// Convert `RevisionNotFound` to `EndRevisionNotFound`. We get the end merkle
311+
// before the start merkle since we want to return an `EndRevisionNotFound` in
312+
// the case where both the start and end keys are not available.
313+
let end_merkle = Merkle::from(self.db.revision(end_hash).map_err(|err| {
314+
if let api::Error::RevisionNotFound { provided } = err {
315+
api::Error::EndRevisionNotFound { provided }
316+
} else {
317+
err
318+
}
319+
})?);
320+
321+
// Convert `RevisionNotFound` to `StartRevisionNotFound`.
322+
let start_merkle = Merkle::from(self.db.revision(start_hash).map_err(|err| {
323+
if let api::Error::RevisionNotFound { provided } = err {
324+
api::Error::StartRevisionNotFound { provided }
325+
} else {
326+
err
327+
}
328+
})?);
329+
330+
end_merkle.change_proof(start_key, end_key, start_merkle.nodestore(), limit)
331+
}
332+
277333
/// Dumps the Trie structure of the latest revision to a DOT (Graphviz) format string.
278334
///
279335
/// # Errors

ffi/src/proofs/change.rs

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

4+
use std::num::NonZeroUsize;
5+
46
#[cfg(feature = "ethhash")]
57
use firewood::ProofError;
68
#[cfg(feature = "ethhash")]
79
use firewood_storage::TrieHash;
810
#[cfg(feature = "ethhash")]
911
use rlp::Rlp;
1012

11-
use firewood::v2::api;
13+
use firewood::v2::api::{self, FrozenChangeProof};
1214

1315
use crate::{
1416
BorrowedBytes, CResult, ChangeProofResult, DatabaseHandle, HashKey, HashResult, Maybe,
@@ -28,11 +30,11 @@ const EMPTY_CODE_HASH: [u8; 32] = [
2830
pub struct CreateChangeProofArgs<'a> {
2931
/// The root hash of the starting revision. This must be provided.
3032
/// If the root is not found in the database, the function will return
31-
/// [`ChangeProofResult::RevisionNotFound`].
33+
/// [`ChangeProofResult::StartRevisionNotFound`].
3234
pub start_root: HashKey,
3335
/// The root hash of the ending revision. This must be provided.
3436
/// If the root is not found in the database, the function will return
35-
/// [`ChangeProofResult::RevisionNotFound`].
37+
/// [`ChangeProofResult::EndRevisionNotFound`].
3638
pub end_root: HashKey,
3739
/// The start key of the range to create the proof for. If `None`, the range
3840
/// starts from the beginning of the keyspace.
@@ -54,7 +56,7 @@ pub struct VerifyChangeProofArgs<'a> {
5456
/// The change proof to verify. If null, the function will return
5557
/// [`VoidResult::NullHandlePointer`]. We need a mutable reference to
5658
/// update the validation context.
57-
pub proof: Option<&'a mut ChangeProofContext>,
59+
pub proof: Option<&'a mut ChangeProofContext<'a>>,
5860
/// The root hash of the starting revision. This must match the starting
5961
/// root of the proof.
6062
pub start_root: HashKey,
@@ -73,12 +75,44 @@ pub struct VerifyChangeProofArgs<'a> {
7375
pub max_length: u32,
7476
}
7577

78+
/// Tracks the state of a proposal created from a change proof. A proposal is
79+
/// created after calling `fwd_db_verify_change_proof` and is committed after
80+
/// calling `fwd_db_verify_and_commit_change_proof`.
81+
#[derive(Debug)]
82+
#[expect(dead_code)]
83+
enum ProposalState<'db> {
84+
Immutable(crate::ProposalHandle<'db>),
85+
Committed(Option<HashKey>),
86+
}
87+
7688
/// FFI context for a parsed or generated change proof.
7789
#[derive(Debug)]
78-
pub struct ChangeProofContext {
79-
_proof: (), // currently not implemented
80-
_validation_context: (), // placeholder for future use
81-
_commit_context: (), // placeholder for future use
90+
#[expect(dead_code)]
91+
pub struct ChangeProofContext<'db> {
92+
proof: FrozenChangeProof,
93+
verification: Option<VerificationContext>,
94+
proposal_state: Option<ProposalState<'db>>,
95+
}
96+
97+
impl From<FrozenChangeProof> for ChangeProofContext<'_> {
98+
fn from(proof: FrozenChangeProof) -> Self {
99+
Self {
100+
proof,
101+
verification: None,
102+
proposal_state: None,
103+
}
104+
}
105+
}
106+
107+
/// FFI context for verifying a change proof
108+
#[derive(Debug)]
109+
#[expect(dead_code)]
110+
struct VerificationContext {
111+
start_root: HashKey,
112+
end_root: HashKey,
113+
start_key: Option<Box<[u8]>>,
114+
end_key: Option<Box<[u8]>>,
115+
max_length: Option<NonZeroUsize>,
82116
}
83117

84118
/// A key range that should be fetched to continue iterating through a range
@@ -183,19 +217,37 @@ impl<'a> CodeIteratorHandle<'a> {
183217
/// # Returns
184218
///
185219
/// - [`ChangeProofResult::NullHandlePointer`] if the caller provided a null pointer.
186-
/// - [`ChangeProofResult::RevisionNotFound`] if the caller provided a start or end root
220+
/// - [`ChangeProofResult::StartRevisionNotFound`] if the caller provided a start root
187221
/// that was not found in the database. The missing root hash is included in the result.
188-
/// The start root is checked first, and if both are missing, only the start root is
222+
/// If both the start root and end root are missing, then only the end root is
223+
/// reported.
224+
/// - [`ChangeProofResult::EndRevisionNotFound`] if the caller provided an end root
225+
/// that was not found in the database. The missing root hash is included in the result.
226+
/// If both the start root and end root are missing, then only the end root is
189227
/// reported.
190228
/// - [`ChangeProofResult::Ok`] containing a pointer to the `ChangeProofContext` if the proof
191229
/// was successfully created.
192230
/// - [`ChangeProofResult::Err`] containing an error message if the proof could not be created.
193231
#[unsafe(no_mangle)]
194-
pub extern "C" fn fwd_db_change_proof(
195-
_db: Option<&DatabaseHandle>,
196-
_args: CreateChangeProofArgs,
197-
) -> ChangeProofResult {
198-
CResult::from_err("not yet implemented")
232+
pub extern "C" fn fwd_db_change_proof<'db>(
233+
db: Option<&'db DatabaseHandle>,
234+
args: CreateChangeProofArgs<'db>,
235+
) -> ChangeProofResult<'db> {
236+
crate::invoke_with_handle(db, |db| {
237+
db.change_proof(
238+
args.start_root.into(),
239+
args.end_root.into(),
240+
args.start_key
241+
.as_ref()
242+
.map(BorrowedBytes::as_slice)
243+
.into_option(),
244+
args.end_key
245+
.as_ref()
246+
.map(BorrowedBytes::as_slice)
247+
.into_option(),
248+
NonZeroUsize::new(args.max_length as usize),
249+
)
250+
})
199251
}
200252

201253
/// Verify a change proof and prepare a proposal to later commit or drop.
@@ -351,7 +403,7 @@ pub extern "C" fn fwd_free_change_proof(proof: Option<Box<ChangeProofContext>>)
351403
crate::invoke_with_handle(proof, drop)
352404
}
353405

354-
impl crate::MetricsContextExt for ChangeProofContext {
406+
impl crate::MetricsContextExt for ChangeProofContext<'_> {
355407
fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
356408
None
357409
}

0 commit comments

Comments
 (0)