1use anchor_lang::{
2 prelude::borsh, solana_program::pubkey::Pubkey, AnchorDeserialize, AnchorSerialize,
3};
4use light_client::rpc::{Rpc, RpcError};
5use light_compressed_account::TreeType;
6use light_registry::{
7 protocol_config::state::{EpochState, ProtocolConfig},
8 sdk::{create_register_forester_epoch_pda_instruction, create_report_work_instruction},
9 utils::{get_epoch_pda_address, get_forester_epoch_pda_from_authority},
10 EpochPda, ForesterEpochPda,
11};
12use solana_sdk::signature::{Keypair, Signature, Signer};
13
14use crate::error::ForesterUtilsError;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ForesterSlot {
23 pub slot: u64,
24 pub start_solana_slot: u64,
25 pub end_solana_slot: u64,
26 pub forester_index: u64,
27}
28
29#[derive(Debug, Default, Clone, PartialEq, Eq)]
30pub struct Forester {
31 pub registration: Epoch,
32 pub active: Epoch,
33 pub report_work: Epoch,
34}
35
36impl Forester {
37 pub fn switch_to_report_work(&mut self) {
38 self.report_work = self.active.clone();
39 self.active = self.registration.clone();
40 }
41
42 pub async fn report_work(
43 &mut self,
44 rpc: &mut impl Rpc,
45 forester_keypair: &Keypair,
46 derivation: &Pubkey,
47 ) -> Result<Signature, RpcError> {
48 let ix = create_report_work_instruction(
49 &forester_keypair.pubkey(),
50 derivation,
51 self.report_work.epoch,
52 );
53 rpc.create_and_send_transaction(&[ix], &forester_keypair.pubkey(), &[forester_keypair])
54 .await
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct TreeAccounts {
60 pub merkle_tree: Pubkey,
61 pub queue: Pubkey,
62 pub is_rolledover: bool,
64 pub tree_type: TreeType,
65}
66
67impl TreeAccounts {
68 pub fn new(
69 merkle_tree: Pubkey,
70 queue: Pubkey,
71 tree_type: TreeType,
72 is_rolledover: bool,
73 ) -> Self {
74 Self {
75 merkle_tree,
76 queue,
77 tree_type,
78 is_rolledover,
79 }
80 }
81}
82
83pub fn get_schedule_for_queue(
84 mut start_solana_slot: u64,
85 queue_pubkey: &Pubkey,
86 protocol_config: &ProtocolConfig,
87 total_epoch_weight: u64,
88 epoch: u64,
89 current_phase_start_slot: u64,
90) -> Result<Vec<Option<ForesterSlot>>, ForesterUtilsError> {
91 let mut vec = Vec::new();
92
93 let current_light_slot = if start_solana_slot >= current_phase_start_slot {
94 (start_solana_slot - current_phase_start_slot) / protocol_config.slot_length
95 } else {
96 return Err(ForesterUtilsError::InvalidSlotNumber);
97 };
98
99 let start_slot = current_light_slot;
100 start_solana_slot =
101 current_phase_start_slot + (current_light_slot * protocol_config.slot_length);
102 let end_slot = protocol_config.active_phase_length / protocol_config.slot_length;
103
104 for light_slot in start_slot..end_slot {
105 let forester_index = ForesterEpochPda::get_eligible_forester_index(
106 light_slot,
107 queue_pubkey,
108 total_epoch_weight,
109 epoch,
110 )
111 .unwrap();
112 vec.push(Some(ForesterSlot {
113 slot: light_slot,
114 start_solana_slot,
115 end_solana_slot: start_solana_slot + protocol_config.slot_length,
116 forester_index,
117 }));
118 start_solana_slot += protocol_config.slot_length;
119 }
120 Ok(vec)
121}
122
123pub fn get_schedule_for_forester_in_queue(
124 start_solana_slot: u64,
125 queue_pubkey: &Pubkey,
126 total_epoch_weight: u64,
127 forester_epoch_pda: &ForesterEpochPda,
128) -> Result<Vec<Option<ForesterSlot>>, ForesterUtilsError> {
129 let mut slots = get_schedule_for_queue(
130 start_solana_slot,
131 queue_pubkey,
132 &forester_epoch_pda.protocol_config,
133 total_epoch_weight,
134 forester_epoch_pda.epoch,
135 forester_epoch_pda.epoch_active_phase_start_slot,
136 )?;
137 slots.iter_mut().for_each(|slot_option| {
138 if let Some(slot) = slot_option {
139 if !forester_epoch_pda.is_eligible(slot.forester_index) {
140 *slot_option = None;
141 }
142 }
143 });
144 Ok(slots)
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct TreeForesterSchedule {
149 pub tree_accounts: TreeAccounts,
150 pub slots: Vec<Option<ForesterSlot>>,
153}
154
155impl TreeForesterSchedule {
156 pub fn new(tree_accounts: TreeAccounts) -> Self {
157 Self {
158 tree_accounts,
159 slots: Vec::new(),
160 }
161 }
162
163 pub fn new_with_schedule(
164 tree_accounts: &TreeAccounts,
165 solana_slot: u64,
166 forester_epoch_pda: &ForesterEpochPda,
167 epoch_pda: &EpochPda,
168 ) -> Result<Self, ForesterUtilsError> {
169 let mut _self = Self {
170 tree_accounts: *tree_accounts,
171 slots: Vec::new(),
172 };
173 _self.slots = get_schedule_for_forester_in_queue(
174 solana_slot,
175 &_self.tree_accounts.queue,
176 epoch_pda.registered_weight,
177 forester_epoch_pda,
178 )?;
179 Ok(_self)
180 }
181
182 pub fn is_eligible(&self, forester_slot: u64) -> bool {
183 self.slots[forester_slot as usize].is_some()
184 }
185}
186
187#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)]
188pub struct EpochPhases {
189 pub registration: Phase,
190 pub active: Phase,
191 pub report_work: Phase,
192 pub post: Phase,
193}
194
195impl EpochPhases {
196 pub fn get_current_phase(&self, current_slot: u64) -> Phase {
197 if current_slot >= self.registration.start && current_slot <= self.registration.end {
198 self.registration.clone()
199 } else if current_slot >= self.active.start && current_slot <= self.active.end {
200 self.active.clone()
201 } else if current_slot >= self.report_work.start && current_slot <= self.report_work.end {
202 self.report_work.clone()
203 } else {
204 self.post.clone()
205 }
206 }
207 pub fn get_current_epoch_state(&self, current_slot: u64) -> EpochState {
208 if current_slot >= self.registration.start && current_slot <= self.registration.end {
209 EpochState::Registration
210 } else if current_slot >= self.active.start && current_slot <= self.active.end {
211 EpochState::Active
212 } else if current_slot >= self.report_work.start && current_slot <= self.report_work.end {
213 EpochState::ReportWork
214 } else {
215 EpochState::Post
216 }
217 }
218}
219
220#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)]
221pub struct Phase {
222 pub start: u64,
223 pub end: u64,
224}
225
226impl Phase {
227 pub fn length(&self) -> u64 {
228 self.end - self.start
229 }
230}
231
232pub fn get_epoch_phases(protocol_config: &ProtocolConfig, epoch: u64) -> EpochPhases {
233 let epoch_start_slot = protocol_config
234 .genesis_slot
235 .saturating_add(epoch.saturating_mul(protocol_config.active_phase_length));
236
237 let registration_start = epoch_start_slot;
238 let registration_end = registration_start
239 .saturating_add(protocol_config.registration_phase_length)
240 .saturating_sub(1);
241
242 let active_start = registration_end.saturating_add(1);
243 let active_end = active_start
244 .saturating_add(protocol_config.active_phase_length)
245 .saturating_sub(1);
246
247 let report_work_start = active_end.saturating_add(1);
248 let report_work_end = report_work_start
249 .saturating_add(protocol_config.report_work_phase_length)
250 .saturating_sub(1);
251
252 let post_start = report_work_end.saturating_add(1);
253 let post_end = u64::MAX;
254
255 EpochPhases {
256 registration: Phase {
257 start: registration_start,
258 end: registration_end,
259 },
260 active: Phase {
261 start: active_start,
262 end: active_end,
263 },
264 report_work: Phase {
265 start: report_work_start,
266 end: report_work_end,
267 },
268 post: Phase {
269 start: post_start,
270 end: post_end,
271 },
272 }
273}
274
275#[derive(Debug, Clone, Default, PartialEq, Eq)]
276pub struct Epoch {
277 pub epoch: u64,
278 pub epoch_pda: Pubkey,
279 pub forester_epoch_pda: Pubkey,
280 pub phases: EpochPhases,
281 pub state: EpochState,
282 pub merkle_trees: Vec<TreeForesterSchedule>,
283}
284
285#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, Default, PartialEq, Eq)]
286pub struct EpochRegistration {
287 pub epoch: u64,
288 pub slots_until_registration_starts: u64,
289 pub slots_until_registration_ends: u64,
290}
291
292impl Epoch {
293 pub async fn slots_until_next_epoch_registration<R: Rpc>(
296 rpc: &mut R,
297 protocol_config: &ProtocolConfig,
298 ) -> Result<EpochRegistration, RpcError> {
299 let current_solana_slot = rpc.get_slot().await?;
300
301 let mut epoch = protocol_config
302 .get_latest_register_epoch(current_solana_slot)
303 .unwrap();
304 let registration_start_slot =
305 protocol_config.genesis_slot + epoch * protocol_config.active_phase_length;
306
307 let registration_end_slot =
308 registration_start_slot + protocol_config.registration_phase_length;
309 if current_solana_slot > registration_end_slot {
310 epoch += 1;
311 }
312 let next_registration_start_slot =
313 protocol_config.genesis_slot + epoch * protocol_config.active_phase_length;
314 let next_registration_end_slot =
315 next_registration_start_slot + protocol_config.registration_phase_length;
316 let slots_until_registration_ends =
317 next_registration_end_slot.saturating_sub(current_solana_slot);
318 let slots_until_registration_starts =
319 next_registration_start_slot.saturating_sub(current_solana_slot);
320 Ok(EpochRegistration {
321 epoch,
322 slots_until_registration_starts,
323 slots_until_registration_ends,
324 })
325 }
326
327 pub async fn register<R: Rpc>(
329 rpc: &mut R,
330 protocol_config: &ProtocolConfig,
331 authority: &Keypair,
332 derivation: &Pubkey,
333 ) -> Result<Option<Epoch>, RpcError> {
334 let epoch_registration =
335 Self::slots_until_next_epoch_registration(rpc, protocol_config).await?;
336 if epoch_registration.slots_until_registration_starts > 0
337 || epoch_registration.slots_until_registration_ends == 0
338 {
339 return Ok(None);
340 }
341
342 let instruction = create_register_forester_epoch_pda_instruction(
343 &authority.pubkey(),
344 derivation,
345 epoch_registration.epoch,
346 );
347 let signature = rpc
348 .create_and_send_transaction(&[instruction], &authority.pubkey(), &[authority])
349 .await?;
350 rpc.confirm_transaction(signature).await?;
351 let epoch_pda_pubkey = get_epoch_pda_address(epoch_registration.epoch);
352 let epoch_pda = rpc
353 .get_anchor_account::<EpochPda>(&epoch_pda_pubkey)
354 .await?
355 .unwrap();
356 let forester_epoch_pda_pubkey =
357 get_forester_epoch_pda_from_authority(derivation, epoch_registration.epoch).0;
358
359 let phases = get_epoch_phases(protocol_config, epoch_pda.epoch);
360 Ok(Some(Self {
361 epoch_pda: epoch_pda_pubkey,
363 forester_epoch_pda: forester_epoch_pda_pubkey,
364 merkle_trees: Vec::new(),
365 epoch: epoch_pda.epoch,
366 state: phases.get_current_epoch_state(rpc.get_slot().await?),
367 phases,
368 }))
369 }
370 pub fn fetch_registered() {}
374
375 pub async fn fetch_account_and_add_trees_with_schedule<R: Rpc>(
376 &mut self,
377 rpc: &mut R,
378 trees: &[TreeAccounts],
379 ) -> Result<(), RpcError> {
380 let current_solana_slot = rpc.get_slot().await?;
381
382 if self.phases.active.end < current_solana_slot
383 || self.phases.active.start > current_solana_slot
384 {
385 println!("current_solana_slot {:?}", current_solana_slot);
386 println!("registration phase {:?}", self.phases.registration);
387 println!("active phase {:?}", self.phases.active);
388 panic!("TODO: throw epoch not active error");
390 }
391 let epoch_pda = rpc
392 .get_anchor_account::<EpochPda>(&self.epoch_pda)
393 .await?
394 .unwrap();
395 let mut forester_epoch_pda = rpc
396 .get_anchor_account::<ForesterEpochPda>(&self.forester_epoch_pda)
397 .await?
398 .unwrap();
399 if forester_epoch_pda.total_epoch_weight.is_none() {
401 forester_epoch_pda.total_epoch_weight = Some(epoch_pda.registered_weight);
402 }
403 self.add_trees_with_schedule(&forester_epoch_pda, &epoch_pda, trees, current_solana_slot)
404 .map_err(|e| {
405 println!("Error adding trees with schedule: {:?}", e);
406 RpcError::AssertRpcError("Error adding trees with schedule".to_string())
407 })?;
408 Ok(())
409 }
410 pub fn add_trees_with_schedule(
415 &mut self,
416 forester_epoch_pda: &ForesterEpochPda,
417 epoch_pda: &EpochPda,
418 trees: &[TreeAccounts],
419 current_solana_slot: u64,
420 ) -> Result<(), ForesterUtilsError> {
421 for tree in trees {
423 let tree_schedule = TreeForesterSchedule::new_with_schedule(
424 tree,
425 current_solana_slot,
426 forester_epoch_pda,
427 epoch_pda,
428 )?;
429 self.merkle_trees.push(tree_schedule);
430 }
431 Ok(())
432 }
433
434 pub fn update_state(&mut self, current_solana_slot: u64) -> EpochState {
435 let current_state = self.phases.get_current_epoch_state(current_solana_slot);
436 if current_state != self.state {
437 self.state = current_state.clone();
438 }
439 current_state
440 }
441
442 pub fn execute_active_phase() {}
452
453 pub fn execute_report_work_phase() {}
458 pub fn execute_post_phase() {}
463}
464
465#[cfg(test)]
466mod test {
467 use super::*;
468
469 #[test]
470 fn test_epoch_phases() {
471 let config = ProtocolConfig {
472 genesis_slot: 200,
473 min_weight: 0,
474 slot_length: 10,
475 registration_phase_length: 100,
476 active_phase_length: 1000,
477 report_work_phase_length: 100,
478 network_fee: 5000,
479 ..Default::default()
480 };
481
482 let epoch = 1;
483 let phases = get_epoch_phases(&config, epoch);
484
485 assert_eq!(phases.registration.start, 1200);
486 assert_eq!(phases.registration.end, 1299);
487
488 assert_eq!(phases.active.start, 1300);
489 assert_eq!(phases.active.end, 2299);
490
491 assert_eq!(phases.report_work.start, 2300);
492 assert_eq!(phases.report_work.end, 2399);
493
494 assert_eq!(phases.post.start, 2400);
495 assert_eq!(phases.post.end, u64::MAX);
496 }
497
498 #[test]
499 fn test_get_schedule_for_queue() {
500 let protocol_config = ProtocolConfig {
501 genesis_slot: 0,
502 min_weight: 100,
503 slot_length: 10,
504 registration_phase_length: 100,
505 active_phase_length: 1000,
506 report_work_phase_length: 100,
507 network_fee: 5000,
508 ..Default::default()
509 };
510
511 let total_epoch_weight = 500;
512 let queue_pubkey = Pubkey::new_unique();
513 let start_solana_slot = 0;
514 let epoch = 0;
515 let current_phase_start_slot = 0;
516
517 let schedule = get_schedule_for_queue(
518 start_solana_slot,
519 &queue_pubkey,
520 &protocol_config,
521 total_epoch_weight,
522 epoch,
523 current_phase_start_slot,
524 )
525 .unwrap();
526
527 let expected_light_slots =
529 (protocol_config.active_phase_length / protocol_config.slot_length) as usize;
530 assert_eq!(schedule.len(), expected_light_slots); assert_eq!(
533 schedule.len(),
534 (protocol_config.active_phase_length / protocol_config.slot_length) as usize
535 );
536
537 for (i, slot_option) in schedule.iter().enumerate() {
538 let slot = slot_option.as_ref().unwrap();
539 assert_eq!(slot.slot, i as u64);
540 assert_eq!(
541 slot.start_solana_slot,
542 start_solana_slot + (i as u64 * protocol_config.slot_length)
543 );
544 assert_eq!(
545 slot.end_solana_slot,
546 slot.start_solana_slot + protocol_config.slot_length
547 );
548 assert!(slot.forester_index < total_epoch_weight);
549 }
550 }
551
552 #[test]
553 fn test_get_schedule_for_queue_offset_phase_start() {
554 let protocol_config = ProtocolConfig {
555 genesis_slot: 1000, min_weight: 100,
557 slot_length: 10,
558 registration_phase_length: 100,
559 active_phase_length: 1000, report_work_phase_length: 100,
561 network_fee: 5000,
562 ..Default::default()
563 };
564
565 let total_epoch_weight = 500;
566 let queue_pubkey = Pubkey::new_unique();
567 let epoch = 0;
568
569 let current_phase_start_slot = 1100;
573
574 let start_solana_slot = current_phase_start_slot;
576
577 let schedule = get_schedule_for_queue(
578 start_solana_slot,
579 &queue_pubkey,
580 &protocol_config,
581 total_epoch_weight,
582 epoch,
583 current_phase_start_slot, )
585 .unwrap();
586
587 let expected_light_slots =
588 (protocol_config.active_phase_length / protocol_config.slot_length) as usize;
589 assert_eq!(schedule.len(), expected_light_slots); let first_slot = schedule[0].as_ref().unwrap();
593 assert_eq!(first_slot.slot, 0); assert_eq!(first_slot.start_solana_slot, current_phase_start_slot);
596 assert_eq!(
597 first_slot.end_solana_slot,
598 current_phase_start_slot + protocol_config.slot_length
599 );
600
601 let second_slot = schedule[1].as_ref().unwrap();
603 assert_eq!(second_slot.slot, 1); assert_eq!(
606 second_slot.start_solana_slot,
607 current_phase_start_slot + protocol_config.slot_length
608 );
609 assert_eq!(
610 second_slot.end_solana_slot,
611 current_phase_start_slot + 2 * protocol_config.slot_length
612 );
613 }
614
615 #[test]
617 fn test_get_schedule_for_queue_mid_phase_start() {
618 let protocol_config = ProtocolConfig {
619 genesis_slot: 0,
620 min_weight: 100,
621 slot_length: 10,
622 registration_phase_length: 100, active_phase_length: 1000, report_work_phase_length: 100,
625 network_fee: 5000,
626 ..Default::default()
627 };
628
629 let total_epoch_weight = 500;
630 let queue_pubkey = Pubkey::new_unique();
631 let epoch = 0;
632 let current_phase_start_slot = 100; let start_solana_slot = 155;
636
637 let schedule = get_schedule_for_queue(
644 start_solana_slot,
645 &queue_pubkey,
646 &protocol_config,
647 total_epoch_weight,
648 epoch,
649 current_phase_start_slot,
650 )
651 .unwrap();
652
653 let expected_light_slots_total =
654 protocol_config.active_phase_length / protocol_config.slot_length; let expected_start_light_slot = 5;
656 let expected_schedule_len =
657 (expected_light_slots_total - expected_start_light_slot) as usize; assert_eq!(schedule.len(), expected_schedule_len); let first_returned_slot = schedule[0].as_ref().unwrap();
663 assert_eq!(first_returned_slot.slot, expected_start_light_slot); let expected_first_solana_start =
666 current_phase_start_slot + expected_start_light_slot * protocol_config.slot_length; assert_eq!(
668 first_returned_slot.start_solana_slot,
669 expected_first_solana_start
670 );
671 assert_eq!(
672 first_returned_slot.end_solana_slot,
673 expected_first_solana_start + protocol_config.slot_length );
675
676 let second_returned_slot = schedule[1].as_ref().unwrap();
678 assert_eq!(second_returned_slot.slot, expected_start_light_slot + 1); assert_eq!(
681 second_returned_slot.start_solana_slot,
682 expected_first_solana_start + protocol_config.slot_length
683 );
684 assert_eq!(
685 second_returned_slot.end_solana_slot,
686 expected_first_solana_start + 2 * protocol_config.slot_length );
688 }
689}