Skip to content

feat: compressible accounts #1850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
21 changes: 21 additions & 0 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 @@ -48,6 +48,7 @@ members = [
"forester-utils",
"forester",
"sparse-merkle-tree",
"program-tests/anchor-compressible-user",
]

resolver = "2"
Expand Down Expand Up @@ -90,6 +91,7 @@ solana-transaction = { version = "2.2" }
solana-transaction-error = { version = "2.2" }
solana-hash = { version = "2.2" }
solana-clock = { version = "2.2" }
solana-rent = { version = "2.2" }
solana-signature = { version = "2.2" }
solana-commitment-config = { version = "2.2" }
solana-account = { version = "2.2" }
Expand Down
13 changes: 3 additions & 10 deletions pnpm-lock.yaml

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

39 changes: 39 additions & 0 deletions program-tests/anchor-compressible-user/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "anchor-compressible-user"
version = "0.1.0"
description = "Simple Anchor program template with user records"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "anchor_compressible_user"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []


[dependencies]
light-sdk = { workspace = true }
light-sdk-types = { workspace = true }
light-hasher = { workspace = true, features = ["solana"] }
solana-program = { workspace = true }
light-macros = { workspace = true, features = ["solana"] }
borsh = { workspace = true }
light-compressed-account = { workspace = true, features = ["solana"] }
anchor-lang = { workspace = true }

[dev-dependencies]
light-program-test = { workspace = true, features = ["devenv"] }
tokio = { workspace = true }
solana-sdk = { workspace = true }

[lints.rust.unexpected_cfgs]
level = "allow"
check-cfg = [
'cfg(target_os, values("solana"))',
'cfg(feature, values("frozen-abi", "no-entrypoint"))',
]
2 changes: 2 additions & 0 deletions program-tests/anchor-compressible-user/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
96 changes: 96 additions & 0 deletions program-tests/anchor-compressible-user/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use anchor_lang::prelude::*;

declare_id!("CompUser11111111111111111111111111111111111");
pub const ADDRESS_SPACE: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG");
pub const RENT_RECIPIENT: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG");

// Simple anchor program retrofitted with compressible accounts.
#[program]
pub mod anchor_compressible_user {
use super::*;

/// Creates a new user record
pub fn create_record(
ctx: Context<CreateRecord>,
name: String,
proof: ValidityProof,
compressed_address: [u8; 32],
address_tree_info: PackedAddressTreeInfo,
output_state_tree_index: u8,
) -> Result<()> {
let user_record = &mut ctx.accounts.user_record;

user_record.owner = ctx.accounts.user.key();
user_record.name = name;
user_record.score = 0;

let cpi_accounts = CpiAccounts::new_with_config(
&ctx.accounts.user, // fee_payer
&ctx.remaining_accounts[..],
CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER),
);
let new_address_params =
address_tree_info.into_new_address_params_packed(user_record.key.to_bytes());

compress_pda_new::<MyPdaAccount>(
&user_record,
compressed_address,
new_address_params,
output_state_tree_index,
proof,
cpi_accounts,
&crate::ID,
&ctx.accounts.rent_recipient,
&ADDRESS_SPACE,
)?;
Ok(())
}

/// Can be the same because the PDA will be decompressed in a separate instruction.
/// Updates an existing user record
pub fn update_record(ctx: Context<UpdateRecord>, name: String, score: u64) -> Result<()> {
let user_record = &mut ctx.accounts.user_record;

user_record.name = name;
user_record.score = score;

Ok(())
}
}

#[derive(Accounts)]
pub struct CreateRecord<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
payer = user,
space = 8 + 32 + 4 + 32 + 8, // discriminator + owner + string len + name + score
seeds = [b"user_record", user.key().as_ref()],
bump,
)]
pub user_record: Account<'info, UserRecord>,
pub system_program: Program<'info, System>,
#[account(address = RENT_RECIPIENT)]
pub rent_recipient: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct UpdateRecord<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"user_record", user.key().as_ref()],
bump,
constraint = user_record.owner == user.key()
)]
pub user_record: Account<'info, UserRecord>,
}

