Skip to content

Commit 52c97ef

Browse files
committed
refactor: decompress runtime to zero copy
1 parent e2f927e commit 52c97ef

5 files changed

Lines changed: 600 additions & 444 deletions

File tree

program-libs/compressed-account/src/instruction_data/with_account_info.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,27 @@ pub struct OutAccountInfo {
117117
pub data: Vec<u8>,
118118
}
119119

120+
/// Fixed-size output metadata WITHOUT the data Vec.
121+
/// Used for zero-allocation PDA collection in two-pass decompression.
122+
#[derive(Debug, Default, Clone, Copy, PartialEq)]
123+
pub struct OutAccountMeta {
124+
pub discriminator: [u8; 8],
125+
pub data_hash: [u8; 32],
126+
pub output_merkle_tree_index: u8,
127+
pub lamports: u64,
128+
}
129+
130+
impl From<&OutAccountInfo> for OutAccountMeta {
131+
fn from(info: &OutAccountInfo) -> Self {
132+
Self {
133+
discriminator: info.discriminator,
134+
data_hash: info.data_hash,
135+
output_merkle_tree_index: info.output_merkle_tree_index,
136+
lamports: info.lamports,
137+
}
138+
}
139+
}
140+
120141
impl TryFrom<OutputCompressedAccountWithPackedContext> for OutAccountInfo {
121142
type Error = CompressedAccountError;
122143

sdk-libs/macros/src/light_pdas/account/decompress_context.rs

Lines changed: 110 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
//! DecompressContext trait generation.
2+
//!
3+
//! Generates the implementation of the DecompressContext trait for the
4+
//! DecompressAccountsIdempotent struct. This uses a zero-allocation two-pass approach:
5+
//! - Pass 1 (collect_layout_and_tokens): Count PDAs, collect output_data_lens, collect tokens
6+
//! - Pass 2 (create_and_write_pda): Create PDA on Solana, return data for zero-copy buffer
27
38
use proc_macro2::TokenStream;
49
use quote::{format_ident, quote};
@@ -15,23 +20,40 @@ pub fn generate_decompress_context_trait_impl(
1520
token_variant_ident: Ident,
1621
lifetime: syn::Lifetime,
1722
) -> Result<TokenStream> {
18-
// Generate match arms that extract idx fields, resolve Pubkeys, construct CtxSeeds
19-
let pda_match_arms: Vec<_> = pda_ctx_seeds
23+
// Generate match arms for collect_layout_and_tokens - count PDAs that need decompression
24+
let collect_layout_pda_arms: Vec<_> = pda_ctx_seeds
25+
.iter()
26+
.map(|info| {
27+
let variant_name = &info.variant_name;
28+
let packed_variant_name = make_packed_variant_name(variant_name);
29+
quote! {
30+
LightAccountVariant::#packed_variant_name { .. } => {
31+
// PDA variant: only count if not already initialized (idempotent check)
32+
if solana_accounts[i].data_is_empty() {
33+
pda_indices[pda_count] = i;
34+
pda_count += 1;
35+
}
36+
}
37+
LightAccountVariant::#variant_name { .. } => {
38+
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
39+
}
40+
}
41+
})
42+
.collect();
43+
44+
// Generate match arms for create_and_write_pda - unpack, derive seeds, create PDA, return data
45+
let create_pda_match_arms: Vec<_> = pda_ctx_seeds
2046
.iter()
2147
.map(|info| {
22-
// Use variant_name for enum variant matching
2348
let variant_name = &info.variant_name;
24-
// Use inner_type for type references (generics, trait bounds)
25-
// Qualify with crate:: to ensure it's accessible from generated code
2649
let inner_type = qualify_type_with_crate(&info.inner_type);
2750
let packed_variant_name = make_packed_variant_name(variant_name);
28-
// Create packed type (also qualified with crate::)
2951
let packed_inner_type = make_packed_type(&info.inner_type)
3052
.expect("inner_type should be a valid type path");
31-
// Use variant_name for CtxSeeds struct (matches what decompress.rs generates)
3253
let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", variant_name);
3354
let ctx_fields = &info.ctx_seed_fields;
3455
let params_only_fields = &info.params_only_seed_fields;
56+
3557
// Generate pattern to extract idx fields from packed variant
3658
let idx_field_patterns: Vec<_> = ctx_fields.iter().map(|field| {
3759
let idx_field = format_ident!("{}_idx", field);
@@ -42,11 +64,12 @@ pub fn generate_decompress_context_trait_impl(
4264
quote! { #field }
4365
}).collect();
4466
// Generate code to resolve idx fields to Pubkeys
67+
// Note: when matching on &compressed_data.data, idx fields are references, so we dereference
4568
let resolve_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| {
4669
let idx_field = format_ident!("{}_idx", field);
4770
quote! {
4871
let #field = *post_system_accounts
49-
.get(#idx_field as usize)
72+
.get(*#idx_field as usize)
5073
.ok_or(solana_program_error::ProgramError::InvalidAccountData)?
5174
.key;
5275
}
@@ -61,38 +84,40 @@ pub fn generate_decompress_context_trait_impl(
6184
quote! { let ctx_seeds = #ctx_seeds_struct_name { #(#field_inits),* }; }
6285
};
6386
// Generate SeedParams update with params-only field values
64-
// Note: variant_seed_params is declared OUTSIDE the match to avoid borrow checker issues
65-
// (the reference passed to handle_packed_pda_variant would outlive the match arm scope)
66-
// params-only fields are stored directly in packed variant (not by reference),
67-
// so we use the value directly without dereferencing
87+
// Note: when matching on &compressed_data.data, params fields are references, so we dereference
6888
let seed_params_update = if params_only_fields.is_empty() {
69-
// No update needed - use the default value declared before match
7089
quote! {}
7190
} else {
7291
let field_inits: Vec<_> = params_only_fields.iter().map(|(field, _, _)| {
73-
quote! { #field: std::option::Option::Some(#field) }
92+
quote! { #field: std::option::Option::Some(*#field) }
7493
}).collect();
7594
quote! { variant_seed_params = SeedParams { #(#field_inits,)* ..Default::default() }; }
7695
};
96+
7797
quote! {
7898
LightAccountVariant::#packed_variant_name { data: packed, #(#idx_field_patterns,)* #(#params_field_patterns,)* .. } => {
7999
#(#resolve_ctx_seeds)*
80100
#ctx_seeds_construction
81101
#seed_params_update
82-
light_sdk::interface::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>(
83-
&*self.rent_sponsor,
84-
cpi_accounts,
85-
address_space,
86-
&solana_accounts[i],
87-
i,
88-
&packed,
89-
&meta,
90-
post_system_accounts,
91-
&mut compressed_pda_infos,
102+
103+
// Unpack the data
104+
let data: #inner_type = <#packed_inner_type as light_sdk::interface::Unpack>::unpack(&packed, post_system_accounts)?;
105+
106+
// Use helper function to derive seeds, verify PDA, create account, and write to zero-copy buffer
107+
// Pass data and compressed_meta by reference to reduce caller stack usage
108+
light_sdk::interface::derive_verify_create_and_write_pda::<#inner_type, _, _>(
92109
&program_id,
110+
&data,
93111
&ctx_seeds,
94-
std::option::Option::Some(&variant_seed_params),
95-
)?;
112+
seed_params,
113+
&variant_seed_params,
114+
compressed_meta,
115+
address_space,
116+
solana_account,
117+
&*self.rent_sponsor,
118+
cpi_accounts,
119+
zc_info,
120+
)
96121
}
97122
LightAccountVariant::#variant_name { .. } => {
98123
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
@@ -102,9 +127,18 @@ pub fn generate_decompress_context_trait_impl(
102127
.collect();
103128

104129
// For mint-only programs (no PDA variants), add an arm for the Empty variant
105-
let empty_variant_arm = if pda_ctx_seeds.is_empty() {
130+
let empty_variant_arm_collect = if pda_ctx_seeds.is_empty() {
131+
quote! {
132+
LightAccountVariant::Empty => {
133+
return std::result::Result::Err(solana_program_error::ProgramError::InvalidAccountData);
134+
}
135+
}
136+
} else {
137+
quote! {}
138+
};
139+
140+
let empty_variant_arm_create = if pda_ctx_seeds.is_empty() {
106141
quote! {
107-
// Mint-only programs have an Empty variant that should never be decompressed
108142
LightAccountVariant::Empty => {
109143
return std::result::Result::Err(solana_program_error::ProgramError::InvalidAccountData);
110144
}
@@ -150,51 +184,65 @@ pub fn generate_decompress_context_trait_impl(
150184
self.ctoken_config.as_ref().map(|a| &**a)
151185
}
152186

153-
fn collect_pda_and_token<'b>(
187+
#[allow(clippy::type_complexity)]
188+
fn collect_layout_and_tokens(
154189
&self,
155-
cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>,
156-
address_space: solana_pubkey::Pubkey,
157-
compressed_accounts: Vec<Self::CompressedData>,
190+
compressed_accounts: &[Self::CompressedData],
158191
solana_accounts: &[solana_account_info::AccountInfo<#lifetime>],
159-
seed_params: std::option::Option<&Self::SeedParams>,
160-
) -> std::result::Result<(
161-
Vec<::light_sdk::compressed_account::CompressedAccountInfo>,
162-
Vec<(Self::PackedTokenData, Self::CompressedMeta)>,
163-
), solana_program_error::ProgramError> {
164-
solana_msg::msg!("collect_pda_and_token: start, {} accounts", compressed_accounts.len());
165-
let post_system_offset = cpi_accounts.system_accounts_end_offset();
166-
let all_infos = cpi_accounts.account_infos();
167-
let post_system_accounts = &all_infos[post_system_offset..];
168-
let program_id = &crate::ID;
169-
170-
solana_msg::msg!("collect_pda_and_token: allocating vecs");
171-
let mut compressed_pda_infos = Vec::with_capacity(compressed_accounts.len());
192+
pda_indices: &mut [usize; light_sdk::interface::MAX_DECOMPRESS_ACCOUNTS],
193+
) -> std::result::Result<(usize, Vec<(Self::PackedTokenData, Self::CompressedMeta)>), solana_program_error::ProgramError> {
194+
let mut pda_count: usize = 0;
172195
let mut compressed_token_accounts = Vec::with_capacity(compressed_accounts.len());
173196

174-
solana_msg::msg!("collect_pda_and_token: starting loop");
175-
for (i, compressed_data) in compressed_accounts.into_iter().enumerate() {
176-
solana_msg::msg!("collect_pda_and_token: processing account {}", i);
177-
let meta = compressed_data.meta;
178-
// Declare variant_seed_params OUTSIDE the match to avoid borrow checker issues
179-
// (reference passed to handle_packed_pda_variant with ? would outlive match arm scope)
180-
let mut variant_seed_params = SeedParams::default();
181-
match compressed_data.data {
182-
#(#pda_match_arms)*
183-
LightAccountVariant::PackedCTokenData(mut data) => {
184-
solana_msg::msg!("collect_pda_and_token: token variant {}", i);
185-
data.token_data.version = 3;
186-
compressed_token_accounts.push((data, meta));
187-
solana_msg::msg!("collect_pda_and_token: token {} done", i);
197+
for (i, compressed_data) in compressed_accounts.iter().enumerate() {
198+
let meta = compressed_data.meta.clone();
199+
match &compressed_data.data {
200+
#(#collect_layout_pda_arms)*
201+
LightAccountVariant::PackedCTokenData(data) => {
202+
let mut token_data = data.clone();
203+
token_data.token_data.version = 3;
204+
compressed_token_accounts.push((token_data, meta));
188205
}
189206
LightAccountVariant::CTokenData(_) => {
190207
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
191208
}
192-
#empty_variant_arm
209+
#empty_variant_arm_collect
193210
}
194211
}
195212

196-
solana_msg::msg!("collect_pda_and_token: loop done, pdas={} tokens={}", compressed_pda_infos.len(), compressed_token_accounts.len());
197-
std::result::Result::Ok((compressed_pda_infos, compressed_token_accounts))
213+
std::result::Result::Ok((pda_count, compressed_token_accounts))
214+
}
215+
216+
#[inline(never)]
217+
#[allow(clippy::too_many_arguments)]
218+
fn create_and_write_pda<'b, 'c>(
219+
&self,
220+
cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>,
221+
address_space: &solana_pubkey::Pubkey,
222+
compressed_data: &Self::CompressedData,
223+
solana_account: &solana_account_info::AccountInfo<#lifetime>,
224+
seed_params: std::option::Option<&Self::SeedParams>,
225+
zc_info: &mut light_sdk::interface::ZCompressedAccountInfoMut<'c>,
226+
) -> std::result::Result<bool, solana_program_error::ProgramError> {
227+
let post_system_offset = cpi_accounts.system_accounts_end_offset();
228+
let all_infos = cpi_accounts.account_infos();
229+
let post_system_accounts = &all_infos[post_system_offset..];
230+
let program_id = crate::ID;
231+
let compressed_meta = &compressed_data.meta;
232+
let mut variant_seed_params = SeedParams::default();
233+
let _ = &variant_seed_params; // Suppress unused warning when no params-only fields
234+
235+
match &compressed_data.data {
236+
#(#create_pda_match_arms)*
237+
LightAccountVariant::PackedCTokenData(_) => {
238+
// Tokens are handled separately, skip here
239+
std::result::Result::Ok(false)
240+
}
241+
LightAccountVariant::CTokenData(_) => {
242+
return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into());
243+
}
244+
#empty_variant_arm_create
245+
}
198246
}
199247

200248
#[inline(never)]

0 commit comments

Comments
 (0)