1use std::num::NonZeroU32;
11use std::sync::Arc;
12
13use jiff::Timestamp;
14
15use crate::budget::BudgetView;
16use crate::cost_table::CostTable;
17use crate::deny::DenyReason;
18use crate::ids::{AccountId, Generation, KeyId, PolicyRevision};
19use crate::units::CostUnits;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum AccountStatus {
25 Active,
26 Suspended,
28 Closed,
31}
32
33impl AccountStatus {
34 #[must_use]
45 pub const fn as_str(self) -> &'static str {
46 match self {
47 AccountStatus::Active => "Active",
48 AccountStatus::Suspended => "Suspended",
49 AccountStatus::Closed => "Closed",
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub enum CapacityClass {
78 #[default]
81 Assured,
82 BestEffort,
85}
86
87impl CapacityClass {
88 #[must_use]
99 pub const fn as_str(self) -> &'static str {
100 match self {
101 CapacityClass::Assured => "Assured",
102 CapacityClass::BestEffort => "BestEffort",
103 }
104 }
105
106 #[must_use]
112 pub const fn may_use_assured_reserve(self) -> bool {
113 matches!(self, CapacityClass::Assured)
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub enum EnforcementMode {
135 #[default]
139 Strict,
140 Elastic { overage_cap: CostUnits },
148}
149
150impl EnforcementMode {
151 #[must_use]
162 pub const fn as_str(self) -> &'static str {
163 match self {
164 EnforcementMode::Strict => "Strict",
165 EnforcementMode::Elastic { .. } => "Elastic",
166 }
167 }
168
169 #[must_use]
171 pub const fn overage_cap(self) -> Option<CostUnits> {
172 match self {
173 EnforcementMode::Strict => None,
174 EnforcementMode::Elastic { overage_cap } => Some(overage_cap),
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183#[cfg_attr(feature = "serde", serde(transparent))]
184pub struct PermissionBits(pub u64);
185
186impl PermissionBits {
187 pub const NONE: PermissionBits = PermissionBits(0);
188 pub const ALL: PermissionBits = PermissionBits(u64::MAX);
189
190 #[inline]
192 #[must_use]
193 pub const fn bit(bit: u32) -> PermissionBits {
194 PermissionBits(1u64 << bit)
195 }
196
197 #[inline]
198 #[must_use]
199 pub const fn union(self, other: PermissionBits) -> PermissionBits {
200 PermissionBits(self.0 | other.0)
201 }
202
203 #[inline]
205 #[must_use]
206 pub const fn contains_all(self, required: PermissionBits) -> bool {
207 self.0 & required.0 == required.0
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub struct WeightedRateLimit {
214 units_per_second: u64,
215 burst_units: u64,
216}
217
218impl WeightedRateLimit {
219 #[must_use]
220 pub const fn units_per_second(self) -> u64 {
221 self.units_per_second
222 }
223
224 #[must_use]
225 pub const fn burst_units(self) -> u64 {
226 self.burst_units
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub struct RequestRateLimit {
233 requests_per_second: NonZeroU32,
234 burst_requests: NonZeroU32,
235}
236
237impl RequestRateLimit {
238 #[must_use]
239 pub const fn requests_per_second(self) -> NonZeroU32 {
240 self.requests_per_second
241 }
242
243 #[must_use]
244 pub const fn burst_requests(self) -> NonZeroU32 {
245 self.burst_requests
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub struct AccountRatePolicy {
257 weighted_rate: Option<WeightedRateLimit>,
258 legacy_weighted_rate: WeightedRateLimit,
259 request_rate: Option<RequestRateLimit>,
260}
261
262impl AccountRatePolicy {
263 #[must_use]
264 pub const fn weighted_rate(self) -> Option<WeightedRateLimit> {
265 self.weighted_rate
266 }
267
268 #[must_use]
269 pub const fn legacy_weighted_rate(self) -> WeightedRateLimit {
270 self.legacy_weighted_rate
271 }
272
273 #[must_use]
274 pub const fn request_rate(self) -> Option<RequestRateLimit> {
275 self.request_rate
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281pub enum ResolvedLimitsError {
282 RequestRatePairIncomplete,
283 PrincipalConcurrencyWithoutAccount,
284 PrincipalConcurrencyExceedsAccount {
285 principal: NonZeroU32,
286 account: NonZeroU32,
287 },
288}
289
290impl std::fmt::Display for ResolvedLimitsError {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 match self {
293 ResolvedLimitsError::RequestRatePairIncomplete => {
294 f.write_str("request_rate_per_second and request_burst must be supplied together")
295 }
296 ResolvedLimitsError::PrincipalConcurrencyWithoutAccount => {
297 f.write_str("principal_max_concurrent_requests requires max_concurrent_requests")
298 }
299 ResolvedLimitsError::PrincipalConcurrencyExceedsAccount { principal, account } => {
300 write!(
301 f,
302 "principal concurrency ceiling {principal} exceeds account ceiling {account}"
303 )
304 }
305 }
306 }
307}
308
309impl std::error::Error for ResolvedLimitsError {}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
320#[cfg_attr(
321 feature = "serde",
322 serde(try_from = "WireResolvedLimits", into = "WireResolvedLimits")
323)]
324#[non_exhaustive]
325pub struct ResolvedLimits {
326 max_items_per_request: u64,
327 weighted_rate: Option<WeightedRateLimit>,
328 legacy_weighted_rate: WeightedRateLimit,
329 request_rate: Option<RequestRateLimit>,
330 max_concurrent_requests: Option<NonZeroU32>,
331 principal_max_concurrent_requests: Option<NonZeroU32>,
332}
333
334impl ResolvedLimits {
335 const COMPATIBILITY_FALLBACK: WeightedRateLimit = WeightedRateLimit {
336 units_per_second: u32::MAX as u64,
337 burst_units: u32::MAX as u64,
338 };
339
340 #[must_use]
344 pub const fn new(max_items_per_request: u64) -> Self {
345 Self {
346 max_items_per_request,
347 weighted_rate: None,
348 legacy_weighted_rate: Self::COMPATIBILITY_FALLBACK,
349 request_rate: None,
350 max_concurrent_requests: None,
351 principal_max_concurrent_requests: None,
352 }
353 }
354
355 #[must_use]
356 pub const fn with_weighted_rate(mut self, units_per_second: u64, burst_units: u64) -> Self {
357 let rate = WeightedRateLimit {
358 units_per_second,
359 burst_units,
360 };
361 self.weighted_rate = Some(rate);
362 self.legacy_weighted_rate = rate;
363 self
364 }
365
366 #[must_use]
369 pub const fn with_weighted_rate_compatibility_fallback(
370 mut self,
371 units_per_second: u64,
372 burst_units: u64,
373 ) -> Self {
374 self.weighted_rate = None;
375 self.legacy_weighted_rate = WeightedRateLimit {
376 units_per_second,
377 burst_units,
378 };
379 self
380 }
381
382 #[must_use]
383 pub const fn with_request_rate(
384 mut self,
385 requests_per_second: NonZeroU32,
386 burst_requests: NonZeroU32,
387 ) -> Self {
388 self.request_rate = Some(RequestRateLimit {
389 requests_per_second,
390 burst_requests,
391 });
392 self
393 }
394
395 pub fn with_concurrency(
396 mut self,
397 max_concurrent_requests: NonZeroU32,
398 principal_max_concurrent_requests: Option<NonZeroU32>,
399 ) -> Result<Self, ResolvedLimitsError> {
400 if let Some(principal) = principal_max_concurrent_requests
401 && principal > max_concurrent_requests
402 {
403 return Err(ResolvedLimitsError::PrincipalConcurrencyExceedsAccount {
404 principal,
405 account: max_concurrent_requests,
406 });
407 }
408 self.max_concurrent_requests = Some(max_concurrent_requests);
409 self.principal_max_concurrent_requests = principal_max_concurrent_requests;
410 Ok(self)
411 }
412
413 #[must_use]
414 pub const fn max_items_per_request(self) -> u64 {
415 self.max_items_per_request
416 }
417
418 #[must_use]
419 pub const fn weighted_rate(self) -> Option<WeightedRateLimit> {
420 self.weighted_rate
421 }
422
423 #[must_use]
426 pub const fn legacy_weighted_rate(self) -> WeightedRateLimit {
427 self.legacy_weighted_rate
428 }
429
430 #[must_use]
431 pub const fn request_rate(self) -> Option<RequestRateLimit> {
432 self.request_rate
433 }
434
435 #[must_use]
438 pub const fn account_rate_policy(self) -> AccountRatePolicy {
439 AccountRatePolicy {
440 weighted_rate: self.weighted_rate,
441 legacy_weighted_rate: self.legacy_weighted_rate,
442 request_rate: self.request_rate,
443 }
444 }
445
446 #[must_use]
447 pub const fn max_concurrent_requests(self) -> Option<NonZeroU32> {
448 self.max_concurrent_requests
449 }
450
451 #[must_use]
452 pub const fn principal_max_concurrent_requests(self) -> Option<NonZeroU32> {
453 self.principal_max_concurrent_requests
454 }
455}
456
457#[cfg(feature = "serde")]
458#[derive(serde::Serialize, serde::Deserialize)]
459struct WireResolvedLimits {
460 max_items_per_request: u64,
461 rate_units_per_second: u64,
462 rate_burst_units: u64,
463 #[serde(default = "default_true", skip_serializing_if = "is_true")]
464 weighted_rate_enabled: bool,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 request_rate_per_second: Option<NonZeroU32>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 request_burst: Option<NonZeroU32>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
470 max_concurrent_requests: Option<NonZeroU32>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 principal_max_concurrent_requests: Option<NonZeroU32>,
473}
474
475#[cfg(feature = "serde")]
476const fn default_true() -> bool {
477 true
478}
479
480#[cfg(feature = "serde")]
481const fn is_true(value: &bool) -> bool {
482 *value
483}
484
485#[cfg(feature = "serde")]
486impl TryFrom<WireResolvedLimits> for ResolvedLimits {
487 type Error = ResolvedLimitsError;
488
489 fn try_from(wire: WireResolvedLimits) -> Result<Self, Self::Error> {
490 let legacy_weighted_rate = WeightedRateLimit {
491 units_per_second: wire.rate_units_per_second,
492 burst_units: wire.rate_burst_units,
493 };
494 let mut limits = Self {
495 max_items_per_request: wire.max_items_per_request,
496 weighted_rate: wire.weighted_rate_enabled.then_some(legacy_weighted_rate),
497 legacy_weighted_rate,
498 request_rate: None,
499 max_concurrent_requests: None,
500 principal_max_concurrent_requests: None,
501 };
502 limits = match (wire.request_rate_per_second, wire.request_burst) {
503 (None, None) => limits,
504 (Some(requests_per_second), Some(burst_requests)) => {
505 limits.with_request_rate(requests_per_second, burst_requests)
506 }
507 _ => return Err(ResolvedLimitsError::RequestRatePairIncomplete),
508 };
509 match wire.max_concurrent_requests {
510 Some(account) => {
511 limits.with_concurrency(account, wire.principal_max_concurrent_requests)
512 }
513 None if wire.principal_max_concurrent_requests.is_some() => {
514 Err(ResolvedLimitsError::PrincipalConcurrencyWithoutAccount)
515 }
516 None => Ok(limits),
517 }
518 }
519}
520
521#[cfg(feature = "serde")]
522impl From<ResolvedLimits> for WireResolvedLimits {
523 fn from(limits: ResolvedLimits) -> Self {
524 Self {
525 max_items_per_request: limits.max_items_per_request,
526 rate_units_per_second: limits.legacy_weighted_rate.units_per_second,
527 rate_burst_units: limits.legacy_weighted_rate.burst_units,
528 weighted_rate_enabled: limits.weighted_rate.is_some(),
529 request_rate_per_second: limits.request_rate.map(|rate| rate.requests_per_second),
530 request_burst: limits.request_rate.map(|rate| rate.burst_requests),
531 max_concurrent_requests: limits.max_concurrent_requests,
532 principal_max_concurrent_requests: limits.principal_max_concurrent_requests,
533 }
534 }
535}
536
537#[derive(Debug, Clone)]
552#[repr(C, align(128))]
553#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
554#[non_exhaustive]
555pub struct AccountSnapshot {
556 pub status: AccountStatus,
557 #[cfg_attr(feature = "serde", serde(default))]
571 pub capacity_class: CapacityClass,
572 pub permissions: PermissionBits,
573 pub valid_until: Timestamp,
576 #[cfg_attr(feature = "serde", serde(default))]
586 pub enforcement_mode: EnforcementMode,
587 pub generation: Generation,
589 pub cost_table: Arc<CostTable>,
590 pub account_id: AccountId,
591 pub key_id: Option<KeyId>,
593 pub limits: ResolvedLimits,
594 #[cfg_attr(feature = "serde", serde(default))]
615 pub budget: Option<BudgetView>,
616 #[cfg_attr(feature = "serde", serde(default))]
629 pub policy_revision: PolicyRevision,
630}
631
632#[derive(Debug)]
634pub struct AccountSnapshotBuilder {
635 account_id: AccountId,
636 key_id: Option<KeyId>,
637 generation: Generation,
638 status: AccountStatus,
639 capacity_class: CapacityClass,
640 enforcement_mode: EnforcementMode,
641 valid_until: Timestamp,
642 permissions: PermissionBits,
643 limits: ResolvedLimits,
644 cost_table: Arc<CostTable>,
645 policy_revision: PolicyRevision,
646}
647
648impl AccountSnapshotBuilder {
649 #[must_use]
650 pub const fn key_id(mut self, key_id: KeyId) -> Self {
651 self.key_id = Some(key_id);
652 self
653 }
654
655 #[must_use]
656 pub const fn enforcement_mode(mut self, enforcement_mode: EnforcementMode) -> Self {
657 self.enforcement_mode = enforcement_mode;
658 self
659 }
660
661 #[must_use]
668 pub const fn capacity_class(mut self, capacity_class: CapacityClass) -> Self {
669 self.capacity_class = capacity_class;
670 self
671 }
672
673 #[must_use]
680 pub const fn policy_revision(mut self, policy_revision: PolicyRevision) -> Self {
681 self.policy_revision = policy_revision;
682 self
683 }
684
685 #[must_use]
686 pub fn build(self) -> AccountSnapshot {
687 AccountSnapshot {
688 account_id: self.account_id,
689 key_id: self.key_id,
690 generation: self.generation,
691 status: self.status,
692 capacity_class: self.capacity_class,
693 enforcement_mode: self.enforcement_mode,
694 valid_until: self.valid_until,
695 permissions: self.permissions,
696 limits: self.limits,
697 cost_table: self.cost_table,
698 budget: None,
701 policy_revision: self.policy_revision,
702 }
703 }
704}
705
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
712pub enum SnapshotValidationError {
713 WeightedRateOutsideGovernorDomain {
717 units_per_second: u64,
718 burst_units: u64,
719 },
720 QuoteOverflow {
722 operation_index: usize,
723 max_items: u64,
724 },
725 QuoteExceedsBurst {
727 operation_index: usize,
728 max_quote: CostUnits,
729 burst_units: CostUnits,
730 },
731 OverageCapBelowMaxQuote {
740 operation_index: usize,
741 max_quote: CostUnits,
742 overage_cap: CostUnits,
743 },
744}
745
746impl std::fmt::Display for SnapshotValidationError {
747 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
748 match self {
749 SnapshotValidationError::WeightedRateOutsideGovernorDomain {
750 units_per_second,
751 burst_units,
752 } => write!(
753 f,
754 "weighted rate ({units_per_second}/s, burst {burst_units}) must fit governor's non-zero u32 domain"
755 ),
756 SnapshotValidationError::QuoteOverflow {
757 operation_index,
758 max_items,
759 } => write!(
760 f,
761 "operation {operation_index} cost overflows at the batch cap of {max_items} items"
762 ),
763 SnapshotValidationError::QuoteExceedsBurst {
764 operation_index,
765 max_quote,
766 burst_units,
767 } => write!(
768 f,
769 "operation {operation_index} can quote {max_quote} units, exceeding the burst of {burst_units} units"
770 ),
771 SnapshotValidationError::OverageCapBelowMaxQuote {
772 operation_index,
773 max_quote,
774 overage_cap,
775 } => write!(
776 f,
777 "operation {operation_index} can quote {max_quote} units, exceeding the overage cap of {overage_cap} units"
778 ),
779 }
780 }
781}
782
783impl std::error::Error for SnapshotValidationError {}
784
785#[derive(Debug, Clone)]
792pub struct PublishableSnapshot {
793 snapshot: Arc<AccountSnapshot>,
794 maximum_quote: Option<CostUnits>,
795}
796
797impl PublishableSnapshot {
798 pub fn try_new(snapshot: Arc<AccountSnapshot>) -> Result<Self, SnapshotValidationError> {
799 let legacy_rate = snapshot.limits.legacy_weighted_rate();
800 if legacy_rate.units_per_second() == 0
801 || legacy_rate.units_per_second() > u64::from(u32::MAX)
802 || legacy_rate.burst_units() == 0
803 || legacy_rate.burst_units() > u64::from(u32::MAX)
804 {
805 return Err(SnapshotValidationError::WeightedRateOutsideGovernorDomain {
806 units_per_second: legacy_rate.units_per_second(),
807 burst_units: legacy_rate.burst_units(),
808 });
809 }
810 let Some((operation_index, maximum_weight)) = snapshot.cost_table.maximum_weight() else {
811 return Ok(PublishableSnapshot {
814 snapshot,
815 maximum_quote: None,
816 });
817 };
818 let max_quote = snapshot
819 .cost_table
820 .quote_weight(maximum_weight, snapshot.limits.max_items_per_request())
821 .map_err(|_| SnapshotValidationError::QuoteOverflow {
822 operation_index,
823 max_items: snapshot.limits.max_items_per_request(),
824 })?
825 .total;
826 let burst_units = CostUnits(legacy_rate.burst_units());
827 if max_quote > burst_units {
828 return Err(SnapshotValidationError::QuoteExceedsBurst {
829 operation_index,
830 max_quote,
831 burst_units,
832 });
833 }
834 if let Some(overage_cap) = snapshot.enforcement_mode.overage_cap()
835 && max_quote > overage_cap
836 {
837 return Err(SnapshotValidationError::OverageCapBelowMaxQuote {
838 operation_index,
839 max_quote,
840 overage_cap,
841 });
842 }
843 Ok(PublishableSnapshot {
844 snapshot,
845 maximum_quote: Some(max_quote),
846 })
847 }
848
849 #[must_use]
850 pub fn as_snapshot(&self) -> &AccountSnapshot {
851 &self.snapshot
852 }
853
854 #[must_use]
858 pub fn maximum_quote(&self) -> Option<CostUnits> {
859 self.maximum_quote
860 }
861
862 #[must_use]
863 pub fn into_inner(self) -> Arc<AccountSnapshot> {
864 self.snapshot
865 }
866
867 #[must_use]
903 pub fn with_budget(&self, budget: Option<BudgetView>) -> Self {
904 let mut snapshot = AccountSnapshot::clone(&self.snapshot);
905 snapshot.budget = budget;
906 PublishableSnapshot {
907 snapshot: Arc::new(snapshot),
908 maximum_quote: self.maximum_quote,
909 }
910 }
911
912 #[must_use]
913 pub fn restamped(&self, status: AccountStatus, generation: Generation) -> Self {
914 let mut snapshot = AccountSnapshot::clone(&self.snapshot);
915 snapshot.status = status;
916 snapshot.generation = generation;
917 PublishableSnapshot {
918 snapshot: Arc::new(snapshot),
919 maximum_quote: self.maximum_quote,
920 }
921 }
922
923 #[must_use]
935 pub fn reclassified(&self, class: CapacityClass, generation: Generation) -> Self {
936 let mut snapshot = AccountSnapshot::clone(&self.snapshot);
937 snapshot.capacity_class = class;
938 snapshot.generation = generation;
939 PublishableSnapshot {
940 snapshot: Arc::new(snapshot),
941 maximum_quote: self.maximum_quote,
942 }
943 }
944}
945
946impl std::ops::Deref for PublishableSnapshot {
947 type Target = AccountSnapshot;
948
949 fn deref(&self) -> &Self::Target {
950 self.as_snapshot()
951 }
952}
953
954impl AsRef<AccountSnapshot> for PublishableSnapshot {
955 fn as_ref(&self) -> &AccountSnapshot {
956 self.as_snapshot()
957 }
958}
959
960impl TryFrom<Arc<AccountSnapshot>> for PublishableSnapshot {
961 type Error = SnapshotValidationError;
962
963 fn try_from(snapshot: Arc<AccountSnapshot>) -> Result<Self, Self::Error> {
964 PublishableSnapshot::try_new(snapshot)
965 }
966}
967
968impl AccountSnapshot {
969 #[must_use]
975 pub fn builder(
976 account_id: AccountId,
977 generation: Generation,
978 status: AccountStatus,
979 valid_until: Timestamp,
980 permissions: PermissionBits,
981 limits: ResolvedLimits,
982 cost_table: Arc<CostTable>,
983 ) -> AccountSnapshotBuilder {
984 AccountSnapshotBuilder {
985 account_id,
986 key_id: None,
987 generation,
988 status,
989 capacity_class: CapacityClass::Assured,
990 enforcement_mode: EnforcementMode::Strict,
991 valid_until,
992 permissions,
993 limits,
994 cost_table,
995 policy_revision: PolicyRevision::UNSTATED,
996 }
997 }
998
999 #[inline]
1003 pub fn admit(&self, now: Timestamp, required: PermissionBits) -> Result<(), DenyReason> {
1004 match self.status {
1005 AccountStatus::Active => {}
1006 AccountStatus::Suspended => return Err(DenyReason::AccountSuspended),
1007 AccountStatus::Closed => return Err(DenyReason::AccountClosed),
1008 }
1009 if now >= self.valid_until {
1010 return Err(DenyReason::SnapshotExpired);
1011 }
1012 if !self.permissions.contains_all(required) {
1013 return Err(DenyReason::MissingPermission);
1014 }
1015 Ok(())
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022 use crate::units::CostUnits;
1023
1024 #[test]
1029 #[cfg(feature = "serde")]
1030 fn account_status_text_matches_its_serde_spelling() {
1031 for status in [
1032 AccountStatus::Active,
1033 AccountStatus::Suspended,
1034 AccountStatus::Closed,
1035 ] {
1036 assert_eq!(
1037 serde_json::to_value(status).expect("a unit variant serializes"),
1038 serde_json::Value::String(status.as_str().to_owned()),
1039 "{status:?} disagrees with its serde spelling"
1040 );
1041 }
1042 }
1043
1044 #[cfg(feature = "serde")]
1048 #[test]
1049 fn capacity_class_text_matches_its_serde_spelling() {
1050 for class in [CapacityClass::Assured, CapacityClass::BestEffort] {
1051 assert_eq!(
1052 serde_json::to_value(class).expect("a unit variant serializes"),
1053 serde_json::Value::String(class.as_str().to_owned()),
1054 "{class:?} disagrees with its serde spelling"
1055 );
1056 }
1057 }
1058
1059 #[test]
1064 fn the_default_class_is_assured() {
1065 assert_eq!(CapacityClass::default(), CapacityClass::Assured);
1066 assert!(CapacityClass::Assured.may_use_assured_reserve());
1067 assert!(!CapacityClass::BestEffort.may_use_assured_reserve());
1068 }
1069
1070 #[cfg(feature = "serde")]
1074 #[test]
1075 fn a_snapshot_without_a_capacity_class_key_decodes_as_assured() {
1076 let snapshot = AccountSnapshot::builder(
1077 AccountId(1),
1078 Generation(1),
1079 AccountStatus::Active,
1080 Timestamp::from_second(10_000).unwrap(),
1081 PermissionBits::ALL,
1082 ResolvedLimits::new(64).with_weighted_rate(1_000, 1_000),
1083 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1084 )
1085 .capacity_class(CapacityClass::BestEffort)
1086 .build();
1087
1088 let mut value = serde_json::to_value(&snapshot).expect("a snapshot serializes");
1089 assert_eq!(
1090 value["capacity_class"],
1091 serde_json::Value::String("BestEffort".to_owned()),
1092 "a stated class is on the wire in its canonical spelling"
1093 );
1094 assert!(
1095 value
1096 .as_object_mut()
1097 .expect("a snapshot is a JSON object")
1098 .remove("capacity_class")
1099 .is_some()
1100 );
1101
1102 let decoded: AccountSnapshot =
1103 serde_json::from_value(value).expect("an older payload still decodes");
1104 assert_eq!(decoded.capacity_class, CapacityClass::Assured);
1105 }
1106
1107 #[test]
1110 fn a_class_survives_a_status_restamp() {
1111 let snapshot = AccountSnapshot::builder(
1112 AccountId(1),
1113 Generation(1),
1114 AccountStatus::Active,
1115 Timestamp::from_second(10_000).unwrap(),
1116 PermissionBits::ALL,
1117 ResolvedLimits::new(64).with_weighted_rate(1_000, 1_000),
1118 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1119 );
1120 assert_eq!(
1121 snapshot.build().capacity_class,
1122 CapacityClass::Assured,
1123 "an unstated class is assured"
1124 );
1125
1126 let publishable = PublishableSnapshot::try_new(Arc::new(
1127 AccountSnapshot::builder(
1128 AccountId(1),
1129 Generation(1),
1130 AccountStatus::Active,
1131 Timestamp::from_second(10_000).unwrap(),
1132 PermissionBits::ALL,
1133 ResolvedLimits::new(64).with_weighted_rate(1_000, 1_000),
1134 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1135 )
1136 .capacity_class(CapacityClass::BestEffort)
1137 .build(),
1138 ))
1139 .expect("the fixture publishes");
1140
1141 let restamped = publishable.restamped(AccountStatus::Suspended, Generation(2));
1142 assert_eq!(
1143 restamped.as_snapshot().capacity_class,
1144 CapacityClass::BestEffort,
1145 "a status change must not silently reclassify the account"
1146 );
1147
1148 let reclassified = publishable.reclassified(CapacityClass::Assured, Generation(3));
1150 assert_eq!(
1151 reclassified.as_snapshot().capacity_class,
1152 CapacityClass::Assured
1153 );
1154 assert_eq!(reclassified.as_snapshot().status, AccountStatus::Active);
1155 assert_eq!(reclassified.as_snapshot().generation, Generation(3));
1156 assert!(
1157 PublishableSnapshot::try_new(Arc::new(AccountSnapshot::clone(
1158 reclassified.as_snapshot()
1159 )))
1160 .is_ok(),
1161 "reclassifying preserves the publication proof"
1162 );
1163 }
1164
1165 #[test]
1170 #[cfg(feature = "serde")]
1171 fn a_snapshot_without_a_budget_key_decodes_as_no_budget() {
1172 let snapshot = AccountSnapshot::builder(
1173 AccountId(1),
1174 Generation(1),
1175 AccountStatus::Active,
1176 Timestamp::from_second(10_000).unwrap(),
1177 PermissionBits::ALL,
1178 ResolvedLimits::new(64).with_weighted_rate(1_000, 1_000),
1179 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1180 )
1181 .build();
1182
1183 let mut value = serde_json::to_value(&snapshot).expect("a snapshot serializes");
1184 let removed = value
1185 .as_object_mut()
1186 .expect("a snapshot is a JSON object")
1187 .remove("budget");
1188 assert!(removed.is_some(), "the field is on the wire when present");
1189
1190 let decoded: AccountSnapshot =
1191 serde_json::from_value(value).expect("an older payload still decodes");
1192 assert_eq!(decoded.budget, None);
1193 }
1194
1195 #[test]
1200 #[cfg(feature = "serde")]
1201 fn a_snapshot_without_a_revision_key_decodes_as_unstated() {
1202 let snapshot = AccountSnapshot::builder(
1203 AccountId(1),
1204 Generation(1),
1205 AccountStatus::Active,
1206 Timestamp::from_second(10_000).unwrap(),
1207 PermissionBits::ALL,
1208 ResolvedLimits::new(64).with_weighted_rate(1_000, 1_000),
1209 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1210 )
1211 .policy_revision(PolicyRevision([0x5a; 32]))
1212 .build();
1213
1214 let mut value = serde_json::to_value(&snapshot).expect("a snapshot serializes");
1215 assert_eq!(
1216 value["policy_revision"],
1217 serde_json::Value::String("5a".repeat(32)),
1218 "a stated revision is on the wire in canonical form"
1219 );
1220 let removed = value
1221 .as_object_mut()
1222 .expect("a snapshot is a JSON object")
1223 .remove("policy_revision");
1224 assert!(removed.is_some(), "the field is on the wire when present");
1225
1226 let decoded: AccountSnapshot =
1227 serde_json::from_value(value).expect("an older payload still decodes");
1228 assert_eq!(decoded.policy_revision, PolicyRevision::UNSTATED);
1229 assert!(decoded.policy_revision.is_unstated());
1230 }
1231
1232 #[test]
1234 fn a_builder_without_a_revision_states_none() {
1235 let snapshot = AccountSnapshot::builder(
1236 AccountId(1),
1237 Generation(1),
1238 AccountStatus::Active,
1239 Timestamp::from_second(10_000).unwrap(),
1240 PermissionBits::ALL,
1241 ResolvedLimits::new(64),
1242 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1243 )
1244 .build();
1245 assert_eq!(snapshot.policy_revision, PolicyRevision::UNSTATED);
1246 }
1247
1248 #[test]
1252 fn with_budget_replaces_and_can_clear() {
1253 let publishable = PublishableSnapshot::try_new(Arc::new(
1254 AccountSnapshot::builder(
1255 AccountId(1),
1256 Generation(1),
1257 AccountStatus::Active,
1258 Timestamp::from_second(10_000).unwrap(),
1259 PermissionBits::ALL,
1260 ResolvedLimits::new(64).with_weighted_rate(1_000, 1_000),
1261 Arc::new(CostTable::builder(CostUnits(1), CostUnits(1)).build()),
1262 )
1263 .build(),
1264 ))
1265 .expect("the test snapshot is publishable");
1266 assert_eq!(publishable.budget, None, "a builder cannot set one");
1267
1268 let view = BudgetView {
1269 balance_at_publish: CostUnits(500),
1270 period_end: None,
1271 };
1272 let stamped = publishable.with_budget(Some(view));
1273 assert_eq!(stamped.budget, Some(view));
1274 assert_eq!(
1275 stamped.maximum_quote(),
1276 publishable.maximum_quote(),
1277 "the publication proof carries over: validation does not read the budget"
1278 );
1279 assert_eq!(stamped.with_budget(None).budget, None);
1280 }
1281
1282 #[test]
1286 #[cfg(feature = "serde")]
1287 fn enforcement_mode_text_matches_its_serde_tag() {
1288 for mode in [
1289 EnforcementMode::Strict,
1290 EnforcementMode::Elastic {
1291 overage_cap: CostUnits(1_000),
1292 },
1293 ] {
1294 let value = serde_json::to_value(mode).expect("a mode serializes");
1295 let tag = match &value {
1296 serde_json::Value::String(tag) => tag.clone(),
1297 serde_json::Value::Object(map) => {
1298 map.keys().next().expect("one variant key").clone()
1299 }
1300 other => panic!("unexpected encoding {other}"),
1301 };
1302 assert_eq!(tag, mode.as_str(), "{mode:?} disagrees with its serde tag");
1303 }
1304 }
1305
1306 #[test]
1310 fn the_cap_does_not_affect_the_mode_label() {
1311 assert_eq!(
1312 EnforcementMode::Elastic {
1313 overage_cap: CostUnits(1)
1314 }
1315 .as_str(),
1316 EnforcementMode::Elastic {
1317 overage_cap: CostUnits(u64::MAX)
1318 }
1319 .as_str()
1320 );
1321 }
1322
1323 #[test]
1327 fn the_default_mode_is_strict() {
1328 assert_eq!(EnforcementMode::default(), EnforcementMode::Strict);
1329 assert_eq!(EnforcementMode::Strict.overage_cap(), None);
1330 }
1331
1332 #[test]
1333 fn concurrency_construction_rejects_a_principal_ceiling_above_the_account() {
1334 let account = NonZeroU32::new(4).unwrap();
1335 assert!(
1336 ResolvedLimits::new(64)
1337 .with_concurrency(account, Some(account))
1338 .is_ok(),
1339 "a principal ceiling equal to the account ceiling is a valid narrowing"
1340 );
1341 let principal = NonZeroU32::new(5).unwrap();
1342 assert_eq!(
1343 ResolvedLimits::new(64)
1344 .with_concurrency(account, Some(principal))
1345 .unwrap_err(),
1346 ResolvedLimitsError::PrincipalConcurrencyExceedsAccount { principal, account }
1347 );
1348 }
1349
1350 #[test]
1351 fn configured_dimensions_are_visible_through_the_public_accessors() {
1352 let requests_per_second = NonZeroU32::new(10).unwrap();
1353 let burst_requests = NonZeroU32::new(20).unwrap();
1354 let account = NonZeroU32::new(4).unwrap();
1355 let principal = NonZeroU32::new(2).unwrap();
1356 let limits = ResolvedLimits::new(64)
1357 .with_request_rate(requests_per_second, burst_requests)
1358 .with_concurrency(account, Some(principal))
1359 .unwrap();
1360
1361 let request_rate = limits.request_rate().expect("request rate is configured");
1362 assert_eq!(request_rate.requests_per_second(), requests_per_second);
1363 assert_eq!(request_rate.burst_requests(), burst_requests);
1364 assert_eq!(limits.max_concurrent_requests(), Some(account));
1365 assert_eq!(limits.principal_max_concurrent_requests(), Some(principal));
1366 }
1367
1368 #[test]
1369 fn account_rate_policy_extracts_exactly_the_rate_dimensions() {
1370 let account = ResolvedLimits::new(999)
1371 .with_weighted_rate(700, 800)
1372 .with_request_rate(NonZeroU32::new(9).unwrap(), NonZeroU32::new(10).unwrap());
1373 let principal = ResolvedLimits::new(64)
1374 .with_weighted_rate_compatibility_fallback(11, 12)
1375 .with_concurrency(
1376 NonZeroU32::new(4).unwrap(),
1377 Some(NonZeroU32::new(2).unwrap()),
1378 )
1379 .unwrap();
1380
1381 let policy = account.account_rate_policy();
1382 assert_eq!(policy.weighted_rate(), account.weighted_rate());
1383 assert_eq!(
1384 policy.legacy_weighted_rate(),
1385 account.legacy_weighted_rate()
1386 );
1387 assert_eq!(policy.request_rate(), account.request_rate());
1388
1389 assert_eq!(principal.max_items_per_request(), 64);
1390 assert_eq!(principal.weighted_rate(), None);
1391 assert_eq!(principal.legacy_weighted_rate().units_per_second(), 11);
1392 assert_eq!(principal.legacy_weighted_rate().burst_units(), 12);
1393 assert_eq!(principal.request_rate(), None);
1394 assert_eq!(principal.max_concurrent_requests(), NonZeroU32::new(4));
1395 assert_eq!(
1396 principal.principal_max_concurrent_requests(),
1397 NonZeroU32::new(2)
1398 );
1399 }
1400
1401 #[test]
1402 #[cfg(feature = "serde")]
1403 fn pre_staged_limits_decode_as_weighted_and_round_trip_canonically() {
1404 let old = serde_json::json!({
1405 "max_items_per_request": 64,
1406 "rate_units_per_second": 1_000,
1407 "rate_burst_units": 2_000
1408 });
1409 let limits: ResolvedLimits = serde_json::from_value(old.clone()).unwrap();
1410 assert_eq!(
1411 limits.weighted_rate(),
1412 Some(WeightedRateLimit {
1413 units_per_second: 1_000,
1414 burst_units: 2_000,
1415 })
1416 );
1417 assert_eq!(serde_json::to_value(limits).unwrap(), old);
1418 }
1419
1420 #[test]
1421 #[cfg(feature = "serde")]
1422 fn disabled_weighted_rate_preserves_the_legacy_fallback() {
1423 let wire = serde_json::json!({
1424 "max_items_per_request": 64,
1425 "rate_units_per_second": 800,
1426 "rate_burst_units": 1_600,
1427 "weighted_rate_enabled": false
1428 });
1429 let limits: ResolvedLimits = serde_json::from_value(wire.clone()).unwrap();
1430 assert_eq!(limits.weighted_rate(), None);
1431 assert_eq!(limits.legacy_weighted_rate().units_per_second(), 800);
1432 assert_eq!(limits.legacy_weighted_rate().burst_units(), 1_600);
1433 assert_eq!(serde_json::to_value(limits).unwrap(), wire);
1434 }
1435
1436 #[test]
1437 fn disabled_weighted_rate_still_validates_the_rollback_burst() {
1438 let mut snapshot = (*priced_snapshot(50, 50, &[(0, 1)], 64, 114)).clone();
1439 snapshot.limits =
1440 ResolvedLimits::new(64).with_weighted_rate_compatibility_fallback(1_000, 113);
1441 assert_eq!(
1442 PublishableSnapshot::try_new(Arc::new(snapshot)).unwrap_err(),
1443 SnapshotValidationError::QuoteExceedsBurst {
1444 operation_index: 0,
1445 max_quote: CostUnits(114),
1446 burst_units: CostUnits(113),
1447 }
1448 );
1449 }
1450
1451 #[test]
1452 #[cfg(feature = "serde")]
1453 fn wire_rejects_partial_rate_pairs_and_widening_principal_limits() {
1454 let partial = serde_json::json!({
1455 "max_items_per_request": 64,
1456 "rate_units_per_second": 1_000,
1457 "rate_burst_units": 2_000,
1458 "request_rate_per_second": 10
1459 });
1460 assert!(serde_json::from_value::<ResolvedLimits>(partial).is_err());
1461
1462 let widening = serde_json::json!({
1463 "max_items_per_request": 64,
1464 "rate_units_per_second": 1_000,
1465 "rate_burst_units": 2_000,
1466 "max_concurrent_requests": 4,
1467 "principal_max_concurrent_requests": 5
1468 });
1469 assert!(serde_json::from_value::<ResolvedLimits>(widening).is_err());
1470
1471 let orphaned_principal = serde_json::json!({
1472 "max_items_per_request": 64,
1473 "rate_units_per_second": 1_000,
1474 "rate_burst_units": 2_000,
1475 "principal_max_concurrent_requests": 4
1476 });
1477 assert!(serde_json::from_value::<ResolvedLimits>(orphaned_principal).is_err());
1478
1479 let equal = serde_json::json!({
1480 "max_items_per_request": 64,
1481 "rate_units_per_second": 1_000,
1482 "rate_burst_units": 2_000,
1483 "max_concurrent_requests": 4,
1484 "principal_max_concurrent_requests": 4
1485 });
1486 assert!(serde_json::from_value::<ResolvedLimits>(equal).is_ok());
1487 }
1488
1489 #[test]
1490 fn publication_rejects_weighted_values_outside_governors_domain() {
1491 for limits in [
1492 ResolvedLimits::new(64).with_weighted_rate(0, 1),
1493 ResolvedLimits::new(64).with_weighted_rate(1, 0),
1494 ResolvedLimits::new(64).with_weighted_rate(u64::from(u32::MAX) + 1, 1),
1495 ResolvedLimits::new(64).with_weighted_rate(1, u64::from(u32::MAX) + 1),
1496 ] {
1497 let mut snapshot = snapshot(AccountStatus::Active, t(1_000));
1498 snapshot.limits = limits;
1499 assert!(matches!(
1500 PublishableSnapshot::try_new(Arc::new(snapshot)),
1501 Err(SnapshotValidationError::WeightedRateOutsideGovernorDomain { .. })
1502 ));
1503 }
1504 }
1505
1506 #[test]
1510 fn a_cap_below_the_worst_quote_is_unpublishable() {
1511 let elastic = |overage_cap: u64| {
1514 let mut snapshot = AccountSnapshot::clone(&priced_snapshot(10, 1, &[(0, 7)], 10, 80));
1515 snapshot.enforcement_mode = EnforcementMode::Elastic {
1516 overage_cap: CostUnits(overage_cap),
1517 };
1518 PublishableSnapshot::try_new(Arc::new(snapshot))
1519 };
1520
1521 assert!(
1522 elastic(80).is_ok(),
1523 "a cap that funds exactly one worst case is publishable"
1524 );
1525 assert_eq!(
1526 elastic(79).unwrap_err(),
1527 SnapshotValidationError::OverageCapBelowMaxQuote {
1528 operation_index: 0,
1529 max_quote: CostUnits(80),
1530 overage_cap: CostUnits(79),
1531 }
1532 );
1533 }
1534
1535 #[test]
1539 fn restamping_cannot_change_the_enforcement_mode() {
1540 let mut snapshot = snapshot(AccountStatus::Active, t(1_000));
1541 snapshot.enforcement_mode = EnforcementMode::Elastic {
1542 overage_cap: CostUnits(5_000),
1543 };
1544 let publishable = PublishableSnapshot::try_new(Arc::new(snapshot)).unwrap();
1545 let restamped = publishable.restamped(AccountStatus::Suspended, Generation(9));
1546 assert_eq!(
1547 restamped.as_snapshot().enforcement_mode,
1548 EnforcementMode::Elastic {
1549 overage_cap: CostUnits(5_000)
1550 }
1551 );
1552 }
1553
1554 #[test]
1560 fn restamping_preserves_everything_validation_depends_on() {
1561 let original = PublishableSnapshot::try_new(Arc::new(snapshot(
1562 AccountStatus::Active,
1563 Timestamp::from_second(1_000).unwrap(),
1564 )))
1565 .expect("the fixture is publishable");
1566
1567 let restamped = original.restamped(AccountStatus::Suspended, Generation(9));
1568
1569 assert_eq!(restamped.status, AccountStatus::Suspended);
1570 assert_eq!(restamped.generation, Generation(9));
1571 assert_eq!(restamped.account_id, original.account_id);
1572 assert_eq!(restamped.key_id, original.key_id);
1573 assert_eq!(restamped.valid_until, original.valid_until);
1574 assert_eq!(restamped.permissions, original.permissions);
1575 assert_eq!(
1576 restamped.limits.max_items_per_request(),
1577 original.limits.max_items_per_request()
1578 );
1579 assert_eq!(
1580 restamped.limits.legacy_weighted_rate().burst_units(),
1581 original.limits.legacy_weighted_rate().burst_units()
1582 );
1583 assert!(
1584 Arc::ptr_eq(&restamped.cost_table, &original.cost_table),
1585 "the cost table is shared, not rebuilt"
1586 );
1587 PublishableSnapshot::try_new(restamped.into_inner())
1590 .expect("status and generation do not affect publishability");
1591 }
1592
1593 fn snapshot(status: AccountStatus, valid_until: Timestamp) -> AccountSnapshot {
1594 AccountSnapshot::builder(
1595 AccountId(1),
1596 Generation(1),
1597 status,
1598 valid_until,
1599 PermissionBits::bit(0).union(PermissionBits::bit(3)),
1600 ResolvedLimits::new(1024).with_weighted_rate(10_000, 50_000),
1601 Arc::new(CostTable::builder(CostUnits(50), CostUnits(50)).build()),
1602 )
1603 .key_id(KeyId(2))
1604 .build()
1605 }
1606
1607 fn t(secs: i64) -> Timestamp {
1608 Timestamp::from_second(secs).unwrap()
1609 }
1610
1611 #[test]
1615 fn builder_requires_account_status_at_construction() {
1616 type Builder = fn(
1617 AccountId,
1618 Generation,
1619 AccountStatus,
1620 Timestamp,
1621 PermissionBits,
1622 ResolvedLimits,
1623 Arc<CostTable>,
1624 ) -> AccountSnapshotBuilder;
1625
1626 let _: Builder = AccountSnapshot::builder;
1627 }
1628
1629 #[test]
1630 fn active_valid_and_permitted_admits() {
1631 let s = snapshot(AccountStatus::Active, t(1_000));
1632 assert_eq!(s.admit(t(999), PermissionBits::bit(0)), Ok(()));
1633 }
1634
1635 #[test]
1636 fn suspended_and_closed_deny() {
1637 let s = snapshot(AccountStatus::Suspended, t(1_000));
1638 assert_eq!(
1639 s.admit(t(0), PermissionBits::NONE),
1640 Err(DenyReason::AccountSuspended)
1641 );
1642 let s = snapshot(AccountStatus::Closed, t(1_000));
1643 assert_eq!(
1644 s.admit(t(0), PermissionBits::NONE),
1645 Err(DenyReason::AccountClosed)
1646 );
1647 }
1648
1649 #[test]
1650 fn expiry_boundary_is_exclusive_of_valid_until() {
1651 let s = snapshot(AccountStatus::Active, t(1_000));
1652 assert_eq!(
1653 s.admit(t(1_000), PermissionBits::NONE),
1654 Err(DenyReason::SnapshotExpired)
1655 );
1656 assert_eq!(
1657 s.admit(t(1_001), PermissionBits::NONE),
1658 Err(DenyReason::SnapshotExpired)
1659 );
1660 }
1661
1662 #[test]
1663 fn missing_permission_denies() {
1664 let s = snapshot(AccountStatus::Active, t(1_000));
1665 assert_eq!(
1666 s.admit(t(0), PermissionBits::bit(1)),
1667 Err(DenyReason::MissingPermission)
1668 );
1669 assert_eq!(
1671 s.admit(t(0), PermissionBits::bit(0).union(PermissionBits::bit(1))),
1672 Err(DenyReason::MissingPermission)
1673 );
1674 }
1675
1676 fn priced_snapshot(
1677 fixed: u64,
1678 minimum: u64,
1679 weights: &[(usize, u64)],
1680 max_items: u64,
1681 burst: u64,
1682 ) -> Arc<AccountSnapshot> {
1683 struct Op(usize);
1684 impl crate::cost_table::OpIndex for Op {
1685 fn index(&self) -> usize {
1686 self.0
1687 }
1688 }
1689
1690 let mut builder = CostTable::builder(CostUnits(fixed), CostUnits(minimum));
1691 for (index, weight) in weights {
1692 builder = builder.weight(&Op(*index), CostUnits(*weight));
1693 }
1694 Arc::new(
1695 AccountSnapshot::builder(
1696 AccountId(1),
1697 Generation(1),
1698 AccountStatus::Active,
1699 t(1_000),
1700 PermissionBits::bit(0).union(PermissionBits::bit(3)),
1701 ResolvedLimits::new(max_items).with_weighted_rate(1_000, burst),
1702 Arc::new(builder.build()),
1703 )
1704 .key_id(KeyId(2))
1705 .build(),
1706 )
1707 }
1708
1709 #[test]
1710 fn publication_uses_the_largest_registered_weight() {
1711 let snapshot = priced_snapshot(10, 1, &[(0, 2), (3, 7), (5, 4)], 10, 79);
1712 assert_eq!(
1713 PublishableSnapshot::try_new(snapshot).unwrap_err(),
1714 SnapshotValidationError::QuoteExceedsBurst {
1715 operation_index: 3,
1716 max_quote: CostUnits(80),
1717 burst_units: CostUnits(79),
1718 }
1719 );
1720 }
1721
1722 #[test]
1723 fn publication_reports_first_operation_when_maximum_weights_tie() {
1724 assert_eq!(
1725 PublishableSnapshot::try_new(priced_snapshot(0, 0, &[(2, 7), (5, 7)], 10, 69))
1726 .unwrap_err(),
1727 SnapshotValidationError::QuoteExceedsBurst {
1728 operation_index: 2,
1729 max_quote: CostUnits(70),
1730 burst_units: CostUnits(69),
1731 }
1732 );
1733 }
1734
1735 #[test]
1736 fn publication_accepts_a_worst_case_quote_equal_to_the_burst() {
1737 let publishable =
1738 PublishableSnapshot::try_new(priced_snapshot(10, 1, &[(0, 7)], 10, 80)).unwrap();
1739 assert_eq!(
1740 publishable.maximum_quote(),
1741 Some(CostUnits(80)),
1742 "the proof passed to the next trust boundary is the quote validated here"
1743 );
1744 }
1745
1746 #[test]
1747 fn publication_rejects_a_minimum_above_the_burst() {
1748 assert!(matches!(
1749 PublishableSnapshot::try_new(priced_snapshot(0, 50, &[(0, 0)], 64, 49)),
1750 Err(SnapshotValidationError::QuoteExceedsBurst {
1751 max_quote: CostUnits(50),
1752 ..
1753 })
1754 ));
1755 }
1756
1757 #[test]
1758 fn publication_rejects_worst_case_quote_overflow() {
1759 assert_eq!(
1760 PublishableSnapshot::try_new(priced_snapshot(
1761 1,
1762 0,
1763 &[(0, u64::MAX)],
1764 2,
1765 u64::from(u32::MAX),
1766 ))
1767 .unwrap_err(),
1768 SnapshotValidationError::QuoteOverflow {
1769 operation_index: 0,
1770 max_items: 2,
1771 }
1772 );
1773 }
1774
1775 #[test]
1776 fn publication_allows_a_table_with_no_registered_operations() {
1777 PublishableSnapshot::try_new(priced_snapshot(100, 100, &[], 64, 1)).unwrap();
1778 }
1779}
1780
1781#[cfg(test)]
1782mod layout {
1783 use super::*;
1784 use std::mem::{align_of, offset_of, size_of};
1785
1786 #[test]
1795 fn stage_one_fields_share_the_first_cache_line() {
1796 const LINE: usize = 64;
1797 for (name, offset) in [
1798 ("status", offset_of!(AccountSnapshot, status)),
1799 ("permissions", offset_of!(AccountSnapshot, permissions)),
1800 ("valid_until", offset_of!(AccountSnapshot, valid_until)),
1801 (
1802 "enforcement_mode",
1803 offset_of!(AccountSnapshot, enforcement_mode),
1804 ),
1805 ] {
1806 assert!(
1807 offset < LINE,
1808 "{name} sits at offset {offset}, past the first {LINE}-byte line \
1809 the request path reads"
1810 );
1811 }
1812 }
1813
1814 #[test]
1818 fn the_policy_revision_is_cold() {
1819 assert!(
1820 offset_of!(AccountSnapshot, policy_revision) >= 64,
1821 "the policy revision belongs outside the stage-one cache line"
1822 );
1823 }
1824
1825 #[test]
1830 fn the_snapshot_stays_one_two_line_object() {
1831 assert_eq!(align_of::<AccountSnapshot>(), 128);
1832 assert_eq!(
1833 size_of::<AccountSnapshot>(),
1834 256,
1835 "a snapshot that outgrew its two lines costs every account an \
1836 extra line; justify the growth or move the new field"
1837 );
1838 }
1839}