1use std::collections::BTreeMap;
7use std::sync::Mutex;
8
9use rustev_contract::execution::CostPolicy;
10use rustev_contract::run::{Charge, CostBound, CostMode, CostSummary, FinalCost};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Refused {
15 Exhausted,
17 Undisclosed,
19}
20
21#[derive(Debug)]
22pub struct Ledger {
23 mode: CostMode,
24 limit: u64,
25 state: Mutex<State>,
26}
27
28#[derive(Debug, Default, Clone)]
29struct State {
30 reserved: u64,
31 observed: u64,
32 estimated: u64,
33 liability: u64,
34 bound_violations: u64,
35 unknown_attempts: u64,
36 open: BTreeMap<String, u64>,
38}
39
40impl Ledger {
41 pub fn new(policy: CostPolicy) -> Self {
42 let (mode, limit) = match policy {
43 CostPolicy::Unlimited => (CostMode::Unlimited, 0),
44 CostPolicy::Hard { max_units } => (CostMode::Hard, max_units),
45 CostPolicy::Estimated { max_units } => (CostMode::Estimated, max_units),
46 };
47 Ledger {
48 mode,
49 limit,
50 state: Mutex::new(State::default()),
51 }
52 }
53
54 pub fn mode(&self) -> CostMode {
55 self.mode
56 }
57
58 fn lock(&self) -> std::sync::MutexGuard<'_, State> {
59 self.state.lock().unwrap_or_else(|e| e.into_inner())
60 }
61
62 pub fn units_for(&self, bound: CostBound) -> Result<u64, Refused> {
65 match (self.mode, bound) {
66 (_, CostBound::Bounded { max_units }) => Ok(max_units),
67 (CostMode::Hard, _) => Err(Refused::Undisclosed),
68 (_, CostBound::Estimated { units }) => Ok(units),
69 (CostMode::Estimated, CostBound::Unknown) => Err(Refused::Undisclosed),
70 (CostMode::Unlimited, CostBound::Unknown) => Ok(0),
71 }
72 }
73
74 pub fn reserve(&self, units: u64) -> Result<(), Refused> {
76 let mut s = self.lock();
77 if self.mode != CostMode::Unlimited {
78 let committed = s
79 .reserved
80 .saturating_add(s.observed)
81 .saturating_add(s.estimated)
82 .saturating_add(s.liability);
83 if committed.saturating_add(units) > self.limit {
84 return Err(Refused::Exhausted);
85 }
86 }
87 s.reserved = s.reserved.saturating_add(units);
88 Ok(())
89 }
90
91 pub fn release(&self, units: u64) {
93 let mut s = self.lock();
94 s.reserved = s.reserved.saturating_sub(units);
95 }
96
97 pub fn settle(&self, attempt_id: &str, reserved: u64, charge: Charge, bounded: bool) {
100 let mut s = self.lock();
101 s.reserved = s.reserved.saturating_sub(reserved);
102 match charge {
103 Charge::Observed { units } => {
104 s.observed = s.observed.saturating_add(units);
105 if bounded && units > reserved {
106 s.bound_violations += 1;
107 }
108 }
109 Charge::Estimated { units } => {
110 s.estimated = s.estimated.saturating_add(units);
111 if bounded && units > reserved {
114 s.bound_violations += 1;
115 }
116 }
117 Charge::Unknown => {
118 s.liability = s.liability.saturating_add(reserved);
119 s.unknown_attempts += 1;
120 s.open.insert(attempt_id.to_string(), reserved);
121 }
122 }
123 }
124
125 pub fn reconcile(&self, attempt_id: &str, observed: u64) -> bool {
128 let mut s = self.lock();
129 let Some(held) = s.open.remove(attempt_id) else {
130 return false;
131 };
132 s.liability = s.liability.saturating_sub(held);
133 s.unknown_attempts = s.unknown_attempts.saturating_sub(1);
134 s.observed = s.observed.saturating_add(observed);
135 true
136 }
137
138 pub fn reserved(&self) -> u64 {
140 self.lock().reserved
141 }
142
143 pub fn summary(&self) -> CostSummary {
144 let s = self.lock().clone();
145 let final_cost = if s.unknown_attempts > 0 {
146 FinalCost::Unknown
147 } else if s.estimated > 0 {
148 FinalCost::IncludesEstimates
149 } else {
150 FinalCost::Known
151 };
152 CostSummary {
153 mode: self.mode,
154 limit: self.limit,
155 observed: s.observed,
156 estimated: s.estimated,
157 liability: s.liability,
158 bound_violations: s.bound_violations,
159 final_cost,
160 within_guaranteed_cap: self.mode == CostMode::Hard
161 && s.unknown_attempts == 0
162 && s.bound_violations == 0,
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn a_hard_ledger_never_reserves_past_its_limit() {
173 let l = Ledger::new(CostPolicy::Hard { max_units: 10 });
174 assert_eq!(l.reserve(6), Ok(()));
175 assert_eq!(l.reserve(5), Err(Refused::Exhausted));
176 assert_eq!(l.reserve(4), Ok(()));
177 l.settle("a", 6, Charge::Unknown, true);
178 l.settle("b", 4, Charge::Observed { units: 3 }, true);
179 assert_eq!(l.reserve(1), Ok(()), "one unit is free after settlement");
180 let s = l.summary();
181 assert_eq!((s.observed, s.liability), (3, 6));
182 assert_eq!(s.final_cost, FinalCost::Unknown);
183 assert!(!s.within_guaranteed_cap);
184 assert!(l.reconcile("a", 2));
185 assert!(!l.reconcile("a", 2), "reconciled once");
186 assert_eq!(l.summary().liability, 0);
187 }
188
189 #[test]
190 fn disclosure_rules_follow_the_mode() {
191 let hard = Ledger::new(CostPolicy::Hard { max_units: 1 });
192 let est = Ledger::new(CostPolicy::Estimated { max_units: 1 });
193 let free = Ledger::new(CostPolicy::Unlimited);
194 let e = CostBound::Estimated { units: 3 };
195 assert_eq!(hard.units_for(e), Err(Refused::Undisclosed));
196 assert_eq!(est.units_for(e), Ok(3));
197 assert_eq!(est.units_for(CostBound::Unknown), Err(Refused::Undisclosed));
198 assert_eq!(free.units_for(CostBound::Unknown), Ok(0));
199 }
200}