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}