Skip to content

Commit cf10c9d

Browse files
authored
Merge pull request #3050 from ProvableHQ/feat/check-block-error
[Feature] Add `CheckBlockError`
2 parents c980e97 + 9fb7b76 commit cf10c9d

File tree

7 files changed

+266
-56
lines changed

7 files changed

+266
-56
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ledger/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ workspace = true
174174
[dependencies.tracing]
175175
workspace = true
176176

177+
[dependencies.thiserror]
178+
workspace = true
179+
features = [ "std" ]
180+
177181
[dev-dependencies.bincode]
178182
workspace = true
179183

ledger/block/src/verify.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ use std::collections::HashSet;
2626
use rayon::prelude::*;
2727

2828
impl<N: Network> Block<N> {
29-
/// Ensures the block is correct.
29+
/// Ensures the block is well-formed and consistent with the previous block.
30+
///
31+
/// # Returns
32+
/// - On success, the sets of transaction and solution IDs that existed in this subDAG but were already included in the previous block.
33+
/// - On failure, the error that caused verification to fail, e.g., invalid block hash, invalid block authority, or invalid transmissions.
3034
pub fn verify(
3135
&self,
3236
previous_block: &Block<N>,

ledger/src/advance.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use anyhow::Context;
1919

2020
impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
2121
/// Returns a candidate for the next block in the ledger, using a committed subdag and its transmissions.
22-
///
22+
/// This candidate can then be passed to [`Ledger::advance_to_next_block`] to be added to the ledger.
23+
///
2324
/// # Panics
2425
/// This function panics if called from an async context.
2526
pub fn prepare_advance_to_next_quorum_block<R: Rng + CryptoRng>(
@@ -36,9 +37,8 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
3637
// Currently, we do not support ratifications from the memory pool.
3738
ensure!(ratifications.is_empty(), "Ratifications are currently unsupported from the memory pool");
3839
// Construct the block template.
39-
let (header, ratifications, solutions, aborted_solution_ids, transactions, aborted_transaction_ids) = self
40-
.construct_block_template(&previous_block, Some(&subdag), ratifications, solutions, transactions, rng)
41-
.with_context(|| "Failed to construct block template")?;
40+
let (header, ratifications, solutions, aborted_solution_ids, transactions, aborted_transaction_ids) =
41+
self.construct_block_template(&previous_block, Some(&subdag), ratifications, solutions, transactions, rng)?;
4242

4343
// Construct the new quorum block.
4444
Block::new_quorum(
@@ -54,6 +54,10 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
5454
}
5555

5656
/// Returns a candidate for the next block in the ledger.
57+
/// This candidate can then be passed to [`Ledger::advance_to_next_block`] to be added to the ledger.
58+
///
59+
/// Note, that beacon blocks are only used for testing purposes.
60+
/// Production code will most likely used `[Ledger::prepare_advance_to_next_quorum_block`] instead.
5761
///
5862
/// # Panics
5963
/// This function panics if called from an async context.
@@ -80,8 +84,7 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
8084
candidate_solutions,
8185
candidate_transactions,
8286
rng,
83-
)
84-
.with_context(|| "Failed to construct block template")?;
87+
)?;
8588

8689
// Construct the new beacon block.
8790
Block::new_beacon(
@@ -99,6 +102,11 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
99102

100103
/// Adds the given block as the next block in the ledger.
101104
///
105+
/// This function expects a valid block, that either was created by a trusted source, or successfully passed
106+
/// the blocks checks (e.g. [`Ledger::check_next_block`]).
107+
/// Note, that it is still possible that this function returns an error for a valid block, if there are concurrent tasks
108+
/// updating the ledger.
109+
///
102110
/// # Panics
103111
/// This function panics if called from an async context.
104112
pub fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {

ledger/src/check_next_block.rs

Lines changed: 132 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
use super::*;
1717

18-
use crate::narwhal::BatchHeader;
18+
use crate::{narwhal::BatchHeader, puzzle::SolutionID};
1919

2020
use 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+
3782
impl<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

Comments
 (0)