1use std::collections::BTreeMap;
5
6use consensus_types::block::Round;
7use mysten_common::sync::notify_read::NotifyRead;
8use parking_lot::RwLock;
9use sui_types::{
10 error::{SuiErrorKind, SuiResult},
11 messages_consensus::ConsensusPosition,
12};
13use tokio::sync::watch;
14use tracing::debug;
15
16pub(crate) const CONSENSUS_STATUS_RETENTION_ROUNDS: u32 = 400;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub(crate) enum ConsensusTxStatus {
23 Rejected,
25 Finalized,
27 Dropped,
36}
37
38#[derive(Debug, Clone)]
39pub(crate) enum NotifyReadConsensusTxStatusResult {
40 Status(ConsensusTxStatus),
42 Expired(u32),
45}
46
47pub(crate) struct ConsensusTxStatusCache {
48 inner: RwLock<Inner>,
49
50 status_notify_read: NotifyRead<ConsensusPosition, ConsensusTxStatus>,
51 last_committed_leader_round_tx: watch::Sender<Option<u32>>,
53 last_committed_leader_round_rx: watch::Receiver<Option<u32>>,
54}
55
56#[derive(Default)]
57struct Inner {
58 transaction_status: BTreeMap<ConsensusPosition, ConsensusTxStatus>,
60 last_committed_leader_round: Option<Round>,
62}
63
64impl ConsensusTxStatusCache {
65 pub(crate) fn new(consensus_gc_depth: Round) -> Self {
66 assert!(
67 consensus_gc_depth < CONSENSUS_STATUS_RETENTION_ROUNDS,
68 "{} vs {}",
69 consensus_gc_depth,
70 CONSENSUS_STATUS_RETENTION_ROUNDS
71 );
72 let (last_committed_leader_round_tx, last_committed_leader_round_rx) = watch::channel(None);
73 Self {
74 inner: Default::default(),
75 status_notify_read: Default::default(),
76 last_committed_leader_round_tx,
77 last_committed_leader_round_rx,
78 }
79 }
80
81 #[cfg(test)]
84 pub(crate) fn set_transaction_status(&self, pos: ConsensusPosition, status: ConsensusTxStatus) {
85 self.set_transaction_statuses(vec![(pos, status)]);
86 }
87
88 pub(crate) fn set_transaction_statuses(
96 &self,
97 updates: Vec<(ConsensusPosition, ConsensusTxStatus)>,
98 ) {
99 let last_committed_leader_round = *self.last_committed_leader_round_rx.borrow();
102 let mut to_notify = Vec::with_capacity(updates.len());
103 {
104 let mut inner = self.inner.write();
105 for (pos, status) in updates {
106 if let Some(last_committed_leader_round) = last_committed_leader_round
107 && pos.block.round + CONSENSUS_STATUS_RETENTION_ROUNDS
108 <= last_committed_leader_round
109 {
110 continue;
112 }
113 let old_status = inner.transaction_status.insert(pos, status);
114 if let Some(old_status) = old_status
115 && old_status != status
116 {
117 panic!(
118 "Conflicting status updates for transaction {:?}: {:?} -> {:?}",
119 pos, old_status, status
120 );
121 }
122 debug!("Transaction status is set for {}: {:?}", pos, status);
123 to_notify.push((pos, status));
124 }
125 }
126 for (pos, status) in to_notify {
127 self.status_notify_read.notify(&pos, &status);
128 }
129 }
130
131 pub(crate) async fn notify_read_transaction_status(
134 &self,
135 consensus_position: ConsensusPosition,
136 ) -> NotifyReadConsensusTxStatusResult {
137 let registration = self.status_notify_read.register_one(&consensus_position);
138 let mut round_rx = self.last_committed_leader_round_rx.clone();
139 {
140 let inner = self.inner.read();
141 if let Some(status) = inner.transaction_status.get(&consensus_position) {
142 return NotifyReadConsensusTxStatusResult::Status(*status);
143 }
144 }
146 let expiration_check = async {
147 loop {
148 if let Some(last_committed_leader_round) = *round_rx.borrow()
149 && consensus_position.block.round + CONSENSUS_STATUS_RETENTION_ROUNDS
150 <= last_committed_leader_round
151 {
152 return last_committed_leader_round;
153 }
154 round_rx
156 .changed()
157 .await
158 .expect("last_committed_leader_round watch channel closed unexpectedly");
159 }
160 };
161 tokio::select! {
162 status = registration => NotifyReadConsensusTxStatusResult::Status(status),
163 last_committed_leader_round = expiration_check => NotifyReadConsensusTxStatusResult::Expired(last_committed_leader_round),
164 }
165 }
166
167 pub(crate) fn update_last_committed_leader_round(&self, last_committed_leader_round: u32) {
168 debug!(
169 "Updating last committed leader round: {}",
170 last_committed_leader_round
171 );
172
173 let mut inner = self.inner.write();
174
175 let Some(leader_round) = inner
180 .last_committed_leader_round
181 .replace(last_committed_leader_round)
182 else {
183 return;
185 };
186
187 while let Some((position, _)) = inner.transaction_status.first_key_value() {
189 if position.block.round + CONSENSUS_STATUS_RETENTION_ROUNDS <= leader_round {
190 inner.transaction_status.pop_first();
191 } else {
192 break;
193 }
194 }
195
196 let _ = self.last_committed_leader_round_tx.send(Some(leader_round));
198 }
199
200 pub(crate) fn get_last_committed_leader_round(&self) -> Option<u32> {
201 *self.last_committed_leader_round_rx.borrow()
202 }
203
204 pub(crate) fn check_position_too_ahead(&self, position: &ConsensusPosition) -> SuiResult<()> {
206 if let Some(last_committed_leader_round) = *self.last_committed_leader_round_rx.borrow()
207 && position.block.round
208 > last_committed_leader_round + CONSENSUS_STATUS_RETENTION_ROUNDS
209 {
210 return Err(SuiErrorKind::ValidatorConsensusLagging {
211 round: position.block.round,
212 last_committed_round: last_committed_leader_round,
213 }
214 .into());
215 }
216 Ok(())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use std::{sync::Arc, time::Duration};
223
224 use super::*;
225 use consensus_types::block::{BlockRef, TransactionIndex};
226
227 fn create_test_tx_position(round: u64, index: u64) -> ConsensusPosition {
228 ConsensusPosition {
229 epoch: Default::default(),
230 block: BlockRef {
231 round: round as u32,
232 author: Default::default(),
233 digest: Default::default(),
234 },
235 index: index as TransactionIndex,
236 }
237 }
238
239 #[tokio::test]
240 async fn test_set_and_get_transaction_status() {
241 let cache = ConsensusTxStatusCache::new(60);
242 let tx_pos = create_test_tx_position(1, 0);
243
244 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
246
247 let result = cache.notify_read_transaction_status(tx_pos).await;
249 assert!(matches!(
250 result,
251 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
252 ));
253 }
254
255 #[tokio::test]
256 async fn test_status_notification() {
257 let cache = Arc::new(ConsensusTxStatusCache::new(60));
258 let tx_pos = create_test_tx_position(1, 0);
259
260 let cache_clone = cache.clone();
262 let handle =
263 tokio::spawn(async move { cache_clone.notify_read_transaction_status(tx_pos).await });
264
265 tokio::time::sleep(Duration::from_millis(10)).await;
267
268 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
270
271 let result = handle.await.unwrap();
273 assert!(matches!(
274 result,
275 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
276 ));
277 }
278
279 #[tokio::test]
280 async fn test_round_expiration() {
281 let cache = ConsensusTxStatusCache::new(60);
282 let tx_pos = create_test_tx_position(1, 0);
283
284 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
286
287 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 1);
289
290 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 2);
293
294 let result = cache.notify_read_transaction_status(tx_pos).await;
296 assert!(matches!(
297 result,
298 NotifyReadConsensusTxStatusResult::Expired(_)
299 ));
300 }
301
302 #[tokio::test]
303 async fn test_cleanup_expired_rounds() {
304 let cache = ConsensusTxStatusCache::new(60);
305
306 for round in 1..=5 {
308 let tx_pos = create_test_tx_position(round, 0);
309 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Rejected);
310 }
311
312 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 2);
314
315 {
317 let inner = cache.inner.read();
318 let rounds = inner
319 .transaction_status
320 .keys()
321 .map(|p| p.block.round)
322 .collect::<Vec<_>>();
323 assert_eq!(rounds, vec![1, 2, 3, 4, 5]);
324 }
325
326 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 3);
329
330 {
332 let inner = cache.inner.read();
333 let rounds = inner
334 .transaction_status
335 .keys()
336 .map(|p| p.block.round)
337 .collect::<Vec<_>>();
338 assert_eq!(rounds, vec![3, 4, 5]);
339 }
340
341 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 4);
344
345 {
347 let inner = cache.inner.read();
348 let rounds = inner
349 .transaction_status
350 .keys()
351 .map(|p| p.block.round)
352 .collect::<Vec<_>>();
353 assert_eq!(rounds, vec![4, 5]);
354 }
355 }
356
357 #[tokio::test]
358 async fn test_concurrent_operations() {
359 let cache = Arc::new(ConsensusTxStatusCache::new(60));
360 let tx_pos = create_test_tx_position(1, 0);
361
362 let mut handles = vec![];
364 for _ in 0..3 {
365 let cache_clone = cache.clone();
366 handles.push(tokio::spawn(async move {
367 cache_clone.notify_read_transaction_status(tx_pos).await
368 }));
369 }
370
371 tokio::time::sleep(Duration::from_millis(10)).await;
373
374 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
376
377 for handle in handles {
379 let result = handle.await.unwrap();
380 assert!(matches!(
381 result,
382 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
383 ));
384 }
385 }
386
387 #[tokio::test]
388 #[should_panic(expected = "Conflicting status updates")]
389 async fn test_out_of_order_status_updates() {
390 let cache = Arc::new(ConsensusTxStatusCache::new(60));
391 let tx_pos = create_test_tx_position(1, 0);
392
393 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
395
396 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Rejected);
398 }
399}