domain_client_operator/
aux_schema.rs

1//! Schema for executor in the aux-db.
2
3use crate::ExecutionReceiptFor;
4use parity_scale_codec::{Decode, Encode};
5use sc_client_api::backend::AuxStore;
6use sp_blockchain::{Error as ClientError, HeaderBackend, Result as ClientResult};
7use sp_domains::bundle::InvalidBundleType;
8use sp_runtime::Saturating;
9use sp_runtime::traits::{
10    Block as BlockT, CheckedMul, CheckedSub, NumberFor, One, SaturatedConversion, Zero,
11};
12use std::collections::BTreeSet;
13use std::sync::Arc;
14use subspace_core_primitives::BlockNumber;
15use subspace_runtime_primitives::{BlockHashFor, DOMAINS_PRUNING_DEPTH_MULTIPLIER};
16
17const EXECUTION_RECEIPT: &[u8] = b"execution_receipt";
18const EXECUTION_RECEIPT_START: &[u8] = b"execution_receipt_start";
19const EXECUTION_RECEIPT_BLOCK_NUMBER: &[u8] = b"execution_receipt_block_number";
20
21/// domain_block_hash => latest_consensus_block_hash
22///
23/// It's important to note that a consensus block could possibly contain no bundles for a specific domain,
24/// leading to the situation where multiple consensus blocks could correspond to the same domain block.
25///
26/// ConsensusBlock10 --> DomainBlock5
27/// ConsensusBlock11 --> DomainBlock5
28/// ConsensusBlock12 --> DomainBlock5
29///
30/// This mapping is designed to track the most recent consensus block that derives the domain block
31/// identified by `domain_block_hash`, e.g., Hash(DomainBlock5) => Hash(ConsensusBlock12).
32const LATEST_CONSENSUS_HASH: &[u8] = b"latest_consensus_hash";
33
34/// consensus_block_hash => best_domain_block_hash
35///
36/// This mapping tracks the mapping of a consensus block and the corresponding domain block derived
37/// until this consensus block:
38/// - Hash(ConsensusBlock10) => Hash(DomainBlock5)
39/// - Hash(ConsensusBlock11) => Hash(DomainBlock5)
40/// - Hash(ConsensusBlock12) => Hash(DomainBlock5)
41const BEST_DOMAIN_HASH: &[u8] = b"best_domain_hash";
42
43/// Tracks a domain block hash and consensus block hash from which domain block is derived from
44/// at a given domain block height.
45const BEST_DOMAIN_HASH_KEYS: &[u8] = b"best_domain_hash_keys";
46
47fn execution_receipt_key(block_hash: impl Encode) -> Vec<u8> {
48    (EXECUTION_RECEIPT, block_hash).encode()
49}
50
51fn load_decode<Backend: AuxStore, T: Decode>(
52    backend: &Backend,
53    key: &[u8],
54) -> ClientResult<Option<T>> {
55    match backend.get_aux(key)? {
56        None => Ok(None),
57        Some(t) => T::decode(&mut &t[..])
58            .map_err(|e| {
59                ClientError::Backend(format!("Operator DB is corrupted. Decode error: {e}"))
60            })
61            .map(Some),
62    }
63}
64
65/// Write an execution receipt to aux storage, optionally prune the receipts that are
66/// too old.
67pub(super) fn write_execution_receipt<Backend, Block, CBlock>(
68    backend: &Backend,
69    oldest_unconfirmed_receipt_number: Option<NumberFor<Block>>,
70    execution_receipt: &ExecutionReceiptFor<Block, CBlock>,
71    challenge_period: NumberFor<CBlock>,
72) -> Result<(), sp_blockchain::Error>
73where
74    Backend: AuxStore,
75    Block: BlockT,
76    CBlock: BlockT,
77{
78    let block_number = execution_receipt.consensus_block_number();
79    let consensus_hash = *execution_receipt.consensus_block_hash();
80
81    let block_number_key = (EXECUTION_RECEIPT_BLOCK_NUMBER, block_number).encode();
82    let mut hashes_at_block_number =
83        load_decode::<_, Vec<CBlock::Hash>>(backend, block_number_key.as_slice())?
84            .unwrap_or_default();
85    hashes_at_block_number.push(consensus_hash);
86
87    let first_saved_receipt =
88        load_decode::<_, NumberFor<CBlock>>(backend, EXECUTION_RECEIPT_START)?
89            .unwrap_or(Zero::zero());
90
91    let mut new_first_saved_receipt = first_saved_receipt;
92
93    let mut keys_to_delete = vec![];
94
95    if let Some(pruning_block_number) =
96        challenge_period.checked_mul(&DOMAINS_PRUNING_DEPTH_MULTIPLIER.saturated_into())
97    {
98        // Delete ER that have confirmed long time ago
99        if let Some(delete_receipts_to) = oldest_unconfirmed_receipt_number
100            .map(|oldest_unconfirmed_receipt_number| {
101                oldest_unconfirmed_receipt_number.saturating_sub(One::one())
102            })
103            .and_then(|latest_confirmed_receipt_number| {
104                latest_confirmed_receipt_number
105                    .saturated_into::<BlockNumber>()
106                    .checked_sub(pruning_block_number.saturated_into())
107            })
108        {
109            new_first_saved_receipt =
110                Into::<NumberFor<CBlock>>::into(delete_receipts_to) + One::one();
111            for receipt_to_delete in first_saved_receipt.saturated_into()..=delete_receipts_to {
112                let delete_block_number_key =
113                    (EXECUTION_RECEIPT_BLOCK_NUMBER, receipt_to_delete).encode();
114
115                if let Some(hashes_to_delete) = load_decode::<_, Vec<CBlock::Hash>>(
116                    backend,
117                    delete_block_number_key.as_slice(),
118                )? {
119                    keys_to_delete.extend(
120                        hashes_to_delete
121                            .into_iter()
122                            .map(|h| (EXECUTION_RECEIPT, h).encode()),
123                    );
124                    keys_to_delete.push(delete_block_number_key);
125                }
126            }
127        }
128    }
129
130    backend.insert_aux(
131        &[
132            (
133                execution_receipt_key(consensus_hash).as_slice(),
134                execution_receipt.encode().as_slice(),
135            ),
136            (
137                block_number_key.as_slice(),
138                hashes_at_block_number.encode().as_slice(),
139            ),
140            (
141                EXECUTION_RECEIPT_START,
142                new_first_saved_receipt.encode().as_slice(),
143            ),
144        ],
145        &keys_to_delete
146            .iter()
147            .map(|k| &k[..])
148            .collect::<Vec<&[u8]>>()[..],
149    )
150}
151
152/// Load the execution receipt for given consensus block hash.
153pub fn load_execution_receipt<Backend, Block, CBlock>(
154    backend: &Backend,
155    consensus_block_hash: CBlock::Hash,
156) -> ClientResult<Option<ExecutionReceiptFor<Block, CBlock>>>
157where
158    Backend: AuxStore,
159    Block: BlockT,
160    CBlock: BlockT,
161{
162    load_decode(
163        backend,
164        execution_receipt_key(consensus_block_hash).as_slice(),
165    )
166}
167
168type MaybeTrackedDomainHashes<Block, CBlock> =
169    Option<BTreeSet<(BlockHashFor<Block>, BlockHashFor<CBlock>)>>;
170
171fn get_tracked_domain_hash_keys<Backend, Block, CBlock>(
172    backend: &Backend,
173    domain_block_number: NumberFor<Block>,
174) -> ClientResult<MaybeTrackedDomainHashes<Block, CBlock>>
175where
176    Backend: AuxStore,
177    Block: BlockT,
178    CBlock: BlockT,
179{
180    load_decode(
181        backend,
182        (BEST_DOMAIN_HASH_KEYS, domain_block_number)
183            .encode()
184            .as_slice(),
185    )
186}
187
188pub(super) fn track_domain_hash_and_consensus_hash<Client, Block, CBlock>(
189    domain_client: &Arc<Client>,
190    best_domain_hash: Block::Hash,
191    latest_consensus_hash: CBlock::Hash,
192    cleanup: bool,
193) -> ClientResult<()>
194where
195    Client: HeaderBackend<Block> + AuxStore,
196    CBlock: BlockT,
197    Block: BlockT,
198{
199    let best_domain_number =
200        domain_client
201            .number(best_domain_hash)?
202            .ok_or(sp_blockchain::Error::MissingHeader(format!(
203                "Block hash: {best_domain_hash:?}"
204            )))?;
205    let mut domain_hash_keys =
206        get_tracked_domain_hash_keys::<_, Block, CBlock>(&**domain_client, best_domain_number)?
207            .unwrap_or_default();
208
209    domain_hash_keys.insert((best_domain_hash, latest_consensus_hash));
210
211    domain_client.insert_aux(
212        &[
213            (
214                (LATEST_CONSENSUS_HASH, best_domain_hash)
215                    .encode()
216                    .as_slice(),
217                latest_consensus_hash.encode().as_slice(),
218            ),
219            (
220                (BEST_DOMAIN_HASH, latest_consensus_hash)
221                    .encode()
222                    .as_slice(),
223                best_domain_hash.encode().as_slice(),
224            ),
225            (
226                (BEST_DOMAIN_HASH_KEYS, best_domain_number)
227                    .encode()
228                    .as_slice(),
229                domain_hash_keys.encode().as_slice(),
230            ),
231        ],
232        vec![],
233    )?;
234
235    if cleanup {
236        cleanup_domain_hash_and_consensus_hash::<_, Block, CBlock>(domain_client)?;
237    }
238
239    Ok(())
240}
241
242fn cleanup_domain_hash_and_consensus_hash<Client, Block, CBlock>(
243    domain_client: &Arc<Client>,
244) -> ClientResult<()>
245where
246    CBlock: BlockT,
247    Block: BlockT,
248    Client: HeaderBackend<Block> + AuxStore,
249{
250    let mut finalized_domain_number = domain_client.info().finalized_number;
251
252    let mut deletions = vec![];
253    while finalized_domain_number > Zero::zero()
254        // exit early if there are not tracked hashes for this finalized block number.
255        && let Some(domain_hash_keys) = &get_tracked_domain_hash_keys::<_, Block, CBlock>(
256            &**domain_client,
257            finalized_domain_number,
258        )?
259    {
260        domain_hash_keys
261            .iter()
262            .for_each(|(domain_hash, consensus_hash)| {
263                deletions.push((LATEST_CONSENSUS_HASH, domain_hash).encode());
264                deletions.push((BEST_DOMAIN_HASH, consensus_hash).encode())
265            });
266
267        deletions.push((BEST_DOMAIN_HASH_KEYS, finalized_domain_number).encode());
268
269        finalized_domain_number = match finalized_domain_number.checked_sub(&One::one()) {
270            None => break,
271            Some(number) => number,
272        }
273    }
274
275    domain_client.insert_aux(
276        [],
277        &deletions
278            .iter()
279            .map(|key| key.as_slice())
280            .collect::<Vec<_>>(),
281    )
282}
283
284pub(super) fn best_domain_hash_for<Backend, Hash, CHash>(
285    backend: &Backend,
286    consensus_hash: &CHash,
287) -> ClientResult<Option<Hash>>
288where
289    Backend: AuxStore,
290    Hash: Decode,
291    CHash: Encode,
292{
293    load_decode(
294        backend,
295        (BEST_DOMAIN_HASH, consensus_hash).encode().as_slice(),
296    )
297}
298
299pub(super) fn latest_consensus_block_hash_for<Backend, Hash, CHash>(
300    backend: &Backend,
301    domain_hash: &Hash,
302) -> ClientResult<Option<CHash>>
303where
304    Backend: AuxStore,
305    Hash: Encode,
306    CHash: Decode,
307{
308    load_decode(
309        backend,
310        (LATEST_CONSENSUS_HASH, domain_hash).encode().as_slice(),
311    )
312}
313
314/// Different kinds of bundle mismatches.
315#[derive(Encode, Decode, Debug, PartialEq)]
316pub(super) enum BundleMismatchType {
317    /// The fraud proof needs to prove the bundle is invalid with `InvalidBundleType`,
318    /// because the bundle is actually an invalid bundle, but it is either marked as valid,
319    /// or as a lower priority invalid type.
320    GoodInvalid(InvalidBundleType),
321    /// The fraud proof needs to prove the `InvalidBundleType` is incorrect,
322    /// because the bundle type is either valid, or a lower priority invalid type.
323    BadInvalid(InvalidBundleType),
324    /// The fraud proof needs to prove the valid bundle contents are incorrect,
325    /// because the bundles are both valid, but their contents are different.
326    ValidBundleContents,
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use domain_test_service::evm_domain_test_runtime::Block;
333    use parking_lot::Mutex;
334    use sp_core::hash::H256;
335    use sp_domains::execution_receipt::execution_receipt_v0::ExecutionReceiptV0;
336    use std::collections::HashMap;
337    use subspace_runtime_primitives::{Balance, Hash};
338    use subspace_test_runtime::Block as CBlock;
339
340    const PRUNING_DEPTH: BlockNumber = 1000;
341
342    type ExecutionReceipt = sp_domains::execution_receipt::ExecutionReceipt<
343        BlockNumber,
344        Hash,
345        BlockNumber,
346        Hash,
347        Balance,
348    >;
349
350    fn create_execution_receipt(consensus_block_number: BlockNumber) -> ExecutionReceipt {
351        ExecutionReceipt::V0(ExecutionReceiptV0 {
352            domain_block_number: consensus_block_number,
353            domain_block_hash: H256::random(),
354            domain_block_extrinsic_root: H256::random(),
355            parent_domain_block_receipt_hash: H256::random(),
356            consensus_block_number,
357            consensus_block_hash: H256::random(),
358            inboxed_bundles: Vec::new(),
359            final_state_root: Default::default(),
360            execution_trace: Default::default(),
361            execution_trace_root: Default::default(),
362            block_fees: Default::default(),
363            transfers: Default::default(),
364        })
365    }
366
367    #[derive(Default)]
368    struct TestClient(Mutex<HashMap<Vec<u8>, Vec<u8>>>);
369
370    impl AuxStore for TestClient {
371        fn insert_aux<
372            'a,
373            'b: 'a,
374            'c: 'a,
375            I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
376            D: IntoIterator<Item = &'a &'b [u8]>,
377        >(
378            &self,
379            insert: I,
380            delete: D,
381        ) -> sp_blockchain::Result<()> {
382            let mut map = self.0.lock();
383            for d in delete {
384                map.remove(&d.to_vec());
385            }
386            for (k, v) in insert {
387                map.insert(k.to_vec(), v.to_vec());
388            }
389            Ok(())
390        }
391
392        fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
393            Ok(self.0.lock().get(key).cloned())
394        }
395    }
396
397    #[test]
398    fn normal_prune_execution_receipt_works() {
399        let block_tree_pruning_depth = 256;
400        let challenge_period = 500;
401        let client = TestClient::default();
402
403        let receipt_start = || {
404            load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
405                .unwrap()
406        };
407
408        let hashes_at = |number: BlockNumber| {
409            load_decode::<_, Vec<Hash>>(
410                &client,
411                (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
412            )
413            .unwrap()
414        };
415
416        let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
417
418        let receipt_at = |consensus_block_hash: Hash| {
419            load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
420        };
421
422        let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
423                                receipt: &ExecutionReceipt| {
424            write_execution_receipt::<_, Block, CBlock>(
425                &client,
426                oldest_unconfirmed_receipt_number,
427                receipt,
428                challenge_period,
429            )
430            .unwrap()
431        };
432
433        assert_eq!(receipt_start(), None);
434
435        // Create as many ER as before any ER being pruned yet
436        let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
437        let block_hash_list = (1..=receipt_count)
438            .map(|block_number| {
439                let receipt = create_execution_receipt(block_number);
440                let consensus_block_hash = *receipt.consensus_block_hash();
441                let oldest_unconfirmed_receipt_number = block_number
442                    .checked_sub(block_tree_pruning_depth)
443                    .map(|n| n + 1);
444                write_receipt_at(oldest_unconfirmed_receipt_number, &receipt);
445                assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
446                assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
447                // No ER have been pruned yet
448                assert_eq!(receipt_start(), Some(0));
449                consensus_block_hash
450            })
451            .collect::<Vec<_>>();
452
453        assert_eq!(receipt_start(), Some(0));
454        assert!(!target_receipt_is_pruned(1));
455
456        // Create `receipt_count + 1` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 1`.
457        let receipt = create_execution_receipt(receipt_count + 1);
458        assert!(receipt_at(*receipt.consensus_block_hash()).is_none());
459        write_receipt_at(Some(PRUNING_DEPTH + 1), &receipt);
460        assert!(receipt_at(*receipt.consensus_block_hash()).is_some());
461        assert_eq!(receipt_start(), Some(1));
462
463        // Create `receipt_count + 2` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 2`.
464        let receipt = create_execution_receipt(receipt_count + 2);
465        write_receipt_at(Some(PRUNING_DEPTH + 2), &receipt);
466        assert!(receipt_at(*receipt.consensus_block_hash()).is_some());
467
468        // ER of block #1 should be pruned, its block number mapping should be pruned as well.
469        assert!(receipt_at(block_hash_list[0]).is_none());
470        assert!(hashes_at(1).is_none());
471        assert!(target_receipt_is_pruned(1));
472        assert_eq!(receipt_start(), Some(2));
473
474        // Create `receipt_count + 3` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 3`.
475        let receipt = create_execution_receipt(receipt_count + 3);
476        let consensus_block_hash1 = *receipt.consensus_block_hash();
477        write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
478        assert!(receipt_at(consensus_block_hash1).is_some());
479        // ER of block #2 should be pruned.
480        assert!(receipt_at(block_hash_list[1]).is_none());
481        assert!(target_receipt_is_pruned(2));
482        assert!(!target_receipt_is_pruned(3));
483        assert_eq!(receipt_start(), Some(3));
484
485        // Multiple hashes attached to the block #`receipt_count + 3`
486        let receipt = create_execution_receipt(receipt_count + 3);
487        let consensus_block_hash2 = *receipt.consensus_block_hash();
488        write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
489        assert!(receipt_at(consensus_block_hash2).is_some());
490        assert_eq!(
491            hashes_at(receipt_count + 3),
492            Some(vec![consensus_block_hash1, consensus_block_hash2])
493        );
494        // No ER pruned since the `oldest_unconfirmed_receipt_number` is the same
495        assert!(!target_receipt_is_pruned(3));
496        assert_eq!(receipt_start(), Some(3));
497    }
498
499    #[test]
500    fn execution_receipts_should_be_kept_against_oldest_unconfirmed_receipt_number() {
501        let block_tree_pruning_depth = 256;
502        let challenge_period = 500;
503        let client = TestClient::default();
504
505        let receipt_start = || {
506            load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
507                .unwrap()
508        };
509
510        let hashes_at = |number: BlockNumber| {
511            load_decode::<_, Vec<Hash>>(
512                &client,
513                (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
514            )
515            .unwrap()
516        };
517
518        let receipt_at = |consensus_block_hash: Hash| {
519            load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
520        };
521
522        let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
523                                receipt: &ExecutionReceipt| {
524            write_execution_receipt::<_, Block, CBlock>(
525                &client,
526                oldest_unconfirmed_receipt_number,
527                receipt,
528                challenge_period,
529            )
530            .unwrap()
531        };
532
533        let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
534
535        assert_eq!(receipt_start(), None);
536
537        // Create as many ER as before any ER being pruned yet, `oldest_unconfirmed_receipt_number` is `Some(1)`,
538        // i.e., no receipt has ever been confirmed/pruned on consensus chain.
539        let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
540
541        let block_hash_list = (1..=receipt_count)
542            .map(|block_number| {
543                let receipt = create_execution_receipt(block_number);
544                let consensus_block_hash = *receipt.consensus_block_hash();
545                write_receipt_at(Some(One::one()), &receipt);
546                assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
547                assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
548                // No ER have been pruned yet
549                assert_eq!(receipt_start(), Some(0));
550                consensus_block_hash
551            })
552            .collect::<Vec<_>>();
553
554        assert_eq!(receipt_start(), Some(0));
555        assert!(!target_receipt_is_pruned(1));
556
557        // Create `receipt_count + 1` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
558        let receipt = create_execution_receipt(receipt_count + 1);
559        assert!(receipt_at(*receipt.consensus_block_hash()).is_none());
560        write_receipt_at(Some(One::one()), &receipt);
561
562        // Create `receipt_count + 2` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
563        let receipt = create_execution_receipt(receipt_count + 2);
564        write_receipt_at(Some(One::one()), &receipt);
565
566        // ER of block #1 and its block number mapping should not be pruned even the size of stored
567        // receipts exceeds the pruning depth.
568        assert!(receipt_at(block_hash_list[0]).is_some());
569        assert!(hashes_at(1).is_some());
570        assert!(!target_receipt_is_pruned(1));
571        assert_eq!(receipt_start(), Some(0));
572
573        // Create `receipt_count + 3` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
574        let receipt = create_execution_receipt(receipt_count + 3);
575        write_receipt_at(Some(One::one()), &receipt);
576
577        // Create `receipt_count + 4` receipt, `oldest_unconfirmed_receipt_number` is `Some(PRUNING_DEPTH + 4)`.
578        let receipt = create_execution_receipt(receipt_count + 4);
579        write_receipt_at(
580            Some(PRUNING_DEPTH + 4), // Now assuming all the missing receipts are confirmed.
581            &receipt,
582        );
583        assert!(receipt_at(block_hash_list[0]).is_none());
584        // receipt and block number mapping for [1, 2, 3] should be pruned.
585        (1..=3).for_each(|pruned| {
586            assert!(hashes_at(pruned).is_none());
587            assert!(target_receipt_is_pruned(pruned));
588        });
589        assert_eq!(receipt_start(), Some(4));
590    }
591}