Skip to content

Commit 41a7fc6

Browse files
authored
Merge pull request #3185 from ProvableHQ/mainnet-hotfix-backport
Avoid current_block RwLock reentry during block checks
2 parents 968fba6 + db14f22 commit 41a7fc6

File tree

3 files changed

+82
-42
lines changed

3 files changed

+82
-42
lines changed

ledger/src/advance.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,11 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
289289
let prover_address = solution.address();
290290
let num_accepted_solutions = accepted_solutions.get(&prover_address).copied().unwrap_or(0);
291291
// Check if the prover has reached their solution limit.
292-
if self.is_solution_limit_reached_inner(previous_block, &prover_address, num_accepted_solutions)
293-
{
292+
if self.is_solution_limit_reached_at_timestamp(
293+
&prover_address,
294+
num_accepted_solutions,
295+
previous_block.timestamp(),
296+
) {
294297
return false;
295298
}
296299
// Check if the solution is valid and update the number of accepted solutions.

ledger/src/check_next_block.rs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,20 +203,17 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
203203
block: &Block<N>,
204204
rng: &mut R,
205205
) -> Result<(), CheckBlockError<N>> {
206-
// Grab lock to the previous block here, to ensure it does not change mid-check.
207-
let previous_block = self.current_block.read();
206+
let latest_block = self.current_block.read();
207+
let latest_block_timestamp = latest_block.timestamp();
208208

209209
// Ensure, again, that the ledger has not advanced yet. This prevents cryptic errors form appearing during the block check.
210-
if block.height() != previous_block.height() + 1 {
211-
return Err(CheckBlockError::InvalidHeight {
212-
expected: previous_block.height() + 1,
213-
actual: block.height(),
214-
});
210+
if block.height() != latest_block.height() + 1 {
211+
return Err(CheckBlockError::InvalidHeight { expected: latest_block.height() + 1, actual: block.height() });
215212
}
216213

217214
// Also ensure the round is valid, otherwise speculation on transactions will fail with a cryptic error.
218-
if block.round() <= previous_block.round() {
219-
return Err(CheckBlockError::InvalidRound { new: block.round(), previous: previous_block.round() });
215+
if block.round() <= latest_block.round() {
216+
return Err(CheckBlockError::InvalidRound { new: block.round(), previous: latest_block.round() });
220217
}
221218

222219
// Ensure the solutions do not already exist.
@@ -240,7 +237,7 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
240237
)?;
241238

242239
// Ensure speculation over the unconfirmed transactions is correct and ensure each transaction is well-formed and unique.
243-
let time_since_last_block = block.timestamp().saturating_sub(previous_block.timestamp());
240+
let time_since_last_block = block.timestamp().saturating_sub(latest_block_timestamp);
244241
let ratified_finalize_operations = self.vm.check_speculate(
245242
state,
246243
time_since_last_block,
@@ -267,13 +264,13 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
267264
// Get the latest epoch hash.
268265
let latest_epoch_hash = match self.current_epoch_hash.read().as_ref() {
269266
Some(epoch_hash) => *epoch_hash,
270-
None => self.get_epoch_hash(previous_block.height())?,
267+
None => self.get_epoch_hash(latest_block.height())?,
271268
};
272269

273270
// Ensure the block is correct.
274271
let (expected_existing_solution_ids, expected_existing_transaction_ids) = block
275272
.verify(
276-
&previous_block,
273+
&latest_block,
277274
self.latest_state_root(),
278275
&previous_committee_lookback,
279276
&committee_lookback,
@@ -291,7 +288,11 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
291288
let prover_address = solution.address();
292289
let num_accepted_solutions = *accepted_solutions.get(&prover_address).unwrap_or(&0);
293290
// Check if the prover has reached their solution limit.
294-
if self.is_solution_limit_reached_inner(&previous_block, &prover_address, num_accepted_solutions) {
291+
if self.is_solution_limit_reached_at_timestamp(
292+
&prover_address,
293+
num_accepted_solutions,
294+
latest_block_timestamp,
295+
) {
295296
return Err(CheckBlockError::SolutionLimitReached { prover_address });
296297
}
297298
// Track the already accepted solutions.

ledger/src/is_solution_limit_reached.rs

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,27 +82,19 @@ pub fn maximum_allowed_solutions_per_epoch<N: Network>(prover_stake: u64, curren
8282
}
8383

8484
impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
85-
/// Returns the number of remaining solutions a prover can submit in the current epoch.
86-
///
87-
/// # Locking
88-
/// This function may deadlock if called while holding a write lock to the current block.
89-
pub fn num_remaining_solutions(&self, prover_address: &Address<N>, additional_solutions_in_block: u64) -> u64 {
90-
self.num_remaining_solutions_inner(&self.current_block.read(), prover_address, additional_solutions_in_block)
91-
}
92-
93-
/// Internal version of [`Self::num_remaining_solutions`] to be used when already holding a lock to the current block.
94-
pub(super) fn num_remaining_solutions_inner(
85+
/// Returns the number of remaining solutions a prover can submit in the current epoch,
86+
/// using the provided ledger timestamp instead of re-reading the current block.
87+
pub(crate) fn num_remaining_solutions_at_timestamp(
9588
&self,
96-
latest_block: &Block<N>,
9789
prover_address: &Address<N>,
9890
additional_solutions_in_block: u64,
91+
current_time: i64,
9992
) -> u64 {
10093
// Fetch the prover's stake.
10194
let prover_stake = self.get_bonded_amount(prover_address).unwrap_or(0);
10295

10396
// Determine the maximum number of solutions allowed based on this prover's stake.
104-
let maximum_allowed_solutions =
105-
maximum_allowed_solutions_per_epoch::<N>(prover_stake, latest_block.timestamp());
97+
let maximum_allowed_solutions = maximum_allowed_solutions_per_epoch::<N>(prover_stake, current_time);
10698

10799
// Fetch the number of solutions the prover has earned rewards for in the current epoch.
108100
let prover_num_solutions_in_epoch = *self.epoch_provers_cache.read().get(prover_address).unwrap_or(&0);
@@ -114,33 +106,44 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
114106
maximum_allowed_solutions.saturating_sub(num_solutions)
115107
}
116108

117-
/// Returns `true` if the given prover address has reached their solution limit for the current epoch.
118-
///
119-
/// # Locking
120-
/// This function may deadlock if called while holding a write lock to the current block.
121-
pub fn is_solution_limit_reached(&self, prover_address: &Address<N>, additional_solutions_in_block: u64) -> bool {
122-
self.is_solution_limit_reached_inner(&self.current_block.read(), prover_address, additional_solutions_in_block)
109+
/// Returns the number of remaining solutions a prover can submit in the current epoch.
110+
pub fn num_remaining_solutions(&self, prover_address: &Address<N>, additional_solutions_in_block: u64) -> u64 {
111+
self.num_remaining_solutions_at_timestamp(
112+
prover_address,
113+
additional_solutions_in_block,
114+
self.latest_timestamp(),
115+
)
123116
}
124117

125-
/// Internal version of [`Self::is_solution_limit_reached`] to be used when already holding a lock to the current block.
126-
pub(super) fn is_solution_limit_reached_inner(
118+
/// Returns `true` if the given prover address has reached their solution limit for the current epoch,
119+
/// using the provided ledger timestamp instead of re-reading the current block.
120+
pub(crate) fn is_solution_limit_reached_at_timestamp(
127121
&self,
128-
latest_block: &Block<N>,
129122
prover_address: &Address<N>,
130123
additional_solutions_in_block: u64,
124+
current_time: i64,
131125
) -> bool {
132-
// Calculate the number of remaining solutions for the prover.
133-
let num_remaining_solutions =
134-
self.num_remaining_solutions_inner(latest_block, prover_address, additional_solutions_in_block);
126+
self.num_remaining_solutions_at_timestamp(prover_address, additional_solutions_in_block, current_time) == 0
127+
}
135128

136-
// If the number of remaining solutions is zero, the limit is reached.
137-
num_remaining_solutions == 0
129+
/// Returns `true` if the given prover address has reached their solution limit for the current epoch.
130+
///
131+
/// # Locking
132+
/// This function may deadlock if called while holding a write lock to the current block.
133+
pub fn is_solution_limit_reached(&self, prover_address: &Address<N>, additional_solutions_in_block: u64) -> bool {
134+
self.is_solution_limit_reached_at_timestamp(
135+
prover_address,
136+
additional_solutions_in_block,
137+
self.latest_timestamp(),
138+
)
138139
}
139140
}
140141

141142
#[cfg(test)]
142143
mod tests {
143144
use super::*;
145+
use crate::test_helpers::{CurrentLedger, LedgerType};
146+
use std::{thread, time::Duration};
144147

145148
type CurrentNetwork = console::network::MainnetV0;
146149

@@ -226,4 +229,37 @@ mod tests {
226229
);
227230
}
228231
}
232+
233+
#[test]
234+
fn test_solution_limit_helper_avoids_current_block_reentry_with_pending_writer() {
235+
let rng = &mut TestRng::default();
236+
237+
let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
238+
let validator_address = Address::try_from(&private_key).unwrap();
239+
let store = ConsensusStore::<CurrentNetwork, LedgerType>::open(StorageMode::new_test(None)).unwrap();
240+
let genesis = VM::from(store).unwrap().genesis_beacon(&private_key, rng).unwrap();
241+
let ledger = CurrentLedger::load(genesis, StorageMode::new_test(None)).unwrap();
242+
243+
let next_block =
244+
ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], vec![], rng).unwrap();
245+
let expected = ledger.num_remaining_solutions(&validator_address, 0);
246+
247+
// Mimic check_next_block() holding a read guard while a writer queues behind it.
248+
let latest_block = ledger.current_block.read();
249+
let latest_timestamp = latest_block.timestamp();
250+
251+
let ledger_clone = ledger.clone();
252+
let next_block_clone = next_block.clone();
253+
let writer = thread::spawn(move || ledger_clone.advance_to_next_block(&next_block_clone));
254+
255+
thread::sleep(Duration::from_millis(50));
256+
assert!(!writer.is_finished(), "advance_to_next_block should be waiting on current_block.write()");
257+
258+
let actual = ledger.num_remaining_solutions_at_timestamp(&validator_address, 0, latest_timestamp);
259+
assert_eq!(actual, expected);
260+
assert_eq!(ledger.is_solution_limit_reached_at_timestamp(&validator_address, 0, latest_timestamp), actual == 0);
261+
262+
drop(latest_block);
263+
writer.join().unwrap().unwrap();
264+
}
229265
}

0 commit comments

Comments
 (0)