Skip to main content

fuel_core_compression/
compress.rs

1use crate::{
2    VersionedCompressedBlock,
3    config::Config,
4    eviction_policy::CacheEvictor,
5    ports::{
6        EvictorDb,
7        TemporalRegistry,
8        UtxoIdToPointer,
9    },
10    registry::{
11        EvictorDbAll,
12        PerRegistryKeyspace,
13        RegistrationsPerTable,
14        TemporalRegistryAll,
15    },
16};
17use anyhow::Context;
18use fuel_core_types::{
19    blockchain::block::Block,
20    fuel_compression::{
21        CompressibleBy,
22        ContextError,
23        RegistryKey,
24    },
25    fuel_tx::{
26        CompressedUtxoId,
27        ScriptCode,
28        TxPointer,
29        UtxoId,
30        input::PredicateCode,
31    },
32    fuel_types::{
33        Address,
34        AssetId,
35        ContractId,
36    },
37    tai64::Tai64,
38};
39use std::collections::{
40    HashMap,
41    HashSet,
42};
43
44#[cfg(not(feature = "fault-proving"))]
45pub mod not_fault_proving {
46    use super::*;
47    pub trait CompressDb: TemporalRegistryAll + EvictorDbAll + UtxoIdToPointer {}
48    impl<T> CompressDb for T where T: TemporalRegistryAll + EvictorDbAll + UtxoIdToPointer {}
49}
50
51#[cfg(feature = "fault-proving")]
52pub mod fault_proving {
53    use super::*;
54    use crate::ports::GetRegistryRoot;
55    pub trait CompressDb:
56        TemporalRegistryAll + EvictorDbAll + UtxoIdToPointer + GetRegistryRoot
57    {
58    }
59    impl<T> CompressDb for T where
60        T: TemporalRegistryAll + EvictorDbAll + UtxoIdToPointer + GetRegistryRoot
61    {
62    }
63}
64
65#[cfg(feature = "fault-proving")]
66use fault_proving::CompressDb;
67
68#[cfg(not(feature = "fault-proving"))]
69use not_fault_proving::CompressDb;
70
71/// This must be called for all new blocks in sequence, otherwise the result will be garbage, since
72/// the registry is valid for only the current block height. On any other height you could be
73/// referring to keys that have already been overwritten, or have not been written to yet.
74pub async fn compress<D>(
75    config: &'_ Config,
76    mut db: D,
77    block: &Block,
78) -> anyhow::Result<VersionedCompressedBlock>
79where
80    D: CompressDb,
81{
82    let target = block.transactions_vec();
83
84    let mut prepare_ctx = PrepareCtx {
85        config,
86        timestamp: block.header().time(),
87        db: &mut db,
88        accessed_keys: Default::default(),
89    };
90    let _ = target.compress_with(&mut prepare_ctx).await?;
91
92    let mut ctx = prepare_ctx.into_compression_context()?;
93    let transactions = target.compress_with(&mut ctx).await?;
94    let registrations: RegistrationsPerTable = ctx.finalize()?;
95
96    #[cfg(feature = "fault-proving")]
97    let registry_root = db
98        .registry_root()
99        .map_err(|e| anyhow::anyhow!("Failed to get registry root: {}", e))?;
100
101    Ok(VersionedCompressedBlock::new(
102        block.header(),
103        registrations,
104        transactions,
105        #[cfg(feature = "fault-proving")]
106        registry_root,
107    ))
108}
109
110/// Preparation pass through the block to collect all keys accessed during compression.
111/// Returns dummy values. The resulting "compressed block" should be discarded.
112struct PrepareCtx<'a, D> {
113    config: &'a Config,
114    /// Current timestamp
115    timestamp: Tai64,
116    /// Database handle
117    db: D,
118    /// Keys accessed during the compression.
119    accessed_keys: PerRegistryKeyspace<HashSet<RegistryKey>>,
120}
121
122impl<D> ContextError for PrepareCtx<'_, D> {
123    type Error = anyhow::Error;
124}
125
126impl<'a, D> CompressibleBy<PrepareCtx<'a, D>> for UtxoId
127where
128    D: CompressDb,
129{
130    async fn compress_with(
131        &self,
132        _ctx: &mut PrepareCtx<'a, D>,
133    ) -> anyhow::Result<CompressedUtxoId> {
134        Ok(CompressedUtxoId {
135            tx_pointer: TxPointer::default(),
136            output_index: 0,
137        })
138    }
139}
140
141#[derive(Debug)]
142struct CompressCtxKeyspace<T> {
143    /// Cache evictor state for this keyspace
144    cache_evictor: CacheEvictor<T>,
145    /// Changes to the temporary registry, to be included in the compressed block header
146    changes: HashMap<RegistryKey, T>,
147    /// Reverse lookup into changes
148    changes_lookup: HashMap<T, RegistryKey>,
149}
150
151macro_rules! compression {
152    ($($ident:ty: $type:ty),*) => { paste::paste! {
153        pub struct CompressCtx<'a, D> {
154            config: &'a Config,
155            timestamp: Tai64,
156            db: D,
157            $($ident: CompressCtxKeyspace<$type>,)*
158        }
159
160        impl<'a, D> PrepareCtx<'a, D> where D: CompressDb {
161            /// Converts the preparation context into a [`CompressCtx`]
162            /// keeping accessed keys to avoid its eviction during compression.
163            /// Initializes the cache evictors from the database, which may fail.
164            pub fn into_compression_context(mut self) -> anyhow::Result<CompressCtx<'a, D>> {
165                Ok(CompressCtx {
166                    $(
167                        $ident: CompressCtxKeyspace {
168                            changes: Default::default(),
169                            changes_lookup: Default::default(),
170                            cache_evictor: CacheEvictor::new_from_db(&mut self.db, self.accessed_keys.$ident.into())?,
171                        },
172                    )*
173                    config: self.config,
174                    timestamp: self.timestamp,
175                    db: self.db,
176                })
177            }
178        }
179
180        impl<'a, D> CompressCtx<'a, D> where D: CompressDb {
181            /// Finalizes the compression context, returning the changes to the registry.
182            /// Commits the registrations and cache evictor states to the database.
183            fn finalize(mut self) -> anyhow::Result<RegistrationsPerTable> {
184                let mut registrations = RegistrationsPerTable::default();
185                $(
186                    self.$ident.cache_evictor.commit(&mut self.db)?;
187                    for (key, value) in self.$ident.changes.into_iter() {
188                        registrations.$ident.push((key, value));
189                    }
190                )*
191                registrations.write_to_registry(&mut self.db, self.timestamp)?;
192                Ok(registrations)
193            }
194        }
195
196        $(
197            impl<'a, D> CompressibleBy<PrepareCtx<'a, D>> for $type
198            where
199                D: TemporalRegistry<$type> + EvictorDb<$type>
200            {
201                async fn compress_with(
202                    &self,
203                    ctx: &mut PrepareCtx<'a, D>,
204                ) -> anyhow::Result<RegistryKey> {
205                    if *self == <$type>::default() {
206                        return Ok(RegistryKey::ZERO);
207                    }
208                    if let Some(found) = ctx.db.registry_index_lookup(self)? {
209                        if !ctx.accessed_keys.$ident.contains(&found) {
210                            let key_timestamp = ctx.db.read_timestamp(&found)
211                                .context("Database invariant violated: no timestamp stored but key found")?;
212                            if ctx.config.is_timestamp_accessible(ctx.timestamp, key_timestamp)? {
213                                ctx.accessed_keys.$ident.insert(found);
214                            }
215                        }
216                    }
217                    Ok(RegistryKey::ZERO)
218                }
219            }
220
221            impl<'a, D> CompressibleBy<CompressCtx<'a, D>> for $type
222            where
223                D: TemporalRegistry<$type> + EvictorDb<$type>
224            {
225                async fn compress_with(
226                    &self,
227                    ctx: &mut CompressCtx<'a, D>,
228                ) -> anyhow::Result<RegistryKey> {
229                    if self == &Default::default() {
230                        return Ok(RegistryKey::DEFAULT_VALUE);
231                    }
232                    if let Some(found) = ctx.$ident.changes_lookup.get(self) {
233                        return Ok(*found);
234                    }
235                    if let Some(found) = ctx.db.registry_index_lookup(self)? {
236                        let key_timestamp = ctx.db.read_timestamp(&found)
237                            .context("Database invariant violated: no timestamp stored but key found")?;
238                        if ctx.config.is_timestamp_accessible(ctx.timestamp, key_timestamp)? {
239                            return Ok(found);
240                        }
241                    }
242
243                    let key = ctx.$ident.cache_evictor.next_key();
244                    let old = ctx.$ident.changes.insert(key, self.clone());
245                    let old_rev = ctx.$ident.changes_lookup.insert(self.clone(), key);
246                    debug_assert!(old.is_none(), "Key collision in registry substitution");
247                    debug_assert!(old_rev.is_none(), "Key collision in registry substitution");
248                    Ok(key)
249                }
250            }
251        )*
252    }};
253}
254
255compression!(
256    address: Address,
257    asset_id: AssetId,
258    contract_id: ContractId,
259    script_code: ScriptCode,
260    predicate_code: PredicateCode
261);
262
263impl<D> ContextError for CompressCtx<'_, D> {
264    type Error = anyhow::Error;
265}
266
267impl<'a, D> CompressibleBy<CompressCtx<'a, D>> for UtxoId
268where
269    D: CompressDb,
270{
271    async fn compress_with(
272        &self,
273        ctx: &mut CompressCtx<'a, D>,
274    ) -> anyhow::Result<CompressedUtxoId> {
275        ctx.db.lookup(*self)
276    }
277}