1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Default, Serialize, Deserialize)]
8pub struct EvuAccount {
9 pub node_id: String,
10 pub balance: i64,
11}
12
13#[derive(Clone, Debug, Default, Serialize, Deserialize)]
14pub struct ReputationRecord {
15 pub node_id: String,
16 pub publish_success_rate: f32,
17 pub validator_accuracy: f32,
18 pub reuse_impact: u64,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct StakePolicy {
23 pub publish_cost: i64,
24 pub reuse_reward: i64,
25 pub validator_penalty: i64,
26}
27
28impl Default for StakePolicy {
29 fn default() -> Self {
30 Self {
31 publish_cost: 1,
32 reuse_reward: 2,
33 validator_penalty: 1,
34 }
35 }
36}
37
38#[derive(Clone, Debug, Default, Serialize, Deserialize)]
39pub struct EconomicsSignal {
40 pub available_evu: i64,
41 pub publish_success_rate: f32,
42 pub validator_accuracy: f32,
43 pub reuse_impact: u64,
44 pub selector_weight: f32,
45}
46
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct ValidationSettlement {
49 pub publisher_delta: i64,
50 pub validator_delta: i64,
51 pub reason: String,
52}
53
54#[derive(Clone, Debug, Default, Serialize, Deserialize)]
55pub struct EvuLedger {
56 pub accounts: Vec<EvuAccount>,
57 pub reputations: Vec<ReputationRecord>,
58}
59
60impl EvuLedger {
61 pub fn can_publish(&self, node_id: &str, policy: &StakePolicy) -> bool {
62 self.accounts
63 .iter()
64 .find(|account| account.node_id == node_id)
65 .map(|account| account.balance >= policy.publish_cost)
66 .unwrap_or(false)
67 }
68
69 pub fn available_balance(&self, node_id: &str) -> Option<i64> {
70 self.accounts
71 .iter()
72 .find(|account| account.node_id == node_id)
73 .map(|account| account.balance)
74 }
75
76 pub fn reserve_publish_stake(
77 &mut self,
78 node_id: &str,
79 policy: &StakePolicy,
80 ) -> Option<ValidationSettlement> {
81 if !self.can_publish(node_id, policy) {
82 return None;
83 }
84 let account = self.account_mut(node_id);
85 account.balance -= policy.publish_cost;
86 Some(ValidationSettlement {
87 publisher_delta: -policy.publish_cost,
88 validator_delta: 0,
89 reason: "publish stake reserved".into(),
90 })
91 }
92
93 pub fn settle_remote_reuse(
94 &mut self,
95 publisher_id: &str,
96 success: bool,
97 policy: &StakePolicy,
98 ) -> ValidationSettlement {
99 if success {
100 {
101 let account = self.account_mut(publisher_id);
102 account.balance += policy.reuse_reward;
103 }
104 {
105 let reputation = self.reputation_mut(publisher_id);
106 reputation.publish_success_rate =
107 blend_metric(reputation.publish_success_rate, 1.0);
108 reputation.reuse_impact = reputation.reuse_impact.saturating_add(1);
109 }
110 ValidationSettlement {
111 publisher_delta: policy.reuse_reward,
112 validator_delta: 0,
113 reason: "remote reuse succeeded".into(),
114 }
115 } else {
116 let reputation = self.reputation_mut(publisher_id);
117 reputation.publish_success_rate = blend_metric(reputation.publish_success_rate, 0.0);
118 reputation.validator_accuracy = blend_metric(reputation.validator_accuracy, 0.0);
119 ValidationSettlement {
120 publisher_delta: 0,
121 validator_delta: -policy.validator_penalty,
122 reason: "remote reuse failed local validation".into(),
123 }
124 }
125 }
126
127 pub fn penalize_validator_divergence(
128 &mut self,
129 validator_id: &str,
130 policy: &StakePolicy,
131 ) -> ValidationSettlement {
132 let reputation = self.reputation_mut(validator_id);
133 reputation.validator_accuracy = blend_metric(reputation.validator_accuracy, 0.0);
134 ValidationSettlement {
135 publisher_delta: 0,
136 validator_delta: -policy.validator_penalty,
137 reason: "validator report diverged from local final validation".into(),
138 }
139 }
140
141 pub fn selector_reputation_bias(&self) -> BTreeMap<String, f32> {
142 self.reputations
143 .iter()
144 .map(|record| {
145 let reuse_bonus = ((record.reuse_impact as f32).ln_1p() / 4.0).min(0.25);
146 let weight = (record.publish_success_rate * 0.55)
147 + (record.validator_accuracy * 0.35)
148 + reuse_bonus;
149 (record.node_id.clone(), weight.clamp(0.0, 1.0))
150 })
151 .collect()
152 }
153
154 pub fn governor_signal(&self, node_id: &str) -> Option<EconomicsSignal> {
155 let balance = self.available_balance(node_id).unwrap_or(0);
156 let reputation = self
157 .reputations
158 .iter()
159 .find(|record| record.node_id == node_id)
160 .cloned()
161 .or_else(|| {
162 self.accounts
163 .iter()
164 .find(|record| record.node_id == node_id)
165 .map(|_| ReputationRecord {
166 node_id: node_id.to_string(),
167 publish_success_rate: 0.5,
168 validator_accuracy: 0.5,
169 reuse_impact: 0,
170 })
171 })?;
172 let selector_weight = self
173 .selector_reputation_bias()
174 .get(node_id)
175 .copied()
176 .unwrap_or(0.0);
177 Some(EconomicsSignal {
178 available_evu: balance,
179 publish_success_rate: reputation.publish_success_rate,
180 validator_accuracy: reputation.validator_accuracy,
181 reuse_impact: reputation.reuse_impact,
182 selector_weight,
183 })
184 }
185
186 fn account_mut(&mut self, node_id: &str) -> &mut EvuAccount {
187 if let Some(index) = self
188 .accounts
189 .iter()
190 .position(|item| item.node_id == node_id)
191 {
192 return &mut self.accounts[index];
193 }
194 self.accounts.push(EvuAccount {
195 node_id: node_id.to_string(),
196 balance: 0,
197 });
198 self.accounts.last_mut().expect("account just inserted")
199 }
200
201 fn reputation_mut(&mut self, node_id: &str) -> &mut ReputationRecord {
202 if let Some(index) = self
203 .reputations
204 .iter()
205 .position(|item| item.node_id == node_id)
206 {
207 return &mut self.reputations[index];
208 }
209 self.reputations.push(ReputationRecord {
210 node_id: node_id.to_string(),
211 publish_success_rate: 0.5,
212 validator_accuracy: 0.5,
213 reuse_impact: 0,
214 });
215 self.reputations
216 .last_mut()
217 .expect("reputation just inserted")
218 }
219}
220
221fn blend_metric(current: f32, observation: f32) -> f32 {
222 ((current * 0.7) + (observation * 0.3)).clamp(0.0, 1.0)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_evu_ledger_can_publish_with_sufficient_balance() {
231 let ledger = EvuLedger {
232 accounts: vec![EvuAccount {
233 node_id: "node1".into(),
234 balance: 10,
235 }],
236 reputations: vec![],
237 };
238 let policy = StakePolicy {
239 publish_cost: 5,
240 ..Default::default()
241 };
242 assert!(ledger.can_publish("node1", &policy));
243 }
244
245 #[test]
246 fn test_evu_ledger_cannot_publish_with_insufficient_balance() {
247 let ledger = EvuLedger {
248 accounts: vec![EvuAccount {
249 node_id: "node1".into(),
250 balance: 3,
251 }],
252 reputations: vec![],
253 };
254 let policy = StakePolicy {
255 publish_cost: 5,
256 ..Default::default()
257 };
258 assert!(!ledger.can_publish("node1", &policy));
259 }
260
261 #[test]
262 fn test_evu_ledger_cannot_publish_unknown_node() {
263 let ledger = EvuLedger::default();
264 let policy = StakePolicy {
265 publish_cost: 5,
266 ..Default::default()
267 };
268 assert!(!ledger.can_publish("unknown_node", &policy));
269 }
270
271 #[test]
272 fn test_default_stake_policy() {
273 let policy = StakePolicy::default();
274 assert_eq!(policy.publish_cost, 1);
275 assert_eq!(policy.reuse_reward, 2);
276 assert_eq!(policy.validator_penalty, 1);
277 }
278
279 #[test]
280 fn test_reputation_record() {
281 let reputation = ReputationRecord {
282 node_id: "node1".into(),
283 publish_success_rate: 0.95,
284 validator_accuracy: 0.88,
285 reuse_impact: 100,
286 };
287 assert_eq!(reputation.node_id, "node1");
288 assert!(reputation.publish_success_rate > 0.9);
289 }
290
291 #[test]
292 fn test_validation_settlement() {
293 let settlement = ValidationSettlement {
294 publisher_delta: 10,
295 validator_delta: 5,
296 reason: "successful validation".into(),
297 };
298 assert_eq!(settlement.publisher_delta, 10);
299 assert_eq!(settlement.validator_delta, 5);
300 }
301
302 #[test]
303 fn reserve_publish_stake_deducts_balance() {
304 let mut ledger = EvuLedger {
305 accounts: vec![EvuAccount {
306 node_id: "node1".into(),
307 balance: 10,
308 }],
309 reputations: vec![],
310 };
311 let policy = StakePolicy::default();
312 let settlement = ledger.reserve_publish_stake("node1", &policy).unwrap();
313 assert_eq!(settlement.publisher_delta, -1);
314 assert_eq!(ledger.available_balance("node1"), Some(9));
315 }
316
317 #[test]
318 fn remote_reuse_success_rewards_balance_and_reputation() {
319 let mut ledger = EvuLedger {
320 accounts: vec![EvuAccount {
321 node_id: "node1".into(),
322 balance: 3,
323 }],
324 reputations: vec![ReputationRecord {
325 node_id: "node1".into(),
326 publish_success_rate: 0.5,
327 validator_accuracy: 0.5,
328 reuse_impact: 0,
329 }],
330 };
331 let settlement = ledger.settle_remote_reuse("node1", true, &StakePolicy::default());
332 assert_eq!(settlement.publisher_delta, 2);
333 assert_eq!(ledger.available_balance("node1"), Some(5));
334 assert!(ledger.reputations[0].publish_success_rate > 0.5);
335 assert_eq!(ledger.reputations[0].reuse_impact, 1);
336 }
337
338 #[test]
339 fn remote_reuse_failure_penalizes_reputation() {
340 let mut ledger = EvuLedger {
341 accounts: vec![EvuAccount {
342 node_id: "node1".into(),
343 balance: 3,
344 }],
345 reputations: vec![ReputationRecord {
346 node_id: "node1".into(),
347 publish_success_rate: 0.8,
348 validator_accuracy: 0.9,
349 reuse_impact: 2,
350 }],
351 };
352 let settlement = ledger.settle_remote_reuse("node1", false, &StakePolicy::default());
353 assert_eq!(settlement.publisher_delta, 0);
354 assert!(settlement.validator_delta < 0);
355 assert!(ledger.reputations[0].publish_success_rate < 0.8);
356 assert!(ledger.reputations[0].validator_accuracy < 0.9);
357 assert_eq!(ledger.available_balance("node1"), Some(3));
358 }
359
360 #[test]
361 fn selector_reputation_bias_prefers_stronger_reputation() {
362 let ledger = EvuLedger {
363 accounts: vec![],
364 reputations: vec![
365 ReputationRecord {
366 node_id: "node-a".into(),
367 publish_success_rate: 0.4,
368 validator_accuracy: 0.4,
369 reuse_impact: 0,
370 },
371 ReputationRecord {
372 node_id: "node-b".into(),
373 publish_success_rate: 0.9,
374 validator_accuracy: 0.9,
375 reuse_impact: 10,
376 },
377 ],
378 };
379 let bias = ledger.selector_reputation_bias();
380 assert!(bias["node-b"] > bias["node-a"]);
381 }
382
383 #[test]
384 fn governor_signal_exposes_balance_and_reputation() {
385 let ledger = EvuLedger {
386 accounts: vec![EvuAccount {
387 node_id: "node1".into(),
388 balance: 7,
389 }],
390 reputations: vec![ReputationRecord {
391 node_id: "node1".into(),
392 publish_success_rate: 0.75,
393 validator_accuracy: 0.5,
394 reuse_impact: 4,
395 }],
396 };
397 let signal = ledger.governor_signal("node1").unwrap();
398 assert_eq!(signal.available_evu, 7);
399 assert_eq!(signal.reuse_impact, 4);
400 assert!(signal.selector_weight > 0.0);
401 }
402}