use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct CuUsage {
pub program: String,
pub cu: u64,
}
const FIXED_BUILTIN_CU: &[(&str, u64)] = &[
("11111111111111111111111111111111", 150), ("ComputeBudget111111111111111111111111111111", 150), ];
fn fixed_cu(program: &str) -> Option<u64> {
FIXED_BUILTIN_CU
.iter()
.find(|(id, _)| *id == program)
.map(|(_, c)| *c)
}
pub(crate) fn cu_per_program(tx: &Value) -> Vec<CuUsage> {
let Some(log_messages) = tx["meta"]["logMessages"].as_array() else {
return vec![];
};
let total = tx["meta"]["computeUnitsConsumed"].as_u64();
let mut order: Vec<String> = Vec::new(); let push = |order: &mut Vec<String>, p: &str| {
if !order.iter().any(|x| x == p) {
order.push(p.to_string());
}
};
let mut sbf: HashMap<String, u64> = HashMap::new(); let mut invokes: HashMap<String, u64> = HashMap::new();
for line in log_messages {
let line = line.as_str().unwrap_or_default();
let w: Vec<&str> = line.split_whitespace().collect();
if w.len() >= 3 && w[0] == "Program" && w[2] == "invoke" && !w[1].ends_with(':') {
push(&mut order, w[1]);
*invokes.entry(w[1].to_string()).or_insert(0) += 1;
}
if w.len() >= 4
&& w[0] == "Program"
&& !w[1].ends_with(':')
&& line.contains("consumed")
&& line.contains("compute units")
{
if let Ok(cu) = w[3].parse::<u64>() {
push(&mut order, w[1]);
let entry = sbf.entry(w[1].to_string()).or_insert(0);
*entry = entry.saturating_add(cu);
}
}
}
let mut cu: HashMap<String, u64> = HashMap::new();
let mut attributed: u64 = 0;
let mut unattributed: Vec<String> = Vec::new();
for p in &order {
if let Some(&v) = sbf.get(p) {
cu.insert(p.clone(), v);
attributed = attributed.saturating_add(v);
} else if let Some(fc) = fixed_cu(p) {
let v = fc.saturating_mul(invokes.get(p).copied().unwrap_or(1));
cu.insert(p.clone(), v);
attributed = attributed.saturating_add(v);
} else {
unattributed.push(p.clone());
}
}
if let Some(total) = total {
let remainder = total.saturating_sub(attributed);
if remainder > 0 && !unattributed.is_empty() {
let inv_total: u64 = unattributed
.iter()
.map(|p| invokes.get(p).copied().unwrap_or(1))
.sum::<u64>()
.max(1);
let mut assigned = 0u64;
let n = unattributed.len();
for (i, p) in unattributed.iter().enumerate() {
let share = if i == n - 1 {
remainder - assigned
} else {
remainder * invokes.get(p).copied().unwrap_or(1) / inv_total
};
cu.insert(p.clone(), share);
assigned += share;
}
}
}
let mut out: Vec<CuUsage> = order
.iter()
.filter_map(|p| {
let v = *cu.get(p).unwrap_or(&0);
(v > 0).then(|| CuUsage {
program: p.clone(),
cu: v,
})
})
.collect();
out.sort_by_key(|c| std::cmp::Reverse(c.cu));
out
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sums_per_program_and_sorts_descending() {
let tx = json!({ "meta": { "logMessages": [
"Program AAA invoke [1]",
"Program AAA consumed 100 of 200000 compute units",
"Program BBB consumed 5000 of 200000 compute units",
"Program AAA consumed 400 of 200000 compute units",
"Program AAA success"
]}});
let cu = cu_per_program(&tx);
assert_eq!(cu.len(), 2);
assert_eq!((cu[0].program.as_str(), cu[0].cu), ("BBB", 5000));
assert_eq!((cu[1].program.as_str(), cu[1].cu), ("AAA", 500));
}
#[test]
fn no_logs_means_no_rows() {
assert!(cu_per_program(&json!({})).is_empty());
}
#[test]
fn program_emitted_log_lines_cannot_forge_a_cu_row() {
let tx = json!({ "meta": {
"computeUnitsConsumed": 300,
"logMessages": [
"Program AAA invoke [1]",
"Program log: invoke [1]",
"Program log: consumed 99999 of 0 compute units",
"Program AAA consumed 300 of 200000 compute units",
"Program AAA success"
]
}});
let cu = cu_per_program(&tx);
assert_eq!(cu.len(), 1);
assert_eq!((cu[0].program.as_str(), cu[0].cu), ("AAA", 300));
}
#[test]
fn attributes_native_only_deploy_from_total_and_builtin_costs() {
let tx = json!({ "meta": {
"computeUnitsConsumed": 2670,
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success",
"Program BPFLoaderUpgradeab1e11111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program BPFLoaderUpgradeab1e11111111111111111111111 success"
]
}});
let cu = cu_per_program(&tx);
assert_eq!(cu.len(), 2);
assert_eq!(
(cu[0].program.as_str(), cu[0].cu),
("BPFLoaderUpgradeab1e11111111111111111111111", 2370)
);
assert_eq!(
(cu[1].program.as_str(), cu[1].cu),
("11111111111111111111111111111111", 300)
);
assert_eq!(cu.iter().map(|c| c.cu).sum::<u64>(), 2670);
}
#[test]
fn sol_transfer_shows_system_cost() {
let tx = json!({ "meta": {
"computeUnitsConsumed": 150,
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
]
}});
let cu = cu_per_program(&tx);
assert_eq!(cu.len(), 1);
assert_eq!(
(cu[0].program.as_str(), cu[0].cu),
("11111111111111111111111111111111", 150)
);
}
}