Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/actions/setup-and-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ runs:
target/deploy/create_address_test_program.so
target/deploy/sdk_anchor_test.so
target/deploy/sdk-compressible-test.so
target/deploy/csdk_anchor_derived_test.so
target/deploy/csdk_anchor_full_derived_test.so
key: ${{ runner.os }}-program-tests-${{ hashFiles('program-tests/**/Cargo.toml', 'program-tests/**/Cargo.lock', 'program-tests/**/*.rs', 'test-programs/**/Cargo.toml', 'test-programs/**/*.rs', 'sdk-tests/**/Cargo.toml', 'sdk-tests/**/*.rs') }}
restore-keys: |
${{ runner.os }}-program-tests-
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- program: native
sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo-test-sbf -p client-test"]'
- program: anchor & pinocchio
sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]'
sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]'
- program: token test
sub-tests: '["cargo-test-sbf -p sdk-token-test"]'
- program: sdk-libs
Expand Down
76 changes: 74 additions & 2 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ members = [
"sdk-tests/sdk-v1-native-test",
"sdk-tests/sdk-token-test",
"sdk-tests/sdk-compressible-test",
"sdk-tests/csdk-anchor-derived-test",
"sdk-tests/csdk-anchor-full-derived-test",
"forester-utils",
"forester",
"sparse-merkle-tree",
Expand Down
6 changes: 5 additions & 1 deletion pnpm-lock.yaml

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

2 changes: 1 addition & 1 deletion program-tests/registry-test/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ async fn test_initialize_protocol_config() {
config: ProgramTestConfig::default(),
transaction_counter: 0,
pre_context: None,
auto_compress_programs: Vec::new(),
auto_mine_cold_state_programs: Vec::new(),
};

let payer = rpc.get_payer().insecure_clone();
Expand Down
3 changes: 3 additions & 0 deletions scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ NO_DEFAULT_CRATES=(
"light-compressed-token-sdk"
"light-compressed-token-types"
"light-sdk"
"sdk-compressible-test"
"csdk-anchor-derived-test"
"csdk-anchor-full-derived-test"
)

for crate in "${NO_DEFAULT_CRATES[@]}"; do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ pub fn compress_and_close_ctoken_accounts<'info>(
};

