Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 27 additions & 32 deletions forester/tests/test_compressible_pda.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ const CSDK_TEST_PROGRAM_ID: &str = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah
// This needs to match the discriminator from csdk_anchor_full_derived_test::state::d1_field_types::single_pubkey::SinglePubkeyRecord
const SINGLE_PUBKEY_RECORD_DISCRIMINATOR: [u8; 8] = csdk_anchor_full_derived_test::state::d1_field_types::single_pubkey::SinglePubkeyRecord::LIGHT_DISCRIMINATOR;

// Rent sponsor pubkey used in tests
const RENT_SPONSOR: Pubkey = solana_sdk::pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG");
/// Get the program's derived rent sponsor PDA
fn program_rent_sponsor() -> Pubkey {
csdk_anchor_full_derived_test::light_rent_sponsor()
}

/// Context returned from forester registration
struct ForesterContext {
Expand Down Expand Up @@ -97,8 +99,8 @@ async fn register_forester<R: Rpc>(
.config;

// Use airdrop for forester
println!("Funding forester {} with 10 SOL", forester_pubkey);
rpc.airdrop_lamports(&forester_pubkey, 10_000_000_000)
println!("Funding forester {} with 2 SOL", forester_pubkey);
rpc.airdrop_lamports(&forester_pubkey, 2_000_000_000)
.await?;
sleep(Duration::from_millis(500)).await;

Expand Down Expand Up @@ -280,30 +282,27 @@ async fn test_compressible_pda_bootstrap() {
let authority = Keypair::try_from(PAYER_KEYPAIR.as_ref()).expect("Invalid PAYER_KEYPAIR");

// Fund the authority account
rpc.airdrop_lamports(&authority.pubkey(), 10_000_000_000)
rpc.airdrop_lamports(&authority.pubkey(), 2_000_000_000)
.await
.expect("Failed to airdrop to authority");

// Fund rent sponsor
rpc.airdrop_lamports(&RENT_SPONSOR, 10_000_000_000)
.await
.expect("Failed to fund rent sponsor");

// Initialize compression config
let (init_config_ix, config_pda) = InitializeRentFreeConfig::new(
// Initialize compression config (includes rent sponsor funding)
let rent_sponsor = program_rent_sponsor();
let (init_config_ixs, config_pda) = InitializeRentFreeConfig::new(
&program_id,
&authority.pubkey(),
&Pubkey::find_program_address(
&[program_id.as_ref()],
&solana_sdk::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"),
)
.0,
RENT_SPONSOR,
rent_sponsor,
authority.pubkey(),
500_000_000, // 0.5 SOL for rent sponsor
)
.build();

rpc.create_and_send_transaction(&[init_config_ix], &authority.pubkey(), &[&authority])
rpc.create_and_send_transaction(&init_config_ixs, &authority.pubkey(), &[&authority])
.await
.expect("Initialize config should succeed");

Expand Down Expand Up @@ -470,30 +469,27 @@ async fn test_compressible_pda_compression() {
let authority = Keypair::try_from(PAYER_KEYPAIR.as_ref()).expect("Invalid PAYER_KEYPAIR");

// Fund the authority account
rpc.airdrop_lamports(&authority.pubkey(), 10_000_000_000)
rpc.airdrop_lamports(&authority.pubkey(), 2_000_000_000)
.await
.expect("Failed to airdrop to authority");

// Fund rent sponsor
rpc.airdrop_lamports(&RENT_SPONSOR, 10_000_000_000)
.await
.expect("Failed to fund rent sponsor");

// Initialize compression config
let (init_config_ix, config_pda) = InitializeRentFreeConfig::new(
// Initialize compression config (includes rent sponsor funding)
let rent_sponsor = program_rent_sponsor();
let (init_config_ixs, config_pda) = InitializeRentFreeConfig::new(
&program_id,
&authority.pubkey(),
&Pubkey::find_program_address(
&[program_id.as_ref()],
&solana_sdk::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"),
)
.0,
RENT_SPONSOR,
rent_sponsor,
authority.pubkey(),
500_000_000, // 0.5 SOL for rent sponsor
)
.build();

rpc.create_and_send_transaction(&[init_config_ix], &authority.pubkey(), &[&authority])
rpc.create_and_send_transaction(&init_config_ixs, &authority.pubkey(), &[&authority])
.await
.expect("Initialize config should succeed");

Expand Down Expand Up @@ -703,33 +699,32 @@ async fn test_compressible_pda_subscription() {
let authority = Keypair::try_from(&PAYER_KEYPAIR[..]).unwrap();

// Fund accounts
rpc.airdrop_lamports(&authority.pubkey(), 10_000_000_000)
rpc.airdrop_lamports(&authority.pubkey(), 2_000_000_000)
.await
.expect("Failed to airdrop to authority");
rpc.airdrop_lamports(&RENT_SPONSOR, 10_000_000_000)
.await
.expect("Failed to fund rent sponsor");

// Wait for indexer
wait_for_indexer(&rpc)
.await
.expect("Failed to wait for indexer");

// Initialize compression config
let (init_config_ix, config_pda) = InitializeRentFreeConfig::new(
// Initialize compression config (includes rent sponsor funding)
let rent_sponsor = program_rent_sponsor();
let (init_config_ixs, config_pda) = InitializeRentFreeConfig::new(
&program_id,
&authority.pubkey(),
&Pubkey::find_program_address(
&[program_id.as_ref()],
&solana_sdk::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"),
)
.0,
RENT_SPONSOR,
rent_sponsor,
authority.pubkey(),
500_000_000, // 0.5 SOL for rent sponsor
)
.build();

rpc.create_and_send_transaction(&[init_config_ix], &authority.pubkey(), &[&authority])
rpc.create_and_send_transaction(&init_config_ixs, &authority.pubkey(), &[&authority])
.await
.expect("Initialize config should succeed");

Expand Down
1 change: 1 addition & 0 deletions sdk-libs/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [
"bincode",
] }
solana-message = { workspace = true }
solana-system-interface = { workspace = true }
spl-token-2022-interface = { workspace = true }
spl-pod = { workspace = true }

Expand Down
43 changes: 40 additions & 3 deletions sdk-libs/client/src/interface/initialize_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSeria
use light_sdk::interface::config::LightConfig;
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::Pubkey;
use solana_system_interface::instruction as system_instruction;

/// Default address tree v2 pubkey.
pub const ADDRESS_TREE_V2: Pubkey =
Expand All @@ -20,18 +21,22 @@ pub const DEFAULT_INIT_WRITE_TOP_UP: u32 = 5_000;
pub struct InitializeCompressionConfigAnchorData {
pub write_top_up: u32,
pub rent_sponsor: Pubkey,
pub rent_sponsor_bump: u8,
pub compression_authority: Pubkey,
pub rent_config: light_compressible::rent::RentConfig,
pub address_space: Vec<Pubkey>,
}

/// Builder for `initialize_compression_config` instruction with sensible defaults.
///
/// Automatically includes a transfer instruction to fund the rent sponsor PDA.
pub struct InitializeRentFreeConfig {
program_id: Pubkey,
fee_payer: Pubkey,
program_data_pda: Pubkey,
authority: Option<Pubkey>,
rent_sponsor: Pubkey,
rent_sponsor_funding: u64,
compression_authority: Pubkey,
rent_config: light_compressible::rent::RentConfig,
write_top_up: u32,
Expand All @@ -40,19 +45,26 @@ pub struct InitializeRentFreeConfig {
}

impl InitializeRentFreeConfig {
/// Creates a new builder for initializing rent-free config.
///
/// # Arguments
/// * `rent_sponsor_funding` - Lamports to transfer to the rent sponsor PDA.
/// This funds the PDA that will pay rent for compressed accounts.
pub fn new(
program_id: &Pubkey,
fee_payer: &Pubkey,
program_data_pda: &Pubkey,
rent_sponsor: Pubkey,
compression_authority: Pubkey,
rent_sponsor_funding: u64,
) -> Self {
Self {
program_id: *program_id,
fee_payer: *fee_payer,
program_data_pda: *program_data_pda,
authority: None,
rent_sponsor,
rent_sponsor_funding,
compression_authority,
rent_config: light_compressible::rent::RentConfig::default(),
write_top_up: DEFAULT_INIT_WRITE_TOP_UP,
Expand Down Expand Up @@ -86,10 +98,34 @@ impl InitializeRentFreeConfig {
self
}

pub fn build(self) -> (Instruction, Pubkey) {
/// Builds the instructions to initialize rent-free config.
///
/// Returns a vector containing:
/// 1. Transfer instruction to fund the rent sponsor PDA
/// 2. Initialize compression config instruction
///
/// Both instructions should be sent in a single atomic transaction.
pub fn build(self) -> (Vec<Instruction>, Pubkey) {
let authority = self.authority.unwrap_or(self.fee_payer);
let (config_pda, _) = LightConfig::derive_pda(&self.program_id, self.config_bump);

// Derive rent sponsor bump (version 1, hardcoded)
let (derived_rent_sponsor, rent_sponsor_bump) =
Pubkey::find_program_address(&[b"rent_sponsor", &1u16.to_le_bytes()], &self.program_id);
assert_eq!(
derived_rent_sponsor, self.rent_sponsor,
"Rent sponsor PDA mismatch: derived {:?} != provided {:?}",
derived_rent_sponsor, self.rent_sponsor
);
Comment on lines +112 to +119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider returning Result instead of panicking on PDA mismatch.

Using assert_eq! in a builder's build() method will cause a panic at runtime if the caller provides an incorrect rent_sponsor. For library code, returning a Result<(Vec<Instruction>, Pubkey), Error> would be more idiomatic and allow callers to handle the error gracefully rather than unwinding the stack.

That said, the current implementation does catch misconfigurations early with a clear error message, so this is a design tradeoff rather than a bug.

♻️ Suggested approach
-    pub fn build(self) -> (Vec<Instruction>, Pubkey) {
+    pub fn build(self) -> Result<(Vec<Instruction>, Pubkey), InitConfigError> {
         let authority = self.authority.unwrap_or(self.fee_payer);
         let (config_pda, _) = LightConfig::derive_pda(&self.program_id, self.config_bump);

         // Derive rent sponsor bump (version 1, hardcoded)
         let (derived_rent_sponsor, rent_sponsor_bump) =
             Pubkey::find_program_address(&[b"rent_sponsor", &1u16.to_le_bytes()], &self.program_id);
-        assert_eq!(
-            derived_rent_sponsor, self.rent_sponsor,
-            "Rent sponsor PDA mismatch: derived {:?} != provided {:?}",
-            derived_rent_sponsor, self.rent_sponsor
-        );
+        if derived_rent_sponsor != self.rent_sponsor {
+            return Err(InitConfigError::RentSponsorMismatch {
+                derived: derived_rent_sponsor,
+                provided: self.rent_sponsor,
+            });
+        }
🤖 Prompt for AI Agents
In `@sdk-libs/client/src/interface/initialize_config.rs` around lines 112 - 119,
Replace the panic via assert_eq! in the builder's validation (the
Pubkey::find_program_address check that compares derived_rent_sponsor to
self.rent_sponsor) with an Err return so build() returns a Result instead of
unwinding; specifically, change the surrounding build()/finalize method
signature to return Result<(Vec<Instruction>, Pubkey), YourErrorType>, compute
(derived_rent_sponsor, rent_sponsor_bump) with Pubkey::find_program_address, and
if they differ return a descriptive Err (e.g., RentSponsorMismatch { expected:
derived_rent_sponsor, found: self.rent_sponsor }) rather than panicking—update
call sites to propagate or handle the Result accordingly.


// 1. Transfer to fund rent sponsor PDA
let transfer_ix = system_instruction::transfer(
&self.fee_payer,
&self.rent_sponsor,
self.rent_sponsor_funding,
);
Comment on lines +121 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the InitializeRentFreeConfig struct definition and its constructor
rg -n "struct InitializeRentFreeConfig\|impl.*InitializeRentFreeConfig" --type rust -A 15 | head -100

Repository: Lightprotocol/light-protocol

Length of output: 54


🏁 Script executed:

# Search for usages of InitializeRentFreeConfig::new
rg -n "InitializeRentFreeConfig\s*\{|InitializeRentFreeConfig::new" --type rust -B 2 -A 5 | head -150

Repository: Lightprotocol/light-protocol

Length of output: 12460


🏁 Script executed:

# Find the initialize_config.rs file to examine the full context
fd "initialize_config.rs" --type f

Repository: Lightprotocol/light-protocol

Length of output: 122


🏁 Script executed:

# Look for CLAUDE.md and rent-related documentation
fd "CLAUDE.md" --type f
fd -e md | grep -i "rent\|config" | head -20

Repository: Lightprotocol/light-protocol

Length of output: 837


🏁 Script executed:

# Read the initialize_config.rs file to see the full implementation
cat -n sdk-libs/client/src/interface/initialize_config.rs

Repository: Lightprotocol/light-protocol

Length of output: 7342


🏁 Script executed:

# Check the CLAUDE.md file at the root to understand rent sponsor semantics
cat -n CLAUDE.md | head -100

Repository: Lightprotocol/light-protocol

Length of output: 6365


🏁 Script executed:

# Check the compressible CLAUDE.md for rent sponsor information
cat -n program-libs/compressible/CLAUDE.md | head -150

Repository: Lightprotocol/light-protocol

Length of output: 2496


🏁 Script executed:

# Search for validation of rent_sponsor_funding across the codebase
rg "rent_sponsor_funding" --type rust -B 2 -A 2

Repository: Lightprotocol/light-protocol

Length of output: 2179


Add validation to prevent zero-amount transfer instruction.

The code creates a transfer instruction without checking if rent_sponsor_funding is zero, which would generate a no-op instruction. Since the rent sponsor PDA is meant to fund rent payments for compressed accounts, zero funding contradicts the intent. Either validate that rent_sponsor_funding > 0 in the constructor, or conditionally skip the transfer instruction when funding is zero.

🤖 Prompt for AI Agents
In `@sdk-libs/client/src/interface/initialize_config.rs` around lines 121 - 126,
The transfer instruction is created even when rent_sponsor_funding is zero,
producing a no-op; modify the logic around system_instruction::transfer that
builds transfer_ix to either validate rent_sponsor_funding > 0 at construction
(where the config or struct is initialized) or skip/omit creating/appending
transfer_ix when rent_sponsor_funding == 0. Ensure you reference and enforce the
rent_sponsor_funding field and change the code that constructs transfer_ix
(system_instruction::transfer(&self.fee_payer, &self.rent_sponsor,
self.rent_sponsor_funding)) to only run when the amount is positive or raise an
error in the initializer.


// 2. Initialize compression config
let accounts = vec![
AccountMeta::new(self.fee_payer, true), // payer
AccountMeta::new(config_pda, false), // config
Expand All @@ -104,6 +140,7 @@ impl InitializeRentFreeConfig {
let instruction_data = InitializeCompressionConfigAnchorData {
write_top_up: self.write_top_up,
rent_sponsor: self.rent_sponsor,
rent_sponsor_bump,
compression_authority: self.compression_authority,
rent_config: self.rent_config,
address_space: self.address_space,
Expand All @@ -121,12 +158,12 @@ impl InitializeRentFreeConfig {
data.extend_from_slice(&DISCRIMINATOR);
data.extend_from_slice(&serialized_data);

let instruction = Instruction {
let init_config_ix = Instruction {
program_id: self.program_id,
accounts,
data,
};

(instruction, config_pda)
(vec![transfer_ix, init_config_ix], config_pda)
}
}
10 changes: 10 additions & 0 deletions sdk-libs/client/src/interface/light_program_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ pub trait LightProgramInterface: Sized {
#[must_use]
fn program_id(&self) -> Pubkey;

/// Returns the program's LightConfig PDA.
/// Used for PDA decompression instructions.
#[must_use]
fn light_config_pda(&self) -> Pubkey;

/// Returns the program's rent sponsor PDA for cPDAs (version 1).
/// Derived via `derive_light_rent_sponsors!` macro.
#[must_use]
fn light_rent_sponsor_pda(&self) -> Pubkey;

/// Construct SDK from root account(s).
fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result<Self, Self::Error>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ pub fn create_compressed_mint_cpi(
instruction_data = instruction_data.with_cpi_context(ctx);
}

let meta_config = if cpi_context_pubkey.is_some() {
let meta_config = if let Some(ctx_pubkey) = cpi_context_pubkey {
MintActionMetaConfig::new_cpi_context(
&instruction_data,
input.payer,
input.mint_authority,
cpi_context_pubkey.unwrap(),
ctx_pubkey,
)?
} else {
MintActionMetaConfig::new_create_mint(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,8 @@ pub fn create_mint_to_compressed_instruction(
instruction_data = instruction_data.with_cpi_context(ctx);
}

let meta_config = if cpi_context_pubkey.is_some() {
MintActionMetaConfig::new_cpi_context(
&instruction_data,
payer,
mint_authority,
cpi_context_pubkey.unwrap(),
)?
let meta_config = if let Some(ctx_pubkey) = cpi_context_pubkey {
MintActionMetaConfig::new_cpi_context(&instruction_data, payer, mint_authority, ctx_pubkey)?
} else {
MintActionMetaConfig::new(
payer,
Expand Down
30 changes: 9 additions & 21 deletions sdk-libs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,34 +339,22 @@ pub fn light_account_derive(input: TokenStream) -> TokenStream {
into_token_stream(light_pdas::account::light_compressible::derive_light_account(input))
}

/// Derives a Rent Sponsor PDA for a program at compile time.
/// Derives the Rent Sponsor PDA at compile time (version 1, hardcoded).
///
/// Seeds: ["rent_sponsor", <u16 version little-endian>]
///
/// ## Example
///
/// ```ignore
/// use light_sdk_macros::derive_light_rent_sponsor_pda;
///
/// pub const RENT_SPONSOR_DATA: ([u8; 32], u8) =
/// derive_light_rent_sponsor_pda!("8Ld9pGkCNfU6A7KdKe1YrTNYJWKMCFqVHqmUvjNmER7B", 1);
/// ```
#[proc_macro]
pub fn derive_light_rent_sponsor_pda(input: TokenStream) -> TokenStream {
rent_sponsor::derive_light_rent_sponsor_pda(input)
}

/// Derives a complete Rent Sponsor configuration for a program at compile time.
///
/// Returns ::light_sdk_types::RentSponsor { program_id, rent_sponsor, bump, version }.
/// Returns a `RentSponsor` struct with the PDA address and bump.
///
/// ## Example
///
/// ```ignore
/// use light_sdk_macros::derive_light_rent_sponsor;
///
/// pub const RENT_SPONSOR: ::light_sdk_types::RentSponsor =
/// derive_light_rent_sponsor!("8Ld9pGkCNfU6A7KdKe1YrTNYJWKMCFqVHqmUvjNmER7B", 1);
/// pub const RENT_SPONSOR: ::light_sdk::sdk_types::RentSponsor =
/// derive_light_rent_sponsor!("8Ld9pGkCNfU6A7KdKe1YrTNYJWKMCFqVHqmUvjNmER7B");
///
/// // Access the pubkey
/// let pubkey = Pubkey::from(RENT_SPONSOR.rent_sponsor);
/// // Access the bump for signing
/// let bump = RENT_SPONSOR.bump;
/// ```
#[proc_macro]
pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream {
Expand Down
2 changes: 2 additions & 0 deletions sdk-libs/macros/src/light_pdas/account/decompress_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ pub fn generate_decompress_context_trait_impl(
#seed_params_update
light_sdk::interface::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>(
&*self.rent_sponsor,
rent_sponsor_seeds,
cpi_accounts,
address_space,
&solana_accounts[i],
Expand Down Expand Up @@ -157,6 +158,7 @@ pub fn generate_decompress_context_trait_impl(
compressed_accounts: Vec<Self::CompressedData>,
solana_accounts: &[solana_account_info::AccountInfo<#lifetime>],
seed_params: std::option::Option<&Self::SeedParams>,
rent_sponsor_seeds: &[&[u8]],
) -> std::result::Result<(
Vec<::light_sdk::compressed_account::CompressedAccountInfo>,
Vec<(Self::PackedTokenData, Self::CompressedMeta)>,
Expand Down
Loading