fuel_core_compression/
lib.rs

1#![deny(clippy::arithmetic_side_effects)]
2#![deny(clippy::cast_possible_truncation)]
3#![deny(unused_crate_dependencies)]
4#![deny(warnings)]
5
6pub mod compress;
7mod compressed_block_payload;
8pub mod config;
9pub mod decompress;
10mod eviction_policy;
11pub mod ports;
12pub mod registry;
13
14pub use config::Config;
15use enum_dispatch::enum_dispatch;
16pub use registry::RegistryKeyspace;
17
18use crate::compressed_block_payload::v0::CompressedBlockPayloadV0;
19#[cfg(feature = "fault-proving")]
20use crate::compressed_block_payload::v1::CompressedBlockPayloadV1;
21use fuel_core_types::{
22    blockchain::{
23        header::{
24            ApplicationHeader,
25            BlockHeader,
26            ConsensusHeader,
27            PartialBlockHeader,
28        },
29        primitives::Empty,
30    },
31    fuel_tx::CompressedTransaction,
32    fuel_types::BlockHeight,
33};
34use registry::RegistrationsPerTable;
35
36/// A compressed block payload MUST implement this trait
37/// It is used to provide a convenient interface for usage within
38/// compression
39#[enum_dispatch]
40pub trait VersionedBlockPayload {
41    fn height(&self) -> &BlockHeight;
42    fn consensus_header(&self) -> &ConsensusHeader<Empty>;
43    fn application_header(&self) -> &ApplicationHeader<Empty>;
44    fn registrations(&self) -> &RegistrationsPerTable;
45    fn transactions(&self) -> Vec<CompressedTransaction>;
46    fn partial_block_header(&self) -> PartialBlockHeader;
47}
48
49/// Versioned compressed block.
50#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
51#[enum_dispatch(VersionedBlockPayload)]
52pub enum VersionedCompressedBlock {
53    V0(CompressedBlockPayloadV0),
54    #[cfg(feature = "fault-proving")]
55    V1(CompressedBlockPayloadV1),
56}
57
58impl VersionedCompressedBlock {
59    fn new(
60        header: &BlockHeader,
61        registrations: RegistrationsPerTable,
62        transactions: Vec<CompressedTransaction>,
63        #[cfg(feature = "fault-proving")]
64        registry_root: crate::compressed_block_payload::v1::RegistryRoot,
65    ) -> Self {
66        #[cfg(not(feature = "fault-proving"))]
67        return Self::V0(CompressedBlockPayloadV0::new(
68            header,
69            registrations,
70            transactions,
71        ));
72        #[cfg(feature = "fault-proving")]
73        Self::V1(CompressedBlockPayloadV1::new(
74            header,
75            registrations,
76            transactions,
77            registry_root,
78        ))
79    }
80}
81
82impl Default for VersionedCompressedBlock {
83    fn default() -> Self {
84        Self::V0(Default::default())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use fuel_core_compression as _;
92    use fuel_core_types::{
93        blockchain::{
94            header::{
95                ApplicationHeader,
96                ConsensusHeader,
97            },
98            primitives::Empty,
99        },
100        fuel_compression::RegistryKey,
101        tai64::Tai64,
102    };
103    use proptest::prelude::*;
104
105    fn keyspace() -> impl Strategy<Value = RegistryKeyspace> {
106        prop_oneof![
107            Just(RegistryKeyspace::Address),
108            Just(RegistryKeyspace::AssetId),
109            Just(RegistryKeyspace::ContractId),
110            Just(RegistryKeyspace::ScriptCode),
111            Just(RegistryKeyspace::PredicateCode),
112        ]
113    }
114
115    #[derive(Debug)]
116    struct PostcardRoundtripStrategy {
117        da_height: u64,
118        prev_root: [u8; 32],
119        height: u32,
120        consensus_parameters_version: u32,
121        state_transition_bytecode_version: u32,
122        registrations: RegistrationsPerTable,
123    }
124
125    fn postcard_roundtrip_strategy() -> impl Strategy<Value = PostcardRoundtripStrategy> {
126        (
127            0..=u64::MAX,
128            prop::array::uniform32(0..=u8::MAX),
129            0..=u32::MAX,
130            0..=u32::MAX,
131            0..=u32::MAX,
132            prop::collection::vec(
133                (
134                    keyspace(),
135                    prop::num::u16::ANY,
136                    prop::array::uniform32(0..=u8::MAX),
137                )
138                    .prop_map(|(ks, rk, arr)| {
139                        let k = RegistryKey::try_from(rk as u32).unwrap();
140                        (ks, k, arr)
141                    }),
142                0..123,
143            ),
144        )
145            .prop_map(
146                |(
147                    da_height,
148                    prev_root,
149                    height,
150                    consensus_parameters_version,
151                    state_transition_bytecode_version,
152                    registration_inputs,
153                )| {
154                    let mut registrations: RegistrationsPerTable = Default::default();
155                    for (ks, key, arr) in registration_inputs {
156                        let value_len_limit = (key.as_u32() % 32) as usize;
157                        match ks {
158                            RegistryKeyspace::Address => {
159                                registrations.address.push((key, arr.into()));
160                            }
161                            RegistryKeyspace::AssetId => {
162                                registrations.asset_id.push((key, arr.into()));
163                            }
164                            RegistryKeyspace::ContractId => {
165                                registrations.contract_id.push((key, arr.into()));
166                            }
167                            RegistryKeyspace::ScriptCode => {
168                                registrations
169                                    .script_code
170                                    .push((key, arr[..value_len_limit].to_vec().into()));
171                            }
172                            RegistryKeyspace::PredicateCode => {
173                                registrations
174                                    .predicate_code
175                                    .push((key, arr[..value_len_limit].to_vec().into()));
176                            }
177                        }
178                    }
179
180                    PostcardRoundtripStrategy {
181                        da_height,
182                        prev_root,
183                        height,
184                        consensus_parameters_version,
185                        state_transition_bytecode_version,
186                        registrations,
187                    }
188                },
189            )
190    }
191
192    /// Serialization for compressed transactions is already tested in fuel-vm,
193    /// but the rest of the block de/serialization is tested here.
194    #[test]
195    fn postcard_roundtrip_v0() {
196        proptest!(|(strategy in postcard_roundtrip_strategy())| {
197            let PostcardRoundtripStrategy {
198                da_height,
199                prev_root,
200                height,
201                consensus_parameters_version,
202                state_transition_bytecode_version,
203                registrations,
204            } = strategy;
205
206            let header = PartialBlockHeader {
207                application: ApplicationHeader {
208                    da_height: da_height.into(),
209                    consensus_parameters_version,
210                    state_transition_bytecode_version,
211                    generated: Empty,
212                },
213                consensus: ConsensusHeader {
214                    prev_root: prev_root.into(),
215                    height: height.into(),
216                    time: Tai64::UNIX_EPOCH,
217                    generated: Empty
218                }
219            };
220
221            let original = VersionedCompressedBlock::V0(CompressedBlockPayloadV0 {
222                registrations,
223                header,
224                transactions: vec![],
225            });
226
227            let compressed = postcard::to_allocvec(&original).unwrap();
228            let decompressed: VersionedCompressedBlock =
229                postcard::from_bytes(&compressed).unwrap();
230
231            let consensus_header = decompressed.consensus_header();
232            let application_header = decompressed.application_header();
233
234            assert_eq!(decompressed.registrations(), original.registrations());
235
236            assert_eq!(application_header.da_height, da_height.into());
237            assert_eq!(consensus_header.prev_root, prev_root.into());
238            assert_eq!(consensus_header.height, height.into());
239            assert_eq!(application_header.consensus_parameters_version, consensus_parameters_version);
240            assert_eq!(application_header.state_transition_bytecode_version, state_transition_bytecode_version);
241
242            assert!(decompressed.transactions().is_empty());
243        });
244    }
245
246    #[cfg(feature = "fault-proving")]
247    #[test]
248    fn postcard_roundtrip_v1() {
249        use compressed_block_payload::v1::{
250            CompressedBlockHeader,
251            CompressedBlockPayloadV1,
252        };
253        use fuel_core_types::blockchain::primitives::BlockId;
254        use std::str::FromStr;
255
256        use crate::compressed_block_payload::v1::RegistryRoot;
257
258        proptest!(|(strategy in postcard_roundtrip_strategy())| {
259            let PostcardRoundtripStrategy {
260                da_height,
261                prev_root,
262                height,
263                consensus_parameters_version,
264                state_transition_bytecode_version,
265                registrations,
266            } = strategy;
267
268            let header = CompressedBlockHeader {
269                application: ApplicationHeader {
270                    da_height: da_height.into(),
271                    consensus_parameters_version,
272                    state_transition_bytecode_version,
273                    generated: Empty,
274                },
275                consensus: ConsensusHeader {
276                    prev_root: prev_root.into(),
277                    height: height.into(),
278                    time: Tai64::UNIX_EPOCH,
279                    generated: Empty,
280                },
281                block_id: BlockId::from_str("0xecea85c17070bc2e65f911310dbd01198f4436052ebba96cded9ddf30c58dd1a").unwrap(),
282                registry_root: RegistryRoot::from_str("0xecea85c17070bc2e65f911310dbd01198f4436052ebba96cded9ddf30c58dd1b").unwrap(),
283            };
284
285
286            let original = VersionedCompressedBlock::V1(CompressedBlockPayloadV1 {
287                header,
288                registrations,
289                transactions: vec![]
290            });
291
292            let compressed = postcard::to_allocvec(&original).unwrap();
293            let decompressed: VersionedCompressedBlock =
294                postcard::from_bytes(&compressed).unwrap();
295
296            let consensus_header = decompressed.consensus_header();
297            let application_header = decompressed.application_header();
298
299            assert_eq!(decompressed.registrations(), original.registrations());
300
301            assert_eq!(application_header.da_height, da_height.into());
302            assert_eq!(consensus_header.prev_root, prev_root.into());
303            assert_eq!(consensus_header.height, height.into());
304            assert_eq!(application_header.consensus_parameters_version, consensus_parameters_version);
305            assert_eq!(application_header.state_transition_bytecode_version, state_transition_bytecode_version);
306
307            assert!(decompressed.transactions().is_empty());
308
309            if let VersionedCompressedBlock::V1(block) = decompressed {
310                assert_eq!(block.header.block_id, header.block_id);
311                assert_eq!(block.header.registry_root, header.registry_root);
312            } else {
313                panic!("Expected V1 block, got {:?}", decompressed);
314            }
315        });
316    }
317}