reth_cli_commands/test_vectors/
compact.rs

1use alloy_eips::eip4895::Withdrawals;
2use alloy_primitives::{hex, Signature, TxKind, B256};
3use arbitrary::Arbitrary;
4use eyre::{Context, Result};
5use proptest::{
6    prelude::{ProptestConfig, RngCore},
7    test_runner::{TestRng, TestRunner},
8};
9use reth_codecs::alloy::{
10    authorization_list::Authorization,
11    genesis_account::GenesisAccount,
12    header::{Header, HeaderExt},
13    transaction::{
14        eip1559::TxEip1559, eip2930::TxEip2930, eip4844::TxEip4844, eip7702::TxEip7702,
15        legacy::TxLegacy,
16    },
17    withdrawal::Withdrawal,
18};
19use reth_db::{
20    models::{
21        AccountBeforeTx, StaticFileBlockWithdrawals, StoredBlockBodyIndices, StoredBlockOmmers,
22        StoredBlockWithdrawals,
23    },
24    ClientVersion,
25};
26use reth_ethereum_primitives::{Receipt, Transaction, TransactionSigned, TxType};
27use reth_fs_util as fs;
28use reth_primitives_traits::{Account, Log, LogData, StorageEntry};
29use reth_prune_types::{PruneCheckpoint, PruneMode};
30use reth_stages_types::{
31    AccountHashingCheckpoint, CheckpointBlockRange, EntitiesCheckpoint, ExecutionCheckpoint,
32    HeadersCheckpoint, IndexHistoryCheckpoint, StageCheckpoint, StageUnitCheckpoint,
33    StorageHashingCheckpoint,
34};
35use reth_trie::{hash_builder::HashBuilderValue, TrieMask};
36use reth_trie_common::{hash_builder::HashBuilderState, StoredNibbles, StoredNibblesSubKey};
37use std::{fs::File, io::BufReader};
38
39pub const VECTORS_FOLDER: &str = "testdata/micro/compact";
40pub const VECTOR_SIZE: usize = 100;
41
42#[macro_export]
43macro_rules! compact_types {
44    (regular: [$($regular_ty:ident),*], identifier: [$($id_ty:ident),*]) => {
45        pub const GENERATE_VECTORS: &[fn(&mut TestRunner) -> eyre::Result<()>] = &[
46            $(
47                generate_vector::<$regular_ty> as fn(&mut TestRunner) -> eyre::Result<()>,
48            )*
49            $(
50                generate_vector::<$id_ty> as fn(&mut TestRunner) -> eyre::Result<()>,
51            )*
52        ];
53
54        pub const READ_VECTORS: &[fn() -> eyre::Result<()>] = &[
55            $(
56                read_vector::<$regular_ty> as fn() -> eyre::Result<()>,
57            )*
58            $(
59                read_vector::<$id_ty> as fn() -> eyre::Result<()>,
60            )*
61        ];
62
63        pub static IDENTIFIER_TYPE: std::sync::LazyLock<std::collections::HashSet<String>> = std::sync::LazyLock::new(|| {
64            let mut map = std::collections::HashSet::new();
65            $(
66                 map.insert(type_name::<$id_ty>());
67            )*
68            map
69        });
70    };
71}
72
73// The type that **actually** implements `Compact` should go here. If it's an alloy type, import the
74// auxiliary type from reth_codecs::alloy instead.
75compact_types!(
76    regular: [
77        // reth-primitives
78        Account,
79        Receipt,
80        // reth_codecs::alloy
81        Authorization,
82        GenesisAccount,
83        Header,
84        HeaderExt,
85        Withdrawal,
86        Withdrawals,
87        TxEip2930,
88        TxEip1559,
89        TxEip4844,
90        TxEip7702,
91        TxLegacy,
92        HashBuilderValue,
93        LogData,
94        Log,
95        // BranchNodeCompact, // todo requires arbitrary
96        TrieMask,
97        // TxDeposit, TODO(joshie): optimism
98        // reth_prune_types
99        PruneCheckpoint,
100        PruneMode,
101        // reth_stages_types
102        AccountHashingCheckpoint,
103        StorageHashingCheckpoint,
104        ExecutionCheckpoint,
105        HeadersCheckpoint,
106        IndexHistoryCheckpoint,
107        EntitiesCheckpoint,
108        CheckpointBlockRange,
109        StageCheckpoint,
110        StageUnitCheckpoint,
111        // reth_db_api
112        StoredBlockOmmers,
113        StoredBlockBodyIndices,
114        StoredBlockWithdrawals,
115        StaticFileBlockWithdrawals,
116        // Manual implementations
117        TransactionSigned,
118        // Bytecode, // todo revm arbitrary
119        StorageEntry,
120        // MerkleCheckpoint, // todo storedsubnode -> branchnodecompact arbitrary
121        AccountBeforeTx,
122        ClientVersion,
123        StoredNibbles,
124        StoredNibblesSubKey,
125        // StorageTrieEntry, // todo branchnodecompact arbitrary
126        // StoredSubNode, // todo branchnodecompact arbitrary
127        HashBuilderState
128    ],
129    // These types require an extra identifier which is usually stored elsewhere (eg. parent type).
130    identifier: [
131        Signature,
132        Transaction,
133        TxType,
134        TxKind
135    ]
136);
137
138/// Generates a vector of type `T` to a file.
139pub fn generate_vectors() -> Result<()> {
140    generate_vectors_with(GENERATE_VECTORS)
141}
142
143pub fn read_vectors() -> Result<()> {
144    read_vectors_with(READ_VECTORS)
145}
146
147/// Generates a vector of type `T` to a file.
148pub fn generate_vectors_with(generator: &[fn(&mut TestRunner) -> eyre::Result<()>]) -> Result<()> {
149    // Prepare random seed for test (same method as used by proptest)
150    let seed = B256::random();
151    println!("Seed for compact test vectors: {:?}", hex::encode_prefixed(seed));
152
153    // Start the runner with the seed
154    let config = ProptestConfig::default();
155    let rng = TestRng::from_seed(config.rng_algorithm, &seed.0);
156    let mut runner = TestRunner::new_with_rng(config, rng);
157
158    fs::create_dir_all(VECTORS_FOLDER)?;
159
160    for generate_fn in generator {
161        generate_fn(&mut runner)?;
162    }
163
164    Ok(())
165}
166
167/// Reads multiple vectors of different types ensuring their correctness by decoding and
168/// re-encoding.
169pub fn read_vectors_with(read: &[fn() -> eyre::Result<()>]) -> Result<()> {
170    fs::create_dir_all(VECTORS_FOLDER)?;
171    let mut errors = None;
172
173    for read_fn in read {
174        if let Err(err) = read_fn() {
175            errors.get_or_insert_with(Vec::new).push(err);
176        }
177    }
178
179    if let Some(err_list) = errors {
180        for error in err_list {
181            eprintln!("{error:?}");
182        }
183        return Err(eyre::eyre!(
184            "If there are missing types, make sure to run `reth test-vectors compact --write` first.\n
185             If it happened during CI, ignore IF it's a new proposed type that `main` branch does not have."
186        ));
187    }
188
189    Ok(())
190}
191
192/// Generates test vectors for a specific type `T`.
193pub fn generate_vector<T>(runner: &mut TestRunner) -> Result<()>
194where
195    T: for<'a> Arbitrary<'a> + reth_codecs::Compact,
196{
197    let type_name = type_name::<T>();
198    print!("{}", &type_name);
199
200    let mut bytes = std::iter::repeat_n(0u8, 256).collect::<Vec<u8>>();
201    let mut compact_buffer = vec![];
202
203    let mut values = Vec::with_capacity(VECTOR_SIZE);
204    for _ in 0..VECTOR_SIZE {
205        runner.rng().fill_bytes(&mut bytes);
206        compact_buffer.clear();
207
208        // Sometimes type T, might require extra arbitrary data, so we retry it a few times.
209        let mut tries = 0;
210        let obj = loop {
211            match T::arbitrary(&mut arbitrary::Unstructured::new(&bytes)) {
212                Ok(obj) => break obj,
213                Err(err) => {
214                    if tries < 5 && matches!(err, arbitrary::Error::NotEnoughData) {
215                        tries += 1;
216                        bytes.extend(std::iter::repeat_n(0u8, 256));
217                    } else {
218                        return Err(err)?
219                    }
220                }
221            }
222        };
223        let res = obj.to_compact(&mut compact_buffer);
224
225        if IDENTIFIER_TYPE.contains(&type_name) {
226            compact_buffer.push(res as u8);
227        }
228
229        values.push(hex::encode(&compact_buffer));
230    }
231
232    serde_json::to_writer(
233        std::io::BufWriter::new(
234            std::fs::File::create(format!("{VECTORS_FOLDER}/{}.json", &type_name)).unwrap(),
235        ),
236        &values,
237    )?;
238
239    println!(" ✅");
240
241    Ok(())
242}
243
244/// Reads a vector of type `T` from a file and compares each item with its reconstructed version
245/// using `T::from_compact`.
246pub fn read_vector<T>() -> Result<()>
247where
248    T: reth_codecs::Compact,
249{
250    let type_name = type_name::<T>();
251    print!("{}", &type_name);
252
253    // Read the file where the vectors are stored
254    let file_path = format!("{VECTORS_FOLDER}/{}.json", &type_name);
255    let file =
256        File::open(&file_path).wrap_err_with(|| format!("Failed to open vector {type_name}."))?;
257    let reader = BufReader::new(file);
258
259    let stored_values: Vec<String> = serde_json::from_reader(reader)?;
260    let mut buffer = vec![];
261
262    for hex_str in stored_values {
263        let mut compact_bytes = hex::decode(hex_str)?;
264        let mut identifier = None;
265        buffer.clear();
266
267        if IDENTIFIER_TYPE.contains(&type_name) {
268            identifier = compact_bytes.pop().map(|b| b as usize);
269        }
270        let len_or_identifier = identifier.unwrap_or(compact_bytes.len());
271
272        let (reconstructed, _) = T::from_compact(&compact_bytes, len_or_identifier);
273        reconstructed.to_compact(&mut buffer);
274        assert_eq!(buffer, compact_bytes, "mismatch {type_name}");
275    }
276
277    println!(" ✅");
278
279    Ok(())
280}
281
282/// Returns the type name for the given type.
283pub fn type_name<T>() -> String {
284    // With alloy type transition <https://github.com/paradigmxyz/reth/pull/15768> the types are renamed, we map them here to the original name so that test vector files remain consistent
285    let name = std::any::type_name::<T>();
286    match name {
287        "alloy_consensus::transaction::typed::EthereumTypedTransaction<alloy_consensus::transaction::eip4844::TxEip4844>" => "Transaction".to_string(),
288        "alloy_consensus::transaction::envelope::EthereumTxEnvelope<alloy_consensus::transaction::eip4844::TxEip4844>" => "TransactionSigned".to_string(),
289        name => {
290            name.split("::").last().unwrap_or(std::any::type_name::<T>()).to_string()
291        }
292    }
293}