1use crate::authority::AuthorityState;
19use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
20use crate::authority::authority_store_tables::AuthorityPerpetualTables;
21use crate::consensus_adapter::ConsensusAdapter;
22use arc_swap::ArcSwap;
23use itertools::Itertools;
24use parking_lot::Mutex;
25use prometheus::{
26 IntCounterVec, IntGauge, IntGaugeVec, Registry, register_int_counter_vec_with_registry,
27 register_int_gauge_vec_with_registry, register_int_gauge_with_registry,
28};
29use std::collections::{BTreeMap, BTreeSet};
30use std::sync::Arc;
31use sui_config::transaction_deny_config::{
32 PeerDenySyncConfig, TransactionDenyConfig, ValidatorEligibility,
33};
34use sui_types::base_types::AuthorityName;
35use sui_types::base_types::ConciseableName;
36use sui_types::committee::{Committee, StakeUnit, TOTAL_VOTING_POWER};
37use sui_types::error::{SuiError, SuiResult};
38use sui_types::messages_consensus::{
39 ConsensusTransaction, SharedTransactionDenyConfig, SharedTransactionDenyConfigV1,
40};
41use sui_types::transaction_deny_rules::{DenyElement, DenyElementKind, TransactionDenyRules};
42use tracing::{debug, info, warn};
43use typed_store::Map;
44
45pub struct TransactionDenyConfigManager {
46 self_authority: AuthorityName,
47 local_config: Arc<TransactionDenyConfig>,
49 sync_config: PeerDenySyncConfig,
51 peer_configs: Mutex<BTreeMap<AuthorityName, SharedTransactionDenyConfig>>,
54 committee: ArcSwap<Committee>,
57 effective_config: ArcSwap<TransactionDenyConfig>,
60 perpetual: Arc<AuthorityPerpetualTables>,
62 broadcast_generation_lock: Mutex<()>,
65 metrics: TransactionDenyConfigMetrics,
66}
67
68impl TransactionDenyConfigManager {
69 pub fn new(
70 self_authority: AuthorityName,
71 local_config: TransactionDenyConfig,
72 sync_config: PeerDenySyncConfig,
73 committee: Arc<Committee>,
74 perpetual: Arc<AuthorityPerpetualTables>,
75 registry: &Registry,
76 ) -> SuiResult<Arc<Self>> {
77 sync_config.validate().map_err(SuiError::from)?;
78 let local_config = Arc::new(local_config);
79
80 let mut peer_configs = BTreeMap::new();
89 for entry in perpetual.shared_transaction_deny_configs.safe_iter() {
90 let (authority, msg) = entry.expect("db error reading shared_transaction_deny_configs");
91 if authority != self_authority && committee.authority_exists(&authority) {
92 peer_configs.insert(authority, msg);
93 }
94 }
95
96 let metrics = TransactionDenyConfigMetrics::new(registry);
97 let evaluation = evaluate(&local_config, &sync_config, &peer_configs, &committee);
98 let effective = local_config.with_rules(evaluation.effective_rules.clone());
99 metrics.record(
100 &local_config,
101 active_proposal_count(&peer_configs),
102 &evaluation,
103 );
104
105 info!(
106 rulesets = sync_config.rulesets.len(),
107 default_buckets = sync_config.default_buckets.len(),
108 seeded_proposals = peer_configs.len(),
109 "TransactionDenyConfigManager initialized",
110 );
111
112 Ok(Arc::new(Self {
113 self_authority,
114 local_config,
115 sync_config,
116 peer_configs: Mutex::new(peer_configs),
117 committee: ArcSwap::from(committee),
118 effective_config: ArcSwap::from_pointee(effective),
119 perpetual,
120 broadcast_generation_lock: Mutex::new(()),
121 metrics,
122 }))
123 }
124
125 pub fn local(&self) -> &Arc<TransactionDenyConfig> {
128 &self.local_config
129 }
130
131 pub fn effective_config(&self) -> &ArcSwap<TransactionDenyConfig> {
133 &self.effective_config
134 }
135
136 pub fn peer_configs_snapshot(&self) -> BTreeMap<AuthorityName, SharedTransactionDenyConfig> {
138 self.peer_configs.lock().clone()
139 }
140
141 pub fn evaluate_status(&self) -> DenyConfigEvaluation {
144 let committee = self.committee.load();
145 let peer_configs = self.peer_configs.lock();
146 evaluate(
147 &self.local_config,
148 &self.sync_config,
149 &peer_configs,
150 &committee,
151 )
152 }
153
154 pub fn may_have_outstanding_broadcast(&self) -> bool {
165 self.perpetual
166 .last_broadcast_deny_generation
167 .get(&())
168 .expect("db error")
169 .is_some()
170 }
171
172 pub fn apply_updates(&self, from: AuthorityName, msgs: Vec<SharedTransactionDenyConfig>) {
176 if msgs.is_empty() {
177 return;
178 }
179 let max_generation = AuthorityState::unixtime_now_ms()
189 .saturating_add(SharedTransactionDenyConfig::MAX_GENERATION_FUTURE_DRIFT_MS);
190 let (evaluation, active_proposals) = {
191 let mut peer_configs = self.peer_configs.lock();
192 let committee = self.committee.load();
196 let mut accepted = false;
197 for msg in msgs {
198 let authority = msg.authority();
199 let generation = msg.generation();
200 if authority != from {
201 warn!(
202 authority = %authority.concise(),
203 from = %from.concise(),
204 "Dropping UpdateTransactionDenyConfig: claimed authority does not match sender",
205 );
206 self.metrics
207 .dropped_updates
208 .with_label_values(&["author_mismatch"])
209 .inc();
210 continue;
211 }
212 if !committee.authority_exists(&authority) {
213 info!(
214 authority = %authority.concise(),
215 "Dropping UpdateTransactionDenyConfig: sender not in committee",
216 );
217 self.metrics
218 .dropped_updates
219 .with_label_values(&["sender_not_in_committee"])
220 .inc();
221 continue;
222 }
223 if generation > max_generation {
224 warn!(
225 authority = %authority.concise(),
226 generation,
227 "Dropping UpdateTransactionDenyConfig: generation too far in the future",
228 );
229 self.metrics
230 .dropped_updates
231 .with_label_values(&["future_generation"])
232 .inc();
233 continue;
234 }
235 if let Some(existing) = peer_configs.get(&authority)
236 && existing.generation() >= generation
237 {
238 debug!(
239 authority = %authority.concise(),
240 new_generation = generation,
241 existing_generation = existing.generation(),
242 "Dropping UpdateTransactionDenyConfig: stale generation",
243 );
244 self.metrics
245 .dropped_updates
246 .with_label_values(&["stale_generation"])
247 .inc();
248 continue;
249 }
250 self.perpetual
252 .shared_transaction_deny_configs
253 .insert(&authority, &msg)
254 .expect("db error");
255 peer_configs.insert(authority, msg);
256 accepted = true;
257 info!(
258 authority = %authority.concise(),
259 generation,
260 "Accepted UpdateTransactionDenyConfig from committee member",
261 );
262 }
263 if !accepted {
264 return;
265 }
266 (
267 evaluate(
268 &self.local_config,
269 &self.sync_config,
270 &peer_configs,
271 &committee,
272 ),
273 active_proposal_count(&peer_configs),
274 )
275 };
276 self.apply_evaluation(evaluation, active_proposals);
277 }
278
279 pub fn update_for_committee(&self, committee: Arc<Committee>) -> SuiResult<()> {
284 let (evaluation, active_proposals) = {
285 let mut peer_configs = self.peer_configs.lock();
286 self.committee.store(committee.clone());
290 let to_remove: Vec<AuthorityName> = peer_configs
291 .keys()
292 .filter(|name| !committee.authority_exists(name))
293 .copied()
294 .collect();
295 for name in &to_remove {
296 self.perpetual
297 .shared_transaction_deny_configs
298 .remove(name)?;
299 peer_configs.remove(name);
300 }
301 if !to_remove.is_empty() {
302 info!(
303 pruned = to_remove.len(),
304 "Pruned UpdateTransactionDenyConfig entries for peers no longer in committee",
305 );
306 }
307 (
308 evaluate(
309 &self.local_config,
310 &self.sync_config,
311 &peer_configs,
312 &committee,
313 ),
314 active_proposal_count(&peer_configs),
315 )
316 };
317 self.apply_evaluation(evaluation, active_proposals);
318 Ok(())
319 }
320
321 pub fn allocate_next_broadcast_generation(&self) -> SuiResult<u64> {
325 let _guard = self.broadcast_generation_lock.lock();
328 let now_ms = AuthorityState::unixtime_now_ms();
329 let last = self
330 .perpetual
331 .last_broadcast_deny_generation
332 .get(&())?
333 .unwrap_or(0);
334 let mut generation = now_ms.max(last.saturating_add(1));
335 if generation > now_ms + SharedTransactionDenyConfig::MAX_GENERATION_FUTURE_DRIFT_MS {
342 warn!(
343 last,
344 new_generation = now_ms,
345 "Persisted broadcast generation is too far in the future; resetting to current time",
346 );
347 generation = now_ms;
348 }
349 self.perpetual
350 .last_broadcast_deny_generation
351 .insert(&(), &generation)?;
352 Ok(generation)
353 }
354
355 fn build_share_message(
357 &self,
358 rules: Option<TransactionDenyRules>,
359 ) -> SuiResult<SharedTransactionDenyConfig> {
360 if let Some(rules) = &rules {
361 rules.check_share_limits().map_err(SuiError::from)?;
362 }
363 let generation = self.allocate_next_broadcast_generation()?;
364 Ok(SharedTransactionDenyConfig::V1(
365 SharedTransactionDenyConfigV1 {
366 authority: self.self_authority,
367 generation,
368 rules,
369 },
370 ))
371 }
372
373 pub fn submit_broadcast(
376 &self,
377 rules: Option<TransactionDenyRules>,
378 consensus_adapter: &Arc<ConsensusAdapter>,
379 epoch_store: &Arc<AuthorityPerEpochStore>,
380 ) -> SuiResult<u64> {
381 let msg = self.build_share_message(rules)?;
382 let generation = msg.generation();
383 let tx = ConsensusTransaction::new_update_transaction_deny_config(msg.clone());
384 info!(?tx, "Updating transaction deny config vote");
385 consensus_adapter.submit(tx, None, epoch_store, None, None)?;
386 self.apply_updates(self.self_authority, vec![msg]);
391 Ok(generation)
392 }
393
394 fn apply_evaluation(&self, evaluation: DenyConfigEvaluation, active_proposals: usize) {
396 self.metrics
397 .record(&self.local_config, active_proposals, &evaluation);
398 let prev = self.effective_config.load();
399 if &evaluation.effective_rules != prev.rules() {
400 let new_effective = self.local_config.with_rules(evaluation.effective_rules);
401 self.effective_config.store(Arc::new(new_effective));
402 }
403 }
404}
405
406fn evaluate(
409 local_config: &TransactionDenyConfig,
410 sync_config: &PeerDenySyncConfig,
411 peer_configs: &BTreeMap<AuthorityName, SharedTransactionDenyConfig>,
412 committee: &Committee,
413) -> DenyConfigEvaluation {
414 let votes: BTreeMap<AuthorityName, &TransactionDenyRules> = peer_configs
415 .iter()
416 .filter(|(name, _)| committee.authority_exists(name))
417 .filter_map(|(name, msg)| msg.rules().map(|rules| (*name, rules)))
418 .collect();
419 let stakes: BTreeMap<AuthorityName, StakeUnit> = committee
420 .members()
421 .map(|(name, stake)| (*name, *stake))
422 .collect();
423 evaluate_deny_configs(local_config.rules(), sync_config, &votes, &stakes)
424}
425
426fn active_proposal_count(
428 peer_configs: &BTreeMap<AuthorityName, SharedTransactionDenyConfig>,
429) -> usize {
430 peer_configs
431 .values()
432 .filter(|m| m.rules().is_some())
433 .count()
434}
435
436pub struct PrelistedRulesetStatus {
437 pub name: String,
438 pub stake_threshold_percent: u16,
439 pub eligible_stake: StakeUnit,
440 pub voted_stake: StakeUnit,
441 pub voters: Vec<AuthorityName>,
442 pub active: bool,
443}
444
445pub struct DefaultBucketStatus {
446 pub name: String,
447 pub element_kinds: BTreeSet<DenyElementKind>,
448 pub stake_threshold_percent: u16,
449 pub eligible_stake: StakeUnit,
450 pub applied_elements: Vec<DenyElement>,
451}
452
453pub struct DenyConfigEvaluation {
455 pub effective_rules: TransactionDenyRules,
458 pub prelisted: Vec<PrelistedRulesetStatus>,
459 pub defaults: Vec<DefaultBucketStatus>,
460}
461
462fn meets_threshold(voted: StakeUnit, eligible: StakeUnit, percent: u16) -> bool {
464 if eligible == 0 {
465 return false;
467 }
468 let voted_share = voted * 100;
471 let required_share = StakeUnit::from(percent) * eligible;
472 voted_share >= required_share
473}
474
475fn eligible_stake(
477 eligibility: &ValidatorEligibility,
478 stakes: &BTreeMap<AuthorityName, StakeUnit>,
479) -> StakeUnit {
480 match eligibility {
481 ValidatorEligibility::Allowlist(set) => {
482 set.iter().filter_map(|name| stakes.get(name)).sum()
483 }
484 ValidatorEligibility::Denylist(set) => {
485 TOTAL_VOTING_POWER
486 - set
487 .iter()
488 .filter_map(|name| stakes.get(name))
489 .sum::<StakeUnit>()
490 }
491 }
492}
493
494pub fn evaluate_deny_configs(
502 local_rules: &TransactionDenyRules,
503 sync_config: &PeerDenySyncConfig,
504 votes: &BTreeMap<AuthorityName, &TransactionDenyRules>,
505 stakes: &BTreeMap<AuthorityName, StakeUnit>,
506) -> DenyConfigEvaluation {
507 let prelisted: Vec<PrelistedRulesetStatus> = sync_config
508 .rulesets
509 .iter()
510 .map(|ruleset| {
511 let eligible_stake = eligible_stake(&ruleset.threshold.eligibility, stakes);
512 let (voters, voted_stake): (Vec<AuthorityName>, StakeUnit) = votes
514 .iter()
515 .filter_map(|(name, rules)| {
516 let stake = stakes.get(name)?;
517 (ruleset.threshold.eligibility.is_eligible(name)
518 && rules.is_superset_of(&ruleset.rules))
519 .then_some((*name, *stake))
520 })
521 .fold((Vec::new(), 0), |(mut voters, total), (name, stake)| {
522 voters.push(name);
523 (voters, total + stake)
524 });
525 PrelistedRulesetStatus {
526 name: ruleset.name.clone(),
527 stake_threshold_percent: ruleset.threshold.stake_threshold_percent,
528 eligible_stake,
529 voted_stake,
530 active: meets_threshold(
531 voted_stake,
532 eligible_stake,
533 ruleset.threshold.stake_threshold_percent,
534 ),
535 voters,
536 }
537 })
538 .collect();
539
540 let kind_to_bucket: BTreeMap<DenyElementKind, usize> = sync_config
543 .default_buckets
544 .iter()
545 .enumerate()
546 .flat_map(|(idx, bucket)| bucket.element_kinds.iter().map(move |k| (*k, idx)))
547 .collect();
548
549 let mut per_bucket_element_stake: Vec<BTreeMap<DenyElement, StakeUnit>> =
552 vec![BTreeMap::new(); sync_config.default_buckets.len()];
553 for (name, rules) in votes {
554 let Some(stake) = stakes.get(name) else {
555 continue;
556 };
557 for element in rules.elements() {
558 let Some(&bucket_idx) = kind_to_bucket.get(&element.kind()) else {
559 continue;
560 };
561 let bucket = &sync_config.default_buckets[bucket_idx];
562 if !bucket.threshold.eligibility.is_eligible(name) {
563 continue;
564 }
565 *per_bucket_element_stake[bucket_idx]
566 .entry(element)
567 .or_default() += *stake;
568 }
569 }
570
571 let defaults: Vec<DefaultBucketStatus> = sync_config
572 .default_buckets
573 .iter()
574 .zip_eq(per_bucket_element_stake)
575 .map(|(bucket, element_stake)| {
576 let eligible_stake = eligible_stake(&bucket.threshold.eligibility, stakes);
577 let applied_elements: Vec<DenyElement> = element_stake
578 .into_iter()
579 .filter(|(_, stake)| {
580 meets_threshold(
581 *stake,
582 eligible_stake,
583 bucket.threshold.stake_threshold_percent,
584 )
585 })
586 .map(|(element, _)| element)
587 .collect();
588 DefaultBucketStatus {
589 name: bucket.name.clone(),
590 element_kinds: bucket.element_kinds.clone(),
591 stake_threshold_percent: bucket.threshold.stake_threshold_percent,
592 eligible_stake,
593 applied_elements,
594 }
595 })
596 .collect();
597
598 let mut effective_rules = local_rules.clone();
599 for (ruleset, status) in sync_config.rulesets.iter().zip_eq(&prelisted) {
600 if status.active {
601 effective_rules.merge(&ruleset.rules);
602 }
603 }
604 for default in &defaults {
605 for element in &default.applied_elements {
606 effective_rules.apply_element(element);
607 }
608 }
609
610 DenyConfigEvaluation {
611 effective_rules,
612 prelisted,
613 defaults,
614 }
615}
616
617struct DenyRulesGauges {
620 user_transaction_disabled: IntGauge,
621 shared_object_disabled: IntGauge,
622 package_publish_disabled: IntGauge,
623 package_upgrade_disabled: IntGauge,
624 num_denied_objects: IntGauge,
625 num_denied_packages: IntGauge,
626 num_denied_addresses: IntGauge,
627}
628
629impl DenyRulesGauges {
630 fn new(registry: &Registry, prefix: &str, layer_help: &str) -> Self {
631 let gauge = |name: &str, help: String| {
632 let g = IntGauge::new(format!("{prefix}_{name}"), help).unwrap();
633 registry.register(Box::new(g.clone())).unwrap();
634 g
635 };
636 Self {
637 user_transaction_disabled: gauge(
638 "user_transaction_disabled",
639 format!("1 if user_transaction_disabled is set in the {layer_help}"),
640 ),
641 shared_object_disabled: gauge(
642 "shared_object_disabled",
643 format!("1 if shared_object_disabled is set in the {layer_help}"),
644 ),
645 package_publish_disabled: gauge(
646 "package_publish_disabled",
647 format!("1 if package_publish_disabled is set in the {layer_help}"),
648 ),
649 package_upgrade_disabled: gauge(
650 "package_upgrade_disabled",
651 format!("1 if package_upgrade_disabled is set in the {layer_help}"),
652 ),
653 num_denied_objects: gauge(
654 "num_denied_objects",
655 format!("Number of objects in the {layer_help} object_deny_list"),
656 ),
657 num_denied_packages: gauge(
658 "num_denied_packages",
659 format!("Number of packages in the {layer_help} package_deny_list"),
660 ),
661 num_denied_addresses: gauge(
662 "num_denied_addresses",
663 format!("Number of addresses in the {layer_help} address_deny_list"),
664 ),
665 }
666 }
667
668 fn set_from(&self, rules: &TransactionDenyRules) {
669 self.user_transaction_disabled
670 .set(rules.user_transaction_disabled as i64);
671 self.shared_object_disabled
672 .set(rules.shared_object_disabled as i64);
673 self.package_publish_disabled
674 .set(rules.package_publish_disabled as i64);
675 self.package_upgrade_disabled
676 .set(rules.package_upgrade_disabled as i64);
677 self.num_denied_objects
678 .set(rules.object_deny_list.len() as i64);
679 self.num_denied_packages
680 .set(rules.package_deny_list.len() as i64);
681 self.num_denied_addresses
682 .set(rules.address_deny_list.len() as i64);
683 }
684}
685
686pub struct TransactionDenyConfigMetrics {
690 local: DenyRulesGauges,
691 effective: DenyRulesGauges,
692 active_proposals: IntGauge,
693 dropped_updates: IntCounterVec,
694 default_bucket_applied_elements: IntGaugeVec,
695 default_bucket_eligible_stake: IntGaugeVec,
696 shared_config_active: IntGaugeVec,
697 shared_config_voted_bps: IntGaugeVec,
698 shared_config_eligible_stake: IntGaugeVec,
699}
700
701impl TransactionDenyConfigMetrics {
702 pub fn new(registry: &Registry) -> Self {
703 Self {
704 local: DenyRulesGauges::new(registry, "tx_deny_config", "local config"),
707 effective: DenyRulesGauges::new(
708 registry,
709 "tx_deny_effective",
710 "effective config (local + threshold-gated peer rules)",
711 ),
712 active_proposals: register_int_gauge_with_registry!(
713 "tx_deny_active_proposals",
714 "Number of committee members with an accepted (Some) deny-rule proposal",
715 registry,
716 )
717 .unwrap(),
718 dropped_updates: register_int_counter_vec_with_registry!(
719 "tx_deny_dropped_updates",
720 "Number of UpdateTransactionDenyConfig messages dropped without effect, by reason",
721 &["reason"],
722 registry,
723 )
724 .unwrap(),
725 default_bucket_applied_elements: register_int_gauge_vec_with_registry!(
726 "tx_deny_default_bucket_applied_elements",
727 "Number of rule elements activated via this default bucket",
728 &["bucket"],
729 registry,
730 )
731 .unwrap(),
732 default_bucket_eligible_stake: register_int_gauge_vec_with_registry!(
733 "tx_deny_default_bucket_eligible_stake",
734 "Total eligible voting stake (denominator) for this default bucket",
735 &["bucket"],
736 registry,
737 )
738 .unwrap(),
739 shared_config_active: register_int_gauge_vec_with_registry!(
740 "tx_deny_ruleset_active",
741 "1 if this pre-listed ruleset currently meets its stake threshold",
742 &["ruleset"],
743 registry,
744 )
745 .unwrap(),
746 shared_config_voted_bps: register_int_gauge_vec_with_registry!(
747 "tx_deny_ruleset_voted_bps",
748 "Voted stake as basis points of eligible stake for this pre-listed ruleset",
749 &["ruleset"],
750 registry,
751 )
752 .unwrap(),
753 shared_config_eligible_stake: register_int_gauge_vec_with_registry!(
754 "tx_deny_ruleset_eligible_stake",
755 "Total eligible voting stake for this pre-listed ruleset",
756 &["ruleset"],
757 registry,
758 )
759 .unwrap(),
760 }
761 }
762
763 pub fn record(
764 &self,
765 local: &TransactionDenyConfig,
766 active_proposals: usize,
767 evaluation: &DenyConfigEvaluation,
768 ) {
769 self.local.set_from(local.rules());
770 self.effective.set_from(&evaluation.effective_rules);
771 self.active_proposals.set(active_proposals as i64);
772
773 for status in &evaluation.prelisted {
774 let labels = &[status.name.as_str()];
775 self.shared_config_active
776 .with_label_values(labels)
777 .set(status.active as i64);
778 self.shared_config_voted_bps
779 .with_label_values(labels)
780 .set(stake_bps(status.voted_stake, status.eligible_stake));
781 self.shared_config_eligible_stake
782 .with_label_values(labels)
783 .set(status.eligible_stake as i64);
784 }
785
786 for default in &evaluation.defaults {
787 let labels = &[default.name.as_str()];
788 self.default_bucket_applied_elements
789 .with_label_values(labels)
790 .set(default.applied_elements.len() as i64);
791 self.default_bucket_eligible_stake
792 .with_label_values(labels)
793 .set(default.eligible_stake as i64);
794 }
795 }
796}
797
798fn stake_bps(voted: StakeUnit, eligible: StakeUnit) -> i64 {
799 if eligible == 0 {
800 0
801 } else {
802 ((voted as u128 * 10_000) / eligible as u128) as i64
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use fastcrypto::traits::VerifyingKey;
810 use sui_config::transaction_deny_config::{
811 DefaultDenyBucket, SharedDenyRuleThreshold, SharedDenyRuleset,
812 TransactionDenyConfigBuilder, ValidatorEligibility,
813 };
814 use sui_types::base_types::{ObjectID, dbg_addr};
815
816 fn fake_name(byte: u8) -> AuthorityName {
817 AuthorityName::new([byte; sui_types::crypto::AuthorityPublicKey::LENGTH])
818 }
819
820 fn rules_with_objects(bytes: &[u8]) -> TransactionDenyRules {
823 TransactionDenyRules {
824 object_deny_list: bytes
825 .iter()
826 .map(|b| ObjectID::from_single_byte(*b))
827 .collect(),
828 ..Default::default()
829 }
830 }
831
832 fn prelisted(
833 name: &str,
834 rules: TransactionDenyRules,
835 eligibility: ValidatorEligibility,
836 threshold: u16,
837 ) -> SharedDenyRuleset {
838 SharedDenyRuleset {
839 name: name.to_string(),
840 rules,
841 threshold: SharedDenyRuleThreshold {
842 eligibility,
843 stake_threshold_percent: threshold,
844 },
845 }
846 }
847
848 fn default_bucket(
849 name: &str,
850 kinds: &[DenyElementKind],
851 eligibility: ValidatorEligibility,
852 threshold: u16,
853 ) -> DefaultDenyBucket {
854 DefaultDenyBucket {
855 name: name.to_string(),
856 element_kinds: kinds.iter().copied().collect(),
857 threshold: SharedDenyRuleThreshold {
858 eligibility,
859 stake_threshold_percent: threshold,
860 },
861 }
862 }
863
864 fn equal_stakes(names: &[AuthorityName]) -> BTreeMap<AuthorityName, StakeUnit> {
867 names.iter().map(|n| (*n, 2500)).collect()
868 }
869
870 fn vote_refs(
871 owned: &BTreeMap<AuthorityName, TransactionDenyRules>,
872 ) -> BTreeMap<AuthorityName, &TransactionDenyRules> {
873 owned.iter().map(|(name, rules)| (*name, rules)).collect()
874 }
875
876 #[test]
877 fn meets_threshold_is_inclusive_and_handles_zero_eligible() {
878 assert!(meets_threshold(5000, 10000, 50));
880 assert!(!meets_threshold(4999, 10000, 50));
881 assert!(!meets_threshold(0, 0, 0));
883 }
884
885 #[test]
886 fn evaluate_prelisted_threshold() {
887 let names: Vec<_> = (1..=4).map(fake_name).collect();
888 let stakes = equal_stakes(&names);
889 let local = TransactionDenyConfigBuilder::new().build();
890 let sync = PeerDenySyncConfig {
891 rulesets: vec![prelisted(
892 "c",
893 rules_with_objects(&[1]),
894 ValidatorEligibility::Allowlist(names.iter().copied().collect()),
895 60,
896 )],
897 ..Default::default()
898 };
899
900 let votes: BTreeMap<_, _> = names[..2]
902 .iter()
903 .map(|n| (*n, rules_with_objects(&[1])))
904 .collect();
905 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
906 assert!(!eval.prelisted[0].active);
907 assert!(
908 !eval
909 .effective_rules
910 .object_deny_list
911 .contains(&ObjectID::from_single_byte(1))
912 );
913
914 let votes: BTreeMap<_, _> = names[..3]
916 .iter()
917 .map(|n| (*n, rules_with_objects(&[1])))
918 .collect();
919 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
920 assert!(eval.prelisted[0].active);
921 assert!(
922 eval.effective_rules
923 .object_deny_list
924 .contains(&ObjectID::from_single_byte(1))
925 );
926 }
927
928 #[test]
929 fn evaluate_superset_votes_for_overlapping_and_nested_configs() {
930 let names: Vec<_> = (1..=4).map(fake_name).collect();
931 let stakes = equal_stakes(&names);
932 let local = TransactionDenyConfigBuilder::new().build();
933 let all = ValidatorEligibility::Allowlist(names.iter().copied().collect());
934 let sync = PeerDenySyncConfig {
935 rulesets: vec![
936 prelisted("x", rules_with_objects(&[1, 2]), all.clone(), 50),
938 prelisted("y", rules_with_objects(&[2, 3]), all.clone(), 50),
939 prelisted("z", rules_with_objects(&[1, 2, 3]), all, 50),
941 ],
942 ..Default::default()
943 };
944
945 let votes: BTreeMap<_, _> = names[..3]
947 .iter()
948 .map(|n| (*n, rules_with_objects(&[1, 2, 3])))
949 .collect();
950 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
951 assert!(eval.prelisted.iter().all(|p| p.active));
952
953 let votes: BTreeMap<_, _> = names[..3]
955 .iter()
956 .map(|n| (*n, rules_with_objects(&[1, 2])))
957 .collect();
958 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
959 assert!(eval.prelisted[0].active); assert!(!eval.prelisted[1].active); assert!(!eval.prelisted[2].active); }
963
964 #[test]
965 fn evaluate_allowlist_vs_denylist_eligibility() {
966 let names: Vec<_> = (1..=4).map(fake_name).collect();
967 let stakes = equal_stakes(&names);
968 let local = TransactionDenyConfigBuilder::new().build();
969
970 let allow = PeerDenySyncConfig {
973 rulesets: vec![prelisted(
974 "c",
975 rules_with_objects(&[1]),
976 ValidatorEligibility::Allowlist(names[..2].iter().copied().collect()),
977 60,
978 )],
979 ..Default::default()
980 };
981 let votes: BTreeMap<_, _> = names[1..]
983 .iter()
984 .map(|n| (*n, rules_with_objects(&[1])))
985 .collect();
986 let eval = evaluate_deny_configs(local.rules(), &allow, &vote_refs(&votes), &stakes);
987 assert_eq!(eval.prelisted[0].eligible_stake, 5000);
988 assert_eq!(eval.prelisted[0].voted_stake, 2500); assert!(!eval.prelisted[0].active);
990
991 let deny = PeerDenySyncConfig {
993 rulesets: vec![prelisted(
994 "c",
995 rules_with_objects(&[1]),
996 ValidatorEligibility::Denylist([names[0]].into_iter().collect()),
997 50,
998 )],
999 ..Default::default()
1000 };
1001 let eval = evaluate_deny_configs(local.rules(), &deny, &vote_refs(&votes), &stakes);
1002 assert_eq!(eval.prelisted[0].eligible_stake, 7500);
1003 assert_eq!(eval.prelisted[0].voted_stake, 7500);
1004 assert!(eval.prelisted[0].active);
1005 }
1006
1007 #[test]
1008 fn evaluate_default_per_element() {
1009 let names: Vec<_> = (1..=4).map(fake_name).collect();
1010 let stakes = equal_stakes(&names);
1011 let local = TransactionDenyConfigBuilder::new().build();
1012 let sync = PeerDenySyncConfig {
1013 default_buckets: vec![default_bucket(
1014 "objs",
1015 &[DenyElementKind::Object],
1016 ValidatorEligibility::default(),
1017 50,
1018 )],
1019 ..Default::default()
1020 };
1021
1022 let mut votes = BTreeMap::new();
1024 votes.insert(names[0], rules_with_objects(&[1, 2]));
1025 votes.insert(names[1], rules_with_objects(&[1]));
1026 votes.insert(names[2], rules_with_objects(&[1]));
1027 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
1028 assert!(
1029 eval.effective_rules
1030 .object_deny_list
1031 .contains(&ObjectID::from_single_byte(1))
1032 );
1033 assert!(
1034 !eval
1035 .effective_rules
1036 .object_deny_list
1037 .contains(&ObjectID::from_single_byte(2))
1038 );
1039 assert_eq!(eval.defaults.len(), 1);
1040 assert_eq!(
1041 eval.defaults[0].applied_elements,
1042 vec![DenyElement::Object(ObjectID::from_single_byte(1))]
1043 );
1044 }
1045
1046 #[test]
1047 fn evaluate_element_counts_for_both_prelisted_and_default() {
1048 let names: Vec<_> = (1..=4).map(fake_name).collect();
1049 let stakes = equal_stakes(&names);
1050 let local = TransactionDenyConfigBuilder::new().build();
1051 let all = ValidatorEligibility::Allowlist(names.iter().copied().collect());
1052 let sync = PeerDenySyncConfig {
1054 rulesets: vec![prelisted("c", rules_with_objects(&[1]), all, 90)],
1055 default_buckets: vec![default_bucket(
1056 "objs",
1057 &[DenyElementKind::Object],
1058 ValidatorEligibility::default(),
1059 50,
1060 )],
1061 ..Default::default()
1062 };
1063 let votes: BTreeMap<_, _> = names[..3]
1066 .iter()
1067 .map(|n| (*n, rules_with_objects(&[1])))
1068 .collect();
1069 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
1070 assert!(!eval.prelisted[0].active);
1071 assert!(
1072 eval.effective_rules
1073 .object_deny_list
1074 .contains(&ObjectID::from_single_byte(1))
1075 );
1076 }
1077
1078 #[test]
1079 fn evaluate_default_buckets_segregate_by_kind() {
1080 let names: Vec<_> = (1..=4).map(fake_name).collect();
1085 let stakes = equal_stakes(&names);
1086 let local = TransactionDenyConfigBuilder::new().build();
1087 let sync = PeerDenySyncConfig {
1088 default_buckets: vec![
1089 default_bucket(
1090 "objs",
1091 &[DenyElementKind::Object],
1092 ValidatorEligibility::default(),
1093 50,
1094 ),
1095 default_bucket(
1096 "kill-switches",
1097 &[DenyElementKind::UserTransactionDisabled],
1098 ValidatorEligibility::default(),
1099 90,
1100 ),
1101 ],
1102 ..Default::default()
1103 };
1104
1105 let proposal = TransactionDenyRules {
1106 object_deny_list: [ObjectID::from_single_byte(1)].into_iter().collect(),
1107 user_transaction_disabled: true,
1108 ..Default::default()
1109 };
1110 let votes: BTreeMap<_, _> = names[..3].iter().map(|n| (*n, proposal.clone())).collect();
1111 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
1112
1113 assert!(
1114 eval.effective_rules
1115 .object_deny_list
1116 .contains(&ObjectID::from_single_byte(1))
1117 );
1118 assert!(!eval.effective_rules.user_transaction_disabled);
1119 assert_eq!(eval.defaults.len(), 2);
1120 assert_eq!(
1121 eval.defaults[0].applied_elements,
1122 vec![DenyElement::Object(ObjectID::from_single_byte(1))],
1123 );
1124 assert!(eval.defaults[1].applied_elements.is_empty());
1125 }
1126
1127 #[test]
1128 fn evaluate_default_unconfigured_kind_never_applies() {
1129 let names: Vec<_> = (1..=4).map(fake_name).collect();
1132 let stakes = equal_stakes(&names);
1133 let local = TransactionDenyConfigBuilder::new().build();
1134 let sync = PeerDenySyncConfig {
1135 default_buckets: vec![default_bucket(
1136 "objs",
1137 &[DenyElementKind::Object],
1138 ValidatorEligibility::default(),
1139 50,
1140 )],
1141 ..Default::default()
1142 };
1143
1144 let proposal = TransactionDenyRules {
1145 user_transaction_disabled: true,
1146 ..Default::default()
1147 };
1148 let votes: BTreeMap<_, _> = names.iter().map(|n| (*n, proposal.clone())).collect();
1149 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
1150
1151 assert!(!eval.effective_rules.user_transaction_disabled);
1152 assert!(eval.defaults[0].applied_elements.is_empty());
1153 }
1154
1155 #[test]
1156 fn evaluate_default_bucket_eligibility_is_per_bucket() {
1157 let names: Vec<_> = (1..=4).map(fake_name).collect();
1162 let stakes = equal_stakes(&names);
1163 let local = TransactionDenyConfigBuilder::new().build();
1164 let objs_allowlist = ValidatorEligibility::Allowlist(names[..2].iter().copied().collect());
1165 let kill_allowlist = ValidatorEligibility::Allowlist(names[2..].iter().copied().collect());
1166 let sync = PeerDenySyncConfig {
1167 default_buckets: vec![
1168 default_bucket("objs", &[DenyElementKind::Object], objs_allowlist, 50),
1169 default_bucket(
1170 "kill-switches",
1171 &[DenyElementKind::UserTransactionDisabled],
1172 kill_allowlist,
1173 50,
1174 ),
1175 ],
1176 ..Default::default()
1177 };
1178
1179 let proposal = TransactionDenyRules {
1181 object_deny_list: [ObjectID::from_single_byte(1)].into_iter().collect(),
1182 user_transaction_disabled: true,
1183 ..Default::default()
1184 };
1185 let votes: BTreeMap<_, _> = names.iter().map(|n| (*n, proposal.clone())).collect();
1186 let eval = evaluate_deny_configs(local.rules(), &sync, &vote_refs(&votes), &stakes);
1187
1188 assert_eq!(eval.defaults[0].eligible_stake, 5000);
1191 assert_eq!(
1192 eval.defaults[0].applied_elements,
1193 vec![DenyElement::Object(ObjectID::from_single_byte(1))],
1194 );
1195 assert_eq!(eval.defaults[1].eligible_stake, 5000);
1198 assert_eq!(
1199 eval.defaults[1].applied_elements,
1200 vec![DenyElement::UserTransactionDisabled],
1201 );
1202 assert!(
1203 eval.effective_rules
1204 .object_deny_list
1205 .contains(&ObjectID::from_single_byte(1))
1206 );
1207 assert!(eval.effective_rules.user_transaction_disabled);
1208 }
1209
1210 #[test]
1211 fn evaluate_local_rules_always_applied() {
1212 let names: Vec<_> = (1..=4).map(fake_name).collect();
1213 let stakes = equal_stakes(&names);
1214 let local = TransactionDenyConfigBuilder::new()
1215 .add_denied_address(dbg_addr(9))
1216 .build();
1217 let sync = PeerDenySyncConfig::default();
1218 let eval = evaluate_deny_configs(local.rules(), &sync, &BTreeMap::new(), &stakes);
1219 assert!(
1220 eval.effective_rules
1221 .address_deny_list
1222 .contains(&dbg_addr(9))
1223 );
1224 }
1225
1226 fn test_committee(size: usize) -> (Arc<Committee>, Vec<AuthorityName>) {
1229 let (committee, _kps) =
1230 Committee::new_simple_test_committee_with_normalized_voting_power(vec![1; size]);
1231 let names: Vec<AuthorityName> = committee.names().copied().collect();
1232 (Arc::new(committee), names)
1233 }
1234
1235 fn open_perpetual() -> (tempfile::TempDir, Arc<AuthorityPerpetualTables>) {
1236 let dir = tempfile::tempdir().unwrap();
1237 let perpetual = Arc::new(AuthorityPerpetualTables::open(dir.path(), None, None));
1238 (dir, perpetual)
1239 }
1240
1241 fn manager_with(
1242 self_authority: AuthorityName,
1243 local: TransactionDenyConfig,
1244 sync_config: PeerDenySyncConfig,
1245 committee: Arc<Committee>,
1246 ) -> (Arc<TransactionDenyConfigManager>, tempfile::TempDir) {
1247 let (dir, perpetual) = open_perpetual();
1248 let registry = Registry::new();
1249 let manager = TransactionDenyConfigManager::new(
1250 self_authority,
1251 local,
1252 sync_config,
1253 committee,
1254 perpetual,
1255 ®istry,
1256 )
1257 .unwrap();
1258 (manager, dir)
1259 }
1260
1261 fn make_msg(
1262 authority: AuthorityName,
1263 generation: u64,
1264 rules: Option<TransactionDenyRules>,
1265 ) -> SharedTransactionDenyConfig {
1266 SharedTransactionDenyConfig::V1(SharedTransactionDenyConfigV1 {
1267 authority,
1268 generation,
1269 rules,
1270 })
1271 }
1272
1273 fn sync_with_prelisted(committee: &Committee) -> PeerDenySyncConfig {
1275 PeerDenySyncConfig {
1276 rulesets: vec![prelisted(
1277 "c",
1278 rules_with_objects(&[1]),
1279 ValidatorEligibility::Allowlist(committee.names().copied().collect()),
1280 50,
1281 )],
1282 ..Default::default()
1283 }
1284 }
1285
1286 #[tokio::test(flavor = "multi_thread")]
1287 async fn apply_updates_rejects_non_committee_sender() {
1288 let (committee, names) = test_committee(4);
1289 let local = TransactionDenyConfigBuilder::new().build();
1290 let (manager, _dir) =
1291 manager_with(names[0], local, sync_with_prelisted(&committee), committee);
1292
1293 let outsider = fake_name(200);
1294 manager.apply_updates(
1295 outsider,
1296 vec![make_msg(outsider, 1, Some(rules_with_objects(&[1])))],
1297 );
1298 assert!(manager.peer_configs_snapshot().is_empty());
1299 }
1300
1301 #[tokio::test(flavor = "multi_thread")]
1304 async fn apply_updates_drops_author_mismatch() {
1305 let (committee, names) = test_committee(4);
1306 let local = TransactionDenyConfigBuilder::new().build();
1307 let (manager, _dir) =
1308 manager_with(names[0], local, sync_with_prelisted(&committee), committee);
1309
1310 manager.apply_updates(
1311 names[2],
1312 vec![make_msg(names[1], 1, Some(rules_with_objects(&[1])))],
1313 );
1314 assert!(manager.peer_configs_snapshot().is_empty());
1315 }
1316
1317 #[tokio::test(flavor = "multi_thread")]
1320 async fn apply_updates_accepts_self_broadcast() {
1321 let (committee, names) = test_committee(4);
1322 let local = TransactionDenyConfigBuilder::new().build();
1323 let (manager, _dir) =
1324 manager_with(names[0], local, sync_with_prelisted(&committee), committee);
1325
1326 manager.apply_updates(
1327 names[0],
1328 vec![make_msg(names[0], 1, Some(rules_with_objects(&[1])))],
1329 );
1330 let snapshot = manager.peer_configs_snapshot();
1331 assert!(
1332 snapshot.contains_key(&names[0]),
1333 "self-broadcast should be accepted into peer_configs",
1334 );
1335 let status = manager.evaluate_status();
1337 assert_eq!(status.prelisted[0].voters, vec![names[0]]);
1338 }
1339
1340 #[tokio::test(flavor = "multi_thread")]
1341 async fn apply_updates_ignores_stale_generations() {
1342 let (committee, names) = test_committee(4);
1343 let local = TransactionDenyConfigBuilder::new().build();
1344 let (manager, _dir) =
1345 manager_with(names[0], local, sync_with_prelisted(&committee), committee);
1346
1347 manager.apply_updates(
1348 names[1],
1349 vec![make_msg(names[1], 100, Some(rules_with_objects(&[1])))],
1350 );
1351 manager.apply_updates(
1353 names[1],
1354 vec![make_msg(names[1], 50, Some(rules_with_objects(&[2])))],
1355 );
1356 let snapshot = manager.peer_configs_snapshot();
1357 assert_eq!(snapshot.get(&names[1]).unwrap().generation(), 100);
1358 }
1359
1360 #[tokio::test(flavor = "multi_thread")]
1364 async fn apply_updates_ignores_far_future_generations() {
1365 let (committee, names) = test_committee(4);
1366 let local = TransactionDenyConfigBuilder::new().build();
1367 let (manager, _dir) =
1368 manager_with(names[0], local, sync_with_prelisted(&committee), committee);
1369
1370 let now_ms = AuthorityState::unixtime_now_ms();
1371 let far_future =
1372 now_ms + SharedTransactionDenyConfig::MAX_GENERATION_FUTURE_DRIFT_MS + 600_000;
1373 manager.apply_updates(
1374 names[1],
1375 vec![make_msg(
1376 names[1],
1377 far_future,
1378 Some(rules_with_objects(&[1])),
1379 )],
1380 );
1381 assert!(manager.peer_configs_snapshot().is_empty());
1382
1383 manager.apply_updates(
1385 names[1],
1386 vec![make_msg(names[1], now_ms, Some(rules_with_objects(&[2])))],
1387 );
1388 assert_eq!(
1389 manager
1390 .peer_configs_snapshot()
1391 .get(&names[1])
1392 .unwrap()
1393 .generation(),
1394 now_ms
1395 );
1396 }
1397
1398 #[tokio::test(flavor = "multi_thread")]
1403 async fn new_seeds_committee_peers_only() {
1404 let (committee, names) = test_committee(4);
1405 let (dir, perpetual) = open_perpetual();
1406 for authority in [names[0], names[1], fake_name(200)] {
1408 perpetual
1409 .shared_transaction_deny_configs
1410 .insert(
1411 &authority,
1412 &make_msg(authority, 5, Some(rules_with_objects(&[1]))),
1413 )
1414 .unwrap();
1415 }
1416
1417 let registry = Registry::new();
1418 let manager = TransactionDenyConfigManager::new(
1419 names[0],
1420 TransactionDenyConfigBuilder::new().build(),
1421 sync_with_prelisted(&committee),
1422 committee,
1423 perpetual,
1424 ®istry,
1425 )
1426 .unwrap();
1427
1428 let snapshot = manager.peer_configs_snapshot();
1429 assert!(
1430 snapshot.contains_key(&names[1]),
1431 "committee peer should be seeded",
1432 );
1433 assert!(
1434 !snapshot.contains_key(&names[0]),
1435 "own persisted broadcast must not be seeded",
1436 );
1437 assert!(
1438 !snapshot.contains_key(&fake_name(200)),
1439 "non-committee authority must not be seeded",
1440 );
1441 drop(dir);
1442 }
1443
1444 #[tokio::test(flavor = "multi_thread")]
1445 async fn update_for_committee_prunes_departed_peer() {
1446 let (committee, names) = test_committee(4);
1447 let local = TransactionDenyConfigBuilder::new().build();
1448 let (manager, _dir) = manager_with(
1449 names[0],
1450 local,
1451 sync_with_prelisted(&committee),
1452 committee.clone(),
1453 );
1454
1455 manager.apply_updates(
1456 names[1],
1457 vec![make_msg(names[1], 1, Some(rules_with_objects(&[1])))],
1458 );
1459 assert!(manager.peer_configs_snapshot().contains_key(&names[1]));
1460
1461 let remaining: BTreeMap<AuthorityName, StakeUnit> = committee
1463 .members()
1464 .filter(|&&(name, _)| name != names[1])
1465 .map(|(name, _)| (*name, 1))
1466 .collect();
1467 let new_committee = Arc::new(Committee::new_for_testing_with_normalized_voting_power(
1468 committee.epoch(),
1469 remaining,
1470 ));
1471 manager.update_for_committee(new_committee).unwrap();
1472 assert!(!manager.peer_configs_snapshot().contains_key(&names[1]));
1473 }
1474
1475 #[tokio::test(flavor = "multi_thread")]
1476 async fn withdrawal_clears_contribution_but_keeps_row() {
1477 let (committee, names) = test_committee(4);
1478 let local = TransactionDenyConfigBuilder::new().build();
1479 let sync = PeerDenySyncConfig {
1480 default_buckets: vec![default_bucket(
1481 "objs",
1482 &[DenyElementKind::Object],
1483 ValidatorEligibility::default(),
1484 10,
1485 )],
1486 ..Default::default()
1487 };
1488 let (manager, _dir) = manager_with(names[0], local, sync, committee);
1489
1490 manager.apply_updates(
1491 names[1],
1492 vec![make_msg(names[1], 10, Some(rules_with_objects(&[1])))],
1493 );
1494 assert!(
1495 manager
1496 .effective_config()
1497 .load()
1498 .get_object_deny_set()
1499 .contains(&ObjectID::from_single_byte(1))
1500 );
1501
1502 manager.apply_updates(names[1], vec![make_msg(names[1], 11, None)]);
1503 assert!(
1504 manager
1505 .effective_config()
1506 .load()
1507 .get_object_deny_set()
1508 .is_empty()
1509 );
1510 let snapshot = manager.peer_configs_snapshot();
1512 let entry = snapshot.get(&names[1]).unwrap();
1513 assert_eq!(entry.generation(), 11);
1514 assert!(entry.rules().is_none());
1515 }
1516
1517 #[tokio::test(flavor = "multi_thread")]
1518 async fn allocate_next_broadcast_generation_is_monotonic() {
1519 let (committee, names) = test_committee(4);
1520 let local = TransactionDenyConfigBuilder::new().build();
1521 let (manager, _dir) =
1522 manager_with(names[0], local, PeerDenySyncConfig::default(), committee);
1523
1524 let g1 = manager.allocate_next_broadcast_generation().unwrap();
1525 let g2 = manager.allocate_next_broadcast_generation().unwrap();
1526 let g3 = manager.allocate_next_broadcast_generation().unwrap();
1527 assert!(g2 > g1);
1528 assert!(g3 > g2);
1529 }
1530
1531 #[tokio::test(flavor = "multi_thread")]
1535 async fn allocate_next_broadcast_generation_resets_far_future_counter() {
1536 let (committee, names) = test_committee(4);
1537 let local = TransactionDenyConfigBuilder::new().build();
1538 let (manager, _dir) =
1539 manager_with(names[0], local, PeerDenySyncConfig::default(), committee);
1540
1541 let poisoned = u64::MAX - 1;
1542 manager
1543 .perpetual
1544 .last_broadcast_deny_generation
1545 .insert(&(), &poisoned)
1546 .unwrap();
1547
1548 let generation = manager.allocate_next_broadcast_generation().unwrap();
1549 let now_ms = AuthorityState::unixtime_now_ms();
1550 assert!(
1551 generation <= now_ms + SharedTransactionDenyConfig::MAX_GENERATION_FUTURE_DRIFT_MS,
1552 "generation {generation} not reset below the future-drift bound",
1553 );
1554 assert!(manager.allocate_next_broadcast_generation().unwrap() > generation);
1556 }
1557
1558 #[tokio::test(flavor = "multi_thread")]
1561 async fn build_share_message_enforces_share_limit() {
1562 let (committee, names) = test_committee(4);
1563 let (manager, _dir) = manager_with(
1564 names[0],
1565 TransactionDenyConfigBuilder::new().build(),
1566 PeerDenySyncConfig::default(),
1567 committee,
1568 );
1569
1570 assert!(
1572 manager
1573 .build_share_message(Some(rules_with_objects(&[1])))
1574 .is_ok()
1575 );
1576 assert!(manager.build_share_message(None).is_ok());
1577
1578 let oversized = TransactionDenyRules {
1581 zklogin_disabled_providers: std::iter::once(
1582 "x".repeat(TransactionDenyRules::MAX_ZKLOGIN_PROVIDER_LENGTH + 1),
1583 )
1584 .collect(),
1585 ..Default::default()
1586 };
1587 assert!(manager.build_share_message(Some(oversized)).is_err());
1588 }
1589
1590 #[tokio::test(flavor = "multi_thread")]
1591 async fn invalid_sync_config_is_rejected() {
1592 let (committee, names) = test_committee(4);
1593 let (_dir, perpetual) = open_perpetual();
1594 let registry = Registry::new();
1595 let bad = PeerDenySyncConfig {
1597 rulesets: vec![prelisted(
1598 "c",
1599 rules_with_objects(&[1]),
1600 ValidatorEligibility::default(),
1601 150,
1602 )],
1603 ..Default::default()
1604 };
1605 assert!(
1606 TransactionDenyConfigManager::new(
1607 names[0],
1608 TransactionDenyConfigBuilder::new().build(),
1609 bad,
1610 committee,
1611 perpetual,
1612 ®istry,
1613 )
1614 .is_err()
1615 );
1616 }
1617}