miracle_api/
lib.rs

1pub mod calculations;
2pub mod consts;
3pub mod dmt;
4pub mod error;
5pub mod event;
6pub mod instruction;
7pub mod loaders;
8pub mod sdk;
9pub mod state;
10pub mod utils;
11
12// Re-export helper functions for easier access
13pub use consts::{calculate_daily_rewards, BASE_DAILY_REWARDS};
14
15pub use calculations::{
16    calculate_community_score, calculate_smooth_community_decay, calculate_time_decay,
17    calculate_weighted_geometric_mean,
18};
19
20pub mod prelude {
21    // Core modules
22    pub use crate::dmt::*;
23    pub use crate::error::*;
24    pub use crate::event::*;
25    pub use crate::instruction::*;
26    pub use crate::loaders::*;
27    pub use crate::state::*;
28
29    // SDK functions (primary source for user-facing functions)
30    pub use crate::sdk::*;
31
32    // Merkle tree types and helpers for off-chain processors
33    pub use crate::sdk::{
34        create_participant_from_wallet, find_participant_by_wallet, find_participant_index,
35        generate_proof_for_wallet, sort_participants_by_id, wallet_to_participant_id, MerkleProof,
36        MerkleTree,
37    };
38
39    // Consts functions (only the ones not duplicated in SDK)
40    pub use crate::consts::{
41        calculate_daily_rewards, calculate_daily_rewards_split, BASE_DAILY_REWARDS, BASIS_POINTS,
42        EPOCH_DURATION, MAX_PAYMENT_PROOF_LENGTH, MAX_SEAL_PROOF_LENGTH, MAX_SUPPLY, ONE_MIRACLE,
43        TEN_MIRACLE,
44    };
45
46    // Calculations functions
47    pub use crate::calculations::{
48        calculate_community_score, calculate_smooth_community_decay, calculate_time_decay,
49        calculate_weighted_geometric_mean,
50    };
51
52    // Utility functions for data conversion and validation
53    pub use crate::utils::*;
54}
55
56use steel::*;
57
58// // Mainnet Formal Miracle Program ID
59// declare_id!("mp2ept1LF9cBM5z2wBn45xF1BjX2haSEiu3LQhAksYz");
60
61// Devnet Test Program ID
62// 0_rnd
63// declare_id!("FiVwirA2beubPcFPGGZMYju8qgKwg6V8QtNSU8acwGvy");
64// 1_rnd
65declare_id!("1stNeamE9aWAxA1bvN6h659hYZP6BKPZuLirNUwPcPD");
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::calculations::{
71        calculate_community_score, calculate_smooth_community_decay, calculate_time_decay,
72        calculate_weighted_geometric_mean,
73    };
74    use crate::consts::{calculate_daily_rewards, BASE_DAILY_REWARDS};
75    use crate::state::{ActivityLimits, CommunityTargets, Config, SocialMarketingConfig};
76
77    // Helper function to create a test config
78    fn create_test_config() -> Config {
79        Config {
80            last_reset_at: 0,
81            // evolver: solana_program::pubkey::Pubkey::default(),
82            // evolvable: 0,
83            claim_rewards_threshold: BASE_DAILY_REWARDS,
84            oracle_authority: solana_program::pubkey::Pubkey::default(),
85            community_targets: CommunityTargets {
86                target_weekly_users: 10000,
87                target_weekly_activity: 50000,
88                target_retention_rate: 7000,
89                _padding: [0u8; 6],
90            },
91            activity_limits: ActivityLimits {
92                max_customer_activity_per_epoch: 5,
93                max_merchant_activity_per_epoch: 50,
94                activity_cap_enabled: 1,
95                claim_cap_enabled: 1,
96                _padding: [0u8; 6],
97            },
98            social_marketing: SocialMarketingConfig {
99                rewards_pool: 9_000_000_000_000,  // Initial value
100                daily_limit_per_user: 1,          // Simple rule: 1 social claim per epoch per user
101                base_reward_per_post: 10_000_000, // Initial value
102                oracle_authority: solana_program::pubkey::Pubkey::default(),
103                // _padding: [0u8; 0],
104            },
105            // Note: Pool tracking fields removed from Config - now tracked in Snapshot and Treasury
106        }
107    }
108
109    #[test]
110    fn test_id() {
111        assert_eq!(id(), ID);
112    }
113
114    #[test]
115    fn test_community_metrics_size() {
116        use state::Metrics;
117        assert_eq!(std::mem::size_of::<Metrics>(), 32);
118    }
119
120    #[test]
121    fn test_community_metrics_event_size() {
122        use event::MetricsUpdatedEvent;
123        assert_eq!(std::mem::size_of::<MetricsUpdatedEvent>(), 32);
124    }
125
126    // ===== ENHANCED SMOOTH DECAY TESTS =====
127
128    #[test]
129    fn test_smooth_community_decay() {
130        // Test boundary conditions
131        assert_eq!(calculate_smooth_community_decay(0), 10000); // 100% rewards (struggling)
132        assert_eq!(calculate_smooth_community_decay(10000), 5000); // 50% rewards (healthy)
133
134        // Test middle values
135        let mid_decay = calculate_smooth_community_decay(5000);
136        assert!(mid_decay >= 5000 && mid_decay <= 10000); // Between 50% and 100%
137
138        // Test smoothness (no sharp cliffs)
139        let decay_7999 = calculate_smooth_community_decay(7999);
140        let decay_8000 = calculate_smooth_community_decay(8000);
141        let decay_8001 = calculate_smooth_community_decay(8001);
142
143        // Should be smooth, not binary (allow for integer rounding)
144        assert!(decay_7999 >= decay_8000);
145        assert!(decay_8000 >= decay_8001);
146        assert!(decay_7999 - decay_8000 <= 10); // Very small difference due to integer math
147        assert!(decay_8000 - decay_8001 <= 10); // Very small difference due to integer math
148    }
149
150    #[test]
151    fn test_daily_rewards() {
152        let base_rewards = BASE_DAILY_REWARDS; // 100,000 MIRACLE
153
154        // Test boundary conditions
155        let expected_0 = (base_rewards as u128)
156            .saturating_mul(calculate_smooth_community_decay(0) as u128)
157            .saturating_mul(calculate_time_decay(0) as u128)
158            .saturating_div(100_000_000u128) as u64;
159        assert_eq!(calculate_daily_rewards(0, 0), expected_0); // 100% rewards
160
161        let expected_100 = (base_rewards as u128)
162            .saturating_mul(calculate_smooth_community_decay(10000) as u128)
163            .saturating_mul(calculate_time_decay(0) as u128)
164            .saturating_div(100_000_000u128) as u64;
165        assert_eq!(calculate_daily_rewards(10000, 0), expected_100); // 60% rewards
166
167        // Test middle values
168        let mid_rewards = calculate_daily_rewards(5000, 0);
169        assert!(mid_rewards > expected_100); // More than 60%
170        assert!(mid_rewards <= base_rewards); // Less than or equal to 100%
171
172        // Test smoothness compared to legacy
173        let rewards_7999 = calculate_daily_rewards(7999, 0);
174        let rewards_8000 = calculate_daily_rewards(8000, 0);
175
176        // New function should be smooth, not binary (allow for integer rounding)
177        assert!(rewards_7999 >= rewards_8000); // Smooth transition
178    }
179
180    #[test]
181    fn test_smooth_decay_formula_verification() {
182        // Verify the smooth decay formula works as expected
183        // Formula: 0.5 + 0.5 * (1 - (score/10000)^2)
184
185        // At score = 0: 0.5 + 0.5 * (1 - 0) = 1.0
186        assert_eq!(calculate_smooth_community_decay(0), 10000);
187
188        // At score = 10000: 0.5 + 0.5 * (1 - 1) = 0.5
189        assert_eq!(calculate_smooth_community_decay(10000), 5000);
190
191        // At score = 5000: 0.5 + 0.5 * (1 - 0.25) = 0.5 + 0.5 * 0.75 = 0.875
192        let expected_5000 = (0.875 * 10000.0) as u64;
193        let actual_5000 = calculate_smooth_community_decay(5000);
194        assert!(actual_5000 >= expected_5000 - 100 && actual_5000 <= expected_5000 + 100);
195
196        // Verify the formula step by step for score = 5000
197        let score = 5000u64;
198        let score_squared = score * score; // 25,000,000
199        let score_ratio_squared_in_bps = score_squared / 10000; // 2,500
200        let quadratic_component = 10000 - score_ratio_squared_in_bps; // 7,500
201        let decay_component = quadratic_component * 5000 / 10000; // 3,750
202        let expected = 5000 + decay_component; // 8,750
203        let actual = calculate_smooth_community_decay(5000);
204        assert_eq!(actual, expected);
205
206        // Verify the formula step by step for score = 3000
207        let score = 3000u64;
208        let score_squared = score * score; // 9,000,000
209        let score_ratio_squared_in_bps = score_squared / 10000; // 900
210        let quadratic_component = 10000 - score_ratio_squared_in_bps; // 9,100
211        let decay_component = quadratic_component * 5000 / 10000; // 4,550
212        let expected = 5000 + decay_component; // 9,550
213        let actual = calculate_smooth_community_decay(3000);
214        assert_eq!(actual, expected);
215    }
216
217    #[test]
218    fn test_weighted_geometric_mean() {
219        let scores = [5000u16, 7000u16, 9000u16]; // 50%, 70%, 90%
220
221        // Test equal weights
222        let equal_weights = [3333u16, 3333u16, 3334u16]; // ~33.33% each
223        let result = calculate_weighted_geometric_mean(scores, equal_weights);
224        // Geometric mean: ∛(5000 × 7000 × 9000) ≈ 6805
225        assert!(result >= 6800 && result <= 6810);
226
227        // Test zero weights
228        let zero_weights = [0u16, 0u16, 0u16];
229        let result_zero = calculate_weighted_geometric_mean(scores, zero_weights);
230        assert_eq!(result_zero, 0);
231
232        // Test single weight
233        let single_weight = [10000u16, 0u16, 0u16];
234        let result_single = calculate_weighted_geometric_mean(scores, single_weight);
235        assert_eq!(result_single, 5000);
236
237        // Test zero scores (geometric mean of zero is zero)
238        let zero_scores = [0u16, 7000u16, 9000u16];
239        let result_zero_score = calculate_weighted_geometric_mean(zero_scores, equal_weights);
240        assert_eq!(result_zero_score, 0);
241
242        // Test geometric mean vs arithmetic mean behavior
243        let mixed_scores = [1000u16, 5000u16, 9000u16]; // Poor, medium, good
244        let arithmetic_result = (1000 + 5000 + 9000) / 3; // = 5000
245        let geometric_result = calculate_weighted_geometric_mean(mixed_scores, equal_weights);
246        // Geometric mean should be lower than arithmetic mean for mixed scores
247        assert!(geometric_result < arithmetic_result);
248    }
249
250    #[test]
251    fn test_geometric_vs_arithmetic_mean_behavior() {
252        // Test case 1: Balanced scores (should be similar)
253        let balanced_scores = [6000u16, 7000u16, 8000u16];
254        let equal_weights = [3333u16, 3333u16, 3334u16];
255
256        let _arithmetic_mean = (6000 + 7000 + 8000) / 3; // = 7000
257        let geometric_mean = calculate_weighted_geometric_mean(balanced_scores, equal_weights);
258
259        // For balanced scores, geometric mean should be close to arithmetic mean
260        assert!(geometric_mean >= 6900 && geometric_mean <= 7100);
261
262        // Test case 2: Mixed scores (geometric mean should be lower)
263        let mixed_scores = [1000u16, 5000u16, 9000u16]; // Poor, medium, good
264        let arithmetic_mean_mixed = (1000 + 5000 + 9000) / 3; // = 5000
265        let geometric_mean_mixed = calculate_weighted_geometric_mean(mixed_scores, equal_weights);
266
267        // Geometric mean should be significantly lower than arithmetic mean
268        assert!(geometric_mean_mixed < arithmetic_mean_mixed);
269        assert!(geometric_mean_mixed >= 2000 && geometric_mean_mixed <= 4000);
270
271        // Test case 3: Very poor performance in one metric
272        let poor_performance = [500u16, 8000u16, 9000u16]; // Very poor, good, good
273        let arithmetic_mean_poor = (500 + 8000 + 9000) / 3; // = 5833
274        let geometric_mean_poor =
275            calculate_weighted_geometric_mean(poor_performance, equal_weights);
276
277        // Geometric mean should heavily penalize the poor performance
278        assert!(geometric_mean_poor < arithmetic_mean_poor);
279        assert!(geometric_mean_poor < 4000); // Should be much lower
280
281        // Test case 4: Zero score (should return zero)
282        let zero_score = [0u16, 8000u16, 9000u16];
283        let geometric_mean_zero = calculate_weighted_geometric_mean(zero_score, equal_weights);
284        assert_eq!(geometric_mean_zero, 0);
285    }
286
287    #[test]
288    fn test_community_score_with_weights() {
289        let config = create_test_config();
290
291        // Test with oracle-provided weights
292        let score = calculate_community_score(&config, 5000, 25000, 5000, 6000, 3000, 1000);
293        assert!(score > 0 && score <= 10000);
294
295        // Test with different weight combinations
296        let score = calculate_community_score(&config, 5000, 25000, 5000, 4000, 3500, 2500);
297        assert!(score > 0 && score <= 10000);
298
299        // Test with maturity weights
300        let score = calculate_community_score(&config, 5000, 25000, 5000, 3000, 3000, 4000);
301        assert!(score > 0 && score <= 10000);
302    }
303
304    #[test]
305    fn test_time_decay() {
306        // 0 years since launch: 100%
307        assert_eq!(calculate_time_decay(0), 10000);
308        // 1 year: 82%
309        assert_eq!(calculate_time_decay(1), 8200);
310        // 3 years: 46%
311        assert_eq!(calculate_time_decay(3), 4600);
312        // 5 years: 10%
313        assert_eq!(calculate_time_decay(5), 1000);
314        // 10 years: 10% (minimum)
315        assert_eq!(calculate_time_decay(10), 1000);
316    }
317
318    #[test]
319    fn test_daily_rewards_with_time_decay() {
320        let base = BASE_DAILY_REWARDS;
321        // 100% community, 0 years: 30% * 100% = 30%
322        let r1 = calculate_daily_rewards(10000, 0);
323        let expected1 = (base as u128)
324            .saturating_mul(calculate_smooth_community_decay(10000) as u128)
325            .saturating_mul(calculate_time_decay(0) as u128)
326            .saturating_div(100_000_000u128) as u64;
327        assert_eq!(r1, expected1);
328        // 100% community, 5 years: 30% * 10% = 3%
329        let r2 = calculate_daily_rewards(10000, 5);
330        let expected2 = (base as u128)
331            .saturating_mul(calculate_smooth_community_decay(10000) as u128)
332            .saturating_mul(calculate_time_decay(5) as u128)
333            .saturating_div(100_000_000u128) as u64;
334        assert_eq!(r2, expected2);
335        // 50% community, 3 years: ~82.5% * 46% = ~37.95%
336        let r3 = calculate_daily_rewards(5000, 3);
337        let expected3 = (base as u128)
338            .saturating_mul(calculate_smooth_community_decay(5000) as u128)
339            .saturating_mul(calculate_time_decay(3) as u128)
340            .saturating_div(100_000_000u128) as u64;
341        assert!((r3 as i64 - expected3 as i64).abs() < 10_000_000); // allow rounding error
342    }
343
344    // ===== ENHANCED VALIDATION TESTS =====
345
346    #[test]
347    fn test_validate_community_metrics_success() {
348        // Valid parameters
349        let result = sdk::validate_community_metrics(
350            5000,   // weekly_active_users
351            7500,   // weekly_retention_rate
352            4000,   // user_weight
353            3500,   // activity_weight
354            2500,   // retention_weight
355            50_000, // weekly_activity_count
356            7000,   // customer_reward_share
357            3000,   // merchant_reward_share
358        );
359        assert!(result.is_ok());
360    }
361
362    #[test]
363    fn test_validate_community_metrics_invalid_weights() {
364        // Weights don't sum to 10000
365        let result = sdk::validate_community_metrics(
366            5000,   // weekly_active_users
367            7500,   // weekly_retention_rate
368            4000,   // user_weight
369            3500,   // activity_weight
370            2000,   // retention_weight (sum = 9500, should be 10000)
371            50_000, // weekly_activity_count
372            7000,   // customer_reward_share
373            3000,   // merchant_reward_share
374        );
375        assert!(result.is_err());
376        assert_eq!(result.unwrap_err(), error::MiracleError::InvalidWeights);
377    }
378
379    #[test]
380    fn test_validate_community_metrics_invalid_retention_rate() {
381        // Retention rate > 10000
382        let result = sdk::validate_community_metrics(
383            5000,   // weekly_active_users
384            15000,  // weekly_retention_rate (should be <= 10000)
385            4000,   // user_weight
386            3500,   // activity_weight
387            2500,   // retention_weight
388            50_000, // weekly_activity_count
389            7000,   // customer_reward_share
390            3000,   // merchant_reward_share
391        );
392        assert!(result.is_err());
393        assert_eq!(
394            result.unwrap_err(),
395            error::MiracleError::InvalidRetentionRate
396        );
397    }
398
399    #[test]
400    fn test_validate_community_metrics_invalid_reward_split() {
401        // Reward shares don't sum to 10000
402        let result = sdk::validate_community_metrics(
403            5000,   // weekly_active_users
404            7500,   // weekly_retention_rate
405            4000,   // user_weight
406            3500,   // activity_weight
407            2500,   // retention_weight
408            50_000, // weekly_activity_count
409            7000,   // customer_reward_share
410            2000,   // merchant_reward_share (sum = 9000, should be 10000)
411        );
412        assert!(result.is_err());
413        assert_eq!(result.unwrap_err(), error::MiracleError::InvalidRewardSplit);
414    }
415
416    #[test]
417    fn test_validate_community_metrics_invalid_activity_count() {
418        // Activity count > 1M
419        let result = sdk::validate_community_metrics(
420            5000,      // weekly_active_users
421            7500,      // weekly_retention_rate
422            4000,      // user_weight
423            3500,      // activity_weight
424            2500,      // retention_weight
425            2_000_000, // weekly_activity_count (should be <= 1M)
426            7000,      // customer_reward_share
427            3000,      // merchant_reward_share
428        );
429        assert!(result.is_err());
430        assert_eq!(
431            result.unwrap_err(),
432            error::MiracleError::InvalidActivityCount
433        );
434    }
435
436    // ===== NEW PAYMENT PLATFORM TESTS =====
437
438    #[test]
439    fn test_activity_based_rewards() {
440        let config = create_test_config();
441
442        // Test that activity count (not volume) is used for rewards
443        let score_low_activity = calculate_community_score(
444            &config, 5000,   // weekly_active_users
445            10_000, // low activity count
446            5000,   // weekly_retention_rate
447            4000,   // user_weight
448            3500,   // activity_weight
449            2500,   // retention_weight
450        );
451
452        let score_high_activity = calculate_community_score(
453            &config, 5000,    // weekly_active_users
454            100_000, // high activity count
455            5000,    // weekly_retention_rate
456            4000,    // user_weight
457            3500,    // activity_weight
458            2500,    // retention_weight
459        );
460
461        // Higher activity should result in higher score
462        assert!(score_high_activity > score_low_activity);
463    }
464
465    #[test]
466    fn test_customer_merchant_split() {
467        // Test customer/merchant reward split calculation
468        let (customer_pool, merchant_pool) = consts::calculate_daily_rewards_split(
469            5000, // community_score
470            1,    // years_since_launch
471            7000, // customer_reward_share (70%)
472            3000, // merchant_reward_share (30%)
473        );
474
475        // Both pools should be positive
476        assert!(customer_pool > 0);
477        assert!(merchant_pool > 0);
478
479        // Customer pool should be larger than merchant pool (70% vs 30%)
480        assert!(customer_pool > merchant_pool);
481
482        // Total should equal the base daily rewards for this community score
483        let total_rewards = calculate_daily_rewards(5000, 1);
484        let total_split = customer_pool + merchant_pool;
485        assert!((total_split as i64 - total_rewards as i64).abs() < 1000); // Allow small rounding error
486    }
487
488    #[test]
489    fn test_boundary_conditions() {
490        let config = create_test_config();
491
492        // Test minimum values
493        let _score_min = calculate_community_score(
494            &config, 1,     // minimum users
495            1,     // minimum activity
496            0,     // minimum retention
497            10000, // user_weight (100%)
498            0,     // activity_weight
499            0,     // retention_weight
500        );
501        // score_min is always >= 0 due to u16 type
502
503        // Test maximum values
504        let score_max = calculate_community_score(
505            &config, 1_000_000, // maximum users
506            1_000_000, // maximum activity
507            10000,     // maximum retention (100%)
508            3333,      // user_weight
509            3333,      // activity_weight
510            3334,      // retention_weight
511        );
512        assert!(score_max <= 10000);
513
514        // Test zero values (should handle gracefully)
515        let _score_zero = calculate_community_score(
516            &config, 0,    // zero users
517            0,    // zero activity
518            0,    // zero retention
519            3333, // user_weight
520            3333, // activity_weight
521            3334, // retention_weight
522        );
523        // score_zero is always >= 0 due to u16 type
524    }
525
526    #[test]
527    fn test_weight_combinations() {
528        // Test different weight combinations
529        let test_cases = vec![
530            (6000, 3000, 1000), // Launch weights
531            (4000, 3500, 2500), // Growth weights
532            (3000, 3000, 4000), // Maturity weights
533            (5000, 5000, 0),    // No retention
534            (0, 0, 10000),      // Only retention
535        ];
536
537        let config = create_test_config();
538        for (user_w, activity_w, retention_w) in test_cases {
539            let score = calculate_community_score(
540                &config,
541                5000,        // weekly_active_users
542                50_000,      // weekly_activity_count
543                5000,        // weekly_retention_rate
544                user_w,      // user_weight
545                activity_w,  // activity_weight
546                retention_w, // retention_weight
547            );
548            assert!(score <= 10000); // score is always >= 0 due to u16 type
549        }
550    }
551}
552
553#[cfg(test)]
554mod date_epoch_tests {
555    use super::*;
556    use crate::consts::{EPOCH_DURATION, START_AT};
557
558    #[test]
559    fn test_timestamp_to_epoch() {
560        // Test START_AT timestamp returns epoch 0
561        assert_eq!(sdk::timestamp_to_epoch(START_AT), 0);
562
563        // Test timestamp before START_AT returns epoch 0
564        assert_eq!(sdk::timestamp_to_epoch(START_AT - 1), 0);
565
566        // Test START_AT + 24 hours returns epoch 1
567        assert_eq!(sdk::timestamp_to_epoch(START_AT + EPOCH_DURATION), 1);
568
569        // Test START_AT + 48 hours returns epoch 2
570        assert_eq!(sdk::timestamp_to_epoch(START_AT + 2 * EPOCH_DURATION), 2);
571    }
572
573    #[test]
574    fn test_epoch_to_timestamp() {
575        // Calculate the start of the UTC day containing START_AT
576        let start_day_beginning = (START_AT / EPOCH_DURATION) * EPOCH_DURATION;
577
578        // Test epoch 0 returns start of UTC day containing START_AT
579        assert_eq!(sdk::epoch_to_timestamp(0), start_day_beginning);
580
581        // Test epoch 1 returns start_day_beginning + EPOCH_DURATION
582        assert_eq!(
583            sdk::epoch_to_timestamp(1),
584            start_day_beginning + EPOCH_DURATION
585        );
586
587        // Test epoch 2 returns start_day_beginning + 2 * EPOCH_DURATION
588        assert_eq!(
589            sdk::epoch_to_timestamp(2),
590            start_day_beginning + 2 * EPOCH_DURATION
591        );
592    }
593
594    #[test]
595    fn test_date_range_to_epochs() {
596        // Test single day (same start and end date)
597        let epochs: Vec<u64> = sdk::date_range_to_epochs(START_AT, START_AT);
598        assert_eq!(epochs, vec![0u64]);
599
600        // Test two days
601        let epochs: Vec<u64> = sdk::date_range_to_epochs(START_AT, START_AT + EPOCH_DURATION);
602        assert_eq!(epochs, vec![0u64, 1u64]);
603
604        // Test three days
605        let epochs: Vec<u64> = sdk::date_range_to_epochs(START_AT, START_AT + 2 * EPOCH_DURATION);
606        assert_eq!(epochs, vec![0u64, 1u64, 2u64]);
607
608        // Test invalid range (start > end)
609        let epochs: Vec<u64> = sdk::date_range_to_epochs(START_AT + EPOCH_DURATION, START_AT);
610        assert_eq!(epochs, vec![] as Vec<u64>);
611    }
612
613    #[test]
614    fn test_round_trip_conversion() {
615        // Test round trip: timestamp -> epoch -> timestamp
616        // Use a timestamp that aligns with the new epoch boundary
617        let start_day_beginning = (START_AT / EPOCH_DURATION) * EPOCH_DURATION;
618        let original_timestamp = start_day_beginning + 5 * EPOCH_DURATION;
619        let epoch = sdk::timestamp_to_epoch(original_timestamp);
620        let converted_timestamp = sdk::epoch_to_timestamp(epoch);
621        assert_eq!(original_timestamp, converted_timestamp);
622
623        // Test round trip: epoch -> timestamp -> epoch
624        let original_epoch = 10;
625        let timestamp = sdk::epoch_to_timestamp(original_epoch);
626        let converted_epoch = sdk::timestamp_to_epoch(timestamp);
627        assert_eq!(original_epoch, converted_epoch);
628    }
629}
630
631#[cfg(test)]
632mod alignment_tests {
633    // Note: History struct alignment test removed since History account no longer exists
634    // The History account was completely removed to save storage costs
635    // Historical data is embedded in SealProof structures for verification
636}