use crate::contract::capability::{Enforcement, EvidenceSet};
use crate::contract::ids::Digest32;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MinGuarantee {
Mediated,
Enforced,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum BudgetDimension {
Wall,
Cpu,
ResidentMemory,
ProcessCount,
HandleCount,
Storage,
Network,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BudgetRequest {
pub limit: u64,
pub guarantee: MinGuarantee,
pub evidence: EvidenceSet,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BudgetRequirements {
pub wall_micros: BudgetRequest,
pub cpu_micros: BudgetRequest,
pub resident_bytes: BudgetRequest,
pub process_count: BudgetRequest,
pub handle_count: BudgetRequest,
pub storage_bytes: BudgetRequest,
pub network_bytes: BudgetRequest,
}
impl BudgetRequest {
#[must_use]
pub fn deny_all() -> Self {
Self {
limit: 0,
guarantee: MinGuarantee::Mediated,
evidence: EvidenceSet::new(),
}
}
#[must_use]
pub fn uniform(limit: u64, guarantee: MinGuarantee) -> Self {
Self {
limit,
guarantee,
evidence: EvidenceSet::new(),
}
}
}
impl BudgetRequirements {
#[must_use]
pub fn deny_all() -> Self {
Self {
wall_micros: BudgetRequest::deny_all(),
cpu_micros: BudgetRequest::deny_all(),
resident_bytes: BudgetRequest::deny_all(),
process_count: BudgetRequest::deny_all(),
handle_count: BudgetRequest::deny_all(),
storage_bytes: BudgetRequest::deny_all(),
network_bytes: BudgetRequest::deny_all(),
}
}
#[must_use]
pub fn uniform(limit: u64, guarantee: MinGuarantee) -> Self {
let dimension = BudgetRequest::uniform(limit, guarantee);
Self {
wall_micros: dimension.clone(),
cpu_micros: dimension.clone(),
resident_bytes: dimension.clone(),
process_count: dimension.clone(),
handle_count: dimension.clone(),
storage_bytes: dimension.clone(),
network_bytes: dimension,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BudgetAvailability {
pub available: u64,
pub enforcement: Enforcement,
pub evidence: EvidenceSet,
pub mechanism: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BudgetProfile {
pub wall_micros: BudgetAvailability,
pub cpu_micros: BudgetAvailability,
pub resident_bytes: BudgetAvailability,
pub process_count: BudgetAvailability,
pub handle_count: BudgetAvailability,
pub storage_bytes: BudgetAvailability,
pub network_bytes: BudgetAvailability,
}
impl BudgetAvailability {
#[must_use]
pub fn unenforced() -> Self {
Self {
available: u64::MAX,
enforcement: Enforcement::Unsupported,
evidence: EvidenceSet::new(),
mechanism: "none/no-budget-enforcement".to_string(),
}
}
}
impl BudgetProfile {
#[must_use]
pub fn all_unenforced() -> Self {
Self {
wall_micros: BudgetAvailability::unenforced(),
cpu_micros: BudgetAvailability::unenforced(),
resident_bytes: BudgetAvailability::unenforced(),
process_count: BudgetAvailability::unenforced(),
handle_count: BudgetAvailability::unenforced(),
storage_bytes: BudgetAvailability::unenforced(),
network_bytes: BudgetAvailability::unenforced(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdmittedBudget {
pub effective_limit: u64,
pub required_guarantee: MinGuarantee,
pub selected_guarantee: Enforcement,
pub required_evidence: EvidenceSet,
pub promised_evidence: EvidenceSet,
pub mechanism: String,
pub profile_digest: Digest32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdmittedBudgets {
pub wall_micros: AdmittedBudget,
pub cpu_micros: AdmittedBudget,
pub resident_bytes: AdmittedBudget,
pub process_count: AdmittedBudget,
pub handle_count: AdmittedBudget,
pub storage_bytes: AdmittedBudget,
pub network_bytes: AdmittedBudget,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DerivedMinimums {
pub wall_micros: u64,
pub cpu_micros: u64,
pub resident_bytes: u64,
pub process_count: u64,
pub handle_count: u64,
pub storage_bytes: u64,
pub network_bytes: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BudgetFailure {
BelowDerivedMinimum,
CapacityExceeded,
GuaranteeInsufficient,
EvidenceMissing,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BudgetRefusal {
pub dimension: BudgetDimension,
pub failure: BudgetFailure,
}
fn enforcement_strength(enforcement: Enforcement) -> u8 {
match enforcement {
Enforcement::Unsupported => 0,
Enforcement::Mediated => 1,
Enforcement::Enforced => 2,
}
}
fn guarantee_strength(guarantee: MinGuarantee) -> u8 {
match guarantee {
MinGuarantee::Mediated => 1,
MinGuarantee::Enforced => 2,
}
}
pub fn adjudicate_dimension(
request: &BudgetRequest,
availability: &BudgetAvailability,
profile_digest: Digest32,
) -> Result<AdmittedBudget, BudgetFailure> {
if request.limit > availability.available {
return Err(BudgetFailure::CapacityExceeded);
}
if enforcement_strength(availability.enforcement) < guarantee_strength(request.guarantee) {
return Err(BudgetFailure::GuaranteeInsufficient);
}
if !request.evidence.is_subset(&availability.evidence) {
return Err(BudgetFailure::EvidenceMissing);
}
Ok(AdmittedBudget {
effective_limit: request.limit,
required_guarantee: request.guarantee,
selected_guarantee: availability.enforcement,
required_evidence: request.evidence.clone(),
promised_evidence: availability.evidence.clone(),
mechanism: availability.mechanism.clone(),
profile_digest,
})
}
pub fn budget_admit(
requirements: &BudgetRequirements,
profile: &BudgetProfile,
derived: &DerivedMinimums,
profile_digest: Digest32,
) -> Result<AdmittedBudgets, BudgetRefusal> {
let intrinsic = [
(
BudgetDimension::Wall,
requirements.wall_micros.limit,
derived.wall_micros,
),
(
BudgetDimension::Cpu,
requirements.cpu_micros.limit,
derived.cpu_micros,
),
(
BudgetDimension::ResidentMemory,
requirements.resident_bytes.limit,
derived.resident_bytes,
),
(
BudgetDimension::ProcessCount,
requirements.process_count.limit,
derived.process_count,
),
(
BudgetDimension::HandleCount,
requirements.handle_count.limit,
derived.handle_count,
),
(
BudgetDimension::Storage,
requirements.storage_bytes.limit,
derived.storage_bytes,
),
(
BudgetDimension::Network,
requirements.network_bytes.limit,
derived.network_bytes,
),
];
for (dimension, limit, minimum) in intrinsic {
if limit < minimum {
return Err(BudgetRefusal {
dimension,
failure: BudgetFailure::BelowDerivedMinimum,
});
}
}
let adjudicate = |dimension: BudgetDimension,
request: &BudgetRequest,
availability: &BudgetAvailability|
-> Result<AdmittedBudget, BudgetRefusal> {
adjudicate_dimension(request, availability, profile_digest)
.map_err(|failure| BudgetRefusal { dimension, failure })
};
Ok(AdmittedBudgets {
wall_micros: adjudicate(
BudgetDimension::Wall,
&requirements.wall_micros,
&profile.wall_micros,
)?,
cpu_micros: adjudicate(
BudgetDimension::Cpu,
&requirements.cpu_micros,
&profile.cpu_micros,
)?,
resident_bytes: adjudicate(
BudgetDimension::ResidentMemory,
&requirements.resident_bytes,
&profile.resident_bytes,
)?,
process_count: adjudicate(
BudgetDimension::ProcessCount,
&requirements.process_count,
&profile.process_count,
)?,
handle_count: adjudicate(
BudgetDimension::HandleCount,
&requirements.handle_count,
&profile.handle_count,
)?,
storage_bytes: adjudicate(
BudgetDimension::Storage,
&requirements.storage_bytes,
&profile.storage_bytes,
)?,
network_bytes: adjudicate(
BudgetDimension::Network,
&requirements.network_bytes,
&profile.network_bytes,
)?,
})
}
#[cfg(test)]
mod budget_tests {
use super::{
adjudicate_dimension, budget_admit, BudgetAvailability, BudgetDimension, BudgetFailure,
BudgetProfile, BudgetRefusal, BudgetRequest, BudgetRequirements, DerivedMinimums,
MinGuarantee,
};
use crate::contract::capability::{Enforcement, EvidenceClaim, EvidenceSet};
fn evidence(claims: &[EvidenceClaim]) -> EvidenceSet {
let mut set = EvidenceSet::new();
for claim in claims {
set.insert(*claim);
}
set
}
fn request(limit: u64) -> BudgetRequest {
BudgetRequest {
limit,
guarantee: MinGuarantee::Mediated,
evidence: evidence(&[EvidenceClaim::ResourceUsage]),
}
}
fn availability(available: u64) -> BudgetAvailability {
BudgetAvailability {
available,
enforcement: Enforcement::Enforced,
evidence: evidence(&[EvidenceClaim::ResourceUsage, EvidenceClaim::TerminalOutcome]),
mechanism: "cgroup".to_string(),
}
}
#[test]
fn unenforced_profile_imposes_no_ceiling_and_no_guarantee() {
let avail = BudgetAvailability::unenforced();
assert_eq!(avail.available, u64::MAX);
assert_eq!(avail.enforcement, Enforcement::Unsupported);
let profile = BudgetProfile::all_unenforced();
assert_eq!(profile.network_bytes.enforcement, Enforcement::Unsupported);
assert!(profile.wall_micros.evidence.is_empty());
}
#[test]
fn deny_all_is_every_limit_zero() {
let reqs = BudgetRequirements::deny_all();
assert_eq!(BudgetRequest::deny_all().limit, 0);
assert_eq!(reqs.wall_micros.limit, 0);
assert_eq!(reqs.network_bytes.limit, 0);
assert_eq!(reqs.process_count.guarantee, MinGuarantee::Mediated);
}
#[test]
fn admits_a_dimension_within_capacity_guarantee_and_evidence() {
let admitted =
adjudicate_dimension(&request(10), &availability(20), [0u8; 32]).expect("admit");
assert_eq!(admitted.effective_limit, 10);
assert_eq!(admitted.selected_guarantee, Enforcement::Enforced);
assert_eq!(admitted.required_guarantee, MinGuarantee::Mediated);
}
#[test]
fn zero_is_a_legitimate_deny_all_bound() {
assert!(adjudicate_dimension(&request(0), &availability(0), [0u8; 32]).is_ok());
}
#[test]
fn adjudication_failures_follow_the_canonical_order() {
assert_eq!(
adjudicate_dimension(&request(100), &availability(20), [0u8; 32]),
Err(BudgetFailure::CapacityExceeded)
);
let weak = BudgetAvailability {
enforcement: Enforcement::Unsupported,
..availability(20)
};
let strict = BudgetRequest {
guarantee: MinGuarantee::Enforced,
..request(10)
};
assert_eq!(
adjudicate_dimension(&strict, &weak, [0u8; 32]),
Err(BudgetFailure::GuaranteeInsufficient)
);
let demanding = BudgetRequest {
evidence: evidence(&[EvidenceClaim::NetworkActivity]),
..request(10)
};
assert_eq!(
adjudicate_dimension(&demanding, &availability(20), [0u8; 32]),
Err(BudgetFailure::EvidenceMissing)
);
}
#[test]
fn derived_minimum_is_checked_before_backend_adjudication() {
let derived = DerivedMinimums {
wall_micros: 5,
..DerivedMinimums::default()
};
let profile = BudgetProfile {
wall_micros: availability(1),
..uniform_profile(20)
};
assert_eq!(
budget_admit(&uniform_requirements(2), &profile, &derived, [0u8; 32]),
Err(BudgetRefusal {
dimension: BudgetDimension::Wall,
failure: BudgetFailure::BelowDerivedMinimum,
})
);
}
fn uniform_requirements(limit: u64) -> BudgetRequirements {
BudgetRequirements {
wall_micros: request(limit),
cpu_micros: request(limit),
resident_bytes: request(limit),
process_count: request(limit),
handle_count: request(limit),
storage_bytes: request(limit),
network_bytes: request(limit),
}
}
fn uniform_profile(available: u64) -> BudgetProfile {
BudgetProfile {
wall_micros: availability(available),
cpu_micros: availability(available),
resident_bytes: availability(available),
process_count: availability(available),
handle_count: availability(available),
storage_bytes: availability(available),
network_bytes: availability(available),
}
}
#[test]
fn admits_all_seven_dimensions() {
let admitted = budget_admit(
&uniform_requirements(10),
&uniform_profile(20),
&DerivedMinimums::default(),
[0u8; 32],
)
.expect("admit all seven");
assert_eq!(admitted.network_bytes.effective_limit, 10);
}
#[test]
fn refusal_names_the_first_failing_dimension_in_canonical_order() {
let mut reqs = uniform_requirements(10);
reqs.cpu_micros.limit = 999; reqs.network_bytes.limit = 999; assert_eq!(
budget_admit(
&reqs,
&uniform_profile(20),
&DerivedMinimums::default(),
[0u8; 32]
),
Err(BudgetRefusal {
dimension: BudgetDimension::Cpu,
failure: BudgetFailure::CapacityExceeded,
})
);
}
#[test]
fn capacity_boundary_is_inclusive() {
let digest = [0u8; 32];
assert!(adjudicate_dimension(&request(20), &availability(20), digest).is_ok());
assert_eq!(
adjudicate_dimension(&request(21), &availability(20), digest),
Err(BudgetFailure::CapacityExceeded)
);
assert!(adjudicate_dimension(&request(u64::MAX), &availability(u64::MAX), digest).is_ok());
assert_eq!(
adjudicate_dimension(&request(u64::MAX), &availability(u64::MAX - 1), digest),
Err(BudgetFailure::CapacityExceeded)
);
}
#[test]
fn derived_minimum_boundary_is_inclusive() {
let digest = [0u8; 32];
let derived = DerivedMinimums {
wall_micros: 5,
..DerivedMinimums::default()
};
let mut reqs = uniform_requirements(5);
assert!(budget_admit(&reqs, &uniform_profile(20), &derived, digest).is_ok());
reqs.wall_micros.limit = 4;
assert_eq!(
budget_admit(&reqs, &uniform_profile(20), &derived, digest),
Err(BudgetRefusal {
dimension: BudgetDimension::Wall,
failure: BudgetFailure::BelowDerivedMinimum,
})
);
}
#[test]
fn guarantee_boundary_is_inclusive() {
let digest = [0u8; 32];
let mediated = BudgetAvailability {
enforcement: Enforcement::Mediated,
..availability(20)
};
assert!(adjudicate_dimension(&request(10), &mediated, digest).is_ok());
let enforced_req = BudgetRequest {
guarantee: MinGuarantee::Enforced,
..request(10)
};
assert_eq!(
adjudicate_dimension(&enforced_req, &mediated, digest),
Err(BudgetFailure::GuaranteeInsufficient)
);
}
fn assert_capacity_refusal_at(reqs: &BudgetRequirements, dimension: BudgetDimension) {
assert_eq!(
budget_admit(
reqs,
&uniform_profile(20),
&DerivedMinimums::default(),
[0u8; 32]
),
Err(BudgetRefusal {
dimension,
failure: BudgetFailure::CapacityExceeded,
}),
"dimension {dimension:?} must own its capacity refusal"
);
}
#[test]
fn each_of_the_seven_dimensions_is_named_at_its_capacity_boundary() {
let mut reqs = uniform_requirements(10);
reqs.wall_micros.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::Wall);
let mut reqs = uniform_requirements(10);
reqs.cpu_micros.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::Cpu);
let mut reqs = uniform_requirements(10);
reqs.resident_bytes.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::ResidentMemory);
let mut reqs = uniform_requirements(10);
reqs.process_count.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::ProcessCount);
let mut reqs = uniform_requirements(10);
reqs.handle_count.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::HandleCount);
let mut reqs = uniform_requirements(10);
reqs.storage_bytes.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::Storage);
let mut reqs = uniform_requirements(10);
reqs.network_bytes.limit = 21;
assert_capacity_refusal_at(&reqs, BudgetDimension::Network);
}
}