Skip to main content

oris_economics/
lib.rs

1//! Non-financial EVU accounting for local publish and validation incentives.
2
3use 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}