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
38impl ConsensusTxStatus {
39 pub(crate) fn metric_label(&self) -> &'static str {
40 match self {
41 ConsensusTxStatus::Finalized => "finalized",
42 ConsensusTxStatus::Rejected => "rejected",
43 ConsensusTxStatus::Dropped => "dropped",
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
49pub(crate) enum NotifyReadConsensusTxStatusResult {
50 Status(ConsensusTxStatus),
52 Expired(u32),
55}
56
57pub(crate) struct ConsensusTxStatusCache {
58 inner: RwLock<Inner>,
59
60 status_notify_read: NotifyRead<ConsensusPosition, ConsensusTxStatus>,
61 last_committed_leader_round_tx: watch::Sender<Option<u32>>,
63 last_committed_leader_round_rx: watch::Receiver<Option<u32>>,
64}
65
66#[derive(Default)]
67struct Inner {
68 transaction_status: BTreeMap<ConsensusPosition, ConsensusTxStatus>,
70 last_committed_leader_round: Option<Round>,
72}
73
74impl ConsensusTxStatusCache {
75 pub(crate) fn new(consensus_gc_depth: Round) -> Self {
76 assert!(
77 consensus_gc_depth < CONSENSUS_STATUS_RETENTION_ROUNDS,
78 "{} vs {}",
79 consensus_gc_depth,
80 CONSENSUS_STATUS_RETENTION_ROUNDS
81 );
82 let (last_committed_leader_round_tx, last_committed_leader_round_rx) = watch::channel(None);
83 Self {
84 inner: Default::default(),
85 status_notify_read: Default::default(),
86 last_committed_leader_round_tx,
87 last_committed_leader_round_rx,
88 }
89 }
90
91 #[cfg(test)]
94 pub(crate) fn set_transaction_status(&self, pos: ConsensusPosition, status: ConsensusTxStatus) {
95 self.set_transaction_statuses(vec![(pos, status)]);
96 }
97
98 pub(crate) fn set_transaction_statuses(
106 &self,
107 updates: Vec<(ConsensusPosition, ConsensusTxStatus)>,
108 ) {
109 let last_committed_leader_round = *self.last_committed_leader_round_rx.borrow();
112 let mut to_notify = Vec::with_capacity(updates.len());
113 {
114 let mut inner = self.inner.write();
115 for (pos, status) in updates {
116 if let Some(last_committed_leader_round) = last_committed_leader_round
117 && pos.block.round + CONSENSUS_STATUS_RETENTION_ROUNDS
118 <= last_committed_leader_round
119 {
120 continue;
122 }
123 let old_status = inner.transaction_status.insert(pos, status);
124 if let Some(old_status) = old_status
125 && old_status != status
126 {
127 panic!(
128 "Conflicting status updates for transaction {:?}: {:?} -> {:?}",
129 pos, old_status, status
130 );
131 }
132 debug!("Transaction status is set for {}: {:?}", pos, status);
133 to_notify.push((pos, status));
134 }
135 }
136 for (pos, status) in to_notify {
137 self.status_notify_read.notify(&pos, &status);
138 }
139 }
140
141 pub(crate) async fn notify_read_transaction_status(
144 &self,
145 consensus_position: ConsensusPosition,
146 ) -> NotifyReadConsensusTxStatusResult {
147 let registration = self.status_notify_read.register_one(&consensus_position);
148 let mut round_rx = self.last_committed_leader_round_rx.clone();
149 {
150 let inner = self.inner.read();
151 if let Some(status) = inner.transaction_status.get(&consensus_position) {
152 return NotifyReadConsensusTxStatusResult::Status(*status);
153 }
154 }
156 let expiration_check = async {
157 loop {
158 if let Some(last_committed_leader_round) = *round_rx.borrow()
159 && consensus_position.block.round + CONSENSUS_STATUS_RETENTION_ROUNDS
160 <= last_committed_leader_round
161 {
162 return last_committed_leader_round;
163 }
164 round_rx
166 .changed()
167 .await
168 .expect("last_committed_leader_round watch channel closed unexpectedly");
169 }
170 };
171 tokio::select! {
172 status = registration => NotifyReadConsensusTxStatusResult::Status(status),
173 last_committed_leader_round = expiration_check => NotifyReadConsensusTxStatusResult::Expired(last_committed_leader_round),
174 }
175 }
176
177 pub(crate) fn update_last_committed_leader_round(&self, last_committed_leader_round: u32) {
178 debug!(
179 "Updating last committed leader round: {}",
180 last_committed_leader_round
181 );
182
183 let mut inner = self.inner.write();
184
185 let Some(leader_round) = inner
190 .last_committed_leader_round
191 .replace(last_committed_leader_round)
192 else {
193 return;
195 };
196
197 while let Some((position, _)) = inner.transaction_status.first_key_value() {
199 if position.block.round + CONSENSUS_STATUS_RETENTION_ROUNDS <= leader_round {
200 inner.transaction_status.pop_first();
201 } else {
202 break;
203 }
204 }
205
206 let _ = self.last_committed_leader_round_tx.send(Some(leader_round));
208 }
209
210 pub(crate) fn get_last_committed_leader_round(&self) -> Option<u32> {
211 *self.last_committed_leader_round_rx.borrow()
212 }
213
214 pub(crate) fn check_position_too_ahead(&self, position: &ConsensusPosition) -> SuiResult<()> {
216 if let Some(last_committed_leader_round) = *self.last_committed_leader_round_rx.borrow()
217 && position.block.round
218 > last_committed_leader_round + CONSENSUS_STATUS_RETENTION_ROUNDS
219 {
220 return Err(SuiErrorKind::ValidatorConsensusLagging {
221 round: position.block.round,
222 last_committed_round: last_committed_leader_round,
223 }
224 .into());
225 }
226 Ok(())
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use std::{sync::Arc, time::Duration};
233
234 use super::*;
235 use consensus_types::block::{BlockRef, TransactionIndex};
236
237 fn create_test_tx_position(round: u64, index: u64) -> ConsensusPosition {
238 ConsensusPosition {
239 epoch: Default::default(),
240 block: BlockRef {
241 round: round as u32,
242 author: Default::default(),
243 digest: Default::default(),
244 },
245 index: index as TransactionIndex,
246 }
247 }
248
249 #[tokio::test]
250 async fn test_set_and_get_transaction_status() {
251 let cache = ConsensusTxStatusCache::new(60);
252 let tx_pos = create_test_tx_position(1, 0);
253
254 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
256
257 let result = cache.notify_read_transaction_status(tx_pos).await;
259 assert!(matches!(
260 result,
261 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
262 ));
263 }
264
265 #[tokio::test]
266 async fn test_status_notification() {
267 let cache = Arc::new(ConsensusTxStatusCache::new(60));
268 let tx_pos = create_test_tx_position(1, 0);
269
270 let cache_clone = cache.clone();
272 let handle =
273 tokio::spawn(async move { cache_clone.notify_read_transaction_status(tx_pos).await });
274
275 tokio::time::sleep(Duration::from_millis(10)).await;
277
278 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
280
281 let result = handle.await.unwrap();
283 assert!(matches!(
284 result,
285 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
286 ));
287 }
288
289 #[tokio::test]
290 async fn test_round_expiration() {
291 let cache = ConsensusTxStatusCache::new(60);
292 let tx_pos = create_test_tx_position(1, 0);
293
294 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
296
297 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 1);
299
300 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 2);
303
304 let result = cache.notify_read_transaction_status(tx_pos).await;
306 assert!(matches!(
307 result,
308 NotifyReadConsensusTxStatusResult::Expired(_)
309 ));
310 }
311
312 #[tokio::test]
313 async fn test_cleanup_expired_rounds() {
314 let cache = ConsensusTxStatusCache::new(60);
315
316 for round in 1..=5 {
318 let tx_pos = create_test_tx_position(round, 0);
319 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Rejected);
320 }
321
322 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 2);
324
325 {
327 let inner = cache.inner.read();
328 let rounds = inner
329 .transaction_status
330 .keys()
331 .map(|p| p.block.round)
332 .collect::<Vec<_>>();
333 assert_eq!(rounds, vec![1, 2, 3, 4, 5]);
334 }
335
336 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 3);
339
340 {
342 let inner = cache.inner.read();
343 let rounds = inner
344 .transaction_status
345 .keys()
346 .map(|p| p.block.round)
347 .collect::<Vec<_>>();
348 assert_eq!(rounds, vec![3, 4, 5]);
349 }
350
351 cache.update_last_committed_leader_round(CONSENSUS_STATUS_RETENTION_ROUNDS + 4);
354
355 {
357 let inner = cache.inner.read();
358 let rounds = inner
359 .transaction_status
360 .keys()
361 .map(|p| p.block.round)
362 .collect::<Vec<_>>();
363 assert_eq!(rounds, vec![4, 5]);
364 }
365 }
366
367 #[tokio::test]
368 async fn test_concurrent_operations() {
369 let cache = Arc::new(ConsensusTxStatusCache::new(60));
370 let tx_pos = create_test_tx_position(1, 0);
371
372 let mut handles = vec![];
374 for _ in 0..3 {
375 let cache_clone = cache.clone();
376 handles.push(tokio::spawn(async move {
377 cache_clone.notify_read_transaction_status(tx_pos).await
378 }));
379 }
380
381 tokio::time::sleep(Duration::from_millis(10)).await;
383
384 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
386
387 for handle in handles {
389 let result = handle.await.unwrap();
390 assert!(matches!(
391 result,
392 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
393 ));
394 }
395 }
396
397 #[tokio::test]
398 #[should_panic(expected = "Conflicting status updates")]
399 async fn test_out_of_order_status_updates() {
400 let cache = Arc::new(ConsensusTxStatusCache::new(60));
401 let tx_pos = create_test_tx_position(1, 0);
402
403 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Finalized);
405
406 cache.set_transaction_status(tx_pos, ConsensusTxStatus::Rejected);
408 }
409}