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::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 std::collections::HashMap;
336    use subspace_runtime_primitives::{Balance, Hash};
337    use subspace_test_runtime::Block as CBlock;
338
339    const PRUNING_DEPTH: BlockNumber = 1000;
340
341    type ExecutionReceipt =
342        sp_domains::ExecutionReceipt<BlockNumber, Hash, BlockNumber, Hash, Balance>;
343
344    fn create_execution_receipt(consensus_block_number: BlockNumber) -> ExecutionReceipt {
345        ExecutionReceipt {
346            domain_block_number: consensus_block_number,
347            domain_block_hash: H256::random(),
348            domain_block_extrinsic_root: H256::random(),
349            parent_domain_block_receipt_hash: H256::random(),
350            consensus_block_number,
351            consensus_block_hash: H256::random(),
352            inboxed_bundles: Vec::new(),
353            final_state_root: Default::default(),
354            execution_trace: Default::default(),
355            execution_trace_root: Default::default(),
356            block_fees: Default::default(),
357            transfers: Default::default(),
358        }
359    }
360
361    #[derive(Default)]
362    struct TestClient(Mutex<HashMap<Vec<u8>, Vec<u8>>>);
363
364    impl AuxStore for TestClient {
365        fn insert_aux<
366            'a,
367            'b: 'a,
368            'c: 'a,
369            I: IntoIterator<Item = &'a (&'c [u8], &'c [u8])>,
370            D: IntoIterator<Item = &'a &'b [u8]>,
371        >(
372            &self,
373            insert: I,
374            delete: D,
375        ) -> sp_blockchain::Result<()> {
376            let mut map = self.0.lock();
377            for d in delete {
378                map.remove(&d.to_vec());
379            }
380            for (k, v) in insert {
381                map.insert(k.to_vec(), v.to_vec());
382            }
383            Ok(())
384        }
385
386        fn get_aux(&self, key: &[u8]) -> sp_blockchain::Result<Option<Vec<u8>>> {
387            Ok(self.0.lock().get(key).cloned())
388        }
389    }
390
391    #[test]
392    fn normal_prune_execution_receipt_works() {
393        let block_tree_pruning_depth = 256;
394        let challenge_period = 500;
395        let client = TestClient::default();
396
397        let receipt_start = || {
398            load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
399                .unwrap()
400        };
401
402        let hashes_at = |number: BlockNumber| {
403            load_decode::<_, Vec<Hash>>(
404                &client,
405                (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
406            )
407            .unwrap()
408        };
409
410        let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
411
412        let receipt_at = |consensus_block_hash: Hash| {
413            load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
414        };
415
416        let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
417                                receipt: &ExecutionReceipt| {
418            write_execution_receipt::<_, Block, CBlock>(
419                &client,
420                oldest_unconfirmed_receipt_number,
421                receipt,
422                challenge_period,
423            )
424            .unwrap()
425        };
426
427        assert_eq!(receipt_start(), None);
428
429        // Create as many ER as before any ER being pruned yet
430        let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
431        let block_hash_list = (1..=receipt_count)
432            .map(|block_number| {
433                let receipt = create_execution_receipt(block_number);
434                let consensus_block_hash = receipt.consensus_block_hash;
435                let oldest_unconfirmed_receipt_number = block_number
436                    .checked_sub(block_tree_pruning_depth)
437                    .map(|n| n + 1);
438                write_receipt_at(oldest_unconfirmed_receipt_number, &receipt);
439                assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
440                assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
441                // No ER have been pruned yet
442                assert_eq!(receipt_start(), Some(0));
443                consensus_block_hash
444            })
445            .collect::<Vec<_>>();
446
447        assert_eq!(receipt_start(), Some(0));
448        assert!(!target_receipt_is_pruned(1));
449
450        // Create `receipt_count + 1` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 1`.
451        let receipt = create_execution_receipt(receipt_count + 1);
452        assert!(receipt_at(receipt.consensus_block_hash).is_none());
453        write_receipt_at(Some(PRUNING_DEPTH + 1), &receipt);
454        assert!(receipt_at(receipt.consensus_block_hash).is_some());
455        assert_eq!(receipt_start(), Some(1));
456
457        // Create `receipt_count + 2` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 2`.
458        let receipt = create_execution_receipt(receipt_count + 2);
459        write_receipt_at(Some(PRUNING_DEPTH + 2), &receipt);
460        assert!(receipt_at(receipt.consensus_block_hash).is_some());
461
462        // ER of block #1 should be pruned, its block number mapping should be pruned as well.
463        assert!(receipt_at(block_hash_list[0]).is_none());
464        assert!(hashes_at(1).is_none());
465        assert!(target_receipt_is_pruned(1));
466        assert_eq!(receipt_start(), Some(2));
467
468        // Create `receipt_count + 3` receipt, `oldest_unconfirmed_receipt_number` is `PRUNING_DEPTH + 3`.
469        let receipt = create_execution_receipt(receipt_count + 3);
470        let consensus_block_hash1 = receipt.consensus_block_hash;
471        write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
472        assert!(receipt_at(consensus_block_hash1).is_some());
473        // ER of block #2 should be pruned.
474        assert!(receipt_at(block_hash_list[1]).is_none());
475        assert!(target_receipt_is_pruned(2));
476        assert!(!target_receipt_is_pruned(3));
477        assert_eq!(receipt_start(), Some(3));
478
479        // Multiple hashes attached to the block #`receipt_count + 3`
480        let receipt = create_execution_receipt(receipt_count + 3);
481        let consensus_block_hash2 = receipt.consensus_block_hash;
482        write_receipt_at(Some(PRUNING_DEPTH + 3), &receipt);
483        assert!(receipt_at(consensus_block_hash2).is_some());
484        assert_eq!(
485            hashes_at(receipt_count + 3),
486            Some(vec![consensus_block_hash1, consensus_block_hash2])
487        );
488        // No ER pruned since the `oldest_unconfirmed_receipt_number` is the same
489        assert!(!target_receipt_is_pruned(3));
490        assert_eq!(receipt_start(), Some(3));
491    }
492
493    #[test]
494    fn execution_receipts_should_be_kept_against_oldest_unconfirmed_receipt_number() {
495        let block_tree_pruning_depth = 256;
496        let challenge_period = 500;
497        let client = TestClient::default();
498
499        let receipt_start = || {
500            load_decode::<_, BlockNumber>(&client, EXECUTION_RECEIPT_START.to_vec().as_slice())
501                .unwrap()
502        };
503
504        let hashes_at = |number: BlockNumber| {
505            load_decode::<_, Vec<Hash>>(
506                &client,
507                (EXECUTION_RECEIPT_BLOCK_NUMBER, number).encode().as_slice(),
508            )
509            .unwrap()
510        };
511
512        let receipt_at = |consensus_block_hash: Hash| {
513            load_execution_receipt::<_, Block, CBlock>(&client, consensus_block_hash).unwrap()
514        };
515
516        let write_receipt_at = |oldest_unconfirmed_receipt_number: Option<BlockNumber>,
517                                receipt: &ExecutionReceipt| {
518            write_execution_receipt::<_, Block, CBlock>(
519                &client,
520                oldest_unconfirmed_receipt_number,
521                receipt,
522                challenge_period,
523            )
524            .unwrap()
525        };
526
527        let target_receipt_is_pruned = |number: BlockNumber| hashes_at(number).is_none();
528
529        assert_eq!(receipt_start(), None);
530
531        // Create as many ER as before any ER being pruned yet, `oldest_unconfirmed_receipt_number` is `Some(1)`,
532        // i.e., no receipt has ever been confirmed/pruned on consensus chain.
533        let receipt_count = PRUNING_DEPTH + block_tree_pruning_depth - 1;
534
535        let block_hash_list = (1..=receipt_count)
536            .map(|block_number| {
537                let receipt = create_execution_receipt(block_number);
538                let consensus_block_hash = receipt.consensus_block_hash;
539                write_receipt_at(Some(One::one()), &receipt);
540                assert_eq!(receipt_at(consensus_block_hash), Some(receipt));
541                assert_eq!(hashes_at(block_number), Some(vec![consensus_block_hash]));
542                // No ER have been pruned yet
543                assert_eq!(receipt_start(), Some(0));
544                consensus_block_hash
545            })
546            .collect::<Vec<_>>();
547
548        assert_eq!(receipt_start(), Some(0));
549        assert!(!target_receipt_is_pruned(1));
550
551        // Create `receipt_count + 1` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
552        let receipt = create_execution_receipt(receipt_count + 1);
553        assert!(receipt_at(receipt.consensus_block_hash).is_none());
554        write_receipt_at(Some(One::one()), &receipt);
555
556        // Create `receipt_count + 2` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
557        let receipt = create_execution_receipt(receipt_count + 2);
558        write_receipt_at(Some(One::one()), &receipt);
559
560        // ER of block #1 and its block number mapping should not be pruned even the size of stored
561        // receipts exceeds the pruning depth.
562        assert!(receipt_at(block_hash_list[0]).is_some());
563        assert!(hashes_at(1).is_some());
564        assert!(!target_receipt_is_pruned(1));
565        assert_eq!(receipt_start(), Some(0));
566
567        // Create `receipt_count + 3` receipt, `oldest_unconfirmed_receipt_number` is `Some(1)`.
568        let receipt = create_execution_receipt(receipt_count + 3);
569        write_receipt_at(Some(One::one()), &receipt);
570
571        // Create `receipt_count + 4` receipt, `oldest_unconfirmed_receipt_number` is `Some(PRUNING_DEPTH + 4)`.
572        let receipt = create_execution_receipt(receipt_count + 4);
573        write_receipt_at(
574            Some(PRUNING_DEPTH + 4), // Now assuming all the missing receipts are confirmed.
575            &receipt,
576        );
577        assert!(receipt_at(block_hash_list[0]).is_none());
578        // receipt and block number mapping for [1, 2, 3] should be pruned.
579        (1..=3).for_each(|pruned| {
580            assert!(hashes_at(pruned).is_none());
581            assert!(target_receipt_is_pruned(pruned));
582        });
583        assert_eq!(receipt_start(), Some(4));
584    }
585}