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::InvalidBundleType;
8use sp_runtime::traits::{
9    Block as BlockT, CheckedMul, CheckedSub, NumberFor, One, SaturatedConversion, Zero,
10};
11use sp_runtime::Saturating;
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: {:?}",
204                best_domain_hash
205            )))?;
206    let mut domain_hash_keys =
207        get_tracked_domain_hash_keys::<_, Block, CBlock>(&**domain_client, best_domain_number)?
208            .unwrap_or_default();
209
210    domain_hash_keys.insert((best_domain_hash, latest_consensus_hash));
211
212    domain_client.insert_aux(
213        &[
214            (
215                (LATEST_CONSENSUS_HASH, best_domain_hash)
216                    .encode()
217                    .as_slice(),
218                latest_consensus_hash.encode().as_slice(),
219            ),
220            (
221                (BEST_DOMAIN_HASH, latest_consensus_hash)
222                    .encode()
223                    .as_slice(),
224                best_domain_hash.encode().as_slice(),
225            ),
226            (
227                (BEST_DOMAIN_HASH_KEYS, best_domain_number)
228                    .encode()
229                    .as_slice(),
230                domain_hash_keys.encode().as_slice(),
231            ),
232        ],
233        vec![],
234    )?;
235
236    if cleanup {
237        cleanup_domain_hash_and_consensus_hash::<_, Block, CBlock>(domain_client)?;
238    }
239
240    Ok(())
241}
242
243fn cleanup_domain_hash_and_consensus_hash<Client, Block, CBlock>(
244    domain_client: &Arc<Client>,
245) -> ClientResult<()>
246where
247    CBlock: BlockT,
248    Block: BlockT,
249    Client: HeaderBackend<Block> + AuxStore,
250{
251    let mut finalized_domain_number = domain_client.info().finalized_number;
252
253    let mut deletions = vec![];
254    while finalized_domain_number > Zero::zero()
255        // exit early if there are not tracked hashes for this finalized block number.
256        && let Some(domain_hash_keys) = &get_tracked_domain_hash_keys::<_, Block, CBlock>(
257            &**domain_client,
258            finalized_domain_number,
259        )?
260    {
261        domain_hash_keys
262            .iter()
263            .for_each(|(domain_hash, consensus_hash)| {
264                deletions.push((LATEST_CONSENSUS_HASH, domain_hash).encode());
265                deletions.push((BEST_DOMAIN_HASH, consensus_hash).encode())
266            });
267
268        deletions.push((BEST_DOMAIN_HASH_KEYS, finalized_domain_number).encode());
269
270        finalized_domain_number = match finalized_domain_number.checked_sub(&One::one()) {
271            None => break,
272            Some(number) => number,
273        }
274    }
275
276    domain_client.insert_aux(
277        [],
278        &deletions
279            .iter()
280            .map(|key| key.as_slice())
281            .collect::<Vec<_>>(),
282    )
283}
284
285pub(super) fn best_domain_hash_for<Backend, Hash, CHash>(
286    backend: &Backend,
287    consensus_hash: &CHash,
288) -> ClientResult<Option<Hash>>
289where
290    Backend: AuxStore,
291    Hash: Decode,
292    CHash: Encode,
293{
294    load_decode(
295        backend,
296        (BEST_DOMAIN_HASH, consensus_hash).encode().as_slice(),
297    )
298}
299
300pub(super) fn latest_consensus_block_hash_for<Backend, Hash, CHash>(
301    backend: &Backend,
302    domain_hash: &Hash,
303) -> ClientResult<Option<CHash>>
304where
305    Backend: AuxStore,
306    Hash: Encode,
307    CHash: Decode,
308{
309    load_decode(
310        backend,
311        (LATEST_CONSENSUS_HASH, domain_hash).encode().as_slice(),
312    )
313}
314
315/// Different kinds of bundle mismatches.
316#[derive(Encode, Decode, Debug, PartialEq)]
317pub(super) enum BundleMismatchType {
318    /// The fraud proof needs to prove the bundle is invalid with `InvalidBundleType`,
319    /// because the bundle is actually an invalid bundle, but it is either marked as valid,
320    /// or as a lower priority invalid type.
321    GoodInvalid(InvalidBundleType),
322    /// The fraud proof needs to prove the `InvalidBundleType` is incorrect,
323    /// because the bundle type is either valid, or a lower priority invalid type.
324    BadInvalid(InvalidBundleType),
325    /// The fraud proof needs to prove the valid bundle contents are incorrect,
326    /// because the bundles are both valid, but their contents are different.
327    ValidBundleContents,
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use domain_test_service::evm_domain_test_runtime::Block;
334    use parking_lot::Mutex;
335    use sp_core::hash::H256;
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 =
343        sp_domains::ExecutionReceipt<BlockNumber, Hash, BlockNumber, Hash, Balance>;
344
345    fn create_execution_receipt(consensus_block_number: BlockNumber) -> ExecutionReceipt {
346        ExecutionReceipt {
347            domain_block_number: consensus_block_number,
348            domain_block_hash: H256::random(),
349            domain_block_extrinsic_root: H256::random(),
350            parent_domain_block_receipt_hash: H256::random(),
351            consensus_block_number,
352            consensus_block_hash: H256::random(),
353            inboxed_bundles: Vec::new(),
354            final_state_root: Default::default(),
355            execution_trace: Default::default(),
356            execution_trace_root: Default::default(),
357            block_fees: Default::default(),
358            transfers: Default::default(),
359        }
360    }
361
362    #[derive(Default)]
363    struct TestClient(Mutex<HashMap<Vec<u8>, Vec<u8>>>);
364
365    impl AuxStore for TestClient {
366        fn insert_aux<
367            'a,
368            'b: 'a,
369            'c: 'a,
370            I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
371            D: IntoIterator<Item = &'a &'b [u8]>,
372        >(
373            &self,
374            insert: I,
375            delete: D,
376        ) -> sp_blockchain::Result<()> {
377            let mut map = self.0.lock();
378            for d in delete {
379                map.remove(&d.to_vec());
380            }
381            for (k, v) in insert {
382                map.insert(k.to_vec(), v.to_vec());
383            }
384            Ok(())
385        }
386
387        fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
388            Ok(self.0.lock().get(key).cloned())
389        }
390    }
391
392    #[test]
393    fn normal_prune_execution_receipt_works() {
394        let block_tree_pruning_depth = 256;
395        let challenge_period = 500;
396        let client = TestClient::default();
397
398        let receipt_start = || {
399            load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
400                .unwrap()
401        };
402
403        let hashes_at = |number: BlockNumber| {
404            load_decode::<_, Vec<Hash>>(
405                &client,
406                (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
407            )
408            .unwrap()
409        };
410
411        let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
412
413        let receipt_at = |consensus_block_hash: Hash| {
414            load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
415        };
416
417        let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
418                                receipt: &ExecutionReceipt| {
419            write_execution_receipt::<_, Block, CBlock>(
420                &client,
421                oldest_unconfirmed_receipt_number,
422                receipt,
423                challenge_period,
424            )
425            .unwrap()
426        };
427
428        assert_eq!(receipt_start(), None);
429
430        // Create as many ER as before any ER being pruned yet
431        let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
432        let block_hash_list = (1..=receipt_count)
433            .map(|block_number| {
434                let receipt = create_execution_receipt(block_number);
435                let consensus_block_hash = receipt.consensus_block_hash;
436                let oldest_unconfirmed_receipt_number = block_number
437                    .checked_sub(block_tree_pruning_depth)
438                    .map(|n| n + 1);
439                write_receipt_at(oldest_unconfirmed_receipt_number, &receipt);
440                assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
441                assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
442                // No ER have been pruned yet
443                assert_eq!(receipt_start(), Some(0));
444                consensus_block_hash
445            })
446            .collect::<Vec<_>>();
447
448        assert_eq!(receipt_start(), Some(0));
449        assert!(!target_receipt_is_pruned(1));
450
451        // Create `receipt_count + 1` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 1`.
452        let receipt = create_execution_receipt(receipt_count + 1);
453        assert!(receipt_at(receipt.consensus_block_hash).is_none());
454        write_receipt_at(Some(PRUNING_DEPTH + 1), &receipt);
455        assert!(receipt_at(receipt.consensus_block_hash).is_some());
456        assert_eq!(receipt_start(), Some(1));
457
458        // Create `receipt_count + 2` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 2`.
459        let receipt = create_execution_receipt(receipt_count + 2);
460        write_receipt_at(Some(PRUNING_DEPTH + 2), &receipt);
461        assert!(receipt_at(receipt.consensus_block_hash).is_some());
462
463        // ER of block #1 should be pruned, its block number mapping should be pruned as well.
464        assert!(receipt_at(block_hash_list[0]).is_none());
465        assert!(hashes_at(1).is_none());
466        assert!(target_receipt_is_pruned(1));
467        assert_eq!(receipt_start(), Some(2));
468
469        // Create `receipt_count + 3` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 3`.
470        let receipt = create_execution_receipt(receipt_count + 3);
471        let consensus_block_hash1 = receipt.consensus_block_hash;
472        write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
473        assert!(receipt_at(consensus_block_hash1).is_some());
474        // ER of block #2 should be pruned.
475        assert!(receipt_at(block_hash_list[1]).is_none());
476        assert!(target_receipt_is_pruned(2));
477        assert!(!target_receipt_is_pruned(3));
478        assert_eq!(receipt_start(), Some(3));
479
480        // Multiple hashes attached to the block #`receipt_count + 3`
481        let receipt = create_execution_receipt(receipt_count + 3);
482        let consensus_block_hash2 = receipt.consensus_block_hash;
483        write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
484        assert!(receipt_at(consensus_block_hash2).is_some());
485        assert_eq!(
486            hashes_at(receipt_count + 3),
487            Some(vec![consensus_block_hash1, consensus_block_hash2])
488        );
489        // No ER pruned since the `oldest_unconfirmed_receipt_number` is the same
490        assert!(!target_receipt_is_pruned(3));
491        assert_eq!(receipt_start(), Some(3));
492    }
493
494    #[test]
495    fn execution_receipts_should_be_kept_against_oldest_unconfirmed_receipt_number() {
496        let block_tree_pruning_depth = 256;
497        let challenge_period = 500;
498        let client = TestClient::default();
499
500        let receipt_start = || {
501            load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
502                .unwrap()
503        };
504
505        let hashes_at = |number: BlockNumber| {
506            load_decode::<_, Vec<Hash>>(
507                &client,
508                (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
509            )
510            .unwrap()
511        };
512
513        let receipt_at = |consensus_block_hash: Hash| {
514            load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
515        };
516
517        let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
518                                receipt: &ExecutionReceipt| {
519            write_execution_receipt::<_, Block, CBlock>(
520                &client,
521                oldest_unconfirmed_receipt_number,
522                receipt,
523                challenge_period,
524            )
525            .unwrap()
526        };
527
528        let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
529
530        assert_eq!(receipt_start(), None);
531
532        // Create as many ER as before any ER being pruned yet, `oldest_unconfirmed_receipt_number` is `Some(1)`,
533        // i.e., no receipt has ever been confirmed/pruned on consensus chain.
534        let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
535
536        let block_hash_list = (1..=receipt_count)
537            .map(|block_number| {
538                let receipt = create_execution_receipt(block_number);
539                let consensus_block_hash = receipt.consensus_block_hash;
540                write_receipt_at(Some(One::one()), &receipt);
541                assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
542                assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
543                // No ER have been pruned yet
544                assert_eq!(receipt_start(), Some(0));
545                consensus_block_hash
546            })
547            .collect::<Vec<_>>();
548
549        assert_eq!(receipt_start(), Some(0));
550        assert!(!target_receipt_is_pruned(1));
551
552        // Create `receipt_count + 1` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
553        let receipt = create_execution_receipt(receipt_count + 1);
554        assert!(receipt_at(receipt.consensus_block_hash).is_none());
555        write_receipt_at(Some(One::one()), &receipt);
556
557        // Create `receipt_count + 2` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
558        let receipt = create_execution_receipt(receipt_count + 2);
559        write_receipt_at(Some(One::one()), &receipt);
560
561        // ER of block #1 and its block number mapping should not be pruned even the size of stored
562        // receipts exceeds the pruning depth.
563        assert!(receipt_at(block_hash_list[0]).is_some());
564        assert!(hashes_at(1).is_some());
565        assert!(!target_receipt_is_pruned(1));
566        assert_eq!(receipt_start(), Some(0));
567
568        // Create `receipt_count + 3` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
569        let receipt = create_execution_receipt(receipt_count + 3);
570        write_receipt_at(Some(One::one()), &receipt);
571
572        // Create `receipt_count + 4` receipt, `oldest_unconfirmed_receipt_number` is `Some(PRUNING_DEPTH + 4)`.
573        let receipt = create_execution_receipt(receipt_count + 4);
574        write_receipt_at(
575            Some(PRUNING_DEPTH + 4), // Now assuming all the missing receipts are confirmed.
576            &receipt,
577        );
578        assert!(receipt_at(block_hash_list[0]).is_none());
579        // receipt and block number mapping for [1, 2, 3] should be pruned.
580        (1..=3).for_each(|pruned| {
581            assert!(hashes_at(pruned).is_none());
582            assert!(target_receipt_is_pruned(pruned));
583        });
584        assert_eq!(receipt_start(), Some(4));
585    }
586}