1515
1616use super :: * ;
1717
18- use crate :: narwhal:: BatchHeader ;
18+ use crate :: { narwhal:: BatchHeader , puzzle :: SolutionID } ;
1919
2020use anyhow:: { Context , bail} ;
2121
@@ -34,51 +34,114 @@ impl<N: Network> Deref for PendingBlock<N> {
3434 }
3535}
3636
37+ impl < N : Network > Debug for PendingBlock < N > {
38+ fn fmt ( & self , f : & mut Formatter ) -> fmt:: Result {
39+ write ! ( f, "PendingBlock {{ height: {}, hash: {} }}" , self . height( ) , self . hash( ) )
40+ }
41+ }
42+
43+ /// Error returned by [`Self::check_block_subdag`] and [`Self::check_block_subdag_inner`].
44+ ///
45+ /// This allows parsing for begning errors, such as the block already existing in the ledger.
46+ #[ derive( thiserror:: Error , Debug ) ]
47+ pub enum CheckBlockError < N : Network > {
48+ #[ error( "Block with hash {hash} already exists in the ledger" ) ]
49+ BlockAlreadyExists { hash : N :: BlockHash } ,
50+ #[ error( "Block has invalid height. Expected {expected}, but got {actual}" ) ]
51+ InvalidHeight { expected : u32 , actual : u32 } ,
52+ #[ error( "Block has invalid hash" ) ]
53+ InvalidHash ,
54+ /// An error related to the given prefix of pending blocks.
55+ #[ error( "The prefix as an error at index {index} - {error:?}" ) ]
56+ InvalidPrefix { index : usize , error : Box < CheckBlockError < N > > } ,
57+ #[ error( "The block contains solution '{solution_id}', but it already exists in the ledger" ) ]
58+ SolutionAlreadyExists { solution_id : SolutionID < N > } ,
59+ #[ error( "Failed to speculate over unconfirmed transactions - {inner}" ) ]
60+ SpeculationFailed { inner : anyhow:: Error } ,
61+ #[ error( "Failed to verify block - {inner}" ) ]
62+ VerificationFailed { inner : anyhow:: Error } ,
63+ #[ error( "Prover '{prover_address}' has reached their solution limit for the current epoch" ) ]
64+ SolutionLimitReached { prover_address : Address < N > } ,
65+ #[ error( "The previous block should contain solution '{solution_id}', but it does not exist in the ledger" ) ]
66+ PreviousSolutionNotFound { solution_id : SolutionID < N > } ,
67+ #[ error( "The previous block should contain solution '{transaction_id}', but it does not exist in the ledger" ) ]
68+ PreviousTransactionNotFound { transaction_id : N :: TransactionID } ,
69+ #[ error( transparent) ]
70+ Other ( #[ from] anyhow:: Error ) ,
71+ }
72+
73+ impl < N : Network > CheckBlockError < N > {
74+ pub fn into_anyhow ( self ) -> anyhow:: Error {
75+ match self {
76+ Self :: Other ( err) => err,
77+ _ => anyhow:: anyhow!( "{self:?}" ) ,
78+ }
79+ }
80+ }
81+
3782impl < N : Network , C : ConsensusStorage < N > > Ledger < N , C > {
3883 /// Checks that the subDAG in a given block is valid, but does not fully verify the block.
3984 ///
4085 /// # Arguments
4186 /// * `block` - The block to check.
42- /// * `pending_block` - A sequence of blocks between the block to check and the current height of the ledger.
87+ /// * `prefix` - A sequence of blocks between the block to check and the current height of the ledger.
88+ ///
89+ /// # Returns
90+ /// * On success, a [`PendingBlock`] representing the block that was checked. Once the prefix of this block has been fully added to the ledger,
91+ /// the [`PendingBlock`] can then be passed to [`Self::check_block_content`] to fully verify it.
92+ /// * On failure, a [`CheckBlockError`] describing the reason the block was rejected.
4393 ///
4494 /// # Notes
4595 /// * This does *not* check that the header of the block is correct or execute/verify any of the transmissions contained within it.
46- ///
4796 /// * In most cases, you want to use [`Self::check_next_block`] instead to perform a full verification.
48- ///
4997 /// * This will reject any blocks with a height <= the current height and any blocks with a height >= the current height + GC.
50- /// For the former, a valid block already exists and,
51- /// for the latter, the comittte is still unknown.
52- pub fn check_block_subdag ( & self , block : Block < N > , pending_blocks : & [ PendingBlock < N > ] ) -> Result < PendingBlock < N > > {
53- self . check_block_subdag_inner ( & block, pending_blocks) ?;
98+ /// For the former, a valid block already exists and,for the latter, the comittee is still unknown.
99+ /// * This function executes atomically, in that there is guaranteed to be no concurrent updates to the ledger during its execution.
100+ /// However there are no ordering guarantees *between* multiple invocations of this function, [`Self::check_block_content`] and [`Self::advance_to_next_block`].
101+ pub fn check_block_subdag (
102+ & self ,
103+ block : Block < N > ,
104+ prefix : & [ PendingBlock < N > ] ,
105+ ) -> Result < PendingBlock < N > , CheckBlockError < N > > {
106+ self . check_block_subdag_inner ( & block, prefix) ?;
54107 Ok ( PendingBlock ( block) )
55108 }
56109
57- fn check_block_subdag_inner ( & self , block : & Block < N > , pending_blocks : & [ PendingBlock < N > ] ) -> Result < ( ) > {
110+ fn check_block_subdag_inner ( & self , block : & Block < N > , prefix : & [ PendingBlock < N > ] ) -> Result < ( ) , CheckBlockError < N > > {
111+ // Grab a lock to the latest_block in the ledger, to prevent concurrent writes to the ledger,
112+ // and to ensure that this check is atomic.
113+ let latest_block = self . current_block . read ( ) ;
114+
58115 // First check that the heights and hashes of the pending block sequence and of the new block are correct.
59116 // The hash checks should be redundant, but we perform them out of extra caution.
60- let mut expected_height = self . latest_height ( ) + 1 ;
61- for pending in pending_blocks {
62- if pending. height ( ) != expected_height {
63- bail ! (
64- "Pending block has invalid height. Expected {expected_height}, but got {actual}." ,
65- actual = pending. height( )
66- ) ;
117+ let mut expected_height = latest_block. height ( ) + 1 ;
118+ for ( index, prefix_block) in prefix. iter ( ) . enumerate ( ) {
119+ if prefix_block. height ( ) != expected_height {
120+ return Err ( CheckBlockError :: InvalidPrefix {
121+ index,
122+ error : Box :: new ( CheckBlockError :: InvalidHeight {
123+ expected : expected_height,
124+ actual : prefix_block. height ( ) ,
125+ } ) ,
126+ } ) ;
67127 }
68128
69- if self . contains_block_hash ( & pending. hash ( ) ) ? {
70- bail ! ( "Hash for pending block '{}' already exists in the ledger" , block. hash( ) )
129+ if self . contains_block_hash ( & prefix_block. hash ( ) ) ? {
130+ return Err ( CheckBlockError :: InvalidPrefix {
131+ index,
132+ error : Box :: new ( CheckBlockError :: BlockAlreadyExists { hash : prefix_block. hash ( ) } ) ,
133+ } ) ;
71134 }
72135
73136 expected_height += 1 ;
74137 }
75138
76139 if self . contains_block_hash ( & block. hash ( ) ) ? {
77- bail ! ( "Block hash '{}' already exists in the ledger" , block. hash( ) )
140+ return Err ( CheckBlockError :: BlockAlreadyExists { hash : block. hash ( ) } ) ;
78141 }
79142
80143 if block. height ( ) != expected_height {
81- bail ! ( "Block has invalid height. Expected { expected_height}, but got {}." , block. height( ) ) ;
144+ return Err ( CheckBlockError :: InvalidHeight { expected : expected_height, actual : block. height ( ) } ) ;
82145 }
83146
84147 // Ensure the certificates in the block subdag have met quorum requirements.
@@ -88,7 +151,7 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
88151 self . check_block_subdag_atomicity ( block) ?;
89152
90153 // Ensure that all leaves of the subdag point to valid batches in other subdags/blocks.
91- self . check_block_subdag_leaves ( block, pending_blocks ) ?;
154+ self . check_block_subdag_leaves ( block, prefix ) ?;
92155
93156 Ok ( ( ) )
94157 }
@@ -98,8 +161,8 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
98161 /// # Panics
99162 /// This function panics if called from an async context.
100163 pub fn check_next_block < R : CryptoRng + Rng > ( & self , block : & Block < N > , rng : & mut R ) -> Result < ( ) > {
101- self . check_block_subdag_inner ( block, & [ ] ) ?;
102- self . check_block_content_inner ( block, rng) ?;
164+ self . check_block_subdag_inner ( block, & [ ] ) . map_err ( |err| err . into_anyhow ( ) ) ?;
165+ self . check_block_content_inner ( block, rng) . map_err ( |err| err . into_anyhow ( ) ) ?;
103166
104167 Ok ( ( ) )
105168 }
@@ -113,22 +176,42 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
113176 /// # Return Value
114177 /// This returns a [`Block`] on success representing the fully verified block.
115178 ///
179+ /// # Notes
180+ /// - This check can only succeed for pending blocks that are a direct successor of the latest block in the ledger.
181+ /// - Execution of this function is atomic, and there is guaranteed to be no concurrent update to the ledger during its execution.
182+ /// - Even though this function may return `Ok(block)`, advancing the ledger to this block may still fail, if there was an update to the ledger
183+ /// *between* calling `check_block_content` and `advance_to_next_block`.
184+ /// If your implementation requires atomicity across these two steps, you need to implement your own locking mechanism.
185+ ///
116186 /// # Panics
117187 /// This function panics if called from an async context.
118- pub fn check_block_content < R : CryptoRng + Rng > ( & self , block : PendingBlock < N > , rng : & mut R ) -> Result < Block < N > > {
188+ pub fn check_block_content < R : CryptoRng + Rng > (
189+ & self ,
190+ block : PendingBlock < N > ,
191+ rng : & mut R ,
192+ ) -> Result < Block < N > , CheckBlockError < N > > {
119193 self . check_block_content_inner ( & block. 0 , rng) ?;
120194 Ok ( block. 0 )
121195 }
122196
123197 /// # Panics
124198 /// This function panics if called from an async context.
125- fn check_block_content_inner < R : CryptoRng + Rng > ( & self , block : & Block < N > , rng : & mut R ) -> Result < ( ) > {
126- let latest_block = self . latest_block ( ) ;
199+ fn check_block_content_inner < R : CryptoRng + Rng > (
200+ & self ,
201+ block : & Block < N > ,
202+ rng : & mut R ,
203+ ) -> Result < ( ) , CheckBlockError < N > > {
204+ let latest_block = self . current_block . read ( ) ;
205+
206+ // Ensure, again, that the ledger has not advanced yet. This prevents cryptic errors form appearing during the block check.
207+ if block. height ( ) != latest_block. height ( ) + 1 {
208+ return Err ( CheckBlockError :: InvalidHeight { expected : latest_block. height ( ) + 1 , actual : block. height ( ) } ) ;
209+ }
127210
128211 // Ensure the solutions do not already exist.
129212 for solution_id in block. solutions ( ) . solution_ids ( ) {
130213 if self . contains_solution_id ( solution_id) ? {
131- bail ! ( "Solution ID { solution_id} already exists in the ledger" ) ;
214+ return Err ( CheckBlockError :: SolutionAlreadyExists { solution_id : * solution_id } ) ;
132215 }
133216 }
134217
@@ -147,17 +230,14 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
147230
148231 // Ensure speculation over the unconfirmed transactions is correct and ensure each transaction is well-formed and unique.
149232 let time_since_last_block = block. timestamp ( ) . saturating_sub ( self . latest_timestamp ( ) ) ;
150- let ratified_finalize_operations = self
151- . vm
152- . check_speculate (
153- state,
154- time_since_last_block,
155- block. ratifications ( ) ,
156- block. solutions ( ) ,
157- block. transactions ( ) ,
158- rng,
159- )
160- . with_context ( || "Failed to speculate over unconfirmed transactions" ) ?;
233+ let ratified_finalize_operations = self . vm . check_speculate (
234+ state,
235+ time_since_last_block,
236+ block. ratifications ( ) ,
237+ block. solutions ( ) ,
238+ block. transactions ( ) ,
239+ rng,
240+ ) ?;
161241
162242 // Retrieve the committee lookback.
163243 let committee_lookback = self
@@ -185,7 +265,7 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
185265 OffsetDateTime :: now_utc ( ) . unix_timestamp ( ) ,
186266 ratified_finalize_operations,
187267 )
188- . with_context ( || "Failed to verify block" ) ?;
268+ . map_err ( |err| CheckBlockError :: VerificationFailed { inner : err } ) ?;
189269
190270 // Ensure that the provers are within their stake bounds.
191271 if let Some ( solutions) = block. solutions ( ) . deref ( ) {
@@ -195,7 +275,7 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
195275 let num_accepted_solutions = * accepted_solutions. get ( & prover_address) . unwrap_or ( & 0 ) ;
196276 // Check if the prover has reached their solution limit.
197277 if self . is_solution_limit_reached ( & prover_address, num_accepted_solutions) {
198- bail ! ( "Prover '{ prover_address}' has reached their solution limit for the current epoch" ) ;
278+ return Err ( CheckBlockError :: SolutionLimitReached { prover_address } ) ;
199279 }
200280 // Track the already accepted solutions.
201281 * accepted_solutions. entry ( prover_address) . or_insert ( 0 ) += 1 ;
@@ -205,14 +285,14 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
205285 // Ensure that each existing solution ID from the block exists in the ledger.
206286 for existing_solution_id in expected_existing_solution_ids {
207287 if !self . contains_solution_id ( & existing_solution_id) ? {
208- bail ! ( "Solution ID '{existing_solution_id}' does not exist in the ledger" ) ;
288+ return Err ( CheckBlockError :: PreviousSolutionNotFound { solution_id : existing_solution_id } ) ;
209289 }
210290 }
211291
212292 // Ensure that each existing transaction ID from the block exists in the ledger.
213293 for existing_transaction_id in expected_existing_transaction_ids {
214294 if !self . contains_transaction_id ( & existing_transaction_id) ? {
215- bail ! ( "Transaction ID '{existing_transaction_id}' does not exist in the ledger" ) ;
295+ return Err ( CheckBlockError :: PreviousTransactionNotFound { transaction_id : existing_transaction_id } ) ;
216296 }
217297 }
218298
@@ -221,15 +301,21 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
221301
222302 /// Check that leaves in the subdag point to batches in other blocks that are valid.
223303 ///
224- /// This does not verify that the batches are signed correctly or that the edges are valid
225- /// (only point to the previous round), as those checks already happened when the node received the batch.
226- fn check_block_subdag_leaves ( & self , block : & Block < N > , previous_blocks : & [ PendingBlock < N > ] ) -> Result < ( ) > {
304+ //
305+ /// # Arguments
306+ /// * `block` - The block to check.
307+ /// * `prefix` - A sequence of [`PendingBlock`]s between the block to check and the current height of the ledger.
308+ ///
309+ /// # Notes
310+ /// This only checks that leaves point to valid batch in the previous round, and *not* hat the batches are signed correctly
311+ /// or that the edges are valid, as those checks already happened when the node received the batch.
312+ fn check_block_subdag_leaves ( & self , block : & Block < N > , prefix : & [ PendingBlock < N > ] ) -> Result < ( ) > {
227313 // Check if the block has a subdag.
228314 let Authority :: Quorum ( subdag) = block. authority ( ) else {
229315 return Ok ( ( ) ) ;
230316 } ;
231317
232- let previous_certs: HashSet < _ > = previous_blocks
318+ let previous_certs: HashSet < _ > = prefix
233319 . iter ( )
234320 . filter_map ( |block| match block. authority ( ) {
235321 Authority :: Quorum ( subdag) => Some ( subdag. certificate_ids ( ) ) ,
0 commit comments