Skip to main content

firewood_ffi/proofs/
change.rs

1// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2// See the file LICENSE.md for licensing terms.
3
4use std::convert::Into;
5use std::num::NonZeroUsize;
6
7use firewood_metrics::firewood_increment;
8#[cfg(feature = "ethhash")]
9use firewood_storage::TrieHash;
10#[cfg(feature = "ethhash")]
11use rlp::Rlp;
12
13use firewood::{
14    ProofError,
15    api::{self, DbView as _, FrozenChangeProof},
16    logger::warn,
17};
18
19use std::cmp::Ordering;
20
21use crate::{
22    BorrowedBytes, ChangeProofResult, DatabaseHandle, HashKey, HashResult, KeyRange, Maybe,
23    NextKeyRangeResult, OwnedBytes, ValueResult, VoidResult,
24    results::{ProposedChangeProofResult, VerifiedChangeProofResult},
25};
26
27#[cfg(feature = "ethhash")]
28const EMPTY_CODE_HASH: [u8; 32] = [
29    // "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
30    0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0,
31    0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70,
32];
33
34/// Arguments for creating a change proof.
35#[derive(Debug)]
36#[repr(C)]
37pub struct CreateChangeProofArgs<'a> {
38    /// The root hash of the starting revision. This must be provided.
39    /// If the root is not found in the database, the function will return
40    /// [`ChangeProofResult::StartRevisionNotFound`].
41    pub start_root: HashKey,
42    /// The root hash of the ending revision. This must be provided.
43    /// If the root is not found in the database, the function will return
44    /// [`ChangeProofResult::EndRevisionNotFound`].
45    pub end_root: HashKey,
46    /// The start key of the range to create the proof for. If `None`, the range
47    /// starts from the beginning of the keyspace.
48    pub start_key: Maybe<BorrowedBytes<'a>>,
49    /// The end key of the range to create the proof for. If `None`, the range
50    /// ends at the end of the keyspace or until `max_length` items have been
51    /// included in the proof.
52    pub end_key: Maybe<BorrowedBytes<'a>>,
53    /// The maximum number of key/value pairs to include in the proof. If the
54    /// range contains more items than this, the proof will be truncated. If
55    /// `0`, there is no limit.
56    pub max_length: u32,
57}
58
59/// Arguments for verifying a change proof.
60#[derive(Debug)]
61#[repr(C)]
62pub struct VerifyChangeProofArgs<'a> {
63    /// The change proof to verify. If null, the function will return
64    /// [`VoidResult::NullHandlePointer`]. We need a mutable reference to
65    /// update the validation context.
66    pub proof: Option<&'a mut ChangeProofContext>,
67    /// The root hash of the starting revision. This must match the starting
68    /// root of the proof.
69    pub start_root: HashKey,
70    /// The root hash of the ending revision. This must match the ending root of
71    /// the proof.
72    pub end_root: HashKey,
73    /// The lower bound of the key range that the proof is expected to cover. If
74    /// `None`, the proof is expected to cover from the start of the keyspace.
75    pub start_key: Maybe<BorrowedBytes<'a>>,
76    /// The upper bound of the key range that the proof is expected to cover. If
77    /// `None`, the proof is expected to cover to the end of the keyspace.
78    pub end_key: Maybe<BorrowedBytes<'a>>,
79    /// The maximum number of key/value pairs that the proof is expected to cover.
80    /// If the proof contains more items than this, it is considered invalid. If
81    /// `0`, there is no limit.
82    pub max_length: u32,
83}
84
85// Arguments for creating a proposal from a verified change proof
86#[derive(Debug)]
87#[repr(C)]
88pub struct ProposedChangeProofArgs<'a> {
89    /// The verified change proof context that will be used to create a proposal.
90    pub proof: Option<&'a mut VerifiedChangeProofContext>,
91}
92
93#[derive(Debug)]
94#[repr(C)]
95pub struct CommittedChangeProofArgs<'a> {
96    // The proposed change proof context that will be used to commit a proposal
97    pub proof: Option<&'a mut ProposedChangeProofContext<'a>>,
98}
99
100/// FFI context for a parsed or generated change proof. This change proof has not
101/// been verified. Calling `verify` on it will generate a `VerifiedChangeProofContext`
102/// and consume the `proof` and replacing it with None.
103#[derive(Debug)]
104pub struct ChangeProofContext {
105    proof: Option<FrozenChangeProof>,
106}
107
108impl From<FrozenChangeProof> for ChangeProofContext {
109    fn from(proof: FrozenChangeProof) -> Self {
110        Self { proof: Some(proof) }
111    }
112}
113
114impl ChangeProofContext {
115    /// Verifies the `ChangeProofContext` and creates a `VerifiedChangeProofContext`
116    /// on success. Calling `verify` consumes the proof, and calling it again will
117    /// return a `ProofIsNone` error.
118    ///
119    /// Currently only performs a cursory verification, such as whether
120    /// the keys in the change proof is sorted.
121    fn verify(
122        &mut self,
123        params: VerificationParams,
124    ) -> Result<VerifiedChangeProofContext, api::Error> {
125        let Some(proof) = self.proof.take() else {
126            return Err(api::Error::ProofError(ProofError::ProofIsNone));
127        };
128
129        let batch_ops = proof.batch_ops();
130
131        // Check to make sure the BatchOp array size is less than or equal to `max_length`
132        if let Some(max_length) = params.max_length
133            && batch_ops.len() > max_length.into()
134        {
135            return Err(api::Error::ProofError(
136                ProofError::ProofIsLargerThanMaxLength,
137            ));
138        }
139
140        // Check the start key is not greater than the first key in the proof.
141        if let (Some(start_key), Some(first_key)) = (&params.start_key, batch_ops.first())
142            && start_key.cmp(first_key.key()) == Ordering::Greater
143        {
144            return Err(api::Error::ProofError(
145                ProofError::StartKeyLargerThanFirstKey,
146            ));
147        }
148
149        // Check the end key is not less than the last key in the proof.
150        if let (Some(end_key), Some(last_key)) = (&params.end_key, batch_ops.last())
151            && end_key.cmp(last_key.key()) == Ordering::Less
152        {
153            return Err(api::Error::ProofError(ProofError::EndKeyLessThanLastKey));
154        }
155
156        // Verify the keys are in sorted order.
157        if batch_ops
158            .iter()
159            .is_sorted_by(|a, b| b.key().cmp(a.key()) == Ordering::Greater)
160        {
161            warn!("change proof verification not yet implemented");
162            Ok(VerifiedChangeProofContext {
163                proof: Some(proof),
164                params,
165            })
166        } else {
167            Err(api::Error::ProofError(ProofError::ChangeProofKeysNotSorted))
168        }
169    }
170}
171
172/// FFI context for a verified change proof. It is created from calling `verify`
173/// on a `ChangeProofContext` and stores the parameters of that call in `params`.
174/// Calling `propose` on it will consume the proof to create a
175/// `ProposedChangeProofContext`.
176#[derive(Debug)]
177pub struct VerifiedChangeProofContext {
178    proof: Option<FrozenChangeProof>,
179    params: VerificationParams,
180}
181
182impl VerifiedChangeProofContext {
183    /// Creates a proposal from the verified change proof context, and stores the change
184    /// proof, database handle, and proposal handle in a `ProposedChangeProofContext`.
185    /// Calling `propose` consumes the proof, and calling it again will return a
186    /// `ProofIsNone` error.
187    fn propose<'db>(
188        &'db mut self,
189        db: &'db DatabaseHandle,
190    ) -> Result<ProposedChangeProofContext<'db>, api::Error> {
191        let Some(proof) = self.proof.take() else {
192            return Err(api::Error::ProofError(ProofError::ProofIsNone));
193        };
194        let proposal = db.apply_change_proof_to_parent(self.params.start_root.into(), &proof)?;
195        let root_hash = proposal.handle.root_hash().map(Into::into);
196        Ok(ProposedChangeProofContext {
197            proof,
198            db,
199            root_hash,
200            end_root: self.params.end_root,
201            end_key: self.params.end_key.clone(),
202            proposal: Some(proposal.handle),
203        })
204    }
205}
206
207/// FFI context for a proposed change proof. It is created from calling `propose`
208/// on a `VerifiedChangeProofContext` and stores the database, proposal handle,
209/// and other parameters need to implement `find_next_key`. Calling `commit` on it
210/// will consume the proof, but `find_next_key` can still be called on it.
211#[expect(unused)]
212#[derive(Debug)]
213pub struct ProposedChangeProofContext<'db> {
214    proof: FrozenChangeProof,
215    db: &'db DatabaseHandle,
216    root_hash: Option<HashKey>,
217    end_root: HashKey,
218    end_key: Option<Box<[u8]>>,
219    proposal: Option<crate::ProposalHandle<'db>>,
220}
221
222impl<'db> ProposedChangeProofContext<'db> {
223    fn find_next_key(&mut self) -> Result<Option<KeyRange>, api::Error> {
224        let Some(last_op) = self.proof.batch_ops().last() else {
225            // no BatchOps in the proof, so we are done
226            return Ok(None);
227        };
228
229        if self.proof.end_proof().is_empty() {
230            // unbounded, so we are done
231            return Ok(None);
232        }
233
234        if let Some(ref end_key) = self.end_key
235            && **last_op.key() >= **end_key
236        {
237            // reached or exceeded the end key, so we are done
238            return Ok(None);
239        }
240
241        Ok(Some((last_op.key().clone(), self.end_key.clone())))
242    }
243
244    /// Consumes proposal handle after being called once.
245    fn commit(&'db mut self) -> Result<Option<HashKey>, api::Error> {
246        let Some(proposal_handle) = self.proposal.take() else {
247            return Err(api::Error::ProofError(ProofError::ProposalIsNone));
248        };
249
250        let result = proposal_handle.commit_proposal();
251        let hash = result?.map(Into::into);
252        firewood_increment!(crate::registry::MERGE_COUNT, 1, "change" => "commit");
253        Ok(hash)
254    }
255}
256
257/// FFI parameters for verifying a change proof
258#[derive(Debug)]
259struct VerificationParams {
260    start_root: HashKey,
261    end_root: HashKey,
262    start_key: Option<Box<[u8]>>,
263    end_key: Option<Box<[u8]>>,
264    max_length: Option<NonZeroUsize>,
265}
266
267/// A key range that should be fetched to continue iterating through a range
268/// or change proof that was truncated. Represents a half-open range
269/// `[start_key, end_key)`. If `end_key` is `None`, the range is unbounded
270/// and continues to the end of the keyspace.
271#[derive(Debug)]
272#[repr(C)]
273pub struct NextKeyRange {
274    /// The start key of the next range to fetch.
275    pub start_key: OwnedBytes,
276
277    /// If set, a non-inclusive upper bound for the next range to fetch. If not
278    /// set, the range is unbounded (this is the final range).
279    pub end_key: Maybe<OwnedBytes>,
280}
281
282#[derive(Debug)]
283#[non_exhaustive]
284pub struct CodeIteratorHandle<'a> {
285    #[cfg(feature = "ethhash")]
286    inner: std::slice::Iter<'a, KeyValuePair>,
287    // uninhabitable fields make the struct impossible to construct when the feature is disabled
288    #[cfg(not(feature = "ethhash"))]
289    void: std::convert::Infallible,
290    #[cfg(not(feature = "ethhash"))]
291    marker: std::marker::PhantomData<&'a ()>,
292}
293
294type KeyValuePair = (Box<[u8]>, Box<[u8]>);
295
296impl Iterator for CodeIteratorHandle<'_> {
297    type Item = Result<HashKey, api::Error>;
298
299    fn next(&mut self) -> Option<Self::Item> {
300        #[cfg(not(feature = "ethhash"))]
301        match self.void {}
302
303        #[cfg(feature = "ethhash")]
304        self.inner.find_map(|(key, value)| {
305            if key.len() != 32 {
306                return None;
307            }
308
309            let Ok(code_hash_slice) = Rlp::new(value).at(3).and_then(|r| r.data()) else {
310                return Some(Err(api::Error::ProofError(ProofError::InvalidValueFormat)));
311            };
312            let code_hash: HashKey = TrieHash::try_from(code_hash_slice).ok()?.into();
313            if code_hash == TrieHash::from(EMPTY_CODE_HASH).into() {
314                return None;
315            }
316
317            Some(Ok(code_hash))
318        })
319    }
320}
321
322impl<'a> CodeIteratorHandle<'a> {
323    /// Create a new code hash iterator from the given key/value pairs.
324    /// The key/value pairs should be the raw entries from the
325    /// underlying proof.
326    ///
327    /// The iterator must be freed after use.
328    ///
329    /// Arguments:
330    /// - `key_values` - The key/value pairs from the proof.
331    ///
332    /// Returns:
333    /// - `Ok(CodeIteratorHandle)` if the iterator was successfully created.
334    /// - `Err(api::Error)` if the iterator could not be created.
335    ///
336    /// # Errors
337    ///
338    /// - Returns `api::Error::FeatureNotSupported` if the `ethhash` feature
339    ///   is not enabled.
340    #[cfg_attr(feature = "ethhash", allow(clippy::missing_const_for_fn))]
341    #[cfg_attr(not(feature = "ethhash"), allow(unused_variables))]
342    pub fn new(key_values: &'a [KeyValuePair]) -> Result<Self, api::Error> {
343        #[cfg(not(feature = "ethhash"))]
344        {
345            Err(api::Error::FeatureNotSupported(
346                "ethhash code hash iterator".to_string(),
347            ))
348        }
349
350        #[cfg(feature = "ethhash")]
351        {
352            Ok(CodeIteratorHandle {
353                inner: key_values.iter(),
354            })
355        }
356    }
357}
358
359/// Create a change proof for the given range of keys between two roots.
360///
361/// # Arguments
362///
363/// - `db` - The database to create the proof from.
364/// - `args` - The arguments for creating the change proof.
365///
366/// # Returns
367///
368/// - [`ChangeProofResult::NullHandlePointer`] if the caller provided a null pointer.
369/// - [`ChangeProofResult::StartRevisionNotFound`] if the caller provided a start root
370///   that was not found in the database. The missing root hash is included in the result.
371///   If both the start root and end root are missing, then only the end root is
372///   reported.
373/// - [`ChangeProofResult::EndRevisionNotFound`] if the caller provided an end root
374///   that was not found in the database. The missing root hash is included in the result.
375///   If both the start root and end root are missing, then only the end root is
376///   reported.
377/// - [`ChangeProofResult::Ok`] containing a pointer to the `ChangeProofContext` if the proof
378///   was successfully created.
379/// - [`ChangeProofResult::Err`] containing an error message if the proof could not be created.
380#[unsafe(no_mangle)]
381pub extern "C" fn fwd_db_change_proof(
382    db: Option<&DatabaseHandle>,
383    args: CreateChangeProofArgs,
384) -> ChangeProofResult {
385    crate::invoke_with_handle(db, |db| {
386        db.change_proof(
387            args.start_root.into(),
388            args.end_root.into(),
389            args.start_key
390                .as_ref()
391                .map(BorrowedBytes::as_slice)
392                .into_option(),
393            args.end_key
394                .as_ref()
395                .map(BorrowedBytes::as_slice)
396                .into_option(),
397            NonZeroUsize::new(args.max_length as usize),
398        )
399    })
400}
401
402/// Verify a change proof and return a `VerifiedChangeProofResult`.
403///
404/// # Arguments
405///
406/// - `args` - The arguments for verifying the change proof.
407///
408/// # Returns
409///
410/// - [`VerifiedChangeProofResult::NullHandlePointer`] if the caller provided a null pointer to the
411///   proof.
412/// - [`VerifiedChangeProofResult::Ok`] if the proof was successfully verified.
413/// - [`VerifiedChangeProofResult::Err`] containing an error message if the proof could not be verified
414///
415/// # Thread Safety
416///
417/// It is not safe to call this function concurrently with the same proof context
418/// nor is it safe to call any other function that accesses the same proof context
419/// concurrently. The caller must ensure exclusive access to the proof context
420/// for the duration of the call.
421#[unsafe(no_mangle)]
422pub extern "C" fn fwd_verify_change_proof(
423    args: VerifyChangeProofArgs,
424) -> VerifiedChangeProofResult {
425    crate::invoke_with_handle(args.proof, |ctx| {
426        let context = VerificationParams {
427            start_root: args.start_root,
428            end_root: args.end_root,
429            start_key: args.start_key.into_option().as_deref().map(Box::from),
430            end_key: args.end_key.into_option().as_deref().map(Box::from),
431            max_length: NonZeroUsize::new(args.max_length as usize),
432        };
433        ctx.verify(context)
434    })
435}
436
437/// Create a proposal from a change proof and return a `ProposedChangeProofResult`.
438///
439/// # Arguments
440///
441/// - `db` - The database to create the proposal.
442/// - `args` - The arguments for verifying the change proof.
443///
444/// # Returns
445///
446/// - [`ProposedChangeProofResult::NullHandlePointer`] if the caller provided a null pointer to either
447///   the database or the proof.
448/// - [`ProposedChangeProofResult::Ok`] if a proposal was successfully created.
449/// - [`ProposedChangeProofResult::Err`] containing an error message if the proposal could not be created.
450///
451/// # Thread Safety
452///
453/// It is not safe to call this function concurrently with the same proof context
454/// nor is it safe to call any other function that accesses the same proof context
455/// concurrently. The caller must ensure exclusive access to the proof context
456/// for the duration of the call.
457#[unsafe(no_mangle)]
458pub extern "C" fn fwd_db_propose_change_proof<'db>(
459    db: Option<&'db DatabaseHandle>,
460    args: ProposedChangeProofArgs<'db>,
461) -> ProposedChangeProofResult<'db> {
462    let handle = db.and_then(|db| args.proof.map(|p| (db, p)));
463    crate::invoke_with_handle(handle, |(db, ctx)| ctx.propose(db))
464}
465
466/// Commit a change proof to the database.
467///
468/// # Arguments
469///
470/// - `args` - The arguments for verifying the change proof, which is just a `ProposedChangeProofContext`.
471///
472/// # Returns
473///
474/// - [`HashResult::NullHandlePointer`] if the caller provided a null pointer to the proof.
475/// - [`HashResult::None`] if the proof resulted in an empty database (i.e., all keys were deleted).
476/// - [`HashResult::Some`] containing the new root hash
477/// - [`HashResult::Err`] containing an error message if the proof could not be committed.
478///
479/// # Thread Safety
480///
481/// It is not safe to call this function concurrently with the same proof context
482/// nor is it safe to call any other function that accesses the same proof context
483/// concurrently. The caller must ensure exclusive access to the proof context
484/// for the duration of the call.
485#[unsafe(no_mangle)]
486pub extern "C" fn fwd_db_commit_change_proof(args: CommittedChangeProofArgs<'_>) -> HashResult {
487    crate::invoke_with_handle(args.proof, |ctx| {
488        ctx.commit().map(|hash_key| hash_key.map(Into::into))
489    })
490}
491
492/// Returns the next key range that should be fetched after processing the
493/// current set of operations in a change proof that was truncated.
494///
495/// # Arguments
496///
497/// - `proof` - A [`ChangeProofContext`] previously returned from the create
498///   methods and has been prepared into a proposal or already committed.
499///
500/// # Returns
501///
502/// - [`NextKeyRangeResult::NullHandlePointer`] if the caller provided a null pointer.
503/// - [`NextKeyRangeResult::NotPrepared`] if the proof has not been prepared into
504///   a proposal nor committed to the database. Should not be possible for a change
505///   proof due to its different interface compared to range proofs.
506/// - [`NextKeyRangeResult::None`] if there are no more keys to fetch.
507/// - [`NextKeyRangeResult::Some`] containing the next key range to fetch.
508/// - [`NextKeyRangeResult::Err`] containing an error message if the next key range
509///   could not be determined.
510///
511/// # Thread Safety
512///
513/// It is not safe to call this function concurrently with the same proof context
514/// nor is it safe to call any other function that accesses the same proof context
515/// concurrently. The caller must ensure exclusive access to the proof context
516/// for the duration of the call.
517#[unsafe(no_mangle)]
518pub extern "C" fn fwd_change_proof_find_next_key_proposed(
519    proof: Option<&mut ProposedChangeProofContext>,
520) -> NextKeyRangeResult {
521    crate::invoke_with_handle(proof, ProposedChangeProofContext::find_next_key)
522}
523
524/// Serialize a `ChangeProof` to bytes.
525///
526/// # Arguments
527///
528/// - `proof` - A [`ChangeProofContext`] previously returned from the create
529///   method. If from a parsed proof, the proof will not be verified before
530///   serialization.
531///
532/// # Returns
533///
534/// - [`ValueResult::NullHandlePointer`] if the caller provided a null pointer.
535/// - [`ValueResult::Some`] containing the serialized bytes if successful.
536/// - [`ValueResult::Err`] if the caller provided a null pointer.
537///
538/// The other [`ValueResult`] variants are not used.
539#[unsafe(no_mangle)]
540pub extern "C" fn fwd_change_proof_to_bytes(proof: Option<&ChangeProofContext>) -> ValueResult {
541    crate::invoke_with_handle(proof, |ctx| {
542        let mut vec = Vec::new();
543        if let Some(proof) = &ctx.proof {
544            proof.write_to_vec(&mut vec);
545        }
546        vec
547    })
548}
549
550/// Deserialize a `ChangeProof` from bytes.
551///
552/// # Arguments
553///
554/// * `bytes` - The bytes to deserialize the proof from.
555///
556/// # Returns
557///
558/// - [`ChangeProofResult::NullHandlePointer`] if the caller provided a null or zero-length slice.
559/// - [`ChangeProofResult::Ok`] containing a pointer to the `ChangeProofContext` if the proof
560///   was successfully parsed. This does not imply that the proof is valid, only that it is
561///   well-formed. The verify method must be called to ensure the proof is cryptographically valid.
562/// - [`ChangeProofResult::Err`] containing an error message if the proof could not be parsed.
563#[unsafe(no_mangle)]
564pub extern "C" fn fwd_change_proof_from_bytes(bytes: BorrowedBytes) -> ChangeProofResult {
565    crate::invoke(move || {
566        FrozenChangeProof::from_slice(&bytes)
567            .map_err(|err| api::Error::ProofError(ProofError::Deserialization(err)))
568    })
569}
570
571/// Frees the memory associated with a `ChangeProofContext`.
572///
573/// # Arguments
574///
575/// * `proof` - The `ChangeProofContext` to free, previously returned from any Rust function.
576///
577/// # Returns
578///
579/// - [`VoidResult::Ok`] if the memory was successfully freed.
580/// - [`VoidResult::Err`] if the process panics while freeing the memory.
581#[unsafe(no_mangle)]
582pub extern "C" fn fwd_free_change_proof(proof: Option<Box<ChangeProofContext>>) -> VoidResult {
583    crate::invoke_with_handle(proof, drop)
584}
585
586/// Frees the memory associated with a `VerifiedChangeProofContext`.
587///
588/// # Arguments
589///
590/// * `proof` - The `VerifiedChangeProofContext` to free, previously returned from any Rust function.
591///
592/// # Returns
593///
594/// - [`VoidResult::Ok`] if the memory was successfully freed.
595/// - [`VoidResult::Err`] if the process panics while freeing the memory.
596#[unsafe(no_mangle)]
597pub extern "C" fn fwd_free_verified_change_proof(
598    proof: Option<Box<VerifiedChangeProofContext>>,
599) -> VoidResult {
600    crate::invoke_with_handle(proof, drop)
601}
602
603/// Frees the memory associated with a `ProposedChangeProofContext`.
604///
605/// # Arguments
606///
607/// * `proof` - The `ProposedChangeProofContext` to free, previously returned from any Rust function.
608///
609/// # Returns
610///
611/// - [`VoidResult::Ok`] if the memory was successfully freed.
612/// - [`VoidResult::Err`] if the process panics while freeing the memory.
613#[unsafe(no_mangle)]
614pub extern "C" fn fwd_free_proposed_change_proof(
615    proof: Option<Box<ProposedChangeProofContext>>,
616) -> VoidResult {
617    crate::invoke_with_handle(proof, drop)
618}
619
620impl crate::MetricsContextExt for ChangeProofContext {
621    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
622        None
623    }
624}
625
626impl crate::MetricsContextExt for VerifiedChangeProofContext {
627    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
628        None
629    }
630}
631
632impl crate::MetricsContextExt for ProposedChangeProofContext<'_> {
633    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
634        None
635    }
636}
637
638impl crate::MetricsContextExt for (&DatabaseHandle, &mut ChangeProofContext) {
639    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
640        self.0.metrics_context()
641    }
642}
643
644impl crate::MetricsContextExt for (&DatabaseHandle, &mut VerifiedChangeProofContext) {
645    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
646        self.0.metrics_context()
647    }
648}
649
650impl crate::MetricsContextExt for CodeIteratorHandle<'_> {
651    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
652        None
653    }
654}