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;
12mod 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    ) -> Self {
64        #[cfg(not(feature = "fault-proving"))]
65        return Self::V0(CompressedBlockPayloadV0::new(
66            header,
67            registrations,
68            transactions,
69        ));
70        #[cfg(feature = "fault-proving")]
71        Self::V1(CompressedBlockPayloadV1::new(
72            header,
73            registrations,
74            transactions,
75        ))
76    }
77}
78
79impl Default for VersionedCompressedBlock {
80    fn default() -> Self {
81        Self::V0(Default::default())
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use fuel_core_compression as _;
89    use fuel_core_types::{
90        blockchain::{
91            header::{
92                ApplicationHeader,
93                ConsensusHeader,
94            },
95            primitives::Empty,
96        },
97        fuel_compression::RegistryKey,
98        tai64::Tai64,
99    };
100    use proptest::prelude::*;
101
102    fn keyspace() -> impl Strategy<Value = RegistryKeyspace> {
103        prop_oneof![
104            Just(RegistryKeyspace::Address),
105            Just(RegistryKeyspace::AssetId),
106            Just(RegistryKeyspace::ContractId),
107            Just(RegistryKeyspace::ScriptCode),
108            Just(RegistryKeyspace::PredicateCode),
109        ]
110    }
111
112    #[derive(Debug)]
113    struct PostcardRoundtripStrategy {
114        da_height: u64,
115        prev_root: [u8; 32],
116        height: u32,
117        consensus_parameters_version: u32,
118        state_transition_bytecode_version: u32,
119        registrations: RegistrationsPerTable,
120    }
121
122    fn postcard_roundtrip_strategy() -> impl Strategy<Value = PostcardRoundtripStrategy> {
123        (
124            0..=u64::MAX,
125            prop::array::uniform32(0..=u8::MAX),
126            0..=u32::MAX,
127            0..=u32::MAX,
128            0..=u32::MAX,
129            prop::collection::vec(
130                (
131                    keyspace(),
132                    prop::num::u16::ANY,
133                    prop::array::uniform32(0..=u8::MAX),
134                )
135                    .prop_map(|(ks, rk, arr)| {
136                        let k = RegistryKey::try_from(rk as u32).unwrap();
137                        (ks, k, arr)
138                    }),
139                0..123,
140            ),
141        )
142            .prop_map(
143                |(
144                    da_height,
145                    prev_root,
146                    height,
147                    consensus_parameters_version,
148                    state_transition_bytecode_version,
149                    registration_inputs,
150                )| {
151                    let mut registrations: RegistrationsPerTable = Default::default();
152                    for (ks, key, arr) in registration_inputs {
153                        let value_len_limit = (key.as_u32() % 32) as usize;
154                        match ks {
155                            RegistryKeyspace::Address => {
156                                registrations.address.push((key, arr.into()));
157                            }
158                            RegistryKeyspace::AssetId => {
159                                registrations.asset_id.push((key, arr.into()));
160                            }
161                            RegistryKeyspace::ContractId => {
162                                registrations.contract_id.push((key, arr.into()));
163                            }
164                            RegistryKeyspace::ScriptCode => {
165                                registrations
166                                    .script_code
167                                    .push((key, arr[..value_len_limit].to_vec().into()));
168                            }
169                            RegistryKeyspace::PredicateCode => {
170                                registrations
171                                    .predicate_code
172                                    .push((key, arr[..value_len_limit].to_vec().into()));
173                            }
174                        }
175                    }
176
177                    PostcardRoundtripStrategy {
178                        da_height,
179                        prev_root,
180                        height,
181                        consensus_parameters_version,
182                        state_transition_bytecode_version,
183                        registrations,
184                    }
185                },
186            )
187    }
188
189    /// Serialization for compressed transactions is already tested in fuel-vm,
190    /// but the rest of the block de/serialization is tested here.
191    #[test]
192    fn postcard_roundtrip_v0() {
193        proptest!(|(strategy in postcard_roundtrip_strategy())| {
194            let PostcardRoundtripStrategy {
195                da_height,
196                prev_root,
197                height,
198                consensus_parameters_version,
199                state_transition_bytecode_version,
200                registrations,
201            } = strategy;
202
203            let header = PartialBlockHeader {
204                application: ApplicationHeader {
205                    da_height: da_height.into(),
206                    consensus_parameters_version,
207                    state_transition_bytecode_version,
208                    generated: Empty,
209                },
210                consensus: ConsensusHeader {
211                    prev_root: prev_root.into(),
212                    height: height.into(),
213                    time: Tai64::UNIX_EPOCH,
214                    generated: Empty
215                }
216            };
217
218            let original = VersionedCompressedBlock::V0(CompressedBlockPayloadV0 {
219                registrations,
220                header,
221                transactions: vec![],
222            });
223
224            let compressed = postcard::to_allocvec(&original).unwrap();
225            let decompressed: VersionedCompressedBlock =
226                postcard::from_bytes(&compressed).unwrap();
227
228            let consensus_header = decompressed.consensus_header();
229            let application_header = decompressed.application_header();
230
231            assert_eq!(decompressed.registrations(), original.registrations());
232
233            assert_eq!(application_header.da_height, da_height.into());
234            assert_eq!(consensus_header.prev_root, prev_root.into());
235            assert_eq!(consensus_header.height, height.into());
236            assert_eq!(application_header.consensus_parameters_version, consensus_parameters_version);
237            assert_eq!(application_header.state_transition_bytecode_version, state_transition_bytecode_version);
238
239            assert!(decompressed.transactions().is_empty());
240        });
241    }
242
243    #[cfg(feature = "fault-proving")]
244    #[test]
245    fn postcard_roundtrip_v1() {
246        use compressed_block_payload::v1::{
247            CompressedBlockHeader,
248            CompressedBlockPayloadV1,
249        };
250        use fuel_core_types::blockchain::primitives::BlockId;
251        use std::str::FromStr;
252
253        proptest!(|(strategy in postcard_roundtrip_strategy())| {
254            let PostcardRoundtripStrategy {
255                da_height,
256                prev_root,
257                height,
258                consensus_parameters_version,
259                state_transition_bytecode_version,
260                registrations,
261            } = strategy;
262
263            let header = CompressedBlockHeader {
264                application: ApplicationHeader {
265                    da_height: da_height.into(),
266                    consensus_parameters_version,
267                    state_transition_bytecode_version,
268                    generated: Empty,
269                },
270                consensus: ConsensusHeader {
271                    prev_root: prev_root.into(),
272                    height: height.into(),
273                    time: Tai64::UNIX_EPOCH,
274                    generated: Empty,
275                },
276                block_id: BlockId::from_str("0xecea85c17070bc2e65f911310dbd01198f4436052ebba96cded9ddf30c58dd1a").unwrap(),
277            };
278
279
280            let original = VersionedCompressedBlock::V1(CompressedBlockPayloadV1 {
281                header,
282                registrations,
283                transactions: vec![]
284            });
285
286            let compressed = postcard::to_allocvec(&original).unwrap();
287            let decompressed: VersionedCompressedBlock =
288                postcard::from_bytes(&compressed).unwrap();
289
290            let consensus_header = decompressed.consensus_header();
291            let application_header = decompressed.application_header();
292
293            assert_eq!(decompressed.registrations(), original.registrations());
294
295            assert_eq!(application_header.da_height, da_height.into());
296            assert_eq!(consensus_header.prev_root, prev_root.into());
297            assert_eq!(consensus_header.height, height.into());
298            assert_eq!(application_header.consensus_parameters_version, consensus_parameters_version);
299            assert_eq!(application_header.state_transition_bytecode_version, state_transition_bytecode_version);
300
301            assert!(decompressed.transactions().is_empty());
302
303            if let VersionedCompressedBlock::V1(block) = decompressed {
304                assert_eq!(block.header.block_id, header.block_id);
305            } else {
306                panic!("Expected V1 block, got {:?}", decompressed);
307            }
308        });
309    }
310}