light_program_test/
registry_sdk.rs

1//! Local registry SDK for program-test.
2//!
3//! This module provides the minimal registry program SDK functionality needed
4//! for forester and compressible modules without requiring the `devenv` feature.
5//! It reimplements the necessary constants, PDA derivation, type definitions,
6//! and instruction builders locally to avoid anchor program dependencies.
7
8use borsh::{BorshDeserialize, BorshSerialize};
9use solana_pubkey::Pubkey;
10use solana_sdk::instruction::{AccountMeta, Instruction};
11
12// ============================================================================
13// Program IDs
14// ============================================================================
15
16/// Registry program ID
17pub const REGISTRY_PROGRAM_ID: Pubkey =
18    solana_pubkey::pubkey!("Lighton6oQpVkeewmo2mcPTQQp7kYHr4fWpAgJyEmDX");
19
20// ============================================================================
21// PDA Seeds
22// ============================================================================
23
24pub const FORESTER_SEED: &[u8] = b"forester";
25pub const FORESTER_EPOCH_SEED: &[u8] = b"forester_epoch";
26pub const PROTOCOL_CONFIG_PDA_SEED: &[u8] = b"authority";
27
28// ============================================================================
29// Instruction Discriminators (from discriminator test)
30// ============================================================================
31
32/// Claim instruction discriminator
33pub const CLAIM_DISCRIMINATOR: [u8; 8] = [62, 198, 214, 193, 213, 159, 108, 210];
34
35/// CompressAndClose instruction discriminator
36pub const COMPRESS_AND_CLOSE_DISCRIMINATOR: [u8; 8] = [96, 94, 135, 18, 121, 42, 213, 117];
37
38/// RegisterForester instruction discriminator
39pub const REGISTER_FORESTER_DISCRIMINATOR: [u8; 8] = [62, 47, 240, 103, 84, 200, 226, 73];
40
41/// RegisterForesterEpoch instruction discriminator
42pub const REGISTER_FORESTER_EPOCH_DISCRIMINATOR: [u8; 8] = [43, 120, 253, 194, 109, 192, 101, 188];
43
44/// FinalizeRegistration instruction discriminator
45pub const FINALIZE_REGISTRATION_DISCRIMINATOR: [u8; 8] = [230, 188, 172, 96, 204, 247, 98, 227];
46
47/// ReportWork instruction discriminator
48#[allow(dead_code)]
49pub const REPORT_WORK_DISCRIMINATOR: [u8; 8] = [170, 110, 232, 47, 145, 213, 138, 162];
50
51// ============================================================================
52// Account Discriminators (for direct account serialization)
53// ============================================================================
54
55/// ProtocolConfigPda account discriminator
56pub const PROTOCOL_CONFIG_PDA_DISCRIMINATOR: [u8; 8] = [96, 176, 239, 146, 1, 254, 99, 146];
57
58/// ForesterPda account discriminator
59pub const FORESTER_PDA_DISCRIMINATOR: [u8; 8] = [51, 47, 187, 86, 82, 153, 117, 5];
60
61/// ForesterEpochPda account discriminator
62pub const FORESTER_EPOCH_PDA_DISCRIMINATOR: [u8; 8] = [29, 117, 211, 141, 99, 143, 250, 114];
63
64/// EpochPda account discriminator
65pub const EPOCH_PDA_DISCRIMINATOR: [u8; 8] = [66, 224, 46, 2, 167, 137, 120, 107];
66
67// ============================================================================
68// PDA Derivation Functions
69// ============================================================================
70
71/// Derives the protocol config PDA address.
72pub fn get_protocol_config_pda_address() -> (Pubkey, u8) {
73    Pubkey::find_program_address(&[PROTOCOL_CONFIG_PDA_SEED], &REGISTRY_PROGRAM_ID)
74}
75
76/// Derives the forester PDA for a given authority.
77pub fn get_forester_pda(authority: &Pubkey) -> (Pubkey, u8) {
78    Pubkey::find_program_address(&[FORESTER_SEED, authority.as_ref()], &REGISTRY_PROGRAM_ID)
79}
80
81/// Derives the forester epoch PDA from forester PDA and epoch.
82pub fn get_forester_epoch_pda(forester_pda: &Pubkey, epoch: u64) -> (Pubkey, u8) {
83    Pubkey::find_program_address(
84        &[
85            FORESTER_EPOCH_SEED,
86            forester_pda.as_ref(),
87            epoch.to_le_bytes().as_slice(),
88        ],
89        &REGISTRY_PROGRAM_ID,
90    )
91}
92
93/// Derives the forester epoch PDA from authority and epoch.
94pub fn get_forester_epoch_pda_from_authority(authority: &Pubkey, epoch: u64) -> (Pubkey, u8) {
95    let forester_pda = get_forester_pda(authority);
96    get_forester_epoch_pda(&forester_pda.0, epoch)
97}
98
99/// Derives the epoch PDA address for a given epoch.
100pub fn get_epoch_pda_address(epoch: u64) -> Pubkey {
101    Pubkey::find_program_address(&[&epoch.to_le_bytes()], &REGISTRY_PROGRAM_ID).0
102}
103
104// ============================================================================
105// Type Definitions
106// ============================================================================
107
108/// Configuration for a forester.
109#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
110pub struct ForesterConfig {
111    /// Fee in percentage points.
112    pub fee: u64,
113}
114
115/// Forester PDA account structure.
116#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
117pub struct ForesterPda {
118    pub authority: Pubkey,
119    pub config: ForesterConfig,
120    pub active_weight: u64,
121    /// Pending weight which will get active once the next epoch starts.
122    pub pending_weight: u64,
123    pub current_epoch: u64,
124    /// Link to previous compressed forester epoch account hash.
125    pub last_compressed_forester_epoch_pda_hash: [u8; 32],
126    pub last_registered_epoch: u64,
127}
128
129/// Protocol configuration.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
131pub struct ProtocolConfig {
132    /// Solana slot when the protocol starts operating.
133    pub genesis_slot: u64,
134    /// Minimum weight required for a forester to register to an epoch.
135    pub min_weight: u64,
136    /// Light protocol slot length
137    pub slot_length: u64,
138    /// Foresters can register for this phase.
139    pub registration_phase_length: u64,
140    /// Foresters can perform work in this phase.
141    pub active_phase_length: u64,
142    /// Foresters can report work to receive performance based rewards in this phase.
143    pub report_work_phase_length: u64,
144    pub network_fee: u64,
145    pub cpi_context_size: u64,
146    pub finalize_counter_limit: u64,
147    /// Placeholder for future protocol updates.
148    pub place_holder: Pubkey,
149    pub address_network_fee: u64,
150    pub place_holder_b: u64,
151    pub place_holder_c: u64,
152    pub place_holder_d: u64,
153    pub place_holder_e: u64,
154    pub place_holder_f: u64,
155}
156
157impl Default for ProtocolConfig {
158    fn default() -> Self {
159        Self {
160            genesis_slot: 0,
161            min_weight: 1,
162            slot_length: 10,
163            registration_phase_length: 100,
164            active_phase_length: 1000,
165            report_work_phase_length: 100,
166            network_fee: 5000,
167            cpi_context_size: 20 * 1024 + 8, // DEFAULT_CPI_CONTEXT_ACCOUNT_SIZE_V2
168            finalize_counter_limit: 100,
169            place_holder: Pubkey::default(),
170            address_network_fee: 10000,
171            place_holder_b: 0,
172            place_holder_c: 0,
173            place_holder_d: 0,
174            place_holder_e: 0,
175            place_holder_f: 0,
176        }
177    }
178}
179
180/// Protocol config PDA account structure.
181/// Includes Anchor's 8-byte discriminator at the start.
182#[derive(Debug, BorshDeserialize)]
183pub struct ProtocolConfigPda {
184    pub authority: Pubkey,
185    pub bump: u8,
186    pub config: ProtocolConfig,
187}
188
189/// Indices for CompressAndClose operation (matches registry program's definition).
190#[derive(Debug, Copy, Clone, BorshSerialize, BorshDeserialize)]
191pub struct CompressAndCloseIndices {
192    pub source_index: u8,
193    pub mint_index: u8,
194    pub owner_index: u8,
195    pub rent_sponsor_index: u8,
196    pub delegate_index: u8,
197}
198
199// ============================================================================
200// Instruction Builders
201// ============================================================================
202
203/// Builds the Claim instruction.
204///
205/// # Accounts (in order)
206/// - authority (signer, writable)
207/// - registered_forester_pda (writable)
208/// - rent_sponsor (writable)
209/// - compression_authority (read-only)
210/// - compressible_config (read-only)
211/// - compressed_token_program (read-only)
212/// - token_accounts (writable, remaining)
213pub fn build_claim_instruction(
214    authority: Pubkey,
215    registered_forester_pda: Pubkey,
216    rent_sponsor: Pubkey,
217    compression_authority: Pubkey,
218    compressible_config: Pubkey,
219    compressed_token_program: Pubkey,
220    token_accounts: &[Pubkey],
221) -> Instruction {
222    let mut accounts = vec![
223        AccountMeta::new(authority, true),
224        AccountMeta::new(registered_forester_pda, false),
225        AccountMeta::new(rent_sponsor, false),
226        AccountMeta::new_readonly(compression_authority, false),
227        AccountMeta::new_readonly(compressible_config, false),
228        AccountMeta::new_readonly(compressed_token_program, false),
229    ];
230
231    for token_account in token_accounts {
232        accounts.push(AccountMeta::new(*token_account, false));
233    }
234
235    Instruction {
236        program_id: REGISTRY_PROGRAM_ID,
237        accounts,
238        data: CLAIM_DISCRIMINATOR.to_vec(),
239    }
240}
241
242/// Builds the CompressAndClose instruction.
243///
244/// # Accounts (in order)
245/// - authority (signer, writable)
246/// - registered_forester_pda (writable)
247/// - compression_authority (writable)
248/// - compressible_config (read-only)
249/// - remaining_accounts
250#[allow(clippy::too_many_arguments)]
251pub fn build_compress_and_close_instruction(
252    authority: Pubkey,
253    registered_forester_pda: Pubkey,
254    compression_authority: Pubkey,
255    compressible_config: Pubkey,
256    authority_index: u8,
257    destination_index: u8,
258    indices: Vec<CompressAndCloseIndices>,
259    remaining_accounts: Vec<AccountMeta>,
260) -> Instruction {
261    let mut accounts = vec![
262        AccountMeta::new(authority, true),
263        AccountMeta::new(registered_forester_pda, false),
264        AccountMeta::new(compression_authority, false),
265        AccountMeta::new_readonly(compressible_config, false),
266    ];
267    accounts.extend(remaining_accounts);
268
269    // Serialize instruction data: discriminator + authority_index + destination_index + indices vec
270    let mut data = COMPRESS_AND_CLOSE_DISCRIMINATOR.to_vec();
271    data.push(authority_index);
272    data.push(destination_index);
273    // Borsh serialize the indices vector
274    indices.serialize(&mut data).unwrap();
275
276    Instruction {
277        program_id: REGISTRY_PROGRAM_ID,
278        accounts,
279        data,
280    }
281}
282
283/// Builds the RegisterForester instruction.
284///
285/// # Accounts (in order):
286/// 1. fee_payer (signer, writable)
287/// 2. authority (signer)
288/// 3. protocol_config_pda (read-only)
289/// 4. forester_pda (writable, init)
290/// 5. system_program (read-only)
291pub fn create_register_forester_instruction(
292    fee_payer: &Pubkey,
293    governance_authority: &Pubkey,
294    forester_authority: &Pubkey,
295    config: ForesterConfig,
296) -> Instruction {
297    let (forester_pda, bump) = get_forester_pda(forester_authority);
298    let (protocol_config_pda, _) = get_protocol_config_pda_address();
299
300    let accounts = vec![
301        AccountMeta::new(*fee_payer, true),
302        AccountMeta::new_readonly(*governance_authority, true),
303        AccountMeta::new_readonly(protocol_config_pda, false),
304        AccountMeta::new(forester_pda, false),
305        AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
306    ];
307
308    // Instruction data: discriminator + bump + authority (pubkey) + config + weight (Option<u64>)
309    let mut data = REGISTER_FORESTER_DISCRIMINATOR.to_vec();
310    data.push(bump);
311    data.extend_from_slice(forester_authority.as_ref());
312    config.serialize(&mut data).unwrap();
313    // weight: Some(1) encoded as Option<u64>
314    data.push(1u8); // Some variant
315    data.extend_from_slice(&1u64.to_le_bytes()); // weight = 1
316
317    Instruction {
318        program_id: REGISTRY_PROGRAM_ID,
319        accounts,
320        data,
321    }
322}
323
324/// Builds the RegisterForesterEpoch instruction.
325///
326/// # Accounts (in order):
327/// 1. fee_payer (signer, writable)
328/// 2. forester_epoch_pda (writable, init)
329/// 3. forester_pda (read-only)
330/// 4. authority (signer)
331/// 5. epoch_pda (writable, init_if_needed)
332/// 6. protocol_config (read-only)
333/// 7. system_program (read-only)
334pub fn create_register_forester_epoch_pda_instruction(
335    authority: &Pubkey,
336    derivation: &Pubkey,
337    epoch: u64,
338) -> Instruction {
339    let (forester_epoch_pda, _bump) = get_forester_epoch_pda_from_authority(derivation, epoch);
340    let (forester_pda, _) = get_forester_pda(derivation);
341    let epoch_pda = get_epoch_pda_address(epoch);
342    let protocol_config_pda = get_protocol_config_pda_address().0;
343
344    let accounts = vec![
345        AccountMeta::new(*authority, true),                    // fee_payer
346        AccountMeta::new(forester_epoch_pda, false),           // forester_epoch_pda
347        AccountMeta::new_readonly(forester_pda, false),        // forester_pda
348        AccountMeta::new_readonly(*authority, true),           // authority
349        AccountMeta::new(epoch_pda, false),                    // epoch_pda
350        AccountMeta::new_readonly(protocol_config_pda, false), // protocol_config
351        AccountMeta::new_readonly(solana_sdk::system_program::id(), false), // system_program
352    ];
353
354    // Instruction data: discriminator + epoch (u64)
355    let mut data = REGISTER_FORESTER_EPOCH_DISCRIMINATOR.to_vec();
356    data.extend_from_slice(&epoch.to_le_bytes());
357
358    Instruction {
359        program_id: REGISTRY_PROGRAM_ID,
360        accounts,
361        data,
362    }
363}
364
365/// Builds the FinalizeRegistration instruction.
366///
367/// # Accounts (in order):
368/// 1. forester_epoch_pda (writable)
369/// 2. authority (signer)
370/// 3. epoch_pda (read-only)
371pub fn create_finalize_registration_instruction(
372    authority: &Pubkey,
373    derivation: &Pubkey,
374    epoch: u64,
375) -> Instruction {
376    let (forester_epoch_pda, _bump) = get_forester_epoch_pda_from_authority(derivation, epoch);
377    let epoch_pda = get_epoch_pda_address(epoch);
378
379    let accounts = vec![
380        AccountMeta::new(forester_epoch_pda, false),
381        AccountMeta::new_readonly(*authority, true),
382        AccountMeta::new_readonly(epoch_pda, false),
383    ];
384
385    Instruction {
386        program_id: REGISTRY_PROGRAM_ID,
387        accounts,
388        data: FINALIZE_REGISTRATION_DISCRIMINATOR.to_vec(),
389    }
390}
391
392// ============================================================================
393// Helper Functions
394// ============================================================================
395
396/// Deserializes a ProtocolConfigPda from account data.
397/// Skips the 8-byte Anchor discriminator automatically.
398pub fn deserialize_protocol_config_pda(data: &[u8]) -> Result<ProtocolConfigPda, std::io::Error> {
399    // Skip 8-byte Anchor discriminator
400    if data.len() < 8 {
401        return Err(std::io::Error::new(
402            std::io::ErrorKind::InvalidData,
403            "Account data too short for discriminator",
404        ));
405    }
406    ProtocolConfigPda::deserialize(&mut &data[8..])
407}
408
409/// Deserializes a ForesterPda from account data.
410/// Skips the 8-byte Anchor discriminator automatically.
411pub fn deserialize_forester_pda(data: &[u8]) -> Result<ForesterPda, std::io::Error> {
412    // Skip 8-byte Anchor discriminator
413    if data.len() < 8 {
414        return Err(std::io::Error::new(
415            std::io::ErrorKind::InvalidData,
416            "Account data too short for discriminator",
417        ));
418    }
419    ForesterPda::deserialize(&mut &data[8..])
420}
421
422// ============================================================================
423// ForesterEpochPda (for direct account serialization)
424// ============================================================================
425
426/// ForesterEpochPda account structure for serialization.
427#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
428pub struct ForesterEpochPda {
429    pub authority: Pubkey,
430    pub config: ForesterConfig,
431    pub epoch: u64,
432    pub weight: u64,
433    pub work_counter: u64,
434    pub has_reported_work: bool,
435    pub forester_index: u64,
436    pub epoch_active_phase_start_slot: u64,
437    pub total_epoch_weight: Option<u64>,
438    pub protocol_config: ProtocolConfig,
439    pub finalize_counter: u64,
440}
441
442/// EpochPda account structure for serialization.
443#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
444pub struct EpochPda {
445    pub epoch: u64,
446    pub protocol_config: ProtocolConfig,
447    pub total_work: u64,
448    pub registered_weight: u64,
449}
450
451// ============================================================================
452// Direct Account Serialization (for LiteSVM set_account)
453// ============================================================================
454
455/// Creates a ProtocolConfig with a very long active phase (effectively infinite).
456/// This allows any slot to be in epoch 0's active phase.
457pub fn protocol_config_for_tests() -> ProtocolConfig {
458    ProtocolConfig {
459        genesis_slot: 0,
460        min_weight: 1,
461        slot_length: 10,
462        registration_phase_length: 0, // No registration phase - always active
463        active_phase_length: u64::MAX / 2, // Very long active phase
464        report_work_phase_length: 0,
465        network_fee: 5000,
466        cpi_context_size: 20 * 1024 + 8,
467        finalize_counter_limit: u64::MAX,
468        place_holder: Pubkey::default(),
469        address_network_fee: 10000,
470        place_holder_b: 0,
471        place_holder_c: 0,
472        place_holder_d: 0,
473        place_holder_e: 0,
474        place_holder_f: 0,
475    }
476}
477
478/// Serializes a ProtocolConfigPda to account data with Anchor discriminator.
479pub fn serialize_protocol_config_pda(
480    authority: Pubkey,
481    bump: u8,
482    config: ProtocolConfig,
483) -> Vec<u8> {
484    let mut data = PROTOCOL_CONFIG_PDA_DISCRIMINATOR.to_vec();
485    authority.serialize(&mut data).unwrap();
486    data.push(bump);
487    config.serialize(&mut data).unwrap();
488    data
489}
490
491/// Serializes a ForesterPda to account data with Anchor discriminator.
492pub fn serialize_forester_pda(forester: &ForesterPda) -> Vec<u8> {
493    let mut data = FORESTER_PDA_DISCRIMINATOR.to_vec();
494    forester.authority.serialize(&mut data).unwrap();
495    forester.config.serialize(&mut data).unwrap();
496    forester.active_weight.serialize(&mut data).unwrap();
497    forester.pending_weight.serialize(&mut data).unwrap();
498    forester.current_epoch.serialize(&mut data).unwrap();
499    forester
500        .last_compressed_forester_epoch_pda_hash
501        .serialize(&mut data)
502        .unwrap();
503    forester.last_registered_epoch.serialize(&mut data).unwrap();
504    data
505}
506
507/// Serializes a ForesterEpochPda to account data with Anchor discriminator.
508pub fn serialize_forester_epoch_pda(epoch_pda: &ForesterEpochPda) -> Vec<u8> {
509    let mut data = FORESTER_EPOCH_PDA_DISCRIMINATOR.to_vec();
510    epoch_pda.serialize(&mut data).unwrap();
511    data
512}
513
514/// Serializes an EpochPda to account data with Anchor discriminator.
515pub fn serialize_epoch_pda(epoch_pda: &EpochPda) -> Vec<u8> {
516    let mut data = EPOCH_PDA_DISCRIMINATOR.to_vec();
517    epoch_pda.serialize(&mut data).unwrap();
518    data
519}
520
521/// Sets up protocol config, forester, and forester epoch accounts for testing.
522/// Uses a very long active phase so any slot is valid for epoch 0.
523///
524/// This allows compress/close operations to work without the full devenv setup.
525pub fn setup_test_protocol_accounts(
526    context: &mut litesvm::LiteSVM,
527    forester_authority: &Pubkey,
528) -> Result<(), String> {
529    let protocol_config = protocol_config_for_tests();
530
531    // 1. Set up ProtocolConfigPda
532    let (protocol_config_pda, protocol_bump) = get_protocol_config_pda_address();
533    let protocol_data = serialize_protocol_config_pda(
534        *forester_authority, // Use forester as governance authority for simplicity
535        protocol_bump,
536        protocol_config,
537    );
538    let protocol_account = solana_account::Account {
539        lamports: 1_000_000_000,
540        data: protocol_data,
541        owner: REGISTRY_PROGRAM_ID,
542        executable: false,
543        rent_epoch: 0,
544    };
545    context
546        .set_account(protocol_config_pda, protocol_account)
547        .map_err(|e| format!("Failed to set protocol config account: {}", e))?;
548
549    // 2. Set up ForesterPda
550    let (forester_pda, _forester_bump) = get_forester_pda(forester_authority);
551    let forester = ForesterPda {
552        authority: *forester_authority,
553        config: ForesterConfig::default(),
554        active_weight: 1,
555        pending_weight: 0,
556        current_epoch: 0,
557        last_compressed_forester_epoch_pda_hash: [0u8; 32],
558        last_registered_epoch: 0,
559    };
560    let forester_data = serialize_forester_pda(&forester);
561    let forester_account = solana_account::Account {
562        lamports: 1_000_000_000,
563        data: forester_data,
564        owner: REGISTRY_PROGRAM_ID,
565        executable: false,
566        rent_epoch: 0,
567    };
568    context
569        .set_account(forester_pda, forester_account)
570        .map_err(|e| format!("Failed to set forester account: {}", e))?;
571
572    // 3. Set up ForesterEpochPda for epoch 0
573    let (forester_epoch_pda, _epoch_bump) =
574        get_forester_epoch_pda_from_authority(forester_authority, 0);
575    let forester_epoch = ForesterEpochPda {
576        authority: *forester_authority,
577        config: ForesterConfig::default(),
578        epoch: 0,
579        weight: 1,
580        work_counter: 0,
581        has_reported_work: false,
582        forester_index: 0,
583        epoch_active_phase_start_slot: 0,
584        total_epoch_weight: Some(1), // Must be Some for active phase
585        protocol_config,
586        finalize_counter: 1, // Already finalized
587    };
588    let forester_epoch_data = serialize_forester_epoch_pda(&forester_epoch);
589    let forester_epoch_account = solana_account::Account {
590        lamports: 1_000_000_000,
591        data: forester_epoch_data,
592        owner: REGISTRY_PROGRAM_ID,
593        executable: false,
594        rent_epoch: 0,
595    };
596    context
597        .set_account(forester_epoch_pda, forester_epoch_account)
598        .map_err(|e| format!("Failed to set forester epoch account: {}", e))?;
599
600    // 4. Set up EpochPda for epoch 0
601    let epoch_pda_address = get_epoch_pda_address(0);
602    let epoch_pda = EpochPda {
603        epoch: 0,
604        protocol_config,
605        total_work: 0,
606        registered_weight: 1, // Must match forester weight
607    };
608    let epoch_pda_data = serialize_epoch_pda(&epoch_pda);
609    let epoch_pda_account = solana_account::Account {
610        lamports: 1_000_000_000,
611        data: epoch_pda_data,
612        owner: REGISTRY_PROGRAM_ID,
613        executable: false,
614        rent_epoch: 0,
615    };
616    context
617        .set_account(epoch_pda_address, epoch_pda_account)
618        .map_err(|e| format!("Failed to set epoch pda account: {}", e))?;
619
620    Ok(())
621}