#[account]
pub struct UserRecord {
pub owner: Pubkey,
pub name: String,
pub score: u64,
}
82 changes: 82 additions & 0 deletions program-tests/anchor-compressible-user/tests/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#![cfg(feature = "test-sbf")]

use anchor_lang::prelude::*;
use anchor_lang::InstructionData;
use anchor_lang::ToAccountMetas;
use solana_program_test::*;
use solana_sdk::{
instruction::Instruction,
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};

#[tokio::test]
async fn test_user_record() {
let program_id = anchor_compressible_user::ID;
let mut program_test = ProgramTest::new(
"anchor_compressible_user",
program_id,
processor!(anchor_compressible_user::entry),
);

let (mut banks_client, payer, recent_blockhash) = program_test.start().await;

// Test create_record
let user = payer;
let (user_record_pda, _bump) = Pubkey::find_program_address(
&[b"user_record", user.pubkey().as_ref()],
&program_id,
);

let accounts = anchor_compressible_user::accounts::CreateRecord {
user: user.pubkey(),
user_record: user_record_pda,
system_program: solana_sdk::system_program::ID,
};

let instruction_data = anchor_compressible_user::instruction::CreateRecord {
name: "Alice".to_string(),
};

let instruction = Instruction {
program_id,
accounts: accounts.to_account_metas(None),
data: instruction_data.data(),
};

let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&user.pubkey()),
&[&user],
recent_blockhash,
);

banks_client.process_transaction(transaction).await.unwrap();

// Test update_record
let accounts = anchor_compressible_user::accounts::UpdateRecord {
user: user.pubkey(),
user_record: user_record_pda,
};

let instruction_data = anchor_compressible_user::instruction::UpdateRecord {
name: "Alice Updated".to_string(),
score: 100,
};

let instruction = Instruction {
program_id,
accounts: accounts.to_account_metas(None),
data: instruction_data.data(),
};

let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&user.pubkey()),
&[&user],
recent_blockhash,
);

banks_client.process_transaction(transaction).await.unwrap();
}
59 changes: 59 additions & 0 deletions program-tests/sdk-test/src/compress_dynamic_pda.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use borsh::{BorshDeserialize, BorshSerialize};
use light_sdk::{
compressible::compress_pda,
cpi::CpiAccounts,
error::LightSdkError,
instruction::{account_meta::CompressedAccountMeta, ValidityProof},
};
use light_sdk_types::CpiAccountsConfig;
use solana_program::account_info::AccountInfo;

use crate::decompress_dynamic_pda::MyPdaAccount;

/// Compresses a PDA back into a compressed account
/// Anyone can call this after the timeout period has elapsed
// TODO: add macro that create the full instruction. and takes: programid, account and seeds, rent_recipient (to hardcode). low code solution.
pub fn compress_dynamic_pda(
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> Result<(), LightSdkError> {
let mut instruction_data = instruction_data;
let instruction_data = CompressFromPdaInstructionData::deserialize(&mut instruction_data)
.map_err(|_| LightSdkError::Borsh)?;

let pda_account = &accounts[1];

// CHECK: hardcoded rent recipient.
let rent_recipient = &accounts[2];
if rent_recipient.key != &crate::create_dynamic_pda::RENT_RECIPIENT {
return Err(LightSdkError::ConstraintViolation);
}

// Cpi accounts
let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER);
let cpi_accounts = CpiAccounts::new_with_config(
&accounts[0],
&accounts[instruction_data.system_accounts_offset as usize..],
config,
);

compress_pda::<MyPdaAccount>(
pda_account,
&instruction_data.compressed_account_meta,
instruction_data.proof,
cpi_accounts,
&crate::ID,
rent_recipient,
)?;

// any other program logic here...

Ok(())
}

#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)]
pub struct CompressFromPdaInstructionData {
pub proof: ValidityProof,
pub compressed_account_meta: CompressedAccountMeta,
pub system_accounts_offset: u8,
}
Loading