miracle_api/dmt/daily_participant_data.rs
1use serde::{Deserialize, Serialize};
2use steel::*;
3
4// External dependencies: solana_program::hash::hashv and hex for string conversion
5
6/// DailyParticipantData represents aggregated payment data for a single participant in a daily epoch.
7///
8/// This structure is used off-chain by the oracle to build Merkle trees for daily payment snapshots.
9/// It aggregates all payments for a single participant (customer or merchant) within a 24-hour epoch.
10///
11/// ## Key Features
12/// - **Aggregation**: Combines multiple payments per participant per day
13/// - **Fair Rewards**: Uses transaction count and total amount for reward calculation
14/// - **Efficient Storage**: Off-chain only, reduces on-chain storage costs
15/// - **Flexible**: Supports both customer and merchant participants
16/// - **Simple Design**: Only necessary fields included (Simple is Best)
17/// - **Transaction Verification**: Includes short transaction signatures for on-chain verification
18///
19/// ## Data Structure
20/// - `participant_id`: Unique identifier for the participant (customer/merchant)
21/// - `first_payment_timestamp`: Timestamp of first payment in the day
22/// - `last_payment_timestamp`: Timestamp of last payment in the day
23/// - `first_payment_tx_sig_short`: First 8 characters of first payment transaction signature
24/// - `last_payment_tx_sig_short`: First 8 characters of last payment transaction signature
25/// - `first_payment_tx_sig_short`: First 8 characters of first payment transaction signature
26/// - `last_payment_tx_sig_short`: First 8 characters of last payment transaction signature
27/// - `payment_count`: Number of transactions for the day
28/// - `participant_type`: Whether this is a customer (0) or merchant (1)
29///
30/// ## Merkle Leaf Construction
31/// The Merkle leaf hash is computed as:
32/// ```rust
33/// use miracle_api::prelude::DailyParticipantData;
34///
35/// let participant_data = DailyParticipantData::new(
36/// [0u8; 32], // participant_id
37/// 1723680000, // first_payment_timestamp
38/// 1723766400, // last_payment_timestamp
39/// [0u8; 8], // first_payment_tx_sig_short
40/// [0u8; 8], // last_payment_tx_sig_short
41/// 5, // payment_count
42/// 0, // participant_type (customer)
43/// );
44/// let leaf_hash = participant_data.compute_leaf_hash();
45/// ```
46///
47/// ## Security Features
48/// - **Timestamp Range**: Provides audit trail and prevents stale data
49/// - **Participant Type**: Distinguishes between customers and merchants for fair allocation
50/// - **Aggregation**: Reduces Merkle tree size while maintaining fairness
51/// - **Uniqueness**: participant_id + daily timestamps provide natural uniqueness
52/// - **Deployment Isolation**: Handled by program ID, not data structure
53/// - **Transaction Verification**: Short signatures enable on-chain payment verification
54/// - **Transaction Verification**: Short signatures enable on-chain verification of payment authenticity
55#[repr(C)]
56#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
57pub struct DailyParticipantData {
58 /// Unique identifier for the participant (customer or merchant).
59 /// This should be a deterministic identifier that can be reproduced
60 /// by both the oracle and the claiming user.
61 pub participant_id: [u8; 32],
62
63 /// Timestamp of the first payment in the daily epoch (Unix timestamp).
64 /// Used for audit trail and to prevent stale data replay.
65 pub first_payment_timestamp: i64,
66
67 /// Timestamp of the last payment in the daily epoch (Unix timestamp).
68 /// Used for audit trail and to ensure data freshness.
69 pub last_payment_timestamp: i64,
70
71 /// First 8 characters of the first payment transaction signature.
72 /// Used for on-chain verification of payment authenticity.
73 /// Provides cost-effective verification without storing full signatures.
74 pub first_payment_tx_sig_short: [u8; 8],
75
76 /// First 8 characters of the last payment transaction signature.
77 /// Used for on-chain verification of payment authenticity.
78 /// Provides cost-effective verification without storing full signatures.
79 pub last_payment_tx_sig_short: [u8; 8],
80
81 /// Number of transactions for this participant in the daily epoch.
82 /// Used for activity-based reward calculation and community metrics.
83 pub payment_count: u32,
84
85 /// Participant type: 0 for customer, 1 for merchant.
86 /// Used to determine reward allocation between customer and merchant pools.
87 pub participant_type: u8,
88
89 /// Padding for future extensibility.
90 pub _padding: [u8; 3],
91}
92
93impl DailyParticipantData {
94 /// Create a new DailyParticipantData instance.
95 ///
96 /// ## Parameters
97 /// - `participant_id`: Unique identifier for the participant
98 /// - `first_payment_timestamp`: Timestamp of first payment
99 /// - `last_payment_timestamp`: Timestamp of last payment
100 /// - `first_payment_tx_sig_short`: First 8 characters of first payment transaction signature
101 /// - `last_payment_tx_sig_short`: First 8 characters of last payment transaction signature
102 /// - `payment_count`: Number of transactions
103 /// - `participant_type`: 0 for customer, 1 for merchant
104 ///
105 /// ## Returns
106 /// - New DailyParticipantData instance
107 pub fn new(
108 participant_id: [u8; 32],
109 first_payment_timestamp: i64,
110 last_payment_timestamp: i64,
111 first_payment_tx_sig_short: [u8; 8],
112 last_payment_tx_sig_short: [u8; 8],
113 payment_count: u32,
114 participant_type: u8,
115 ) -> Self {
116 Self {
117 participant_id,
118 first_payment_timestamp,
119 last_payment_timestamp,
120 first_payment_tx_sig_short,
121 last_payment_tx_sig_short,
122 payment_count,
123 participant_type,
124 _padding: [0u8; 3],
125 }
126 }
127
128 /// Compute the Merkle leaf hash for this participant data.
129 ///
130 /// ## Returns
131 /// - 32-byte hash that will be used as a leaf in the Merkle tree
132 ///
133 /// ## Security
134 /// This hash includes all fields to ensure data integrity and prevent
135 /// replay attacks across different epochs. The short transaction signatures
136 /// are included to enable on-chain verification of payment authenticity.
137 ///
138 /// ## Implementation
139 /// Uses Solana's standard hashv function with SHA-256 for cryptographic security.
140 /// All fields are included in the hash to prevent any data manipulation.
141 pub fn compute_leaf_hash(&self) -> [u8; 32] {
142 use solana_program::hash::hashv;
143
144 // Create a deterministic byte representation of all fields
145 let mut data = Vec::new();
146
147 // Add all fields in a deterministic order (same as struct field order)
148 data.extend_from_slice(&self.participant_id);
149 data.extend_from_slice(&self.first_payment_timestamp.to_le_bytes());
150 data.extend_from_slice(&self.last_payment_timestamp.to_le_bytes());
151 data.extend_from_slice(&self.first_payment_tx_sig_short);
152 data.extend_from_slice(&self.last_payment_tx_sig_short);
153 data.extend_from_slice(&self.payment_count.to_le_bytes());
154 data.push(self.participant_type);
155 // Note: _padding is not included in hash for determinism
156
157 // Compute cryptographic hash using Solana's standard hash function
158 hashv(&[&data]).to_bytes()
159 }
160
161 /// Validate the participant data for consistency.
162 ///
163 /// ## Returns
164 /// - `Ok(())` if data is valid
165 /// - `Err(&str)` with error message if validation fails
166 ///
167 /// ## Validation Rules
168 /// - Participant type must be 0 or 1
169 /// - Payment count must be greater than 0
170 /// - First timestamp must be before or equal to last timestamp
171 /// - Short signatures must not be all zeros (basic validation)
172 pub fn validate(&self) -> Result<(), &'static str> {
173 if self.participant_type > 1 {
174 return Err("Invalid participant type");
175 }
176 if self.payment_count == 0 {
177 return Err("Payment count must be greater than 0");
178 }
179 if self.first_payment_timestamp > self.last_payment_timestamp {
180 return Err("First timestamp must be before or equal to last timestamp");
181 }
182 if self.first_payment_tx_sig_short == [0u8; 8] {
183 return Err("First payment transaction signature short cannot be all zeros");
184 }
185 if self.last_payment_tx_sig_short == [0u8; 8] {
186 return Err("Last payment transaction signature short cannot be all zeros");
187 }
188
189 Ok(())
190 }
191
192 /// Check if this participant is a customer.
193 ///
194 /// ## Returns
195 /// - `true` if participant_type is 0 (customer)
196 /// - `false` if participant_type is 1 (merchant)
197 pub fn is_customer(&self) -> bool {
198 self.participant_type == 0
199 }
200
201 /// Check if this participant is a merchant.
202 ///
203 /// ## Returns
204 /// - `true` if participant_type is 1 (merchant)
205 /// - `false` if participant_type is 0 (customer)
206 pub fn is_merchant(&self) -> bool {
207 self.participant_type == 1
208 }
209
210 // /// Get the first payment transaction signature short as a string.
211 // ///
212 // /// ## Returns
213 // /// - String representation of the first payment transaction signature short
214 // pub fn first_payment_tx_sig_short_str(&self) -> String {
215 // hex::encode(&self.first_payment_tx_sig_short)
216 // }
217
218 // /// Get the last payment transaction signature short as a string.
219 // ///
220 // /// ## Returns
221 // /// - String representation of the last payment transaction signature short
222 // pub fn last_payment_tx_sig_short_str(&self) -> String {
223 // hex::encode(&self.last_payment_tx_sig_short)
224 // }
225
226 /// Deserialize from bytes manually
227 pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
228 let expected_size = std::mem::size_of::<Self>();
229 if data.len() < expected_size {
230 return Err("Insufficient data for DailyParticipantData");
231 }
232
233 let participant_id: [u8; 32] = data[0..32]
234 .try_into()
235 .map_err(|_| "Invalid participant_id")?;
236 let first_payment_timestamp = i64::from_le_bytes(
237 data[32..40]
238 .try_into()
239 .map_err(|_| "Invalid first_payment_timestamp")?,
240 );
241 let last_payment_timestamp = i64::from_le_bytes(
242 data[40..48]
243 .try_into()
244 .map_err(|_| "Invalid last_payment_timestamp")?,
245 );
246 let first_payment_tx_sig_short: [u8; 8] = data[48..56]
247 .try_into()
248 .map_err(|_| "Invalid first_payment_tx_sig_short")?;
249 let last_payment_tx_sig_short: [u8; 8] = data[56..64]
250 .try_into()
251 .map_err(|_| "Invalid last_payment_tx_sig_short")?;
252 let payment_count = u32::from_le_bytes(
253 data[64..68]
254 .try_into()
255 .map_err(|_| "Invalid payment_count")?,
256 );
257 let participant_type = data[68];
258
259 Ok(Self {
260 participant_id,
261 first_payment_timestamp,
262 last_payment_timestamp,
263 first_payment_tx_sig_short,
264 last_payment_tx_sig_short,
265 payment_count,
266 participant_type,
267 _padding: [0u8; 3],
268 })
269 }
270}