sui_core/authority/
transaction_reject_reason_cache.rs1use std::collections::BTreeMap;
5
6use consensus_config::AuthorityIndex;
7use consensus_types::block::{BlockDigest, BlockRef, TransactionIndex};
8use mysten_metrics::monitored_scope;
9use parking_lot::RwLock;
10use sui_types::committee::EpochId;
11use sui_types::error::SuiError;
12use sui_types::messages_consensus::ConsensusPosition;
13use tracing::trace;
14
15use crate::authority::consensus_tx_status_cache::CONSENSUS_STATUS_RETENTION_ROUNDS;
16
17#[cfg(test)]
18use consensus_types::block::Round;
19
20pub(crate) struct TransactionRejectReasonCache {
38 cache: RwLock<BTreeMap<ConsensusPosition, SuiError>>,
39 retention_rounds: u32,
40 epoch: EpochId,
41}
42
43impl TransactionRejectReasonCache {
44 pub fn new(retention_rounds: Option<u32>, epoch: EpochId) -> Self {
45 Self {
46 cache: Default::default(),
47 retention_rounds: retention_rounds.unwrap_or(CONSENSUS_STATUS_RETENTION_ROUNDS),
48 epoch,
49 }
50 }
51
52 pub fn set_rejection_vote_reason(&self, position: ConsensusPosition, reason: &SuiError) {
56 debug_assert_eq!(position.epoch, self.epoch, "Epoch mismatch");
57 self.cache.write().insert(position, reason.clone());
58 }
59
60 pub fn get_rejection_vote_reason(&self, position: ConsensusPosition) -> Option<SuiError> {
64 debug_assert_eq!(position.epoch, self.epoch, "Epoch mismatch");
65 self.cache.read().get(&position).cloned()
66 }
67
68 pub fn set_last_committed_leader_round(&self, round: u32) {
70 let _scope =
71 monitored_scope("TransactionRejectReasonCache::set_last_committed_leader_round");
72 let cut_off_round = round.saturating_sub(self.retention_rounds) + 1;
73 let cut_off_position = ConsensusPosition {
74 epoch: self.epoch,
75 block: BlockRef::new(cut_off_round, AuthorityIndex::MIN, BlockDigest::MIN),
76 index: TransactionIndex::MIN,
77 };
78
79 let mut cache = self.cache.write();
80 let remaining = cache.split_off(&cut_off_position);
81 trace!("Cleaned up {} entries", cache.len());
82 *cache = remaining;
83 }
84}
85
86#[cfg(test)]
87mod test {
88 use sui_types::error::SuiErrorKind;
89
90 use super::*;
91
92 #[tokio::test]
93 async fn test_set_rejection_vote_reason_and_get_reason() {
94 let cache = TransactionRejectReasonCache::new(None, 1);
95 let position = ConsensusPosition {
96 epoch: 1,
97 block: BlockRef::new(1, AuthorityIndex::MAX, BlockDigest::MAX),
98 index: 1,
99 };
100
101 {
103 let reason = SuiErrorKind::ValidatorHaltedAtEpochEnd.into();
104 cache.set_rejection_vote_reason(position, &reason);
105 assert_eq!(cache.get_rejection_vote_reason(position), Some(reason));
106 }
107
108 {
110 let reason = SuiErrorKind::InvalidTransactionDigest.into();
111 cache.set_rejection_vote_reason(position, &reason);
112 assert_eq!(cache.get_rejection_vote_reason(position), Some(reason));
113 }
114
115 {
117 let position = ConsensusPosition {
118 epoch: 1,
119 block: BlockRef::new(1, AuthorityIndex::MAX, BlockDigest::MIN),
120 index: 2,
121 };
122 assert_eq!(cache.get_rejection_vote_reason(position), None);
123 }
124 }
125
126 #[tokio::test]
127 async fn test_set_last_committed_leader_round() {
128 const RETENTION_ROUNDS: u32 = 4;
129 const TOTAL_ROUNDS: u32 = 10;
130 let cache = TransactionRejectReasonCache::new(Some(RETENTION_ROUNDS), 1);
131
132 let position = |round: Round, transaction_index: u16| ConsensusPosition {
133 epoch: 1,
134 block: BlockRef::new(
135 round,
136 AuthorityIndex::new_for_test(transaction_index as u32),
137 BlockDigest::MAX,
138 ),
139 index: transaction_index,
140 };
141
142 for round in 0..TOTAL_ROUNDS {
144 for transaction_index in 0..5 {
145 cache.set_rejection_vote_reason(
146 position(round, transaction_index),
147 &SuiErrorKind::InvalidTransactionDigest.into(),
148 );
149 }
150 }
151
152 cache.set_last_committed_leader_round(6);
154
155 for round in 0..TOTAL_ROUNDS {
157 for transaction_index in 0..5 {
158 let position = position(round, transaction_index);
159 if round <= 2 {
160 assert_eq!(cache.get_rejection_vote_reason(position), None);
161 } else {
162 assert_eq!(
163 cache.get_rejection_vote_reason(position),
164 Some(SuiErrorKind::InvalidTransactionDigest.into())
165 );
166 }
167 }
168 }
169
170 cache.set_last_committed_leader_round(10);
172
173 for round in 0..TOTAL_ROUNDS {
175 for transaction_index in 0..5 {
176 let position = position(round, transaction_index);
177 if round <= 6 {
178 assert_eq!(cache.get_rejection_vote_reason(position), None);
179 } else {
180 assert_eq!(
181 cache.get_rejection_vote_reason(position),
182 Some(SuiErrorKind::InvalidTransactionDigest.into())
183 );
184 }
185 }
186 }
187 }
188}