Skip to main content

miden_node_store/state/view/
inclusion_proofs.rs

1//! Inclusion proof query.
2//!
3//! Provides note and block inclusion proofs relative to a reference block. The reference block
4//! cannot be newer than the view's chain tip.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use miden_protocol::block::BlockNumber;
9use miden_protocol::note::{NoteId, NoteInclusionProof};
10use miden_protocol::transaction::PartialBlockchain;
11
12use super::StateView;
13use crate::errors::{GetBlockInclusionProofsError, GetNoteInclusionProofsError};
14
15impl StateView {
16    /// Fetches inclusion proofs for notes included at or before the reference block.
17    pub async fn get_note_inclusion_proofs(
18        &self,
19        reference_block: BlockNumber,
20        note_ids: BTreeSet<NoteId>,
21    ) -> Result<BTreeMap<NoteId, NoteInclusionProof>, GetNoteInclusionProofsError> {
22        let latest_block_num = self.tip();
23        let reference_block = self.scope_block(reference_block).ok_or(
24            GetNoteInclusionProofsError::ReferenceBlockAfterTip {
25                reference_block,
26                latest_block_num: *latest_block_num,
27            },
28        )?;
29
30        let note_commitments = note_ids.into_iter().map(|note_id| note_id.as_word()).collect();
31        self.db
32            .select_note_inclusion_proofs(note_commitments, reference_block)
33            .await
34            .map_err(GetNoteInclusionProofsError::SelectNoteInclusionProofError)
35    }
36
37    /// Fetches inclusion proofs for blocks relative to the reference block.
38    ///
39    /// The returned partial blockchain contains all requested blocks before the reference block.
40    pub async fn get_block_inclusion_proofs(
41        &self,
42        reference_block: BlockNumber,
43        block_numbers: BTreeSet<BlockNumber>,
44    ) -> Result<PartialBlockchain, GetBlockInclusionProofsError> {
45        let latest_block_num = self.tip();
46        let reference_block = self.scope_block(reference_block).ok_or(
47            GetBlockInclusionProofsError::ReferenceBlockAfterTip {
48                reference_block,
49                latest_block_num: *latest_block_num,
50            },
51        )?;
52
53        if let Some(&block_num) = block_numbers.last()
54            && block_num > *reference_block
55        {
56            return Err(GetBlockInclusionProofsError::BlockAfterReferenceBlock {
57                block_num,
58                reference_block: *reference_block,
59            });
60        }
61
62        let mut blocks = block_numbers;
63
64        // The partial blockchain describes the chain state before the reference block.
65        blocks.remove(&*reference_block);
66
67        // All blocks are at or below the reference block and the view tip.
68        let scoped_blocks = blocks
69            .iter()
70            .map(|&block| {
71                self.scope_block(block)
72                    .expect("requested blocks must not exceed the reference block")
73            })
74            .collect::<Vec<_>>();
75
76        // SAFETY:
77        // - The reference block was scoped against the view's blockchain.
78        // - The reference block was removed from the set.
79        // - All remaining block numbers are less than the reference block.
80        let partial_mmr = self
81            .blockchain()
82            .partial_mmr_from_blocks(&blocks, *reference_block)
83            .expect("all requested blocks must exist before the reference block");
84
85        let headers = self
86            .db
87            .select_block_headers(scoped_blocks.into_iter())
88            .await
89            .map_err(GetBlockInclusionProofsError::SelectBlockHeaderError)?;
90
91        // SAFETY:
92        // - The headers match the blocks in the partial MMR.
93        // - No header exceeds the chain length of the partial MMR.
94        // - The BTreeSet removes duplicate block numbers.
95        //
96        // The headers and the partial MMR use the same block set. The unchecked constructor is safe.
97        Ok(PartialBlockchain::new_unchecked(partial_mmr, headers)
98            .expect("partial mmr and block headers should be consistent"))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use miden_node_utils::fee::{test_fee_params, test_protocol_config};
105    use miden_protocol::block::ValidatorConfig;
106    use miden_protocol::testing::random_secret_key::random_secret_key;
107
108    use super::*;
109    use crate::GenesisState;
110    use crate::state::State;
111
112    #[tokio::test]
113    async fn block_inclusion_proofs_use_the_requested_block() {
114        let data_directory = tempfile::tempdir().expect("tempdir should be created");
115        bootstrap_store(data_directory.path()).await;
116        let (state, _block_writer, _proof_writer) = State::for_tests(data_directory.path()).await;
117
118        let partial_blockchain = state
119            .view()
120            .get_block_inclusion_proofs(BlockNumber::GENESIS, BTreeSet::new())
121            .await
122            .expect("block inclusion proofs should be returned");
123        assert_eq!(partial_blockchain.chain_length(), BlockNumber::GENESIS);
124
125        let error = state
126            .view()
127            .get_block_inclusion_proofs(
128                BlockNumber::GENESIS.child(),
129                BTreeSet::from([BlockNumber::GENESIS]),
130            )
131            .await
132            .expect_err("a reference block after the tip should fail");
133        assert!(matches!(
134            error,
135            GetBlockInclusionProofsError::ReferenceBlockAfterTip {
136                reference_block,
137                latest_block_num,
138            } if reference_block == BlockNumber::GENESIS.child()
139                && latest_block_num == BlockNumber::GENESIS
140        ));
141
142        let error = state
143            .view()
144            .get_block_inclusion_proofs(
145                BlockNumber::GENESIS,
146                BTreeSet::from([BlockNumber::GENESIS.child()]),
147            )
148            .await
149            .expect_err("a requested block after the reference block should fail");
150        assert!(matches!(
151            error,
152            GetBlockInclusionProofsError::BlockAfterReferenceBlock {
153                block_num,
154                reference_block,
155            } if block_num == BlockNumber::GENESIS.child()
156                && reference_block == BlockNumber::GENESIS
157        ));
158    }
159
160    #[tokio::test]
161    async fn note_inclusion_proofs_reject_a_reference_block_after_the_tip() {
162        let data_directory = tempfile::tempdir().expect("tempdir should be created");
163        bootstrap_store(data_directory.path()).await;
164        let (state, _block_writer, _proof_writer) = State::for_tests(data_directory.path()).await;
165
166        let error = state
167            .view()
168            .get_note_inclusion_proofs(BlockNumber::GENESIS.child(), BTreeSet::new())
169            .await
170            .expect_err("a reference block after the tip should fail");
171        assert!(matches!(
172            error,
173            GetNoteInclusionProofsError::ReferenceBlockAfterTip {
174                reference_block,
175                latest_block_num,
176            } if reference_block == BlockNumber::GENESIS.child()
177                && latest_block_num == BlockNumber::GENESIS
178        ));
179    }
180
181    async fn bootstrap_store(path: &std::path::Path) {
182        let signer = random_secret_key();
183        let genesis_state = GenesisState::new(
184            vec![],
185            test_fee_params(),
186            1,
187            ValidatorConfig::new(vec![signer.public_key()], 1)
188                .expect("validator config should be valid"),
189            test_protocol_config(),
190        );
191        let genesis_block = genesis_state.into_block().expect("genesis block should be created");
192
193        State::bootstrap(genesis_block, path).await.expect("store should bootstrap");
194    }
195}