Skip to main content

firewood_ffi/
proposal.rs

1// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2// See the file LICENSE.md for licensing terms.
3
4use firewood::api::{self, BoxKeyValueIter, DbView, HashKey, IntoBatchIter, Proposal as _};
5
6use crate::{IteratorHandle, iterator::CreateIteratorResult, metrics::MetricsContextExt};
7use firewood_metrics::{firewood_increment, firewood_record, fwd_expensive_timed_result};
8
9/// An opaque wrapper around a Proposal that also retains a reference to the
10/// database handle it was created from.
11#[derive(Debug)]
12pub struct ProposalHandle<'db> {
13    hash_key: Option<HashKey>,
14    proposal: firewood::db::Proposal<'db>,
15    handle: &'db crate::DatabaseHandle,
16}
17
18impl<'db> DbView for ProposalHandle<'db> {
19    type Iter<'view>
20        = <firewood::db::Proposal<'db> as DbView>::Iter<'view>
21    where
22        Self: 'view;
23
24    fn root_hash(&self) -> Option<HashKey> {
25        self.proposal.root_hash()
26    }
27
28    fn val<K: api::KeyType>(&self, key: K) -> Result<Option<firewood::merkle::Value>, api::Error> {
29        self.proposal.val(key)
30    }
31
32    fn single_key_proof<K: api::KeyType>(&self, key: K) -> Result<api::FrozenProof, api::Error> {
33        self.proposal.single_key_proof(key)
34    }
35
36    fn range_proof<K: api::KeyType>(
37        &self,
38        first_key: Option<K>,
39        last_key: Option<K>,
40        limit: Option<std::num::NonZeroUsize>,
41    ) -> Result<api::FrozenRangeProof, api::Error> {
42        self.proposal.range_proof(first_key, last_key, limit)
43    }
44
45    fn iter_option<K: api::KeyType>(
46        &self,
47        first_key: Option<K>,
48    ) -> Result<Self::Iter<'_>, api::Error> {
49        self.proposal.iter_option(first_key)
50    }
51
52    fn dump_to_string(&self) -> Result<String, api::Error> {
53        self.proposal.dump_to_string()
54    }
55}
56
57impl ProposalHandle<'_> {
58    /// Returns the root hash of the proposal.
59    #[must_use]
60    pub fn hash_key(&self) -> Option<crate::HashKey> {
61        self.hash_key.clone().map(Into::into)
62    }
63
64    /// Consume and commit a proposal.
65    ///
66    /// # Errors
67    ///
68    /// This function will return an error if committing the proposal fails or if the
69    /// proposal is empty.
70    pub fn commit_proposal(self) -> Result<Option<HashKey>, api::Error> {
71        let ProposalHandle {
72            hash_key,
73            proposal,
74            handle,
75        } = self;
76
77        // promote the proposal to the handle's cached view so that it can be used
78        // for future reads while the proposal is being committed
79        if let Some(ref hash_key) = hash_key {
80            _ = handle.get_root(hash_key.clone());
81        }
82
83        let (commit_result, commit_time) =
84            fwd_expensive_timed_result!(crate::registry::COMMIT_MS_BUCKET, proposal.commit());
85        commit_result?;
86
87        // clear the cached view so that it does not hold onto the proposal view
88        handle.clear_cached_view();
89
90        firewood_increment!(crate::registry::COMMIT_MS, commit_time.as_millis() as u64);
91        firewood_increment!(crate::registry::COMMIT_COUNT, 1);
92
93        Ok(hash_key)
94    }
95
96    /// Creates an iterator on the proposal starting from the given key.
97    #[must_use]
98    #[allow(clippy::missing_panics_doc)]
99    pub fn iter_from(&self, first_key: Option<&[u8]>) -> CreateIteratorResult<'_> {
100        let it = self
101            .iter_option(first_key)
102            .expect("infallible; see issue #1329");
103        CreateIteratorResult(IteratorHandle::new(
104            self.proposal.view(),
105            Box::new(it) as BoxKeyValueIter<'_>,
106            self.handle.metrics_context(),
107        ))
108    }
109}
110
111#[derive(Debug)]
112pub struct CreateProposalResult<'db> {
113    pub handle: ProposalHandle<'db>,
114}
115
116impl<'db> CreateProposalResult<'db> {
117    pub(crate) fn new(
118        handle: &'db crate::DatabaseHandle,
119        f: impl FnOnce() -> Result<firewood::db::Proposal<'db>, api::Error>,
120    ) -> Result<Self, api::Error> {
121        let (proposal_result, propose_time) =
122            fwd_expensive_timed_result!(crate::registry::PROPOSE_MS_BUCKET, f());
123        let proposal = proposal_result?;
124        firewood_increment!(crate::registry::PROPOSE_MS, propose_time.as_millis() as u64);
125        firewood_increment!(crate::registry::PROPOSE_COUNT, 1);
126        firewood_record!(
127            crate::registry::PROPOSE_MS_BUCKET,
128            propose_time.as_secs_f64() * 1000.0,
129            expensive
130        );
131
132        let hash_key = proposal.root_hash();
133
134        Ok(CreateProposalResult {
135            handle: ProposalHandle {
136                hash_key,
137                proposal,
138                handle,
139            },
140        })
141    }
142}
143
144/// A trait that abstracts over database handles and proposal handles for creating proposals.
145///
146/// This trait allows functions to work with both [`DatabaseHandle`] and [`ProposalHandle`]
147/// uniformly when creating new proposals. It provides a common interface for:
148/// - Getting the underlying database handle
149/// - Creating proposals from key-value pairs
150/// - Creating proposal handles with timing information
151///
152/// This abstraction enables proposal chaining (creating proposals on top of other proposals)
153/// while maintaining a consistent API.
154///
155/// [`DatabaseHandle`]: crate::DatabaseHandle
156pub trait CView<'db> {
157    /// Returns a reference to the database handle that is ultimately used to
158    /// create the proposal. For the database handle, this returns itself. For,
159    /// a proposal handle, this returns the handle that was used to create the
160    /// proposal.
161    fn handle(&self) -> &'db crate::DatabaseHandle;
162
163    /// Create a [`firewood::db::Proposal`] with the provided key-value pairs.
164    ///
165    /// # Errors
166    ///
167    /// This function will return a database error if the proposal could not be
168    /// created.
169    fn create_proposal(
170        self,
171        values: impl IntoBatchIter,
172    ) -> Result<firewood::db::Proposal<'db>, api::Error>;
173
174    /// Create a [`ProposalHandle`] from the values and return it with timing
175    /// information.
176    ///
177    /// # Errors
178    ///
179    /// This function will return a database error if the proposal could not be
180    /// created or if the proposal is empty.
181    fn create_proposal_handle(
182        self,
183        values: impl IntoBatchIter,
184    ) -> Result<CreateProposalResult<'db>, api::Error>
185    where
186        Self: Sized,
187    {
188        let handle = self.handle();
189        CreateProposalResult::new(handle, || self.create_proposal(values))
190    }
191}
192
193impl<'db> CView<'db> for &ProposalHandle<'db> {
194    fn handle(&self) -> &'db crate::DatabaseHandle {
195        self.handle
196    }
197
198    fn create_proposal(
199        self,
200        values: impl IntoBatchIter,
201    ) -> Result<firewood::db::Proposal<'db>, api::Error> {
202        self.proposal.propose(values)
203    }
204}
205
206impl crate::MetricsContextExt for ProposalHandle<'_> {
207    fn metrics_context(&self) -> Option<firewood_metrics::MetricsContext> {
208        self.handle.metrics_context()
209    }
210}