// Determine rent recipient from extension or use default
let actual_rent_sponsor = if rent_sponsor_pubkey.is_none() {
let actual_rent_sponsor = if let Some(sponsor) = rent_sponsor_pubkey {
sponsor
} else {
// Check if there's a rent recipient in the compressible extension
if let Some(extensions) = &compressed_token.extensions {
for extension in extensions {
Expand All @@ -360,8 +362,6 @@ pub fn compress_and_close_ctoken_accounts<'info>(
}
}
rent_sponsor_pubkey.ok_or(TokenSdkError::InvalidAccountData)?
} else {
rent_sponsor_pubkey.unwrap()
};

let destination_pubkey = if with_compression_authority {
Expand Down Expand Up @@ -404,6 +404,7 @@ pub fn compress_and_close_ctoken_accounts<'info>(
/// subset of `remaining_accounts`.
#[allow(clippy::too_many_arguments)]
#[profile]
#[allow(clippy::extra_unused_lifetimes)]
pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>(
token_accounts_to_compress: &[AccountInfoToCompress<'info>],
fee_payer: AccountInfo<'info>,
Expand Down
187 changes: 187 additions & 0 deletions sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! Runtime helpers for token decompression.
use light_ctoken_types::instructions::transfer2::MultiInputTokenDataWithContext;
use light_sdk::{cpi::v2::CpiAccounts, instruction::ValidityProof};
use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress;
use solana_account_info::AccountInfo;
use solana_msg::msg;
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

use crate::compat::PackedCTokenData;

/// Trait for getting token account seeds.
pub trait CTokenSeedProvider: Copy {
/// Type of accounts struct needed for seed derivation.
type Accounts<'info>;

/// Get seeds for the token account PDA (used for decompression).
fn get_seeds<'a, 'info>(
&self,
accounts: &'a Self::Accounts<'info>,
remaining_accounts: &'a [AccountInfo<'info>],
) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;

/// Get authority seeds for signing during compression.
///
/// TODO: consider removing.
fn get_authority_seeds<'a, 'info>(
&self,
accounts: &'a Self::Accounts<'info>,
remaining_accounts: &'a [AccountInfo<'info>],
) -> Result<(Vec<Vec<u8>>, Pubkey), ProgramError>;
}
Comment on lines +12 to +32
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

Safety fixes in decompression runtime look solid; consider clarifying get_authority_seeds

The runtime now:

  • Validates owner_index / mint_index against packed_accounts.len() before indexing.
  • Only derives a CPI context pubkey when has_pdas is true.
  • Uses plain Vec for token_decompress_indices.

This resolves the earlier panic/logic risks in this path.

The only remaining nit is the CTokenSeedProvider::get_authority_seeds method: it’s still required by the trait but unused in this runtime and marked with a TODO. Either document it as reserved for future use or move it to a separate trait so implementors don’t have to provide dead code today.

Also applies to: 60-79, 81-163

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/compressible/decompress_runtime.rs around
lines 12 to 32, the trait CTokenSeedProvider still requires get_authority_seeds
even though the decompression runtime never uses it; either extract
get_authority_seeds into a new separate trait (e.g.,
CTokenAuthoritySeedProvider) so implementors of CTokenSeedProvider aren’t forced
to implement unused methods, or add a default implementation that returns an
Err/unused-ok value and update docs to mark it reserved for future use; update
trait docs and any implementations accordingly so no dead/unnecessary
implementations are required.


/// Token decompression processor.
#[inline(never)]
#[allow(clippy::too_many_arguments)]
pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>(
accounts_for_seeds: &A,
remaining_accounts: &[AccountInfo<'info>],
fee_payer: &AccountInfo<'info>,
ctoken_program: &AccountInfo<'info>,
ctoken_rent_sponsor: &AccountInfo<'info>,
ctoken_cpi_authority: &AccountInfo<'info>,
ctoken_config: &AccountInfo<'info>,
config: &AccountInfo<'info>,
ctoken_accounts: Vec<(
PackedCTokenData<V>,
CompressedAccountMetaNoLamportsNoAddress,
)>,
proof: ValidityProof,
cpi_accounts: &CpiAccounts<'b, 'info>,
post_system_accounts: &[AccountInfo<'info>],
has_pdas: bool,
program_id: &Pubkey,
) -> Result<(), ProgramError>
where
V: CTokenSeedProvider<Accounts<'info> = A>,
A: 'info,
{
let mut token_decompress_indices: Vec<
crate::compressed_token::decompress_full::DecompressFullIndices,
> = Vec::with_capacity(ctoken_accounts.len());
let mut token_signers_seed_groups: Vec<Vec<Vec<u8>>> =
Vec::with_capacity(ctoken_accounts.len());
let packed_accounts = post_system_accounts;

let authority = cpi_accounts
.authority()
.map_err(|_| ProgramError::MissingRequiredSignature)?;
let cpi_context_pubkey = if has_pdas {
Some(
*cpi_accounts
.cpi_context()
.map_err(|_| ProgramError::MissingRequiredSignature)?
.key,
)
} else {
None
};

for (token_data, meta) in ctoken_accounts.into_iter() {
let owner_index: u8 = token_data.token_data.owner;
let mint_index: u8 = token_data.token_data.mint;

let mint_index_usize = mint_index as usize;
if mint_index_usize >= packed_accounts.len() {
msg!(
"mint_index {} out of bounds (len: {})",
mint_index_usize,
packed_accounts.len()
);
return Err(ProgramError::InvalidAccountData);
}
let mint_info = &packed_accounts[mint_index_usize];

let owner_index_usize = owner_index as usize;
if owner_index_usize >= packed_accounts.len() {
msg!(
"owner_index {} out of bounds (len: {})",
owner_index_usize,
packed_accounts.len()
);
return Err(ProgramError::InvalidAccountData);
}
let owner_info = &packed_accounts[owner_index_usize];

// Use trait method to get seeds (program-specific)
let (ctoken_signer_seeds, derived_token_account_address) = token_data
.variant
.get_seeds(accounts_for_seeds, remaining_accounts)?;

if derived_token_account_address != *owner_info.key {
msg!(
"derived_token_account_address: {:?}",
derived_token_account_address
);
msg!("owner_info.key: {:?}", owner_info.key);
return Err(ProgramError::InvalidAccountData);
}

let seed_refs: Vec<&[u8]> = ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect();
let seeds_slice: &[&[u8]] = &seed_refs;

crate::ctoken::create_token_account::create_ctoken_account_signed(
*program_id,
fee_payer.clone(),
(*owner_info).clone(),
(*mint_info).clone(),
*authority.key,
seeds_slice,
ctoken_rent_sponsor.clone(),
ctoken_config.clone(),
Some(2),
None,
)?;

let source = MultiInputTokenDataWithContext {
owner: token_data.token_data.owner,
amount: token_data.token_data.amount,
has_delegate: token_data.token_data.has_delegate,
delegate: token_data.token_data.delegate,
mint: token_data.token_data.mint,
version: token_data.token_data.version,
merkle_context: meta.tree_info.into(),
root_index: meta.tree_info.root_index,
};
let decompress_index = crate::compressed_token::decompress_full::DecompressFullIndices {
source,
destination_index: owner_index,
};
token_decompress_indices.push(decompress_index);
token_signers_seed_groups.push(ctoken_signer_seeds);
}

let ctoken_ix =
crate::compressed_token::decompress_full::decompress_full_ctoken_accounts_with_indices(
*fee_payer.key,
proof,
cpi_context_pubkey,
&token_decompress_indices,
packed_accounts,
)
.map_err(ProgramError::from)?;

let mut all_account_infos: Vec<AccountInfo<'info>> =
Vec::with_capacity(1 + post_system_accounts.len() + 3);
all_account_infos.push(fee_payer.clone());
all_account_infos.push(ctoken_cpi_authority.clone());
all_account_infos.push(ctoken_program.clone());
all_account_infos.push(ctoken_rent_sponsor.clone());
all_account_infos.push(config.clone());
all_account_infos.extend_from_slice(post_system_accounts);

let signer_seed_refs: Vec<Vec<&[u8]>> = token_signers_seed_groups
.iter()
.map(|group| group.iter().map(|s| s.as_slice()).collect())
.collect();
let signer_seed_slices: Vec<&[&[u8]]> = signer_seed_refs.iter().map(|g| g.as_slice()).collect();

solana_cpi::invoke_signed(
&ctoken_ix,
all_account_infos.as_slice(),
signer_seed_slices.as_slice(),
)?;

Ok(())
}
2 changes: 2 additions & 0 deletions sdk-libs/compressed-token-sdk/src/compressible/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod claim;
pub mod decompress_runtime;
pub mod withdraw_funding_pool;

pub use claim::*;
pub use decompress_runtime::{process_decompress_tokens_runtime, CTokenSeedProvider};
pub use withdraw_funding_pool::*;
Loading