@@ -5,159 +5,132 @@ use super::karma::KarmaSystem;
55
66pub struct RewardEngine ;
77
8- /// Detailed breakdown of a reward calculation split.
9- /// Used internally or for transparency.
10- #[ derive( Debug , Clone ) ]
11- pub struct RewardCalculation {
12- pub pool_amount : u64 ,
13- pub burn_amount : u64 ,
14- }
15-
168impl RewardEngine {
17- /// Hardcoded splits per role
18- const SPLIT_HARDWARE : f64 = 0.10 ;
19- const SPLIT_DATA : f64 = 0.40 ;
20- const SPLIT_TEACHER : f64 = 0.50 ;
9+ /// Hardcoded splits per role as per SPEC
10+ pub const SPLIT_HARDWARE : f64 = 0.10 ;
11+ pub const SPLIT_DATA : f64 = 0.40 ;
12+ pub const SPLIT_TEACHER : f64 = 0.50 ;
2113
22- /// Calculates the split allocation for a TOTAL reward pool based on role rules.
23- /// This returns the *max potential* pool for that role group from the total.
24- ///
25- /// # Arguments
26- /// * `total_reward` - The total tokens available (minted or paid).
27- /// * `role` - The role to get the fraction for.
28- ///
29- /// # Returns
30- /// Struct with `pool_amount` (share) and `burn_amount` (if applicable, though burn usually happens pre-split).
31- /// NOTE: User request asks for `calculate_split(total, role)`.
32- pub fn calculate_split ( total_reward : u64 , role : ContributionRole ) -> u64 {
33- let fraction = match role {
34- ContributionRole :: Hardware => Self :: SPLIT_HARDWARE ,
35- ContributionRole :: DataProvider => Self :: SPLIT_DATA ,
36- ContributionRole :: Teacher => Self :: SPLIT_TEACHER ,
37- } ;
38- ( total_reward as f64 * fraction) as u64
39- }
14+ /// Constant base reward per unit of data/work
15+ pub const BASE_REWARD_UNIT : u64 = 100 ;
4016
41- /// Calculates block rewards with specific Service Payment burn logic (80%).
42- pub fn calculate_block_rewards (
43- total_input : u64 ,
44- contributions : & Vec < Contribution > ,
17+ /// Calculates and distributes rewards for a validated contribution.
18+ ///
19+ /// Following the SPEC formula:
20+ /// E = B_base * Q * DataSize
21+ /// R_H = E * 0.10
22+ /// R_D = (E * 0.40) * Km(D)
23+ /// R_V = (E * 0.50) / N * Km(V_i)
24+ pub fn distribute_rewards (
25+ contribution : & Contribution ,
4526 users : & HashMap < Uuid , UserProfile > ,
46- is_service_payment : bool ,
4727 ) -> Vec < RewardReceipt > {
4828 let mut receipts = Vec :: new ( ) ;
4929
50- // 1. Deflation: Service Payment Burn (80%)
51- let ( distributable, initial_burn) = if is_service_payment {
52- let burn = ( total_input as f64 * 0.80 ) as u64 ;
53- ( total_input - burn, burn)
54- } else {
55- ( total_input, 0 )
56- } ;
57-
58- if initial_burn > 0 {
59- receipts. push ( RewardReceipt {
60- amount_minted : 0 ,
61- amount_burned : initial_burn,
62- recipient : "Service_Payment_Burn" . to_string ( ) ,
63- } ) ;
64- }
30+ // 1. Calculate Total Emission (E)
31+ // E = Base * Quality * Size
32+ let emission = ( Self :: BASE_REWARD_UNIT as f64
33+ * contribution. quality_score
34+ * contribution. data_size as f64 ) as u64 ;
6535
66- if contributions. is_empty ( ) {
67- // Burn remainder if no work done
68- if distributable > 0 {
69- receipts. push ( RewardReceipt {
70- amount_minted : 0 ,
71- amount_burned : distributable,
72- recipient : "No_Work_Burn" . to_string ( ) ,
73- } ) ;
74- }
75- return receipts;
36+ if emission == 0 {
37+ return receipts;
7638 }
7739
78- // 2. Separate Contributions
79- let mut hw_contribs = Vec :: new ( ) ;
80- let mut data_contribs = Vec :: new ( ) ;
81- let mut teacher_contribs = Vec :: new ( ) ;
40+ // 2. Hardware Provider (10% fixed)
41+ let r_h = ( emission as f64 * Self :: SPLIT_HARDWARE ) as u64 ;
42+ if r_h > 0 {
43+ let recipient = users. get ( & contribution. hardware_provider )
44+ . map ( |u| u. wallet_address . clone ( ) )
45+ . unwrap_or_else ( || "Unknown_Hardware" . to_string ( ) ) ;
8246
83- for c in contributions {
84- match c. role {
85- ContributionRole :: Hardware => hw_contribs. push ( c) ,
86- ContributionRole :: DataProvider => data_contribs. push ( c) ,
87- ContributionRole :: Teacher => teacher_contribs. push ( c) ,
88- }
47+ receipts. push ( RewardReceipt {
48+ amount_minted : r_h,
49+ amount_burned : 0 ,
50+ recipient,
51+ } ) ;
8952 }
9053
91- // 3. Calculate Pools using `calculate_split` logic
92- // We use the distributable amount as the base for the split.
93- // We handle each pool separately.
54+ // 3. Data Provider (40% adjusted by Karma)
55+ let pool_d = ( emission as f64 * Self :: SPLIT_DATA ) as u64 ;
56+ let km_d = users. get ( & contribution. dataset_provider )
57+ . map ( |u| KarmaSystem :: calculate_multiplier ( u. karma_score ) )
58+ . unwrap_or ( KarmaSystem :: MIN_MULTIPLIER ) ;
9459
95- let pool_hw = Self :: calculate_split ( distributable, ContributionRole :: Hardware ) ;
96- let pool_data = Self :: calculate_split ( distributable, ContributionRole :: DataProvider ) ;
97- let pool_teacher = Self :: calculate_split ( distributable, ContributionRole :: Teacher ) ;
60+ let r_d = ( pool_d as f64 * km_d) as u64 ;
61+ if r_d > 0 {
62+ let recipient = users. get ( & contribution. dataset_provider )
63+ . map ( |u| u. wallet_address . clone ( ) )
64+ . unwrap_or_else ( || "Unknown_Data" . to_string ( ) ) ;
9865
99- let mut total_distributed = 0 ;
100- total_distributed += Self :: distribute_pool ( pool_hw, & hw_contribs, users, & mut receipts) ;
101- total_distributed += Self :: distribute_pool ( pool_data, & data_contribs, users, & mut receipts) ;
102- total_distributed += Self :: distribute_pool ( pool_teacher, & teacher_contribs, users, & mut receipts) ;
103-
104- // Burn specific remainder (rounding errors or empty pools)
105- let remainder = distributable. saturating_sub ( total_distributed) ;
106- if remainder > 0 {
10766 receipts. push ( RewardReceipt {
108- amount_minted : 0 ,
109- amount_burned : remainder ,
110- recipient : "Unused_Pool_Burn" . to_string ( ) ,
67+ amount_minted : r_d ,
68+ amount_burned : 0 ,
69+ recipient,
11170 } ) ;
11271 }
11372
114- receipts
115- }
116-
117- fn distribute_pool (
118- pool_amount : u64 ,
119- contribs : & [ & Contribution ] ,
120- users : & HashMap < Uuid , UserProfile > ,
121- receipts : & mut Vec < RewardReceipt > ,
122- ) -> u64 {
123- if contribs. is_empty ( ) || pool_amount == 0 {
124- return 0 ;
73+ // Burn if Km < 1.0 (anti-inflation for low trust)
74+ if km_d < 1.0 {
75+ let burn_d = pool_d. saturating_sub ( r_d) ;
76+ if burn_d > 0 {
77+ receipts. push ( RewardReceipt {
78+ amount_minted : 0 ,
79+ amount_burned : burn_d,
80+ recipient : "Low_Karma_Data_Burn" . to_string ( ) ,
81+ } ) ;
82+ }
12583 }
12684
127- // Score = Quality * KarmaMultiplier
128- let mut scores: Vec < ( f64 , & Contribution ) > = Vec :: new ( ) ;
129- let mut total_score = 0.0 ;
130-
131- for c in contribs {
132- let karma = users. get ( & c. contributor_id ) . map ( |u| u. karma_score ) . unwrap_or ( 50.0 ) ;
133- let multiplier = KarmaSystem :: calculate_multiplier ( karma) ;
134- let score = c. quality_score * multiplier;
135-
136- scores. push ( ( score, c) ) ;
137- total_score += score;
85+ // 4. Validators/Teachers (50% divided and adjusted by Karma)
86+ if !contribution. validators . is_empty ( ) {
87+ let pool_v = ( emission as f64 * Self :: SPLIT_TEACHER ) as u64 ;
88+ let n = contribution. validators . len ( ) as f64 ;
89+ let share_v = pool_v as f64 / n;
90+
91+ for v_id in & contribution. validators {
92+ let km_v = users. get ( v_id)
93+ . map ( |u| KarmaSystem :: calculate_multiplier ( u. karma_score ) )
94+ . unwrap_or ( KarmaSystem :: MIN_MULTIPLIER ) ;
95+
96+ let r_v = ( share_v * km_v) as u64 ;
97+ if r_v > 0 {
98+ let recipient = users. get ( v_id)
99+ . map ( |u| u. wallet_address . clone ( ) )
100+ . unwrap_or_else ( || "Unknown_Validator" . to_string ( ) ) ;
101+
102+ receipts. push ( RewardReceipt {
103+ amount_minted : r_v,
104+ amount_burned : 0 ,
105+ recipient,
106+ } ) ;
107+ }
108+
109+ // Burn if Km < 1.0
110+ if km_v < 1.0 {
111+ let burn_v = ( share_v * ( 1.0 - km_v) ) as u64 ;
112+ if burn_v > 0 {
113+ receipts. push ( RewardReceipt {
114+ amount_minted : 0 ,
115+ amount_burned : burn_v,
116+ recipient : "Low_Karma_Validator_Burn" . to_string ( ) ,
117+ } ) ;
118+ }
119+ }
120+ }
138121 }
139122
140- if total_score <= 1e-6 { return 0 ; }
141-
142- let mut distributed_here = 0 ;
143- for ( score, c) in scores {
144- let ratio = score / total_score;
145- let amount = ( pool_amount as f64 * ratio) as u64 ;
146-
147- if amount > 0 {
148- let recipient = users. get ( & c. contributor_id )
149- . map ( |u| u. wallet_address . clone ( ) )
150- . unwrap_or_else ( || "Unknown" . to_string ( ) ) ;
123+ receipts
124+ }
151125
152- receipts . push ( RewardReceipt {
153- amount_minted : amount ,
154- amount_burned : 0 ,
155- recipient ,
156- } ) ;
157- distributed_here += amount ;
158- }
126+ /// Processes client payment with 80% deflationary burn.
127+ pub fn process_payment_and_burn ( amount : u64 ) -> RewardReceipt {
128+ let burn_amount = ( amount as f64 * 0.80 ) as u64 ;
129+ RewardReceipt {
130+ amount_minted : 0 ,
131+ amount_burned : burn_amount ,
132+ recipient : "Deflationary_Burn" . to_string ( ) ,
159133 }
160- distributed_here
161134 }
162135}
163136
@@ -166,49 +139,55 @@ mod tests {
166139 use super :: * ;
167140
168141 #[ test]
169- fn test_split_logic ( ) {
170- let total = 1000 ;
171- assert_eq ! ( RewardEngine :: calculate_split( total, ContributionRole :: Hardware ) , 100 ) ;
172- assert_eq ! ( RewardEngine :: calculate_split( total, ContributionRole :: DataProvider ) , 400 ) ;
173- assert_eq ! ( RewardEngine :: calculate_split( total, ContributionRole :: Teacher ) , 500 ) ;
174- }
175-
176- #[ test]
177- fn test_service_payment_burn ( ) {
178- let total = 1000 ;
179- let contrib_id = Uuid :: new_v4 ( ) ;
180- let user = UserProfile {
181- wallet_address : "w1" . into ( ) ,
182- soulbound_id : Uuid :: new_v4 ( ) ,
183- karma_score : 80.0 ,
184- is_human_verified : true ,
185- balance : 0 ,
186- last_action_timestamp : 0 ,
187- } ;
142+ fn test_reward_distribution_spec ( ) {
188143 let mut users = HashMap :: new ( ) ;
189- users. insert ( contrib_id, user) ;
190-
191- let contrib = Contribution {
192- contributor_id : contrib_id,
193- role : ContributionRole :: DataProvider , // 40%
144+ let hw_id = Uuid :: new_v4 ( ) ;
145+ let data_id = Uuid :: new_v4 ( ) ;
146+ let v_id = Uuid :: new_v4 ( ) ;
147+
148+ users. insert ( hw_id, UserProfile {
149+ wallet_address : "hw_addr" . into ( ) ,
150+ karma_score : 50.0 ,
151+ ..UserProfile :: new ( "hw_addr" . into ( ) )
152+ } ) ;
153+ users. insert ( data_id, UserProfile {
154+ wallet_address : "data_addr" . into ( ) ,
155+ karma_score : 80.0 , // 5.0x multiplier
156+ ..UserProfile :: new ( "data_addr" . into ( ) )
157+ } ) ;
158+ users. insert ( v_id, UserProfile {
159+ wallet_address : "v_addr" . into ( ) ,
160+ karma_score : 10.0 , // 0.1x multiplier
161+ ..UserProfile :: new ( "v_addr" . into ( ) )
162+ } ) ;
163+
164+ let contribution = Contribution {
165+ id : Uuid :: new_v4 ( ) ,
166+ dataset_provider : data_id,
167+ hardware_provider : hw_id,
168+ validators : vec ! [ v_id] ,
194169 quality_score : 1.0 ,
170+ data_size : 100 ,
195171 } ;
196172
197- let receipts = RewardEngine :: calculate_block_rewards (
198- total,
199- & vec ! [ contrib] ,
200- & users,
201- true // is_service_payment
202- ) ;
203-
204- // 1. Initial Burn: 800 (80% of 1000)
205- let burn_receipt = receipts. iter ( ) . find ( |r| r. recipient == "Service_Payment_Burn" ) . unwrap ( ) ;
206- assert_eq ! ( burn_receipt. amount_burned, 800 ) ;
207-
208- // 2. Distributable: 200
209- // Data Pool share: 40% of 200 = 80
210- // Since only 1 contributor, they get full pool.
211- let mint_receipt = receipts. iter ( ) . find ( |r| r. recipient == "w1" ) . unwrap ( ) ;
212- assert_eq ! ( mint_receipt. amount_minted, 80 ) ;
173+ let receipts = RewardEngine :: distribute_rewards ( & contribution, & users) ;
174+
175+ // Emission = 100 * 1.0 * 100 = 10,000
176+ // HW = 10,000 * 0.10 = 1,000
177+ // Data = (10,000 * 0.40) * 5.0 = 4,000 * 5.0 = 20,000
178+ // Validator = (10,000 * 0.50 / 1) * 0.1 = 5,000 * 0.1 = 500
179+ // Burn V = 5,000 * 0.9 = 4,500
180+
181+ let hw_r = receipts. iter ( ) . find ( |r| r. recipient == "hw_addr" ) . unwrap ( ) ;
182+ assert_eq ! ( hw_r. amount_minted, 1000 ) ;
183+
184+ let data_r = receipts. iter ( ) . find ( |r| r. recipient == "data_addr" ) . unwrap ( ) ;
185+ assert_eq ! ( data_r. amount_minted, 20000 ) ;
186+
187+ let v_r = receipts. iter ( ) . find ( |r| r. recipient == "v_addr" ) . unwrap ( ) ;
188+ assert_eq ! ( v_r. amount_minted, 500 ) ;
189+
190+ let burn_v = receipts. iter ( ) . find ( |r| r. recipient == "Low_Karma_Validator_Burn" ) . unwrap ( ) ;
191+ assert_eq ! ( burn_v. amount_burned, 4500 ) ;
213192 }
214193}
0 commit comments