From 0c89e48d01dd51aeefcceee6233680ebf23437b7 Mon Sep 17 00:00:00 2001 From: Brandon LeBlanc Date: Wed, 17 Sep 2025 13:34:04 -0400 Subject: [PATCH 1/6] feat(ffi): fake verifying range proofs --- ffi/proofs_test.go | 36 ++++++++++++++++++++++++------------ ffi/src/proofs/range.rs | 11 ++++++++--- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/ffi/proofs_test.go b/ffi/proofs_test.go index 5b58bea253..ef3704ba6b 100644 --- a/ffi/proofs_test.go +++ b/ffi/proofs_test.go @@ -76,15 +76,18 @@ func TestRangeProofPartialRange(t *testing.T) { r.NoError(err) // get a proof over some partial range - proof1 := rangeProofWithAndWithoutRoot(t, db, root, nothing(), nothing()) + proof1, proof1Bytes := rangeProofWithAndWithoutRoot(t, db, root, nothing(), nothing()) // get a proof over a different range - proof2 := rangeProofWithAndWithoutRoot(t, db, root, something([]byte("key2")), something([]byte("key3"))) + startKey := something([]byte("key2")) + endKey := something([]byte("key3")) + proof2, proof2Bytes := rangeProofWithAndWithoutRoot(t, db, root, startKey, endKey) // ensure the proofs are different - r.NotEqual(proof1, proof2) + r.NotEqual(proof1Bytes, proof2Bytes) - // TODO(https://github.com/ava-labs/firewood/issues/738): verify the proofs + r.NoError(proof1.Verify(root, nothing(), nothing(), maxProofLen)) + r.NoError(proof2.Verify(root, startKey, endKey, maxProofLen)) } func TestRangeProofDiffersAfterUpdate(t *testing.T) { @@ -97,7 +100,7 @@ func TestRangeProofDiffersAfterUpdate(t *testing.T) { r.NoError(err) // get a proof - proof := rangeProofWithAndWithoutRoot(t, db, root1, nothing(), nothing()) + proof1, proof1Bytes := rangeProofWithAndWithoutRoot(t, db, root1, nothing(), nothing()) // insert more data root2, err := db.Update(keys[50:], vals[50:]) @@ -105,10 +108,13 @@ func TestRangeProofDiffersAfterUpdate(t *testing.T) { r.NotEqual(root1, root2) // get a proof again - proof2 := rangeProofWithAndWithoutRoot(t, db, root2, nothing(), nothing()) + proof2, proof2Bytes := rangeProofWithAndWithoutRoot(t, db, root2, nothing(), nothing()) // ensure the proofs are different - r.NotEqual(proof, proof2) + r.NotEqual(proof1Bytes, proof2Bytes) + + r.NoError(proof1.Verify(root1, nothing(), nothing(), maxProofLen)) + r.NoError(proof2.Verify(root2, nothing(), nothing(), maxProofLen)) } func TestRoundTripSerialization(t *testing.T) { @@ -121,7 +127,7 @@ func TestRoundTripSerialization(t *testing.T) { r.NoError(err) // get a proof - proofBytes := rangeProofWithAndWithoutRoot(t, db, root, nothing(), nothing()) + _, proofBytes := rangeProofWithAndWithoutRoot(t, db, root, nothing(), nothing()) // Deserialize the proof. proof := new(RangeProof) @@ -133,6 +139,8 @@ func TestRoundTripSerialization(t *testing.T) { r.NoError(err) r.Equal(proofBytes, serialized) + r.NoError(proof.Verify(root, nothing(), nothing(), maxProofLen)) + r.NoError(proof.Free()) } @@ -144,7 +152,7 @@ func rangeProofWithAndWithoutRoot( db *Database, root []byte, startKey, endKey maybe, -) []byte { +) (*RangeProof, []byte) { r := require.New(t) proof1, err := db.RangeProof(maybe{hasValue: false}, startKey, endKey, maxProofLen) @@ -152,16 +160,20 @@ func rangeProofWithAndWithoutRoot( r.NotNil(proof1) proof1Bytes, err := proof1.MarshalBinary() r.NoError(err) - r.NoError(proof1.Free()) + t.Cleanup(func() { + r.NoError(proof1.Free()) + }) proof2, err := db.RangeProof(maybe{hasValue: true, value: root}, startKey, endKey, maxProofLen) r.NoError(err) r.NotNil(proof2) proof2Bytes, err := proof2.MarshalBinary() r.NoError(err) - r.NoError(proof2.Free()) + t.Cleanup(func() { + r.NoError(proof2.Free()) + }) r.Equal(proof1Bytes, proof2Bytes) - return proof1Bytes + return proof1, proof1Bytes } diff --git a/ffi/src/proofs/range.rs b/ffi/src/proofs/range.rs index 34dd322310..c592262581 100644 --- a/ffi/src/proofs/range.rs +++ b/ffi/src/proofs/range.rs @@ -3,7 +3,10 @@ use std::num::NonZeroUsize; -use firewood::v2::api::{self, FrozenRangeProof}; +use firewood::{ + logger::warn, + v2::api::{self, FrozenRangeProof}, +}; use crate::{ BorrowedBytes, CResult, DatabaseHandle, HashResult, Maybe, NextKeyRangeResult, @@ -154,7 +157,8 @@ pub extern "C" fn fwd_db_range_proof( /// for the duration of the call. #[unsafe(no_mangle)] pub extern "C" fn fwd_range_proof_verify(_args: VerifyRangeProofArgs) -> VoidResult { - CResult::from_err("not yet implemented") + warn!("fwd_range_proof_verify not yet implemented"); + VoidResult::Ok } /// Verify a range proof and prepare a proposal to later commit or drop. If the @@ -184,7 +188,8 @@ pub extern "C" fn fwd_db_verify_range_proof( _db: Option<&DatabaseHandle>, _args: VerifyRangeProofArgs, ) -> VoidResult { - CResult::from_err("not yet implemented") + warn!("fwd_db_verify_range_proof not yet implemented"); + VoidResult::Ok } /// Verify and commit a range proof to the database. From 6305126f8c589eb655b309ee16d3ad7e14c688b5 Mon Sep 17 00:00:00 2001 From: Brandon LeBlanc Date: Wed, 17 Sep 2025 13:48:50 -0400 Subject: [PATCH 2/6] feat: commit range proofs --- ffi/firewood.h | 4 ++-- ffi/src/handle.rs | 12 ++++++++---- ffi/src/proofs/range.rs | 10 +++++++--- ffi/src/value/borrowed.rs | 9 +++++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/ffi/firewood.h b/ffi/firewood.h index 9fb52e7e55..a21fe5cafc 100644 --- a/ffi/firewood.h +++ b/ffi/firewood.h @@ -1016,8 +1016,8 @@ struct HashResult fwd_db_verify_and_commit_change_proof(const struct DatabaseHan * concurrently. The caller must ensure exclusive access to the proof context * for the duration of the call. */ -struct HashResult fwd_db_verify_and_commit_range_proof(const struct DatabaseHandle *_db, - struct VerifyRangeProofArgs _args); +struct HashResult fwd_db_verify_and_commit_range_proof(const struct DatabaseHandle *db, + struct VerifyRangeProofArgs args); /** * Verify a change proof and prepare a proposal to later commit or drop. diff --git a/ffi/src/handle.rs b/ffi/src/handle.rs index 6e0e2aabc3..7030f5a7ab 100644 --- a/ffi/src/handle.rs +++ b/ffi/src/handle.rs @@ -5,11 +5,14 @@ use firewood::{ db::{Db, DbConfig}, manager::RevisionManagerConfig, merkle::Value, - v2::api::{self, ArcDynDbView, Db as _, DbView, HashKey, HashKeyExt, KeyType, Proposal as _}, + v2::api::{ + self, ArcDynDbView, Db as _, DbView, HashKey, HashKeyExt, KeyType, KeyValuePairIter, + Proposal as _, + }, }; use metrics::counter; -use crate::{BorrowedBytes, DatabaseHandle, KeyValuePair}; +use crate::{BorrowedBytes, DatabaseHandle}; /// Arguments for creating or opening a database. These are passed to [`fwd_open_db`] /// @@ -147,11 +150,12 @@ impl DatabaseHandle<'_> { /// An error is returned if the proposal could not be created. pub fn create_batch<'kvp>( &self, - values: impl AsRef<[KeyValuePair<'kvp>]> + 'kvp, + // values: impl AsRef<[KeyValuePair<'kvp>]> + 'kvp, + values: impl IntoIterator + 'kvp, ) -> Result, api::Error> { let start = coarsetime::Instant::now(); - let proposal = self.db.propose(values.as_ref())?; + let proposal = self.db.propose(values)?; let propose_time = start.elapsed().as_millis(); counter!("firewood.ffi.propose_ms").increment(propose_time); diff --git a/ffi/src/proofs/range.rs b/ffi/src/proofs/range.rs index c592262581..6e73ce3860 100644 --- a/ffi/src/proofs/range.rs +++ b/ffi/src/proofs/range.rs @@ -224,10 +224,14 @@ pub extern "C" fn fwd_db_verify_range_proof( /// for the duration of the call. #[unsafe(no_mangle)] pub extern "C" fn fwd_db_verify_and_commit_range_proof( - _db: Option<&DatabaseHandle>, - _args: VerifyRangeProofArgs, + db: Option<&DatabaseHandle>, + args: VerifyRangeProofArgs, ) -> HashResult { - CResult::from_err("not yet implemented") + let handle = match (db, args.proof) { + (Some(db), Some(proof)) => Some((db, proof)), + _ => None, + }; + crate::invoke_with_handle(handle, |(db, proof)| db.create_batch(&proof.proof)) } /// Returns the next key range that should be fetched after processing the diff --git a/ffi/src/value/borrowed.rs b/ffi/src/value/borrowed.rs index 2050483669..1ffd3266de 100644 --- a/ffi/src/value/borrowed.rs +++ b/ffi/src/value/borrowed.rs @@ -123,6 +123,15 @@ impl Ord for BorrowedSlice<'_, T> { } } +impl<'a, T> IntoIterator for BorrowedSlice<'a, T> { + type IntoIter = std::slice::Iter<'a, T>; + type Item = &'a T; + + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() + } +} + impl fmt::Display for BorrowedBytes<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let precision = f.precision().unwrap_or(64); From cc66513a630126ac3e7139ca5991932a376b5278 Mon Sep 17 00:00:00 2001 From: Brandon LeBlanc Date: Wed, 17 Sep 2025 14:04:19 -0400 Subject: [PATCH 3/6] stub find next key --- ffi/firewood.h | 2 +- ffi/src/proofs/range.rs | 16 +++++++++++++--- ffi/src/value/results.rs | 13 +++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ffi/firewood.h b/ffi/firewood.h index a21fe5cafc..a2641af02c 100644 --- a/ffi/firewood.h +++ b/ffi/firewood.h @@ -1367,7 +1367,7 @@ struct Value fwd_propose_on_proposal(const struct DatabaseHandle *db, * concurrently. The caller must ensure exclusive access to the proof context * for the duration of the call. */ -struct NextKeyRangeResult fwd_range_proof_find_next_key(struct RangeProofContext *_proof); +struct NextKeyRangeResult fwd_range_proof_find_next_key(struct RangeProofContext *proof); /** * Deserialize a `RangeProof` from bytes. diff --git a/ffi/src/proofs/range.rs b/ffi/src/proofs/range.rs index 6e73ce3860..bbe0ffae1f 100644 --- a/ffi/src/proofs/range.rs +++ b/ffi/src/proofs/range.rs @@ -9,7 +9,7 @@ use firewood::{ }; use crate::{ - BorrowedBytes, CResult, DatabaseHandle, HashResult, Maybe, NextKeyRangeResult, + BorrowedBytes, DatabaseHandle, HashResult, Maybe, NextKeyRange, NextKeyRangeResult, RangeProofResult, ValueResult, VoidResult, }; @@ -264,9 +264,19 @@ pub extern "C" fn fwd_db_verify_and_commit_range_proof( /// for the duration of the call. #[unsafe(no_mangle)] pub extern "C" fn fwd_range_proof_find_next_key( - _proof: Option<&mut RangeProofContext>, + proof: Option<&mut RangeProofContext>, ) -> NextKeyRangeResult { - CResult::from_err("not yet implemented") + crate::invoke_with_handle(proof, |ctx| { + Ok::<_, api::Error>( + ctx.proof + .key_values() + .last() + .map(|(last_key, _)| NextKeyRange { + start_key: last_key.to_vec().into(), + end_key: Maybe::None, + }), + ) + }) } /// Serialize a `RangeProof` to bytes. diff --git a/ffi/src/value/results.rs b/ffi/src/value/results.rs index 4ef89350a0..215f2a6111 100644 --- a/ffi/src/value/results.rs +++ b/ffi/src/value/results.rs @@ -272,6 +272,19 @@ pub enum NextKeyRangeResult { Err(OwnedBytes), } +impl From, E>> for NextKeyRangeResult +where + E: fmt::Display, +{ + fn from(value: Result, E>) -> Self { + match value { + Ok(None) => NextKeyRangeResult::None, + Ok(Some(range)) => NextKeyRangeResult::Some(range), + Err(err) => NextKeyRangeResult::Err(err.to_string().into_bytes().into()), + } + } +} + /// Helper trait to handle the different result types returned from FFI functions. /// /// Once Try trait is stable, we can use that instead of this trait: From e0cc25b22abc01016c0ad156293c23db8969e4a5 Mon Sep 17 00:00:00 2001 From: Brandon LeBlanc Date: Wed, 17 Sep 2025 14:34:44 -0400 Subject: [PATCH 4/6] check for nil --- ffi/proofs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/proofs.go b/ffi/proofs.go index 6373619f2d..b5d2fb5fdc 100644 --- a/ffi/proofs.go +++ b/ffi/proofs.go @@ -363,7 +363,7 @@ func (r *NextKeyRange) Free() error { var err1, err2 error err1 = r.startKey.Free() - if r.endKey.HasValue() { + if r.endKey != nil && r.endKey.HasValue() { err2 = r.endKey.Value().Free() } From 888c1c38c199d579aa057050f02eb8fd3d4e7318 Mon Sep 17 00:00:00 2001 From: Brandon LeBlanc Date: Wed, 17 Sep 2025 14:36:08 -0400 Subject: [PATCH 5/6] again --- ffi/proofs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffi/proofs.go b/ffi/proofs.go index b5d2fb5fdc..c5a336cca9 100644 --- a/ffi/proofs.go +++ b/ffi/proofs.go @@ -343,7 +343,7 @@ func (r *NextKeyRange) StartKey() []byte { // HasEndKey returns true if this key range has an exclusive end key. func (r *NextKeyRange) HasEndKey() bool { - return r.endKey.HasValue() + return r.endKey != nil && r.endKey.HasValue() } // EndKey returns the exclusive end key of this key range if it exists or nil if From 94e2d06e718b5dcc02cf7f793219704ddad3738c Mon Sep 17 00:00:00 2001 From: Brandon LeBlanc Date: Wed, 17 Sep 2025 14:47:23 -0400 Subject: [PATCH 6/6] del comment --- ffi/src/handle.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ffi/src/handle.rs b/ffi/src/handle.rs index 7030f5a7ab..aaff78db22 100644 --- a/ffi/src/handle.rs +++ b/ffi/src/handle.rs @@ -150,7 +150,6 @@ impl DatabaseHandle<'_> { /// An error is returned if the proposal could not be created. pub fn create_batch<'kvp>( &self, - // values: impl AsRef<[KeyValuePair<'kvp>]> + 'kvp, values: impl IntoIterator + 'kvp, ) -> Result, api::Error> { let start = coarsetime::Instant::now();