lean_ctx/core/context_kernel/
receipt_chain.rs1use std::sync::{Mutex, MutexGuard, OnceLock};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use super::accounting_fix::PostDeliveryAccounting;
7use super::token_envelope::TokenEnvelope;
8use super::types::ReceiptOutcome;
9
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct ChainEntry {
13 pub chain_id: u64,
15 pub source: String,
17 pub envelope: TokenEnvelope,
19 pub accounting: PostDeliveryAccounting,
21 pub outcome: ReceiptOutcome,
23 pub kernel_supplemented: bool,
25 pub kernel_budget_used: usize,
27 pub recorded_at: u64,
29}
30
31#[derive(Default)]
32struct ReceiptChain {
33 entries: Vec<ChainEntry>,
34 next_id: u64,
35}
36
37static CHAIN: OnceLock<Mutex<ReceiptChain>> = OnceLock::new();
38
39#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
41pub struct ChainSummary {
42 pub total_entries: usize,
44 pub accepted: usize,
46 pub rejected: usize,
48 pub kernel_supplemented: usize,
50 pub avg_kernel_budget: f64,
52 pub total_tokens_delivered: usize,
54 pub total_phantom_savings_pct: f64,
56}
57
58fn chain() -> MutexGuard<'static, ReceiptChain> {
59 let mutex = CHAIN.get_or_init(|| Mutex::new(ReceiptChain::default()));
60 match mutex.lock() {
61 Ok(guard) => guard,
62 Err(poisoned) => poisoned.into_inner(),
63 }
64}
65
66pub fn record_chain_entry(
68 source: &str,
69 envelope: TokenEnvelope,
70 accounting: PostDeliveryAccounting,
71 outcome: ReceiptOutcome,
72 kernel_supplemented: bool,
73 kernel_budget_used: usize,
74) -> u64 {
75 let mut chain = chain();
76 let chain_id = chain.next_id;
77 chain.next_id = chain.next_id.saturating_add(1);
78 let recorded_at = SystemTime::now()
79 .duration_since(UNIX_EPOCH)
80 .map_or(0, |duration| duration.as_secs());
81 chain.entries.push(ChainEntry {
82 chain_id,
83 source: source.to_owned(),
84 envelope,
85 accounting,
86 outcome,
87 kernel_supplemented,
88 kernel_budget_used,
89 recorded_at,
90 });
91 chain_id
92}
93
94pub fn chain_length() -> usize {
96 chain().entries.len()
97}
98
99pub fn chain_entries() -> Vec<ChainEntry> {
101 chain().entries.clone()
102}
103
104pub fn chain_summary() -> ChainSummary {
106 let chain = chain();
107 let mut summary = ChainSummary {
108 total_entries: chain.entries.len(),
109 ..ChainSummary::default()
110 };
111 for entry in &chain.entries {
112 match entry.outcome {
113 ReceiptOutcome::Accepted => summary.accepted += 1,
114 ReceiptOutcome::Rejected => summary.rejected += 1,
115 ReceiptOutcome::Partial | ReceiptOutcome::Unknown => {}
116 }
117 if entry.kernel_supplemented {
118 summary.kernel_supplemented += 1;
119 summary.avg_kernel_budget += entry.kernel_budget_used as f64;
120 }
121 summary.total_tokens_delivered = summary
122 .total_tokens_delivered
123 .saturating_add(entry.accounting.delivered_tokens);
124 summary.total_phantom_savings_pct += entry.accounting.phantom_savings_pct;
125 }
126 if summary.kernel_supplemented > 0 {
127 summary.avg_kernel_budget /= summary.kernel_supplemented as f64;
128 }
129 summary
130}
131
132pub fn accepted_entries() -> Vec<ChainEntry> {
134 outcome_entries(ReceiptOutcome::Accepted)
135}
136
137pub fn rejected_entries() -> Vec<ChainEntry> {
139 outcome_entries(ReceiptOutcome::Rejected)
140}
141
142fn outcome_entries(outcome: ReceiptOutcome) -> Vec<ChainEntry> {
143 chain()
144 .entries
145 .iter()
146 .filter(|entry| entry.outcome == outcome)
147 .cloned()
148 .collect()
149}
150
151pub fn kernel_hit_rate() -> f64 {
153 let summary = chain_summary();
154 if summary.total_entries == 0 {
155 0.0
156 } else {
157 summary.kernel_supplemented as f64 / summary.total_entries as f64
158 }
159}
160
161pub fn reset_chain() {
163 *chain() = ReceiptChain::default();
164}
165
166#[cfg(test)]
167mod tests {
168 use std::sync::{Mutex, MutexGuard};
169
170 use super::*;
171 use crate::core::context_kernel::accounting_fix::compute_honest_accounting;
172
173 static TEST_LOCK: Mutex<()> = Mutex::new(());
174
175 fn isolated() -> MutexGuard<'static, ()> {
176 let guard = match TEST_LOCK.lock() {
177 Ok(guard) => guard,
178 Err(poisoned) => poisoned.into_inner(),
179 };
180 reset_chain();
181 guard
182 }
183
184 fn record(outcome: ReceiptOutcome, supplemented: bool) -> u64 {
185 record_chain_entry(
186 "test",
187 TokenEnvelope::default(),
188 compute_honest_accounting(100, 50, 10, 5),
189 outcome,
190 supplemented,
191 if supplemented { 10 } else { 0 },
192 )
193 }
194
195 fn record_many(outcome: ReceiptOutcome, count: usize) {
196 for _ in 0..count {
197 record(outcome, false);
198 }
199 }
200
201 macro_rules! isolated_test {
202 ($name:ident, $body:block) => {
203 #[test]
204 fn $name() {
205 let _guard = isolated();
206 $body
207 }
208 };
209 }
210
211 isolated_test!(record_and_retrieve, {
212 record_many(ReceiptOutcome::Unknown, 3);
213 assert_eq!(chain_length(), 3);
214 });
215
216 isolated_test!(chain_ids_monotonic, {
217 assert_eq!(record(ReceiptOutcome::Unknown, false), 0);
218 assert_eq!(record(ReceiptOutcome::Unknown, false), 1);
219 assert_eq!(record(ReceiptOutcome::Unknown, false), 2);
220 });
221
222 isolated_test!(summary_counts, {
223 record_many(ReceiptOutcome::Accepted, 5);
224 record_many(ReceiptOutcome::Rejected, 2);
225 let summary = chain_summary();
226 assert_eq!(summary.accepted, 5);
227 assert_eq!(summary.rejected, 2);
228 });
229
230 isolated_test!(kernel_hit_rate_tracks_supplements, {
231 for supplemented in [true, true, true, false, false] {
232 record(ReceiptOutcome::Unknown, supplemented);
233 }
234 assert!((kernel_hit_rate() - 0.6).abs() < f64::EPSILON);
235 });
236
237 isolated_test!(accepted_filter, {
238 record(ReceiptOutcome::Accepted, false);
239 record(ReceiptOutcome::Rejected, false);
240 record(ReceiptOutcome::Accepted, false);
241 assert_eq!(accepted_entries().len(), 2);
242 });
243
244 isolated_test!(reset_clears, {
245 record(ReceiptOutcome::Accepted, true);
246 reset_chain();
247 assert_eq!(chain_length(), 0);
248 assert_eq!(record(ReceiptOutcome::Unknown, false), 0);
249 });
250}