diff --git a/.github/workflows/light-examples-tests.yml b/.github/workflows/light-examples-tests.yml index 03de67e190..2c0d1d355a 100644 --- a/.github/workflows/light-examples-tests.yml +++ b/.github/workflows/light-examples-tests.yml @@ -56,7 +56,7 @@ jobs: - program: sdk-test-program sub-tests: '["cargo-test-sbf -p sdk-test"]' - program: sdk-anchor-test-program - sub-tests: '["cargo-test-sbf -p sdk-anchor-test"]' + sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-pinocchio-test"]' steps: - name: Checkout sources diff --git a/Cargo.lock b/Cargo.lock index f110459e05..d611c7be53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,10 +1235,14 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressed-token", + "light-hasher", "light-program-test", "light-prover-client", "light-sdk", + "light-sdk-pinocchio", + "light-sdk-types", "light-test-utils", + "light-zero-copy", "rand 0.8.5", "solana-account", "solana-account-decoder-client-types", @@ -1404,6 +1408,7 @@ dependencies = [ "light-hasher", "light-program-test", "light-sdk", + "light-sdk-types", "solana-sdk", "tokio", ] @@ -1435,6 +1440,7 @@ dependencies = [ "light-compressed-account", "light-hasher", "light-sdk", + "light-sdk-types", "light-system-program-anchor", ] @@ -3629,6 +3635,8 @@ dependencies = [ "light-hasher", "light-macros", "light-sdk-macros", + "light-sdk-types", + "light-zero-copy", "num-bigint 0.4.6", "solana-account-info", "solana-cpi", @@ -3646,7 +3654,9 @@ dependencies = [ "borsh 0.10.4", "light-compressed-account", "light-hasher", + "light-macros", "light-poseidon 0.3.0", + "light-sdk-types", "prettyplease", "proc-macro2", "quote", @@ -3654,6 +3664,38 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "light-sdk-pinocchio" +version = "0.12.0" +dependencies = [ + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-sdk-macros", + "light-sdk-types", + "light-zero-copy", + "pinocchio", + "solana-pubkey", + "thiserror 2.0.12", +] + +[[package]] +name = "light-sdk-types" +version = "0.9.1" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-zero-copy", + "solana-pubkey", + "thiserror 2.0.12", +] + [[package]] name = "light-sparse-merkle-tree" version = "0.1.0" @@ -5277,11 +5319,29 @@ dependencies = [ "light-program-test", "light-prover-client", "light-sdk", + "light-sdk-types", "light-test-utils", "solana-sdk", "tokio", ] +[[package]] +name = "sdk-pinocchio-test" +version = "1.0.0" +dependencies = [ + "borsh 0.10.4", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-pinocchio", + "light-sdk-types", + "pinocchio", + "solana-sdk", + "tokio", +] + [[package]] name = "sdk-test" version = "1.0.0" @@ -5292,6 +5352,7 @@ dependencies = [ "light-macros", "light-program-test", "light-sdk", + "light-sdk-types", "solana-program", "solana-sdk", "tokio", @@ -8833,6 +8894,7 @@ dependencies = [ "light-prover-client", "light-registry", "light-sdk", + "light-sdk-types", "light-system-program-anchor", "light-test-utils", "light-verifier", @@ -8861,6 +8923,7 @@ dependencies = [ "light-prover-client", "light-registry", "light-sdk", + "light-sdk-types", "light-system-program-anchor", "light-test-utils", "light-verifier", @@ -9150,6 +9213,7 @@ dependencies = [ "light-program-test", "light-prover-client", "light-sdk", + "light-sdk-types", "light-system-program-anchor", "light-test-utils", "light-verifier", diff --git a/Cargo.toml b/Cargo.toml index cd00d86c91..5a98246284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ members = [ "sdk-libs/client", "sdk-libs/macros", "sdk-libs/sdk", + "sdk-libs/sdk-pinocchio", + "sdk-libs/sdk-types", "sdk-libs/photon-api", "sdk-libs/program-test", "xtask", @@ -38,6 +40,7 @@ members = [ # Issue is that anchor discriminator now returns a slice instead of an array "program-tests/sdk-anchor-test/programs/sdk-anchor-test", "program-tests/sdk-test", + "program-tests/sdk-pinocchio-test", "program-tests/create-address-test-program", "program-tests/utils", "program-tests/merkle-tree", @@ -153,7 +156,9 @@ light-merkle-tree-reference = { path = "program-tests/merkle-tree", version = "2 light-heap = { path = "program-libs/heap", version = "1.1.0" } light-prover-client = { path = "prover/client", version = "1.3.0" } light-sdk = { path = "sdk-libs/sdk", version = "0.12.0" } +light-sdk-pinocchio = { path = "sdk-libs/sdk-pinocchio", version = "0.12.0" } light-sdk-macros = { path = "sdk-libs/macros", version = "0.6.0" } +light-sdk-types = { path = "sdk-libs/sdk-types", version = "0.9.1" } light-compressed-account = { path = "program-libs/compressed-account", version = "0.2.0" } light-account-checks = { path = "program-libs/account-checks", version = "0.2.0" } light-verifier = { path = "program-libs/verifier", version = "2.0.0" } diff --git a/examples/anchor/counter/Cargo.toml b/examples/anchor/counter/Cargo.toml index a357412257..9d9815bc51 100644 --- a/examples/anchor/counter/Cargo.toml +++ b/examples/anchor/counter/Cargo.toml @@ -23,6 +23,7 @@ idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] anchor-lang = { workspace = true } light-hasher = { workspace = true, features = ["solana"] } light-sdk = { workspace = true, features = ["anchor"] } +light-sdk-types = { workspace = true } light-compressed-account = { workspace = true } [dev-dependencies] diff --git a/examples/anchor/counter/src/lib.rs b/examples/anchor/counter/src/lib.rs index f0b26e4670..e23d545efd 100644 --- a/examples/anchor/counter/src/lib.rs +++ b/examples/anchor/counter/src/lib.rs @@ -4,16 +4,20 @@ use anchor_lang::{prelude::*, AnchorDeserialize, Discriminator}; use light_sdk::{ account::LightAccount, address::v1::derive_address, - cpi::{CpiAccounts, CpiInputs}, + cpi::{CpiAccounts, CpiInputs, CpiSigner}, + derive_light_cpi_signer, instruction::{ account_meta::{CompressedAccountMeta, CompressedAccountMetaClose}, - tree_info::PackedAddressTreeInfo, + PackedAddressTreeInfo, ValidityProof, }, - LightDiscriminator, LightHasher, ValidityProof, + LightDiscriminator, LightHasher, }; declare_id!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); + #[program] pub mod counter { @@ -33,15 +37,14 @@ pub mod counter { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let (address, address_seed) = derive_address( &[b"counter", ctx.accounts.signer.key().as_ref()], - &light_cpi_accounts.tree_accounts() - [address_tree_info.address_merkle_tree_pubkey_index as usize] - .key(), + &address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, &crate::ID, ); @@ -94,9 +97,8 @@ pub mod counter { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let cpi_inputs = CpiInputs::new( proof, @@ -133,9 +135,8 @@ pub mod counter { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let cpi_inputs = CpiInputs::new( proof, @@ -170,9 +171,8 @@ pub mod counter { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let cpi_inputs = CpiInputs::new( proof, vec![counter.to_account_info().map_err(ProgramError::from)?], @@ -207,9 +207,8 @@ pub mod counter { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let cpi_inputs = CpiInputs::new( proof, diff --git a/examples/anchor/counter/tests/test.rs b/examples/anchor/counter/tests/test.rs index ea5da12d73..bc45fc3751 100644 --- a/examples/anchor/counter/tests/test.rs +++ b/examples/anchor/counter/tests/test.rs @@ -10,8 +10,7 @@ use light_sdk::{ address::v1::derive_address, instruction::{ account_meta::{CompressedAccountMeta, CompressedAccountMetaClose}, - accounts::SystemAccountMetaConfig, - pack_accounts::PackedAccounts, + PackedAccounts, SystemAccountMetaConfig, }, }; use solana_sdk::{ @@ -138,7 +137,7 @@ where .value; let output_state_tree_index = rpc - .get_random_state_tree_info() + .get_random_state_tree_info()? .pack_output_tree_index(&mut remaining_accounts)?; let packed_address_tree_info = rpc_result .pack_tree_infos(&mut remaining_accounts) diff --git a/examples/anchor/memo/tests/test.rs b/examples/anchor/memo/tests/test.rs index 8180322070..cf151b2c47 100644 --- a/examples/anchor/memo/tests/test.rs +++ b/examples/anchor/memo/tests/test.rs @@ -17,7 +17,7 @@ use light_sdk::{ address::derive_address, instruction_data::LightInstructionData, tree_info::{AddressTreeInfo, PackedAccounts}, - utils::get_cpi_authority_pda, + find_cpi_signer_macro, verify::find_cpi_signer, PROGRAM_ID_ACCOUNT_COMPRESSION, PROGRAM_ID_LIGHT_SYSTEM, PROGRAM_ID_NOOP, }; @@ -66,7 +66,7 @@ async fn test_memo_program() { &memo::ID, ); - let account_compression_authority = get_cpi_authority_pda(&PROGRAM_ID_LIGHT_SYSTEM); + let account_compression_authority = find_cpi_signer_macro!(&PROGRAM_ID_LIGHT_SYSTEM); let registered_program_pda = Pubkey::find_program_address( &[PROGRAM_ID_LIGHT_SYSTEM.to_bytes().as_slice()], &PROGRAM_ID_ACCOUNT_COMPRESSION, diff --git a/examples/anchor/name-service-without-macros/tests/test.rs b/examples/anchor/name-service-without-macros/tests/test.rs index fc4ac44163..689aafa9bb 100644 --- a/examples/anchor/name-service-without-macros/tests/test.rs +++ b/examples/anchor/name-service-without-macros/tests/test.rs @@ -20,7 +20,7 @@ use light_sdk::{ error::LightSdkError, instruction_data::LightInstructionData, tree_info::{AddressTreeInfo, PackedAccounts}, - utils::get_cpi_authority_pda, + find_cpi_signer_macro, verify::find_cpi_signer, PROGRAM_ID_ACCOUNT_COMPRESSION, PROGRAM_ID_LIGHT_SYSTEM, PROGRAM_ID_NOOP, }; @@ -76,7 +76,7 @@ async fn test_name_service() { &name_service_without_macros::ID, ); - let account_compression_authority = get_cpi_authority_pda(&PROGRAM_ID_LIGHT_SYSTEM); + let account_compression_authority = find_cpi_signer_macro!(&PROGRAM_ID_LIGHT_SYSTEM); let registered_program_pda = Pubkey::find_program_address( &[PROGRAM_ID_LIGHT_SYSTEM.to_bytes().as_slice()], &PROGRAM_ID_ACCOUNT_COMPRESSION, diff --git a/examples/anchor/name-service/tests/test.rs b/examples/anchor/name-service/tests/test.rs index e37eacc6bb..35070e673f 100644 --- a/examples/anchor/name-service/tests/test.rs +++ b/examples/anchor/name-service/tests/test.rs @@ -20,7 +20,7 @@ use light_sdk::{ pack_address_merkle_context, pack_merkle_context, AddressTreeInfo, MerkleContext, PackedAddressTreeInfo, PackedMerkleContext, PackedAccounts, }, - utils::get_cpi_authority_pda, + find_cpi_signer_macro, verify::find_cpi_signer, PROGRAM_ID_ACCOUNT_COMPRESSION, PROGRAM_ID_LIGHT_SYSTEM, PROGRAM_ID_NOOP, }; @@ -83,7 +83,7 @@ async fn test_name_service() { let address_merkle_context = pack_address_merkle_context(&address_merkle_context, &mut remaining_accounts); - let account_compression_authority = get_cpi_authority_pda(&PROGRAM_ID_LIGHT_SYSTEM); + let account_compression_authority = find_cpi_signer_macro!(&PROGRAM_ID_LIGHT_SYSTEM); let registered_program_pda = Pubkey::find_program_address( &[PROGRAM_ID_LIGHT_SYSTEM.to_bytes().as_slice()], &PROGRAM_ID_ACCOUNT_COMPRESSION, diff --git a/examples/anchor/token-escrow/Cargo.toml b/examples/anchor/token-escrow/Cargo.toml index 24b8311f74..56f10b2f67 100644 --- a/examples/anchor/token-escrow/Cargo.toml +++ b/examples/anchor/token-escrow/Cargo.toml @@ -27,6 +27,7 @@ account-compression = { workspace = true, features = ["cpi"] } light-hasher = { workspace = true } light-sdk = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor"] } +light-sdk-types = { workspace = true } [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = { workspace = true } diff --git a/examples/anchor/token-escrow/src/escrow_with_compressed_pda/escrow.rs b/examples/anchor/token-escrow/src/escrow_with_compressed_pda/escrow.rs index 88f3c33619..dfcf6a3787 100644 --- a/examples/anchor/token-escrow/src/escrow_with_compressed_pda/escrow.rs +++ b/examples/anchor/token-escrow/src/escrow_with_compressed_pda/escrow.rs @@ -136,15 +136,10 @@ fn cpi_compressed_pda_transfer<'info>( let light_accounts = CpiAccounts::new_with_config( ctx.accounts.signer.as_ref(), &system_accounts, - CpiAccountsConfig { - self_program: crate::ID, - cpi_context: true, - ..Default::default() - }, - ) - .unwrap(); + CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + ); - verify_borsh(&light_accounts, &inputs_struct).map_err(ProgramError::from)?; + verify_borsh(light_accounts, &inputs_struct).map_err(ProgramError::from)?; Ok(()) } diff --git a/examples/anchor/token-escrow/src/escrow_with_compressed_pda/sdk.rs b/examples/anchor/token-escrow/src/escrow_with_compressed_pda/sdk.rs index de69bf115d..f89f9362c0 100644 --- a/examples/anchor/token-escrow/src/escrow_with_compressed_pda/sdk.rs +++ b/examples/anchor/token-escrow/src/escrow_with_compressed_pda/sdk.rs @@ -13,6 +13,7 @@ use light_compressed_token::process_transfer::{ transfer_sdk::{create_inputs_and_remaining_accounts_checked, to_account_metas}, TokenTransferOutputData, }; +use light_sdk::{constants::CPI_AUTHORITY_PDA_SEED, find_cpi_signer_macro}; use light_test_utils::pack::{ add_and_get_remaining_account_indices, pack_merkle_context, pack_new_address_params, }; @@ -98,7 +99,7 @@ pub fn create_escrow_instruction( let compressed_token_cpi_authority_pda = get_cpi_authority_pda().0; let account_compression_authority = light_system_program::utils::get_cpi_authority_pda(&light_system_program::ID); - let cpi_authority_pda = light_sdk::utils::get_cpi_authority_pda(&crate::ID); + let cpi_authority_pda = find_cpi_signer_macro!(&crate::ID).0; let accounts = crate::accounts::EscrowCompressedTokensWithCompressedPda { signer: *input_params.signer, diff --git a/examples/anchor/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs b/examples/anchor/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs index 8b92845cdc..0ffdb10b1a 100644 --- a/examples/anchor/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs +++ b/examples/anchor/token-escrow/src/escrow_with_compressed_pda/withdrawal.rs @@ -160,14 +160,9 @@ fn cpi_compressed_pda_withdrawal<'info>( let light_accounts = CpiAccounts::new_with_config( ctx.accounts.signer.as_ref(), &system_accounts, - CpiAccountsConfig { - self_program: crate::ID, - cpi_context: true, - ..Default::default() - }, - ) - .unwrap(); - verify_borsh(&light_accounts, &inputs_struct).unwrap(); + CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + ); + verify_borsh(light_accounts, &inputs_struct).unwrap(); Ok(()) } diff --git a/examples/anchor/token-escrow/src/lib.rs b/examples/anchor/token-escrow/src/lib.rs index 5f978cbbed..268ded15fa 100644 --- a/examples/anchor/token-escrow/src/lib.rs +++ b/examples/anchor/token-escrow/src/lib.rs @@ -3,6 +3,7 @@ use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; use light_compressed_token::process_transfer::{ InputTokenDataWithContext, PackedTokenTransferOutputData, }; +use light_sdk::{cpi::CpiSigner, derive_light_cpi_signer}; pub mod escrow_with_compressed_pda; pub mod escrow_with_pda; @@ -23,6 +24,9 @@ pub enum EscrowError { declare_id!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("GRLu2hKaAiMbxpkAM1HeXzks9YeGuz18SEgXEizVvPqX"); + #[program] pub mod token_escrow { diff --git a/forester-utils/src/instructions/address_batch_update.rs b/forester-utils/src/instructions/address_batch_update.rs index 31eaa4db62..fc1d790626 100644 --- a/forester-utils/src/instructions/address_batch_update.rs +++ b/forester-utils/src/instructions/address_batch_update.rs @@ -8,13 +8,14 @@ use light_batched_merkle_tree::{ }, }; use light_client::{indexer::Indexer, rpc::Rpc}; -use light_compressed_account::hash_chain::create_hash_chain_from_slice; +use light_compressed_account::{ + hash_chain::create_hash_chain_from_slice, instruction_data::compressed_proof::CompressedProof, +}; use light_hasher::{bigint::bigint_to_be_bytes_array, Poseidon}; use light_prover_client::{ proof_client::ProofClient, proof_types::batch_address_append::get_batch_address_append_circuit_inputs, }; -use light_sdk::light_compressed_account::instruction_data::compressed_proof::CompressedProof; use light_sparse_merkle_tree::{ changelog::ChangelogEntry, indexed_changelog::IndexedChangelogEntry, SparseMerkleTree, }; diff --git a/forester-utils/src/registry.rs b/forester-utils/src/registry.rs index f62e31a95f..b52547d398 100644 --- a/forester-utils/src/registry.rs +++ b/forester-utils/src/registry.rs @@ -324,7 +324,7 @@ pub async fn create_rollover_state_merkle_tree_instructions( rpc.get_minimum_balance_for_rent_exemption(account_size) .await .unwrap(), - &light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, + &Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), Some(new_cpi_context_keypair), ); let instruction = create_rollover_state_merkle_tree_instruction( diff --git a/forester/src/indexer_type.rs b/forester/src/indexer_type.rs index d510e3c468..f4d69b7850 100644 --- a/forester/src/indexer_type.rs +++ b/forester/src/indexer_type.rs @@ -15,7 +15,7 @@ use light_merkle_tree_reference::MerkleTree; use light_program_test::indexer::{ state_tree::StateMerkleTreeBundle, TestIndexer, TestIndexerExtensions, }; -use light_sdk::{STATE_MERKLE_TREE_CANOPY_DEPTH, STATE_MERKLE_TREE_HEIGHT}; +use light_sdk::constants::{STATE_MERKLE_TREE_CANOPY_DEPTH, STATE_MERKLE_TREE_HEIGHT}; use solana_program::pubkey::Pubkey; use solana_sdk::{signature::Keypair, signer::Signer}; use tokio::sync::Mutex; diff --git a/program-libs/account-checks/src/account_info/account_info_trait.rs b/program-libs/account-checks/src/account_info/account_info_trait.rs index c39bd32ed9..03b5cf4729 100644 --- a/program-libs/account-checks/src/account_info/account_info_trait.rs +++ b/program-libs/account-checks/src/account_info/account_info_trait.rs @@ -4,6 +4,7 @@ use crate::error::AccountError; /// Trait to abstract over different AccountInfo implementations (pinocchio vs solana) pub trait AccountInfoTrait { + type Pubkey: Copy + Clone; type DataRef<'a>: Deref where Self: 'a; @@ -13,6 +14,8 @@ pub trait AccountInfoTrait { /// Return raw byte array for maximum compatibility fn key(&self) -> [u8; 32]; + /// Return the pubkey in the native format + fn pubkey(&self) -> Self::Pubkey; fn is_writable(&self) -> bool; fn is_signer(&self) -> bool; fn executable(&self) -> bool; @@ -26,6 +29,9 @@ pub trait AccountInfoTrait { /// Check ownership safely - each implementation handles this without exposing owner fn is_owned_by(&self, program: &[u8; 32]) -> bool; + /// Convert byte array to native Pubkey type + fn pubkey_from_bytes(bytes: [u8; 32]) -> Self::Pubkey; + /// PDA functions - each implementation uses its own backend fn find_program_address(seeds: &[&[u8]], program_id: &[u8; 32]) -> ([u8; 32], u8); fn create_program_address( diff --git a/program-libs/account-checks/src/account_info/pinocchio.rs b/program-libs/account-checks/src/account_info/pinocchio.rs index 190e03c77d..2b4f6ff2a9 100644 --- a/program-libs/account-checks/src/account_info/pinocchio.rs +++ b/program-libs/account-checks/src/account_info/pinocchio.rs @@ -3,6 +3,7 @@ use crate::error::AccountError; /// Implement trait for pinocchio AccountInfo impl AccountInfoTrait for pinocchio::account_info::AccountInfo { + type Pubkey = [u8; 32]; type DataRef<'a> = pinocchio::account_info::Ref<'a, [u8]>; type DataRefMut<'a> = pinocchio::account_info::RefMut<'a, [u8]>; @@ -10,6 +11,14 @@ impl AccountInfoTrait for pinocchio::account_info::AccountInfo { *self.key() } + fn pubkey(&self) -> Self::Pubkey { + *self.key() + } + + fn pubkey_from_bytes(bytes: [u8; 32]) -> Self::Pubkey { + bytes + } + fn is_writable(&self) -> bool { self.is_writable() } diff --git a/program-libs/account-checks/src/account_info/solana.rs b/program-libs/account-checks/src/account_info/solana.rs index 7c68b80dfe..14a5507d51 100644 --- a/program-libs/account-checks/src/account_info/solana.rs +++ b/program-libs/account-checks/src/account_info/solana.rs @@ -3,19 +3,28 @@ use crate::error::AccountError; /// Implement trait for solana AccountInfo impl AccountInfoTrait for solana_account_info::AccountInfo<'_> { - type DataRef<'a> - = std::cell::Ref<'a, [u8]> + type Pubkey = solana_pubkey::Pubkey; + type DataRef<'b> + = std::cell::Ref<'b, [u8]> where - Self: 'a; - type DataRefMut<'a> - = std::cell::RefMut<'a, [u8]> + Self: 'b; + type DataRefMut<'b> + = std::cell::RefMut<'b, [u8]> where - Self: 'a; + Self: 'b; fn key(&self) -> [u8; 32] { self.key.to_bytes() } + fn pubkey(&self) -> Self::Pubkey { + *self.key + } + + fn pubkey_from_bytes(bytes: [u8; 32]) -> Self::Pubkey { + solana_pubkey::Pubkey::from(bytes) + } + fn is_writable(&self) -> bool { self.is_writable } diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index 3f789363a8..9c79f9ca24 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -40,3 +40,42 @@ impl<'a> Deserialize<'a> for CompressedProof { Ok(Ref::<&[u8], CompressedProof>::from_prefix(bytes)?) } } + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] +pub struct ValidityProof(pub Option); + +impl ValidityProof { + pub fn new(proof: Option) -> Self { + Self(proof) + } +} + +impl From for ValidityProof { + fn from(proof: CompressedProof) -> Self { + Self(Some(proof)) + } +} + +impl From> for ValidityProof { + fn from(proof: Option) -> Self { + Self(proof) + } +} +impl From<&CompressedProof> for ValidityProof { + fn from(proof: &CompressedProof) -> Self { + Self(Some(*proof)) + } +} + +impl From<&Option> for ValidityProof { + fn from(proof: &Option) -> Self { + Self(*proof) + } +} + +#[allow(clippy::from_over_into)] +impl Into> for ValidityProof { + fn into(self) -> Option { + self.0 + } +} diff --git a/program-tests/client-test/Cargo.toml b/program-tests/client-test/Cargo.toml index 4283fb8e71..6580607de4 100644 --- a/program-tests/client-test/Cargo.toml +++ b/program-tests/client-test/Cargo.toml @@ -19,6 +19,10 @@ light-program-test = { workspace = true, features = ["devenv"] } light-prover-client = { workspace = true, features = ["devenv"] } light-test-utils = { workspace = true } light-sdk = { workspace = true } +light-sdk-pinocchio = { workspace = true } +light-sdk-types = { workspace = true } +light-zero-copy = { workspace = true } +light-hasher = { workspace = true } light-compressed-account = { workspace = true } light-compressed-token = { workspace = true } diff --git a/program-tests/client-test/tests/light_client.rs b/program-tests/client-test/tests/light_client.rs index ba86a6b791..0ee41972ca 100644 --- a/program-tests/client-test/tests/light_client.rs +++ b/program-tests/client-test/tests/light_client.rs @@ -13,9 +13,8 @@ use light_compressed_token::mint_sdk::{ use light_program_test::accounts::test_accounts::TestAccounts; use light_prover_client::prover::ProverConfig; use light_sdk::{ - address::v1::derive_address, + address::{v1::derive_address, NewAddressParams}, token::{AccountState, TokenData}, - NewAddressParams, }; use light_test_utils::{system_program::create_invoke_instruction, Rpc, RpcError}; use solana_compute_budget_interface::ComputeBudgetInstruction; diff --git a/program-tests/client-test/tests/light_program_test.rs b/program-tests/client-test/tests/light_program_test.rs index 374dd6a21c..087674193a 100644 --- a/program-tests/client-test/tests/light_program_test.rs +++ b/program-tests/client-test/tests/light_program_test.rs @@ -14,9 +14,8 @@ use light_program_test::{ accounts::test_accounts::TestAccounts, program_test::LightProgramTest, ProgramTestConfig, }; use light_sdk::{ - address::v1::derive_address, + address::{v1::derive_address, NewAddressParams}, token::{AccountState, TokenData}, - NewAddressParams, }; use light_test_utils::{system_program::create_invoke_instruction, RpcError}; use solana_sdk::{ diff --git a/program-tests/client-test/tests/sdk_compat.rs b/program-tests/client-test/tests/sdk_compat.rs new file mode 100644 index 0000000000..615ba49749 --- /dev/null +++ b/program-tests/client-test/tests/sdk_compat.rs @@ -0,0 +1,110 @@ +use light_hasher::HasherError; +use light_sdk::error::LightSdkError as SolanaLightSdkError; +use light_sdk_pinocchio::error::LightSdkError as PinocchioLightSdkError; +use light_zero_copy::errors::ZeroCopyError; + +fn generate_all_solana_errors() -> Vec { + vec![ + SolanaLightSdkError::ConstraintViolation, + SolanaLightSdkError::InvalidLightSystemProgram, + SolanaLightSdkError::ExpectedAccounts, + SolanaLightSdkError::ExpectedAddressTreeInfo, + SolanaLightSdkError::ExpectedAddressRootIndex, + SolanaLightSdkError::ExpectedData, + SolanaLightSdkError::ExpectedDiscriminator, + SolanaLightSdkError::ExpectedHash, + SolanaLightSdkError::ExpectedLightSystemAccount("test".to_string()), + SolanaLightSdkError::ExpectedMerkleContext, + SolanaLightSdkError::ExpectedRootIndex, + SolanaLightSdkError::TransferFromNoInput, + SolanaLightSdkError::TransferFromNoLamports, + SolanaLightSdkError::TransferFromInsufficientLamports, + SolanaLightSdkError::TransferIntegerOverflow, + SolanaLightSdkError::Borsh, + SolanaLightSdkError::FewerAccountsThanSystemAccounts, + SolanaLightSdkError::InvalidCpiSignerAccount, + SolanaLightSdkError::MissingField("test".to_string()), + SolanaLightSdkError::OutputStateTreeIndexIsNone, + SolanaLightSdkError::InitAddressIsNone, + SolanaLightSdkError::InitWithAddressIsNone, + SolanaLightSdkError::InitWithAddressOutputIsNone, + SolanaLightSdkError::MetaMutAddressIsNone, + SolanaLightSdkError::MetaMutInputIsNone, + SolanaLightSdkError::MetaMutOutputLamportsIsNone, + SolanaLightSdkError::MetaMutOutputIsNone, + SolanaLightSdkError::MetaCloseAddressIsNone, + SolanaLightSdkError::MetaCloseInputIsNone, + SolanaLightSdkError::Hasher(HasherError::IntegerOverflow), + SolanaLightSdkError::ZeroCopy(ZeroCopyError::Full), + ] +} + +fn generate_all_pinocchio_errors() -> Vec { + vec![ + PinocchioLightSdkError::ConstraintViolation, + PinocchioLightSdkError::InvalidLightSystemProgram, + PinocchioLightSdkError::ExpectedAccounts, + PinocchioLightSdkError::ExpectedAddressTreeInfo, + PinocchioLightSdkError::ExpectedAddressRootIndex, + PinocchioLightSdkError::ExpectedData, + PinocchioLightSdkError::ExpectedDiscriminator, + PinocchioLightSdkError::ExpectedHash, + PinocchioLightSdkError::ExpectedLightSystemAccount("test".to_string()), + PinocchioLightSdkError::ExpectedMerkleContext, + PinocchioLightSdkError::ExpectedRootIndex, + PinocchioLightSdkError::TransferFromNoInput, + PinocchioLightSdkError::TransferFromNoLamports, + PinocchioLightSdkError::TransferFromInsufficientLamports, + PinocchioLightSdkError::TransferIntegerOverflow, + PinocchioLightSdkError::Borsh, + PinocchioLightSdkError::FewerAccountsThanSystemAccounts, + PinocchioLightSdkError::InvalidCpiSignerAccount, + PinocchioLightSdkError::MissingField("test".to_string()), + PinocchioLightSdkError::OutputStateTreeIndexIsNone, + PinocchioLightSdkError::InitAddressIsNone, + PinocchioLightSdkError::InitWithAddressIsNone, + PinocchioLightSdkError::InitWithAddressOutputIsNone, + PinocchioLightSdkError::MetaMutAddressIsNone, + PinocchioLightSdkError::MetaMutInputIsNone, + PinocchioLightSdkError::MetaMutOutputLamportsIsNone, + PinocchioLightSdkError::MetaMutOutputIsNone, + PinocchioLightSdkError::MetaCloseAddressIsNone, + PinocchioLightSdkError::MetaCloseInputIsNone, + PinocchioLightSdkError::Hasher(HasherError::IntegerOverflow), + PinocchioLightSdkError::ZeroCopy(ZeroCopyError::Full), + ] +} + +#[test] +fn test_error_compatibility() { + let solana_errors = generate_all_solana_errors(); + let pinocchio_errors = generate_all_pinocchio_errors(); + + // Ensure both SDKs have the same number of error variants + assert_eq!( + solana_errors.len(), + pinocchio_errors.len(), + "SDKs have different number of error variants" + ); + + // Test string representations + for (solana_error, pinocchio_error) in solana_errors.iter().zip(pinocchio_errors.iter()) { + assert_eq!( + solana_error.to_string(), + pinocchio_error.to_string(), + "String representations differ for error variants" + ); + } + + // Test error codes (consuming the values) + for (solana_error, pinocchio_error) in + solana_errors.into_iter().zip(pinocchio_errors.into_iter()) + { + let solana_code: u32 = solana_error.into(); + let pinocchio_code: u32 = pinocchio_error.into(); + assert_eq!( + solana_code, pinocchio_code, + "Error codes differ for error variants" + ); + } +} diff --git a/program-tests/create-address-test-program/Cargo.toml b/program-tests/create-address-test-program/Cargo.toml index 06f62020a7..f25726d3be 100644 --- a/program-tests/create-address-test-program/Cargo.toml +++ b/program-tests/create-address-test-program/Cargo.toml @@ -24,4 +24,5 @@ anchor-lang = { workspace = true } light-system-program-anchor = { workspace = true, features = ["cpi"] } account-compression = { workspace = true, features = ["cpi"] } light-compressed-account = { workspace = true, features = ["anchor"] } -light-sdk = { workspace = true, features = ["anchor", "v2"] } +light-sdk = { workspace = true, features = ["anchor", "v2", "small_ix"] } +light-sdk-types = { workspace = true } diff --git a/program-tests/create-address-test-program/src/lib.rs b/program-tests/create-address-test-program/src/lib.rs index 4f223a7203..b2c1c4ec6c 100644 --- a/program-tests/create-address-test-program/src/lib.rs +++ b/program-tests/create-address-test-program/src/lib.rs @@ -7,20 +7,28 @@ use anchor_lang::{ solana_program::{instruction::Instruction, pubkey::Pubkey}, InstructionData, }; +use light_sdk::{cpi::CpiSigner, derive_light_cpi_signer}; use light_system_program::utils::get_registered_program_pda; pub mod create_pda; pub use create_pda::*; use light_compressed_account::instruction_data::{ compressed_proof::CompressedProof, data::NewAddressParamsPacked, }; -use light_sdk::cpi::CpiAccountsConfig; +use light_sdk::{ + constants::LIGHT_SYSTEM_PROGRAM_ID, + cpi::{ + invoke_light_system_program, to_account_metas, to_account_metas_small, CpiAccountsConfig, + }, +}; + declare_id!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); + #[program] pub mod system_cpi_test { - use light_sdk::{cpi::invoke_light_system_program, PROGRAM_ID_LIGHT_SYSTEM}; - use super::*; pub fn create_compressed_pda<'info>( @@ -56,28 +64,37 @@ pub mod system_cpi_test { let (account_infos, account_metas) = if small_ix { use light_sdk::cpi::CpiAccountsSmall; let cpi_accounts = - CpiAccountsSmall::new_with_config(&fee_payer, ctx.remaining_accounts, config) - .map_err(ProgramError::from)?; - let account_infos = cpi_accounts.to_account_infos(); - - let account_metas = cpi_accounts.to_account_metas(); + CpiAccountsSmall::new_with_config(&fee_payer, ctx.remaining_accounts, config); + let account_infos = cpi_accounts + .to_account_infos() + .into_iter() + .cloned() + .collect::>(); + + let account_metas = to_account_metas_small(cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; (account_infos, account_metas) } else { use light_sdk::cpi::CpiAccounts; let cpi_accounts = - CpiAccounts::new_with_config(&fee_payer, ctx.remaining_accounts, config) - .map_err(ProgramError::from)?; - let account_infos = cpi_accounts.to_account_infos(); - - let account_metas = cpi_accounts.to_account_metas(); + CpiAccounts::new_with_config(&fee_payer, ctx.remaining_accounts, config); + let account_infos = cpi_accounts + .to_account_infos() + .into_iter() + .cloned() + .collect::>(); + + let account_metas = + to_account_metas(cpi_accounts).map_err(|_| ErrorCode::AccountNotEnoughKeys)?; (account_infos, account_metas) }; let instruction = Instruction { - program_id: PROGRAM_ID_LIGHT_SYSTEM, + program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), accounts: account_metas, data: inputs, }; - invoke_light_system_program(&crate::ID, &account_infos, instruction) + let cpi_config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + invoke_light_system_program(&account_infos, instruction, cpi_config.bump()) .map_err(ProgramError::from)?; Ok(()) } diff --git a/program-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml b/program-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml index 691faa2826..f0faca1233 100644 --- a/program-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml +++ b/program-tests/sdk-anchor-test/programs/sdk-anchor-test/Cargo.toml @@ -24,6 +24,7 @@ idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] light-hasher = { workspace = true, features = ["solana"] } anchor-lang = { workspace = true } light-sdk = { workspace = true, features = ["anchor", "v2"] } +light-sdk-types = { workspace = true } [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = { workspace = true } diff --git a/program-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs b/program-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs index 72f9dabcb3..9f5f0367d5 100644 --- a/program-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs +++ b/program-tests/sdk-anchor-test/programs/sdk-anchor-test/src/lib.rs @@ -4,19 +4,23 @@ use anchor_lang::{prelude::*, Discriminator}; use light_sdk::{ account::LightAccount, address::v1::derive_address, - cpi::{CpiAccounts, CpiInputs}, - instruction::{account_meta::CompressedAccountMeta, tree_info::PackedAddressTreeInfo}, - LightDiscriminator, LightHasher, NewAddressParamsPacked, ValidityProof, + cpi::{CpiAccounts, CpiInputs, CpiSigner}, + derive_light_cpi_signer, + instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo, ValidityProof}, + LightDiscriminator, LightHasher, }; declare_id!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); + #[program] pub mod sdk_anchor_test { use super::*; - pub fn with_nested_data<'info>( + pub fn create_compressed_account<'info>( ctx: Context<'_, '_, '_, 'info, WithNestedData<'info>>, proof: ValidityProof, address_tree_info: PackedAddressTreeInfo, @@ -26,23 +30,17 @@ pub mod sdk_anchor_test { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let (address, address_seed) = derive_address( &[b"compressed", name.as_bytes()], - &light_cpi_accounts.tree_accounts() - [address_tree_info.address_merkle_tree_pubkey_index as usize] - .key(), + &address_tree_info + .get_tree_pubkey(&light_cpi_accounts) + .map_err(|_| ErrorCode::AccountNotEnoughKeys)?, &crate::ID, ); - let new_address_params = NewAddressParamsPacked { - seed: address_seed, - address_queue_account_index: address_tree_info.address_queue_pubkey_index, - address_merkle_tree_root_index: address_tree_info.root_index, - address_merkle_tree_account_index: address_tree_info.address_merkle_tree_pubkey_index, - }; + let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); let mut my_compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init( &crate::ID, @@ -68,7 +66,7 @@ pub mod sdk_anchor_test { Ok(()) } - pub fn update_nested_data<'info>( + pub fn update_compressed_account<'info>( ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, proof: ValidityProof, my_compressed_account: MyCompressedAccount, @@ -87,9 +85,8 @@ pub mod sdk_anchor_test { let light_cpi_accounts = CpiAccounts::new( ctx.accounts.signer.as_ref(), ctx.remaining_accounts, - crate::ID, - ) - .map_err(ProgramError::from)?; + crate::LIGHT_CPI_SIGNER, + ); let cpi_inputs = CpiInputs::new( proof, @@ -117,6 +114,7 @@ pub mod sdk_anchor_test { #[event] #[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] pub struct MyCompressedAccount { + #[hash] pub name: String, pub nested: NestedData, } diff --git a/program-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs b/program-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs index 8429af67e3..96949c26e5 100644 --- a/program-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs +++ b/program-tests/sdk-anchor-test/programs/sdk-anchor-test/tests/test.rs @@ -1,6 +1,6 @@ #![cfg(feature = "test-sbf")] -use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; +use anchor_lang::AnchorDeserialize; use light_client::indexer::CompressedAccount; use light_program_test::{ indexer::TestIndexerExtensions, program_test::LightProgramTest, AddressWithTree, Indexer, @@ -8,15 +8,12 @@ use light_program_test::{ }; use light_sdk::{ address::v1::derive_address, - instruction::{ - account_meta::CompressedAccountMeta, accounts::SystemAccountMetaConfig, - pack_accounts::PackedAccounts, - }, + instruction::{account_meta::CompressedAccountMeta, PackedAccounts, SystemAccountMetaConfig}, }; use light_test_utils::{Rpc, RpcError}; use sdk_anchor_test::{MyCompressedAccount, NestedData}; use solana_sdk::{ - instruction::Instruction, + instruction::{AccountMeta, Instruction}, signature::{Keypair, Signature, Signer}, }; @@ -29,15 +26,13 @@ async fn test_sdk_test() { let address_tree_info = rpc.get_address_tree_v1(); - rpc.get_state_merkle_tree_account(); - let (address, _) = derive_address( &[b"compressed", b"test".as_slice()], &address_tree_info.tree, &sdk_anchor_test::ID, ); - with_nested_data("test".to_string(), &mut rpc, &payer, &address) + create_compressed_account("test".to_string(), &mut rpc, &payer, &address) .await .unwrap(); @@ -52,7 +47,7 @@ async fn test_sdk_test() { let record = MyCompressedAccount::deserialize(&mut &record[..]).unwrap(); assert_eq!(record.nested.one, 1); - update_nested_data( + update_compressed_account( &mut rpc, NestedData { one: 2, @@ -89,7 +84,7 @@ async fn test_sdk_test() { assert_eq!(record.nested.one, 2); } -async fn with_nested_data( +async fn create_compressed_account( name: String, rpc: &mut LightProgramTest, payer: &Keypair, @@ -116,33 +111,36 @@ async fn with_nested_data( let output_tree_index = rpc .get_random_state_tree_info() + .unwrap() .pack_output_tree_index(&mut remaining_accounts) .unwrap(); let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); - let instruction_data = sdk_anchor_test::instruction::WithNestedData { - proof: rpc_result.proof, - address_tree_info: packed_accounts.address_trees[0], - name, - output_tree_index, - }; - - let accounts = sdk_anchor_test::accounts::WithNestedData { - signer: payer.pubkey(), - }; - let instruction = Instruction { program_id: sdk_anchor_test::ID, - accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), - data: instruction_data.data(), + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::CreateCompressedAccount { + proof: rpc_result.proof, + address_tree_info: packed_accounts.address_trees[0], + output_tree_index, + name, + } + .data() + }, }; rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) .await } -async fn update_nested_data( +async fn update_compressed_account( rpc: &mut LightProgramTest, nested_data: NestedData, payer: &Keypair, @@ -170,25 +168,27 @@ async fn update_nested_data( &mut compressed_account.data.as_mut().unwrap().data.as_slice(), ) .unwrap(); - let instruction_data = sdk_anchor_test::instruction::UpdateNestedData { - proof: rpc_result.proof, - my_compressed_account, - account_meta: CompressedAccountMeta { - tree_info: packed_tree_accounts.packed_tree_infos[0], - address: compressed_account.address.unwrap(), - output_state_tree_index: packed_tree_accounts.output_tree_index, - }, - nested_data, - }; - - let accounts = sdk_anchor_test::accounts::UpdateNestedData { - signer: payer.pubkey(), - }; - let instruction = Instruction { program_id: sdk_anchor_test::ID, - accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), - data: instruction_data.data(), + accounts: [ + vec![AccountMeta::new(payer.pubkey(), true)], + remaining_accounts, + ] + .concat(), + data: { + use anchor_lang::InstructionData; + sdk_anchor_test::instruction::UpdateCompressedAccount { + proof: rpc_result.proof, + my_compressed_account, + account_meta: CompressedAccountMeta { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + output_state_tree_index: packed_tree_accounts.output_tree_index, + }, + nested_data, + } + .data() + }, }; rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) diff --git a/program-tests/sdk-pinocchio-test/Cargo.toml b/program-tests/sdk-pinocchio-test/Cargo.toml new file mode 100644 index 0000000000..66a47f253c --- /dev/null +++ b/program-tests/sdk-pinocchio-test/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "sdk-pinocchio-test" +version = "1.0.0" +description = "Test program using generalized account compression" +repository = "https://github.com/Lightprotocol/light-protocol" +license = "Apache-2.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "sdk_pinocchio_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +test-sbf = [] +default = [] + +[dependencies] +light-sdk-pinocchio = { workspace = true, features = ["v2"] } +light-sdk-types = { workspace = true } +light-hasher = { workspace = true } +pinocchio = { workspace = true } +light-macros = { workspace = true } +borsh = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +tokio = { workspace = true } +solana-sdk = { workspace = true } +light-hasher = { workspace = true, features = ["solana"] } +light-compressed-account = { workspace = true, features = ["solana"] } +light-sdk = { workspace = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/program-tests/sdk-pinocchio-test/Xargo.toml b/program-tests/sdk-pinocchio-test/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/program-tests/sdk-pinocchio-test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/program-tests/sdk-pinocchio-test/src/create_pda.rs b/program-tests/sdk-pinocchio-test/src/create_pda.rs new file mode 100644 index 0000000000..0b732750ea --- /dev/null +++ b/program-tests/sdk-pinocchio-test/src/create_pda.rs @@ -0,0 +1,87 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk_pinocchio::{ + account::LightAccount, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + error::LightSdkError, + instruction::PackedAddressTreeInfo, + light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array, + LightDiscriminator, LightHasher, ValidityProof, +}; +use pinocchio::account_info::AccountInfo; + +/// CU usage: +/// - sdk pre system program cpi 10,942 CU +/// - total with V1 tree: 307,784 CU +/// - total with V2 tree: 138,876 CU +pub fn create_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + let mut instruction_data = instruction_data; + let instruction_data = CreatePdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + 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, + ); + + let address_tree_info = instruction_data.address_tree_info; + let (address, address_seed) = if BATCHED { + let tree_pubkey = instruction_data + .address_tree_info + .get_tree_pubkey(&cpi_accounts)?; + let address_seed = hashv_to_bn254_field_size_be_const_array::<3>(&[ + b"compressed", + instruction_data.data.as_slice(), + ])?; + let address = light_sdk_pinocchio::light_compressed_account::address::derive_address( + &address_seed, + &tree_pubkey, + &crate::ID, + ); + (address, address_seed) + } else { + light_sdk_pinocchio::address::v1::derive_address( + &[b"compressed", instruction_data.data.as_slice()], + &address_tree_info.get_tree_pubkey(&cpi_accounts)?, + &crate::ID, + ) + }; + + let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); + + let mut my_compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init( + &crate::ID, + Some(address), + instruction_data.output_merkle_tree_index, + ); + + my_compressed_account.data = instruction_data.data; + + let cpi_inputs = CpiInputs::new_with_address( + instruction_data.proof, + vec![my_compressed_account.to_account_info()?], + vec![new_address_params], + ); + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + Ok(()) +} + +#[derive( + Clone, Debug, Default, LightHasher, LightDiscriminator, BorshDeserialize, BorshSerialize, +)] +pub struct MyCompressedAccount { + pub data: [u8; 31], +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct CreatePdaInstructionData { + pub proof: ValidityProof, + pub address_tree_info: PackedAddressTreeInfo, + pub output_merkle_tree_index: u8, + pub data: [u8; 31], + pub system_accounts_offset: u8, + pub tree_accounts_offset: u8, +} diff --git a/program-tests/sdk-pinocchio-test/src/lib.rs b/program-tests/sdk-pinocchio-test/src/lib.rs new file mode 100644 index 0000000000..6f17905374 --- /dev/null +++ b/program-tests/sdk-pinocchio-test/src/lib.rs @@ -0,0 +1,48 @@ +use light_macros::pubkey_array; +use light_sdk_pinocchio::{derive_light_cpi_signer, error::LightSdkError, CpiSigner}; +use pinocchio::{ + account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, +}; +pub mod create_pda; +pub mod update_pda; + +pub const ID: Pubkey = pubkey_array!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); + +entrypoint!(process_instruction); + +#[repr(u8)] +pub enum InstructionType { + CreatePdaBorsh = 0, + UpdatePdaBorsh = 1, +} + +impl TryFrom for InstructionType { + type Error = LightSdkError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(InstructionType::CreatePdaBorsh), + 1 => Ok(InstructionType::UpdatePdaBorsh), + _ => panic!("Invalid instruction discriminator."), + } + } +} + +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError> { + let discriminator = InstructionType::try_from(instruction_data[0]).unwrap(); + match discriminator { + InstructionType::CreatePdaBorsh => { + create_pda::create_pda::(accounts, &instruction_data[1..]) + } + InstructionType::UpdatePdaBorsh => { + update_pda::update_pda::(accounts, &instruction_data[1..]) + } + }?; + Ok(()) +} diff --git a/program-tests/sdk-pinocchio-test/src/update_pda.rs b/program-tests/sdk-pinocchio-test/src/update_pda.rs new file mode 100644 index 0000000000..5c18ac74f1 --- /dev/null +++ b/program-tests/sdk-pinocchio-test/src/update_pda.rs @@ -0,0 +1,68 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk_pinocchio::{ + account::LightAccount, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, + error::LightSdkError, + instruction::account_meta::CompressedAccountMeta, + ValidityProof, +}; +use pinocchio::{account_info::AccountInfo, log::sol_log_compute_units}; + +use crate::create_pda::MyCompressedAccount; + +/// CU usage: +/// - sdk pre system program 9,183k CU +/// - total with V2 tree: 50,194 CU (proof by index) +/// - total with V2 tree: 67,723 CU (proof by index) +pub fn update_pda( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), LightSdkError> { + sol_log_compute_units(); + let mut instruction_data = instruction_data; + let instruction_data = UpdatePdaInstructionData::deserialize(&mut instruction_data) + .map_err(|_| LightSdkError::Borsh)?; + sol_log_compute_units(); + + let mut my_compressed_account = LightAccount::<'_, MyCompressedAccount>::new_mut( + &crate::ID, + &instruction_data.my_compressed_account.meta, + MyCompressedAccount { + data: instruction_data.my_compressed_account.data, + }, + )?; + sol_log_compute_units(); + + my_compressed_account.data = instruction_data.new_data; + + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + sol_log_compute_units(); + let cpi_accounts = CpiAccounts::new_with_config( + &accounts[0], + &accounts[instruction_data.system_accounts_offset as usize..], + config, + ); + sol_log_compute_units(); + let cpi_inputs = CpiInputs::new( + instruction_data.proof, + vec![my_compressed_account.to_account_info()?], + ); + sol_log_compute_units(); + cpi_inputs.invoke_light_system_program(cpi_accounts)?; + + Ok(()) +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct UpdatePdaInstructionData { + pub proof: ValidityProof, + pub my_compressed_account: UpdateMyCompressedAccount, + pub new_data: [u8; 31], + pub system_accounts_offset: u8, +} + +#[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +pub struct UpdateMyCompressedAccount { + pub meta: CompressedAccountMeta, + pub data: [u8; 31], +} diff --git a/program-tests/sdk-pinocchio-test/tests/test.rs b/program-tests/sdk-pinocchio-test/tests/test.rs new file mode 100644 index 0000000000..0b18f9c436 --- /dev/null +++ b/program-tests/sdk-pinocchio-test/tests/test.rs @@ -0,0 +1,203 @@ +// #![cfg(feature = "test-sbf")] + +use borsh::BorshSerialize; +use light_compressed_account::{ + address::derive_address, compressed_account::CompressedAccountWithMerkleContext, + hashv_to_bn254_field_size_be, +}; +use light_program_test::{ + program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, +}; +use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; +use light_sdk_pinocchio::instruction::{account_meta::CompressedAccountMeta, PackedStateTreeInfo}; +use sdk_pinocchio_test::{ + create_pda::CreatePdaInstructionData, + update_pda::{UpdateMyCompressedAccount, UpdatePdaInstructionData}, +}; +use solana_sdk::{ + instruction::Instruction, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +#[tokio::test] +async fn test_sdk_test() { + let config = ProgramTestConfig::new_v2( + false, + Some(vec![( + "sdk_pinocchio_test", + Pubkey::new_from_array(sdk_pinocchio_test::ID), + )]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let address_tree_pubkey = rpc.get_address_merkle_tree_v2(); + let account_data = [1u8; 31]; + + // // V1 trees + // let (address, _) = light_sdk::address::derive_address( + // &[b"compressed", &account_data], + // &address_tree_info, + // &Pubkey::new_from_array(sdk_pinocchio_test::ID), + // ); + // Batched trees + let address_seed = hashv_to_bn254_field_size_be(&[b"compressed", account_data.as_slice()]); + let address = derive_address( + &address_seed, + &address_tree_pubkey.to_bytes(), + &sdk_pinocchio_test::ID, + ); + + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + create_pda( + &payer, + &mut rpc, + &output_queue, + account_data, + address_tree_pubkey, + address, + ) + .await + .unwrap(); + + let compressed_pda = rpc + .indexer() + .unwrap() + .get_compressed_accounts_by_owner( + &Pubkey::new_from_array(sdk_pinocchio_test::ID), + None, + None, + ) + .await + .unwrap() + .value + .items[0] + .clone(); + assert_eq!(compressed_pda.address.unwrap(), address); + + update_pda(&payer, &mut rpc, [2u8; 31], compressed_pda.into()) + .await + .unwrap(); +} + +pub async fn create_pda( + payer: &Keypair, + rpc: &mut LightProgramTest, + merkle_tree_pubkey: &Pubkey, + account_data: [u8; 31], + address_tree_pubkey: Pubkey, + address: [u8; 32], +) -> Result<(), RpcError> { + let system_account_meta_config = + SystemAccountMetaConfig::new(Pubkey::new_from_array(sdk_pinocchio_test::ID)); + let mut accounts = PackedAccounts::default(); + accounts.add_pre_accounts_signer(payer.pubkey()); + accounts.add_system_accounts(system_account_meta_config); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![AddressWithTree { + address, + tree: address_tree_pubkey, + }], + None, + ) + .await? + .value; + + let output_merkle_tree_index = accounts.insert_or_get(*merkle_tree_pubkey); + let packed_address_tree_info = rpc_result.pack_tree_infos(&mut accounts).address_trees[0]; + let (accounts, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas(); + let instruction_data = CreatePdaInstructionData { + proof: rpc_result.proof, + address_tree_info: packed_address_tree_info, + data: account_data, + output_merkle_tree_index, + system_accounts_offset: system_accounts_offset as u8, + tree_accounts_offset: tree_accounts_offset as u8, + }; + let inputs = instruction_data.try_to_vec().unwrap(); + + let instruction = Instruction { + program_id: Pubkey::new_from_array(sdk_pinocchio_test::ID), + accounts, + data: [&[0u8][..], &inputs[..]].concat(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await?; + Ok(()) +} + +pub async fn update_pda( + payer: &Keypair, + rpc: &mut LightProgramTest, + new_account_data: [u8; 31], + compressed_account: CompressedAccountWithMerkleContext, +) -> Result<(), RpcError> { + let system_account_meta_config = + SystemAccountMetaConfig::new(Pubkey::new_from_array(sdk_pinocchio_test::ID)); + let mut accounts = PackedAccounts::default(); + accounts.add_pre_accounts_signer(payer.pubkey()); + accounts.add_system_accounts(system_account_meta_config); + + let rpc_result = rpc + .get_validity_proof(vec![compressed_account.hash().unwrap()], vec![], None) + .await? + .value; + + let packed_accounts = rpc_result + .pack_tree_infos(&mut accounts) + .state_trees + .unwrap(); + + let light_sdk_meta = CompressedAccountMeta { + tree_info: packed_accounts.packed_tree_infos[0], + address: compressed_account.compressed_account.address.unwrap(), + output_state_tree_index: packed_accounts.output_tree_index, + }; + + // Convert to pinocchio CompressedAccountMeta + let meta = CompressedAccountMeta { + tree_info: PackedStateTreeInfo { + root_index: light_sdk_meta.tree_info.root_index, + prove_by_index: light_sdk_meta.tree_info.prove_by_index, + merkle_tree_pubkey_index: light_sdk_meta.tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: light_sdk_meta.tree_info.queue_pubkey_index, + leaf_index: light_sdk_meta.tree_info.leaf_index, + }, + address: light_sdk_meta.address, + output_state_tree_index: light_sdk_meta.output_state_tree_index, + }; + + let (accounts, system_accounts_offset, _) = accounts.to_account_metas(); + let instruction_data = UpdatePdaInstructionData { + my_compressed_account: UpdateMyCompressedAccount { + meta, + data: compressed_account + .compressed_account + .data + .unwrap() + .data + .try_into() + .unwrap(), + }, + proof: light_sdk_pinocchio::ValidityProof(None), + new_data: new_account_data, + system_accounts_offset: system_accounts_offset as u8, + }; + let inputs = instruction_data.try_to_vec().unwrap(); + + let instruction = Instruction { + program_id: Pubkey::new_from_array(sdk_pinocchio_test::ID), + accounts, + data: [&[1u8][..], &inputs[..]].concat(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await?; + Ok(()) +} diff --git a/program-tests/sdk-test/Cargo.toml b/program-tests/sdk-test/Cargo.toml index f46b53929a..6929b36a55 100644 --- a/program-tests/sdk-test/Cargo.toml +++ b/program-tests/sdk-test/Cargo.toml @@ -19,7 +19,8 @@ test-sbf = [] default = [] [dependencies] -light-sdk = { workspace = true, features = ["solana"] } +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"] } diff --git a/program-tests/sdk-test/src/create_pda.rs b/program-tests/sdk-test/src/create_pda.rs index 980c47678e..ecd900fddf 100644 --- a/program-tests/sdk-test/src/create_pda.rs +++ b/program-tests/sdk-test/src/create_pda.rs @@ -1,21 +1,18 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ account::LightAccount, - cpi::{ - create_light_system_progam_instruction_invoke_cpi, invoke_light_system_program, - CpiAccounts, CpiAccountsConfig, CpiInputs, - }, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, error::LightSdkError, - hash_to_field_size::hashv_to_bn254_field_size_be_const_array, - instruction::tree_info::PackedAddressTreeInfo, - LightDiscriminator, LightHasher, NewAddressParamsPacked, ValidityProof, + instruction::{PackedAddressTreeInfo, ValidityProof}, + light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array, + LightDiscriminator, LightHasher, }; use solana_program::account_info::AccountInfo; +/// TODO: write test program with A8JgviaEAByMVLBhcebpDQ7NMuZpqBTBigC1b83imEsd (inconvenient program id) /// CU usage: /// - sdk pre system program cpi 10,942 CU -/// - total with V1 tree: 307,784 CU -/// - total with V2 tree: 138,876 CU +/// - total with V2 tree: 45,758 CU pub fn create_pda( accounts: &[AccountInfo], instruction_data: &[u8], @@ -23,17 +20,12 @@ pub fn create_pda( let mut instruction_data = instruction_data; let instruction_data = CreatePdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; - let config = CpiAccountsConfig { - self_program: crate::ID, - cpi_context: false, - sol_pool_pda: false, - sol_compression_recipient: false, - }; + 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, - )?; + ); let address_tree_info = instruction_data.address_tree_info; let (address, address_seed) = if BATCHED { @@ -42,32 +34,22 @@ pub fn create_pda( instruction_data.data.as_slice(), ]) .unwrap(); + // to_bytes will go away as soon as we have a light_sdk::address::v2::derive_address + let address_tree_pubkey = address_tree_info.get_tree_pubkey(&cpi_accounts)?.to_bytes(); let address = light_compressed_account::address::derive_address( &address_seed, - &cpi_accounts.tree_accounts()[instruction_data - .address_tree_info - .address_merkle_tree_pubkey_index - as usize] - .key - .to_bytes(), + &address_tree_pubkey, &crate::ID.to_bytes(), ); (address, address_seed) } else { light_sdk::address::v1::derive_address( &[b"compressed", instruction_data.data.as_slice()], - cpi_accounts.tree_accounts() - [address_tree_info.address_merkle_tree_pubkey_index as usize] - .key, + &address_tree_info.get_tree_pubkey(&cpi_accounts)?, &crate::ID, ) }; - let new_address_params = NewAddressParamsPacked { - seed: address_seed, - address_queue_account_index: address_tree_info.address_queue_pubkey_index, - address_merkle_tree_root_index: address_tree_info.root_index, - address_merkle_tree_account_index: address_tree_info.address_merkle_tree_pubkey_index, - }; + let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); let mut my_compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init( &crate::ID, @@ -82,9 +64,7 @@ pub fn create_pda( vec![my_compressed_account.to_account_info()?], vec![new_address_params], ); - let instruction = create_light_system_progam_instruction_invoke_cpi(cpi_inputs, &cpi_accounts)?; - - invoke_light_system_program(&crate::ID, &cpi_accounts.to_account_infos(), instruction)?; + cpi_inputs.invoke_light_system_program(cpi_accounts)?; Ok(()) } diff --git a/program-tests/sdk-test/src/lib.rs b/program-tests/sdk-test/src/lib.rs index dfbb77f309..8fb2b71b2c 100644 --- a/program-tests/sdk-test/src/lib.rs +++ b/program-tests/sdk-test/src/lib.rs @@ -1,5 +1,5 @@ use light_macros::pubkey; -use light_sdk::error::LightSdkError; +use light_sdk::{cpi::CpiSigner, derive_light_cpi_signer, error::LightSdkError}; use solana_program::{ account_info::AccountInfo, entrypoint, program_error::ProgramError, pubkey::Pubkey, }; @@ -8,6 +8,8 @@ pub mod create_pda; pub mod update_pda; pub const ID: Pubkey = pubkey!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy"); entrypoint!(process_instruction); diff --git a/program-tests/sdk-test/src/update_pda.rs b/program-tests/sdk-test/src/update_pda.rs index 6aa4c75cb5..b946e3baaa 100644 --- a/program-tests/sdk-test/src/update_pda.rs +++ b/program-tests/sdk-test/src/update_pda.rs @@ -1,25 +1,23 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_sdk::{ account::LightAccount, - cpi::{ - create_light_system_progam_instruction_invoke_cpi, invoke_light_system_program, - CpiAccounts, CpiAccountsConfig, CpiInputs, - }, + cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}, error::LightSdkError, - instruction::account_meta::CompressedAccountMeta, - ValidityProof, + instruction::{account_meta::CompressedAccountMeta, ValidityProof}, }; -use solana_program::account_info::AccountInfo; +use solana_program::{account_info::AccountInfo, log::sol_log_compute_units}; use crate::create_pda::MyCompressedAccount; /// CU usage: /// - sdk pre system program 9,183k CU /// - total with V2 tree: 50,194 CU (proof by index) +/// - 51,609 pub fn update_pda( accounts: &[AccountInfo], instruction_data: &[u8], ) -> Result<(), LightSdkError> { + sol_log_compute_units(); let mut instruction_data = instruction_data; let instruction_data = UpdatePdaInstructionData::deserialize(&mut instruction_data) .map_err(|_| LightSdkError::Borsh)?; @@ -31,27 +29,24 @@ pub fn update_pda( data: instruction_data.my_compressed_account.data, }, )?; + sol_log_compute_units(); my_compressed_account.data = instruction_data.new_data; - let config = CpiAccountsConfig { - self_program: crate::ID, - cpi_context: false, - sol_pool_pda: false, - sol_compression_recipient: false, - }; + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + sol_log_compute_units(); let cpi_accounts = CpiAccounts::new_with_config( &accounts[0], &accounts[instruction_data.system_accounts_offset as usize..], config, - )?; + ); + sol_log_compute_units(); let cpi_inputs = CpiInputs::new( instruction_data.proof, vec![my_compressed_account.to_account_info()?], ); - let instruction = create_light_system_progam_instruction_invoke_cpi(cpi_inputs, &cpi_accounts)?; - - invoke_light_system_program(&crate::ID, &cpi_accounts.to_account_infos(), instruction)?; + sol_log_compute_units(); + cpi_inputs.invoke_light_system_program(cpi_accounts)?; Ok(()) } diff --git a/program-tests/sdk-test/tests/test.rs b/program-tests/sdk-test/tests/test.rs index 8d4ec91bb4..5465d7b37a 100644 --- a/program-tests/sdk-test/tests/test.rs +++ b/program-tests/sdk-test/tests/test.rs @@ -9,8 +9,7 @@ use light_program_test::{ program_test::LightProgramTest, AddressWithTree, Indexer, ProgramTestConfig, Rpc, RpcError, }; use light_sdk::instruction::{ - account_meta::CompressedAccountMeta, accounts::SystemAccountMetaConfig, - pack_accounts::PackedAccounts, + account_meta::CompressedAccountMeta, PackedAccounts, SystemAccountMetaConfig, }; use sdk_test::{ create_pda::CreatePdaInstructionData, @@ -44,7 +43,7 @@ async fn test_sdk_test() { &address_tree_pubkey.to_bytes(), &sdk_test::ID.to_bytes(), ); - let ouput_queue = rpc.get_random_state_tree_info().queue; + let ouput_queue = rpc.get_random_state_tree_info().unwrap().queue; create_pda( &payer, &mut rpc, diff --git a/program-tests/system-cpi-test/Cargo.toml b/program-tests/system-cpi-test/Cargo.toml index a2bfa77ddb..351c309767 100644 --- a/program-tests/system-cpi-test/Cargo.toml +++ b/program-tests/system-cpi-test/Cargo.toml @@ -39,6 +39,7 @@ light-test-utils = { workspace = true, features = ["devenv"] } [dev-dependencies] light-client = { workspace = true, features = ["devenv"] } light-sdk = { workspace = true, features = ["anchor"] } +light-sdk-types = { workspace = true } light-program-test = { workspace = true, features = ["devenv"] } tokio = { workspace = true } light-prover-client = { workspace = true, features = ["devenv"] } diff --git a/program-tests/system-cpi-test/tests/test.rs b/program-tests/system-cpi-test/tests/test.rs index 92c80074af..b8e30c92a9 100644 --- a/program-tests/system-cpi-test/tests/test.rs +++ b/program-tests/system-cpi-test/tests/test.rs @@ -1836,7 +1836,12 @@ async fn perform_create_pda( } else { let input_account_len = input_accounts.as_ref().unwrap().len(); index += input_account_len; - Some(account_root_indices[..index].to_vec()) + Some( + account_root_indices[..index] + .iter() + .map(|x| x.root_index()) + .collect::>(), + ) }; let read_only_accounts = read_only_accounts.as_ref().map(|read_only_accounts| { @@ -1844,7 +1849,8 @@ async fn perform_create_pda( .iter() .map(|x| { index += 1; - x.into_read_only(account_root_indices[index - 1]).unwrap() + x.into_read_only(account_root_indices[index - 1].root_index()) + .unwrap() }) .collect::>() }); @@ -1983,7 +1989,10 @@ pub async fn perform_with_input_accounts Vec { - let cpi_signer = find_cpi_signer_macro!(&config.self_program).0; + let cpi_signer = Pubkey::new_from_array(LIGHT_CPI_SIGNER.cpi_signer); println!("cpi signer {:?}", cpi_signer); let default_pubkeys = SystemAccountPubkeys::default(); let mut vec = if config.small_ix { diff --git a/program-tests/system-test/tests/test.rs b/program-tests/system-test/tests/test.rs index 8bd9aede1c..e6270e776d 100644 --- a/program-tests/system-test/tests/test.rs +++ b/program-tests/system-test/tests/test.rs @@ -234,7 +234,7 @@ pub async fn failing_transaction_inputs( .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(); (root_indices, proof_rpc_res.value.proof.0) } else { @@ -1042,7 +1042,7 @@ async fn invoke_test() { .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), &Vec::new(), Some(proof), @@ -1095,7 +1095,7 @@ async fn invoke_test() { .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), &Vec::new(), Some(proof), @@ -1132,7 +1132,7 @@ async fn invoke_test() { .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), &Vec::new(), Some(proof), @@ -1593,7 +1593,7 @@ async fn test_with_compression() { .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), &Vec::new(), Some(proof), @@ -1964,7 +1964,9 @@ async fn batch_invoke_test() { // No proof since value is in output queue assert!(proof_rpc_result.value.proof.0.is_none()); // No root index since value is in output queue - assert!(proof_rpc_result.value.accounts[0].root_index.is_none()); + assert!(proof_rpc_result.value.accounts[0] + .root_index + .proof_by_index()); let input_compressed_accounts = vec![compressed_account_with_context.compressed_account]; @@ -2186,7 +2188,7 @@ async fn batch_invoke_test() { .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), &Vec::new(), Some(proof), @@ -2483,7 +2485,7 @@ pub async fn double_spend_compressed_account( .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), &Vec::new(), proof_rpc_result.value.proof.0, diff --git a/program-tests/utils/src/e2e_test_env.rs b/program-tests/utils/src/e2e_test_env.rs index 9a5f62ca06..e673f54104 100644 --- a/program-tests/utils/src/e2e_test_env.rs +++ b/program-tests/utils/src/e2e_test_env.rs @@ -148,9 +148,9 @@ use light_registry::{ ForesterConfig, }; use light_sdk::{ + address::NewAddressParamsAssignedPacked, + constants::{ADDRESS_MERKLE_TREE_ROOTS, CPI_AUTHORITY_PDA_SEED, STATE_MERKLE_TREE_ROOTS}, token::{AccountState, TokenDataWithMerkleContext}, - NewAddressParamsAssignedPacked, ADDRESS_MERKLE_TREE_ROOTS, CPI_AUTHORITY_PDA_SEED, - STATE_MERKLE_TREE_ROOTS, }; use light_sparse_merkle_tree::{ changelog::ChangelogEntry, indexed_changelog::IndexedChangelogEntry, SparseMerkleTree, @@ -2656,12 +2656,7 @@ where .await .unwrap(); - root_indices = proof_rpc_res - .value - .accounts - .iter() - .map(|x| x.root_index) - .collect::>(); + root_indices = proof_rpc_res.value.get_root_indices(); if let Some(proof_rpc_res) = proof_rpc_res.value.proof.0 { proof = Some(proof_rpc_res); @@ -2692,21 +2687,16 @@ where } if !read_only_accounts.is_empty() { - let account_root_indices: Vec<_> = proof_rpc_res - .value - .accounts - .iter() - .map(|x| x.root_index) - .collect(); + let account_root_indices: Vec<_> = proof_rpc_res.value.get_root_indices(); for (i, input_account) in read_only_accounts.iter_mut().enumerate() { - if let Some(root_index) = account_root_indices - .get(i + input_accounts.len()) - .copied() - .flatten() + if let Some(root_index) = + account_root_indices.get(i + input_accounts.len()).copied() { - input_account.root_index = root_index; - } else { - input_account.merkle_context.prove_by_index = true; + if let Some(root_index) = root_index { + input_account.root_index = root_index; + } else { + input_account.merkle_context.prove_by_index = true; + } } } } diff --git a/program-tests/utils/src/pack.rs b/program-tests/utils/src/pack.rs index a86b4495fe..8bd4f474b1 100644 --- a/program-tests/utils/src/pack.rs +++ b/program-tests/utils/src/pack.rs @@ -6,11 +6,13 @@ use light_compressed_account::{ PackedCompressedAccountWithMerkleContext, PackedMerkleContext, PackedReadOnlyCompressedAccount, ReadOnlyCompressedAccount, }, - instruction_data::data::{NewAddressParams, ReadOnlyAddress}, + instruction_data::data::{ + NewAddressParams, OutputCompressedAccountWithPackedContext, ReadOnlyAddress, + }, }; -use light_sdk::{ - NewAddressParamsAssigned, NewAddressParamsAssignedPacked, NewAddressParamsPacked, - OutputCompressedAccountWithPackedContext, PackedReadOnlyAddress, +use light_sdk::address::{ + NewAddressParamsAssigned, NewAddressParamsAssignedPacked, PackedNewAddressParams, + PackedReadOnlyAddress, }; use solana_sdk::pubkey::Pubkey; @@ -82,16 +84,16 @@ pub fn pack_read_only_accounts( pub fn pack_new_address_params( new_address_params: &[NewAddressParams], remaining_accounts: &mut HashMap, -) -> Vec { +) -> Vec { let mut new_address_params_packed = new_address_params .iter() - .map(|x| NewAddressParamsPacked { + .map(|x| PackedNewAddressParams { seed: x.seed, address_merkle_tree_root_index: x.address_merkle_tree_root_index, address_merkle_tree_account_index: 0, // will be assigned later address_queue_account_index: 0, // will be assigned later }) - .collect::>(); + .collect::>(); let mut next_index: usize = remaining_accounts.len(); for (i, params) in new_address_params.iter().enumerate() { match remaining_accounts.get(¶ms.address_merkle_tree_pubkey.into()) { diff --git a/program-tests/utils/src/spl.rs b/program-tests/utils/src/spl.rs index ceb4ee04d1..163f96757e 100644 --- a/program-tests/utils/src/spl.rs +++ b/program-tests/utils/src/spl.rs @@ -620,12 +620,7 @@ pub async fn compressed_transfer_22_test< &authority_signer.pubkey(), // authority &input_merkle_tree_context, &output_compressed_accounts, - &rpc_result - .value - .accounts - .iter() - .map(|x| x.root_index) - .collect::>(), + &rpc_result.value.get_root_indices(), &rpc_result.value.proof.0, input_compressed_account_token_data .iter() @@ -795,8 +790,8 @@ pub async fn decompress_test>(), // root_indices + .map(|x| x.root_index.root_index()) + .collect::>(), &Some(proof_rpc_result.value.proof.0.unwrap_or_default()), input_compressed_accounts .iter() @@ -1190,7 +1185,7 @@ pub async fn approve_test>(), proof: proof_rpc_result.value.proof.0.unwrap_or_default(), }; @@ -1355,7 +1350,7 @@ pub async fn revoke_test>(), proof: proof_rpc_result.value.proof.0.unwrap_or_default(), }; @@ -1521,7 +1516,7 @@ pub async fn freeze_or_thaw_test< .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(), proof: proof_rpc_result.value.proof.0.unwrap_or_default(), }; @@ -1825,7 +1820,7 @@ pub async fn create_burn_test_instruction>(), proof, mint, diff --git a/program-tests/utils/src/system_program.rs b/program-tests/utils/src/system_program.rs index 42fc4bece7..54a12e7511 100644 --- a/program-tests/utils/src/system_program.rs +++ b/program-tests/utils/src/system_program.rs @@ -363,7 +363,7 @@ pub async fn compressed_transaction_test< .value .accounts .iter() - .map(|x| x.root_index) + .map(|x| x.root_index.root_index()) .collect::>(); if let Some(proof_rpc_res) = proof_rpc_res.value.proof.0 { diff --git a/scripts/devenv.sh b/scripts/devenv.sh index cfffd6f612..66c3867751 100755 --- a/scripts/devenv.sh +++ b/scripts/devenv.sh @@ -17,6 +17,7 @@ deactivate () { unset RUSTUP_HOME unset CARGO_HOME unset LIGHT_PROTOCOL_OLD_RUST_PATH + unset CARGO_FEATURES } # Stop early if already in devenv. @@ -67,6 +68,9 @@ export PATH export REDIS_URL="redis://localhost:6379" +# Enable small_ix feature by default in devenv +export CARGO_FEATURES="small_ix" + if [[ "$(uname)" == "Darwin" ]]; then LIGHT_PROTOCOL_OLD_CPATH="${CPATH:-}" export CPATH="/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include:${CPATH:-}" diff --git a/scripts/format.sh b/scripts/format.sh index 92861e9c64..1b009c0dbc 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -23,8 +23,11 @@ cargo clippy \ # Make sure that tests compile cargo test-sbf -p system-test --no-run cargo test-sbf -p system-cpi-test --no-run +cargo test-sbf -p system-cpi-v2-test --no-run cargo test-sbf -p e2e-test --no-run cargo test-sbf -p compressed-token-test --no-run cargo test-sbf -p token-escrow --no-run cargo test-sbf -p sdk-test --no-run cargo test-sbf -p sdk-anchor-test --no-run +cargo test-sbf -p client-test --no-run +cargo test-sbf -p sdk-pinocchio-test --no-run diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index 06dea1e963..16862e6836 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -40,7 +40,7 @@ solana-address-lookup-table-interface = { version = "2.2.1", features = [ light-merkle-tree-metadata = { workspace = true, features = ["solana"] } light-concurrent-merkle-tree = { workspace = true } light-indexed-merkle-tree = { workspace = true } -light-sdk = { workspace = true, features = ["solana"] } +light-sdk = { workspace = true } light-hasher = { workspace = true } light-compressed-account = { workspace = true, features = ["solana"] } diff --git a/sdk-libs/client/src/indexer/mod.rs b/sdk-libs/client/src/indexer/mod.rs index c356b4074e..c66baf2d0b 100644 --- a/sdk-libs/client/src/indexer/mod.rs +++ b/sdk-libs/client/src/indexer/mod.rs @@ -17,8 +17,8 @@ pub use types::{ AccountProofInputs, Address, AddressMerkleTreeAccounts, AddressProofInputs, AddressQueueIndex, AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, Hash, MerkleProof, MerkleProofWithContext, NewAddressProofWithContext, NextTreeInfo, OwnerBalance, ProofOfLeaf, - SignatureWithMetadata, StateMerkleTreeAccounts, TokenAccount, TokenBalance, TreeInfo, - ValidityProofWithContext, + RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, TokenAccount, TokenBalance, + TreeInfo, ValidityProofWithContext, }; mod options; pub use options::*; diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index ade6cb9bb9..51d804e40b 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -3,17 +3,13 @@ use light_compressed_account::{ CompressedAccount as ProgramCompressedAccount, CompressedAccountData, CompressedAccountWithMerkleContext, }, + instruction_data::compressed_proof::CompressedProof, TreeType, }; use light_indexed_merkle_tree::array::IndexedElement; use light_sdk::{ - instruction::{ - pack_accounts::PackedAccounts, - tree_info::{PackedAddressTreeInfo, PackedStateTreeInfo}, - }, - light_compressed_account::instruction_data::compressed_proof::CompressedProof, + instruction::{PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof}, token::{AccountState, TokenData}, - ValidityProof, }; use num_bigint::BigUint; use solana_pubkey::Pubkey; @@ -88,7 +84,7 @@ impl ValidityProofWithContext { pub fn get_root_indices(&self) -> Vec> { self.accounts .iter() - .map(|account| account.root_index) + .map(|account| account.root_index.root_index()) .collect() } @@ -104,20 +100,54 @@ impl ValidityProofWithContext { pub struct AccountProofInputs { pub hash: [u8; 32], pub root: [u8; 32], - pub root_index: Option, + pub root_index: RootIndex, pub leaf_index: u64, pub tree_info: TreeInfo, } +#[derive(Clone, Default, Copy, Debug, PartialEq)] +pub struct RootIndex { + proof_by_index: bool, + root_index: u16, +} + +impl RootIndex { + pub fn new_none() -> Self { + Self { + proof_by_index: true, + root_index: 0, + } + } + + pub fn new_some(root_index: u16) -> Self { + Self { + proof_by_index: false, + root_index, + } + } + + pub fn proof_by_index(&self) -> bool { + self.proof_by_index + } + + pub fn root_index(&self) -> Option { + if !self.proof_by_index { + Some(self.root_index) + } else { + None + } + } +} + impl AccountProofInputs { pub fn from_api_model( value: &photon_api::models::AccountProofInputs, ) -> Result { let root_index = { if value.root_index.prove_by_index { - None + RootIndex::new_none() } else { - Some(value.root_index.root_index) + RootIndex::new_some(value.root_index.root_index) } }; Ok(Self { @@ -173,11 +203,11 @@ impl ValidityProofWithContext { let merkle_tree_pubkey_index = packed_accounts.insert_or_get(account.tree_info.tree); let queue_pubkey_index = packed_accounts.insert_or_get(account.tree_info.queue); let tree_info_packed = PackedStateTreeInfo { - root_index: account.root_index.unwrap_or_default(), + root_index: account.root_index.root_index, merkle_tree_pubkey_index, queue_pubkey_index, leaf_index: account.leaf_index as u32, - prove_by_index: account.root_index.is_none(), + prove_by_index: account.root_index.proof_by_index(), }; packed_tree_infos.push(tree_info_packed); @@ -262,7 +292,7 @@ impl ValidityProofWithContext { Ok(AccountProofInputs { hash: decode_base58_to_fixed_array(&value.leaves[i])?, root: decode_base58_to_fixed_array(&value.roots[i])?, - root_index: Some(value.root_indices[i] as u16), + root_index: RootIndex::new_some(value.root_indices[i] as u16), leaf_index: value.leaf_indices[i] as u64, tree_info: TreeInfo { tree_type: tree_info.tree_type, diff --git a/sdk-libs/client/src/lib.rs b/sdk-libs/client/src/lib.rs index 98033e9c82..e69f88231f 100644 --- a/sdk-libs/client/src/lib.rs +++ b/sdk-libs/client/src/lib.rs @@ -1,3 +1,81 @@ +//! # Light Client +//! +//! A client library for interacting with Light Protocol compressed accounts and RPC endpoints. +//! +//! ## Features +//! - Connect to various RPC endpoints (local test validator, devnet/mainnet) +//! - Query compressed accounts and validity proofs from RPC endpoints +//! - Support for both v1 and v2 merkle trees (with v2 feature) +//! - Start local test validator with Light Protocol programs +//! +//! ## Prerequisites +//! +//! For local test validator usage, install the Light CLI: +//! ```bash +//! npm i -g @lightprotocol/zk-compression-cli +//! ``` +//! +//! ## Example +//! +//! ```no_run +//! use light_client::{ +//! rpc::{LightClient, RpcConfig, Rpc}, +//! indexer::{Indexer, IndexerRpcConfig, RetryConfig}, +//! local_test_validator::{spawn_validator, LightValidatorConfig}, +//! }; +//! use light_prover_client::prover::ProverConfig; +//! use solana_pubkey::Pubkey; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Start local test validator with Light Protocol programs +//! let config = LightValidatorConfig { +//! enable_indexer: true, +//! prover_config: Some(ProverConfig::default()), +//! wait_time: 75, +//! sbf_programs: vec![], +//! limit_ledger_size: None, +//! }; +//! spawn_validator(config).await; +//! +//! // Connect to the validator +//! let mut rpc = LightClient::new(RpcConfig::local()).await?; +//! +//! // Or connect to devnet/mainnet: +//! // let mut rpc = LightClient::new(RpcConfig::new("https://devnet.helius-rpc.com/?api-key=YOUR_KEY")).await?; +//! // let mut rpc = LightClient::new(RpcConfig::new("https://mainnet.helius-rpc.com/?api-key=YOUR_KEY")).await?; +//! +//! let owner = Pubkey::new_unique(); +//! +//! // Create indexer config for queries +//! let slot = rpc.get_slot().await?; +//! let config = IndexerRpcConfig { +//! slot, +//! retry_config: RetryConfig::default(), +//! }; +//! +//! // Query compressed accounts using Indexer trait +//! let accounts = rpc +//! .get_compressed_accounts_by_owner(&owner, None, Some(config)) +//! .await?; +//! +//! println!("Found {} compressed accounts", accounts.value.items.len()); +//! +//! // Get validity proofs for creating transactions +//! let rpc_result = rpc +//! .get_validity_proof( +//! vec![], // add account hashes here +//! vec![], // add addresses with address tree here +//! None +//! ) +//! .await?; +//! +//! println!("Got validity proof and proof inputs {:?}", rpc_result.value); +//! +//! Ok(()) +//! } +//! ``` + pub mod constants; pub mod fee; pub mod indexer; diff --git a/sdk-libs/client/src/rpc/client.rs b/sdk-libs/client/src/rpc/client.rs index f5d7fc2f0b..4a95783607 100644 --- a/sdk-libs/client/src/rpc/client.rs +++ b/sdk-libs/client/src/rpc/client.rs @@ -89,7 +89,7 @@ pub struct LightClient { pub payer: Keypair, pub retry_config: RetryConfig, pub indexer: Option, - pub active_state_merkle_trees: Vec, + pub state_merkle_trees: Vec, } impl Debug for LightClient { @@ -130,7 +130,7 @@ impl LightClient { payer, retry_config, indexer, - active_state_merkle_trees: Vec::new(), + state_merkle_trees: Vec::new(), }; if config.fetch_active_tree { new.get_latest_active_state_trees().await?; @@ -682,22 +682,20 @@ impl Rpc for LightClient { &res[0].nullify_table, ) .await?; - self.active_state_merkle_trees = res.clone(); + self.state_merkle_trees = res.clone(); Ok(res) } /// Fetch the latest state tree addresses from the cluster. fn get_state_tree_infos(&self) -> Vec { - self.active_state_merkle_trees.to_vec() + self.state_merkle_trees.to_vec() } /// Gets a random active state tree. /// State trees are cached and have to be fetched or set. - fn get_random_state_tree_info(&self) -> TreeInfo { - use rand::Rng; + fn get_random_state_tree_info(&self) -> Result { let mut rng = rand::thread_rng(); - - self.active_state_merkle_trees[rng.gen_range(0..self.active_state_merkle_trees.len())] + select_state_tree_info(&mut rng, &self.state_merkle_trees) } fn get_address_tree_v1(&self) -> TreeInfo { @@ -712,3 +710,36 @@ impl Rpc for LightClient { } impl MerkleTreeExt for LightClient {} + +/// Selects a random state tree from the provided list. +/// +/// This function should be used together with `get_state_tree_infos()` to first +/// retrieve the list of state trees, then select one randomly. +/// +/// # Arguments +/// * `rng` - A mutable reference to a random number generator +/// * `state_trees` - A slice of `TreeInfo` representing state trees +/// +/// # Returns +/// A randomly selected `TreeInfo` from the provided list, or an error if the list is empty +/// +/// # Errors +/// Returns `RpcError::NoStateTreesAvailable` if the provided slice is empty +/// +/// # Example +/// ```ignore +/// use rand::thread_rng; +/// let tree_infos = client.get_state_tree_infos(); +/// let mut rng = thread_rng(); +/// let selected_tree = select_state_tree_info(&mut rng, &tree_infos)?; +/// ``` +pub fn select_state_tree_info( + rng: &mut R, + state_trees: &[TreeInfo], +) -> Result { + if state_trees.is_empty() { + return Err(RpcError::NoStateTreesAvailable); + } + + Ok(state_trees[rng.gen_range(0..state_trees.len())]) +} diff --git a/sdk-libs/client/src/rpc/errors.rs b/sdk-libs/client/src/rpc/errors.rs index 23305b7402..2875b0ecb2 100644 --- a/sdk-libs/client/src/rpc/errors.rs +++ b/sdk-libs/client/src/rpc/errors.rs @@ -51,6 +51,11 @@ pub enum RpcError { #[error("Indexer error: {0}")] IndexerError(#[from] IndexerError), + + #[error( + "No state trees available, use rpc.get_latest_active_state_trees() to fetch state trees" + )] + NoStateTreesAvailable, } impl From for RpcError { @@ -77,6 +82,7 @@ impl Clone for RpcError { RpcError::StateTreeLookupTableNotFound => RpcError::StateTreeLookupTableNotFound, RpcError::InvalidStateTreeLookupTable => RpcError::InvalidStateTreeLookupTable, RpcError::NullifyTableNotFound => RpcError::NullifyTableNotFound, + RpcError::NoStateTreesAvailable => RpcError::NoStateTreesAvailable, } } } diff --git a/sdk-libs/client/src/rpc/rpc_trait.rs b/sdk-libs/client/src/rpc/rpc_trait.rs index 019deb76fb..6ee3b19d73 100644 --- a/sdk-libs/client/src/rpc/rpc_trait.rs +++ b/sdk-libs/client/src/rpc/rpc_trait.rs @@ -195,7 +195,7 @@ pub trait Rpc: Send + Sync + Debug + 'static { /// Gets a random state tree info. /// State trees are cached and have to be fetched or set. - fn get_random_state_tree_info(&self) -> TreeInfo; + fn get_random_state_tree_info(&self) -> Result; fn get_address_tree_v1(&self) -> TreeInfo; diff --git a/sdk-libs/macros/Cargo.toml b/sdk-libs/macros/Cargo.toml index 90db169c1f..2fa5fba869 100644 --- a/sdk-libs/macros/Cargo.toml +++ b/sdk-libs/macros/Cargo.toml @@ -10,16 +10,18 @@ edition = "2021" proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } +solana-pubkey = { workspace = true, features = ["curve25519", "sha2"] } light-hasher = { workspace = true } -# ark-bn254 = { workspace = true } light-poseidon = { workspace = true } [dev-dependencies] light-compressed-account = { workspace = true } +light-sdk-types = { workspace = true } prettyplease = "0.2.29" solana-pubkey = { workspace = true, features = ["borsh"] } borsh = { workspace = true } +light-macros = { workspace = true } [lib] proc-macro = true diff --git a/sdk-libs/macros/src/cpi_signer.rs b/sdk-libs/macros/src/cpi_signer.rs new file mode 100644 index 0000000000..d27403df1d --- /dev/null +++ b/sdk-libs/macros/src/cpi_signer.rs @@ -0,0 +1,95 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, LitStr}; + +pub fn derive_light_cpi_signer_pda(input: TokenStream) -> TokenStream { + // Parse the input - just a program ID string literal + let program_id_lit = parse_macro_input!(input as LitStr); + let program_id_str = program_id_lit.value(); + + // Compute the PDA at compile time using solana-pubkey with "cpi_authority" seed + use std::str::FromStr; + + // Parse program ID at compile time + let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) { + Ok(id) => id, + Err(_) => { + return syn::Error::new( + program_id_lit.span(), + "Invalid program ID format. Expected a base58 encoded public key", + ) + .to_compile_error() + .into(); + } + }; + + // Use fixed "cpi_authority" seed + let seeds = &[b"cpi_authority".as_slice()]; + + // Compute the PDA at compile time + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id); + + // Generate the output code with precomputed byte array and bump + let pda_bytes = pda.to_bytes(); + let bytes = pda_bytes + .iter() + .map(|b| proc_macro2::Literal::u8_unsuffixed(*b)); + + let output = quote! { + ([#(#bytes),*], #bump) + }; + + output.into() +} + +pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream { + // Parse the input - just a program ID string literal + let program_id_lit = parse_macro_input!(input as LitStr); + let program_id_str = program_id_lit.value(); + + // Compute the PDA at compile time using solana-pubkey with "cpi_authority" seed + use std::str::FromStr; + + // Parse program ID at compile time + let program_id = match solana_pubkey::Pubkey::from_str(&program_id_str) { + Ok(id) => id, + Err(_) => { + return syn::Error::new( + program_id_lit.span(), + "Invalid program ID format. Expected a base58 encoded public key", + ) + .to_compile_error() + .into(); + } + }; + + // Use fixed "cpi_authority" seed + let seeds = &[b"cpi_authority".as_slice()]; + + // Compute the PDA at compile time + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id); + + // Generate the output code with precomputed CpiSigner struct + let program_id_bytes = program_id.to_bytes(); + let pda_bytes = pda.to_bytes(); + + let program_id_literals = program_id_bytes + .iter() + .map(|b| proc_macro2::Literal::u8_unsuffixed(*b)); + let cpi_signer_literals = pda_bytes + .iter() + .map(|b| proc_macro2::Literal::u8_unsuffixed(*b)); + + let output = quote! { + { + // Use the CpiSigner type with absolute path to avoid import dependency + ::light_sdk_types::CpiSigner { + program_id: [#(#program_id_literals),*], + cpi_signer: [#(#cpi_signer_literals),*], + bump: #bump, + } + } + }; + + output.into() +} diff --git a/sdk-libs/macros/src/discriminator.rs b/sdk-libs/macros/src/discriminator.rs index 176749dcff..1d289db888 100644 --- a/sdk-libs/macros/src/discriminator.rs +++ b/sdk-libs/macros/src/discriminator.rs @@ -44,7 +44,7 @@ mod tests { let output = discriminator(input).unwrap(); let output = output.to_string(); - assert!(output.contains("impl Discriminator for MyAccount")); + assert!(output.contains("impl LightDiscriminator for MyAccount")); assert!(output.contains("[181 , 255 , 112 , 42 , 17 , 188 , 66 , 199]")); } } diff --git a/sdk-libs/macros/src/hasher/light_hasher.rs b/sdk-libs/macros/src/hasher/light_hasher.rs index 720e647227..911cc35f73 100644 --- a/sdk-libs/macros/src/hasher/light_hasher.rs +++ b/sdk-libs/macros/src/hasher/light_hasher.rs @@ -125,17 +125,15 @@ impl ::light_hasher::DataHasher for MyAccount { use ::light_hasher::Hasher; use ::light_hasher::to_byte_array::ToByteArray; #[cfg(debug_assertions)] - { + { if std::env::var("RUST_BACKTRACE").is_ok() { - let debug_prints: Vec<[u8;32]> = vec![ - self.a.to_byte_array()?, - self.b.to_byte_array()?, - self.c.to_byte_array()?, - self.d.to_byte_array()?, + let debug_prints: Vec<[u8; 32]> = vec![ + self.a.to_byte_array() ?, self.b.to_byte_array() ?, self.c + .to_byte_array() ?, self.d.to_byte_array() ?, ]; + println!("DataHasher::hash inputs {:?}", debug_prints); } - println!("DataHasher::hash inputs {:?}", debug_prints); - } + } H::hashv( &[ self.a.to_byte_array()?.as_slice(), diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 8f2bc52ec8..8fbbcebd71 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -7,6 +7,7 @@ use traits::process_light_traits; mod account; mod accounts; +mod cpi_signer; mod discriminator; mod hasher; mod program; @@ -270,3 +271,50 @@ pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { .unwrap_or_else(|err| err.to_compile_error()) .into() } + +/// Derives a Light Protocol CPI signer address at compile time +/// +/// This macro computes the CPI signer PDA using the "cpi_authority" seed +/// for the given program ID at compile time. +/// +/// ## Usage +/// +/// ``` +/// use light_sdk_macros::derive_light_cpi_signer_pda; +/// // Derive CPI signer for your program +/// const CPI_SIGNER_DATA: ([u8; 32], u8) = derive_light_cpi_signer_pda!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); +/// const CPI_SIGNER: [u8; 32] = CPI_SIGNER_DATA.0; +/// const CPI_SIGNER_BUMP: u8 = CPI_SIGNER_DATA.1; +/// ``` +/// +/// This macro computes the PDA during compile time and returns a tuple of ([u8; 32], bump). +#[proc_macro] +pub fn derive_light_cpi_signer_pda(input: TokenStream) -> TokenStream { + cpi_signer::derive_light_cpi_signer_pda(input) +} + +/// Derives a complete Light Protocol CPI configuration at compile time +/// +/// This macro computes the program ID, CPI signer PDA, and bump seed +/// for the given program ID at compile time. +/// +/// ## Usage +/// +/// ``` +/// use light_sdk_macros::derive_light_cpi_signer; +/// use light_sdk_types::CpiSigner; +/// // Derive complete CPI signer for your program +/// const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); +/// +/// // Access individual fields: +/// const PROGRAM_ID: [u8; 32] = LIGHT_CPI_SIGNER.program_id; +/// const CPI_SIGNER: [u8; 32] = LIGHT_CPI_SIGNER.cpi_signer; +/// const BUMP: u8 = LIGHT_CPI_SIGNER.bump; +/// ``` +/// +/// This macro computes all values during compile time and returns a CpiSigner struct +/// containing the program ID, CPI signer address, and bump seed. +#[proc_macro] +pub fn derive_light_cpi_signer(input: TokenStream) -> TokenStream { + cpi_signer::derive_light_cpi_signer(input) +} diff --git a/sdk-libs/macros/tests/pda.rs b/sdk-libs/macros/tests/pda.rs new file mode 100644 index 0000000000..2982cba542 --- /dev/null +++ b/sdk-libs/macros/tests/pda.rs @@ -0,0 +1,77 @@ +use std::str::FromStr; + +use light_sdk_macros::derive_light_cpi_signer; +use light_sdk_types::CpiSigner; +use solana_pubkey::Pubkey; + +#[test] +fn test_compute_pda_basic() { + // Test with a known program ID using fixed "cpi_authority" seed + const RESULT: CpiSigner = + derive_light_cpi_signer!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); + + // Verify the result has valid fields + assert_eq!(RESULT.program_id.len(), 32); + assert_eq!(RESULT.cpi_signer.len(), 32); + + // Verify this matches runtime computation + let runtime_result = Pubkey::find_program_address( + &[b"cpi_authority"], + &Pubkey::from_str("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7").unwrap(), + ); + + assert_eq!(RESULT.cpi_signer, runtime_result.0.to_bytes()); + assert_eq!(RESULT.bump, runtime_result.1); +} + +#[test] +fn test_cpi_signer() { + // Test that the macro can be used in const contexts + const PDA_RESULT: CpiSigner = + derive_light_cpi_signer!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); + + // Extract individual components in const context + const PROGRAM_ID: [u8; 32] = PDA_RESULT.program_id; + const CPI_SIGNER: [u8; 32] = PDA_RESULT.cpi_signer; + const BUMP: u8 = PDA_RESULT.bump; + + // Verify they're valid + assert_eq!( + PROGRAM_ID, + light_macros::pubkey_array!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7") + ); + assert_eq!( + CPI_SIGNER, + [ + 251, 179, 40, 117, 16, 92, 174, 133, 181, 180, 68, 118, 7, 237, 191, 225, 69, 39, 191, + 180, 35, 145, 28, 164, 4, 35, 191, 209, 82, 122, 38, 117 + ] + ); + assert_eq!(BUMP, 255); +} + +#[test] +fn test_cpi_signer_2() { + // Test that the macro can be used in const contexts + const PDA_RESULT: CpiSigner = + derive_light_cpi_signer!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); + + // Extract individual components in const context + const PROGRAM_ID: [u8; 32] = PDA_RESULT.program_id; + const CPI_SIGNER: [u8; 32] = PDA_RESULT.cpi_signer; + const BUMP: u8 = PDA_RESULT.bump; + + // Verify they're valid + assert_eq!( + PROGRAM_ID, + light_macros::pubkey_array!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq") + ); + assert_eq!( + CPI_SIGNER, + [ + 20, 12, 243, 109, 120, 11, 194, 48, 169, 64, 170, 103, 246, 66, 224, 151, 74, 116, 57, + 84, 0, 180, 16, 126, 175, 149, 24, 207, 85, 137, 3, 207 + ] + ); + assert_eq!(BUMP, 255); +} diff --git a/sdk-libs/program-test/src/accounts/initialize.rs b/sdk-libs/program-test/src/accounts/initialize.rs index b080322e7e..c70e3f8ba7 100644 --- a/sdk-libs/program-test/src/accounts/initialize.rs +++ b/sdk-libs/program-test/src/accounts/initialize.rs @@ -206,7 +206,7 @@ pub async fn initialize_accounts( } let registered_system_program_pda = - get_registered_program_pda(&light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM); + get_registered_program_pda(&Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID)); let registered_registry_program_pda = get_registered_program_pda(&light_registry::ID); let forester_epoch = if *register_forester_and_advance_to_active_phase { let mut registered_epoch = Epoch::register( diff --git a/sdk-libs/program-test/src/accounts/state_tree.rs b/sdk-libs/program-test/src/accounts/state_tree.rs index 4f17d1183a..8f5d226934 100644 --- a/sdk-libs/program-test/src/accounts/state_tree.rs +++ b/sdk-libs/program-test/src/accounts/state_tree.rs @@ -157,7 +157,7 @@ pub async fn create_state_merkle_tree_and_queue_account( &payer.pubkey(), ProtocolConfig::default().cpi_context_size as usize, rent_cpi_config, - &light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, + &Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), Some(cpi_context_keypair), ); diff --git a/sdk-libs/program-test/src/accounts/state_tree_v2.rs b/sdk-libs/program-test/src/accounts/state_tree_v2.rs index 40e2b62f84..a6458015ac 100644 --- a/sdk-libs/program-test/src/accounts/state_tree_v2.rs +++ b/sdk-libs/program-test/src/accounts/state_tree_v2.rs @@ -10,6 +10,7 @@ use light_registry::{ protocol_config::state::ProtocolConfig, }; use solana_instruction::Instruction; +use solana_pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signature, Signer}; pub async fn create_batched_state_merkle_tree( @@ -59,7 +60,7 @@ pub async fn create_batched_state_merkle_tree( &payer.pubkey(), ProtocolConfig::default().cpi_context_size as usize, rent_cpi_config, - &light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, + &Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), Some(cpi_context_keypair), ); let instruction = if registry { diff --git a/sdk-libs/program-test/src/accounts/test_accounts.rs b/sdk-libs/program-test/src/accounts/test_accounts.rs index e606d0d767..115577c71c 100644 --- a/sdk-libs/program-test/src/accounts/test_accounts.rs +++ b/sdk-libs/program-test/src/accounts/test_accounts.rs @@ -64,9 +64,9 @@ impl TestAccounts { governance_authority_pda: Pubkey::default(), group_pda: Pubkey::default(), forester: Keypair::from_bytes(&FORESTER_TEST_KEYPAIR).unwrap(), - registered_program_pda: get_registered_program_pda( - &light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, - ), + registered_program_pda: get_registered_program_pda(&Pubkey::from( + light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID, + )), registered_registry_program_pda: get_registered_program_pda(&light_registry::ID), registered_forester_pda: Pubkey::default(), forester_epoch: None, // Set to None or to an appropriate Epoch value if needed @@ -103,7 +103,7 @@ impl TestAccounts { payer.pubkey(), protocol_config_pda, group_pda, - light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, + Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), ); let cpi_context_keypair = Keypair::from_bytes(&SIGNATURE_CPI_TEST_KEYPAIR).unwrap(); diff --git a/sdk-libs/program-test/src/indexer/address_tree.rs b/sdk-libs/program-test/src/indexer/address_tree.rs index 9806ecd00a..cb998d0ebe 100644 --- a/sdk-libs/program-test/src/indexer/address_tree.rs +++ b/sdk-libs/program-test/src/indexer/address_tree.rs @@ -11,7 +11,7 @@ use light_indexed_merkle_tree::{ reference::IndexedMerkleTree, }; use light_prover_client::proof_types::non_inclusion::v2::NonInclusionMerkleProofInputs; -use light_sdk::STATE_MERKLE_TREE_ROOTS; +use light_sdk::constants::STATE_MERKLE_TREE_ROOTS; use num_bigint::{BigInt, BigUint}; use num_traits::ops::bytes::FromBytes; diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 92b025ca18..efd15eba7c 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -20,8 +20,8 @@ use light_client::{ GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MerkleProofWithContext, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, - Response, RetryConfig, SignatureWithMetadata, StateMerkleTreeAccounts, TokenAccount, - TokenBalance, ValidityProofWithContext, + Response, RetryConfig, RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, + TokenAccount, TokenBalance, ValidityProofWithContext, }, rpc::{Rpc, RpcError}, }; @@ -60,8 +60,8 @@ use light_prover_client::{ }, }; use light_sdk::{ + light_hasher::Hash, token::{TokenData, TokenDataWithMerkleContext}, - Hash, }; use log::info; use num_bigint::{BigInt, BigUint}; @@ -477,11 +477,13 @@ impl Indexer for TestIndexer { if accounts.output_queue_batch_size.is_some() && accounts.leaf_index_in_queue_range(*index as usize)? { + use light_client::indexer::RootIndex; + indices_to_remove.push(i); proof_inputs.push(AccountProofInputs { hash: *compressed_account, root: [0u8; 32], - root_index: None, + root_index: RootIndex::new_none(), leaf_index: accounts .output_queue_elements .iter() @@ -563,7 +565,6 @@ impl Indexer for TestIndexer { } } root_indices - // root_indices.into_iter().map(|x| x.unwrap_or(0)).collect() }; Ok(Response { @@ -1932,7 +1933,7 @@ impl TestIndexer { }); account_proof_inputs.push(AccountProofInputs { - root_index: Some(root_index), + root_index: RootIndex::new_some(root_index), root: merkle_root, leaf_index: leaf_index as u64, hash: accounts[i], diff --git a/sdk-libs/program-test/src/lib.rs b/sdk-libs/program-test/src/lib.rs index 016763f208..e1825673de 100644 --- a/sdk-libs/program-test/src/lib.rs +++ b/sdk-libs/program-test/src/lib.rs @@ -1,3 +1,117 @@ +//! # Light Program Test +//! +//! A fast local test environment for Solana programs using compressed accounts and tokens. +//! +//! ## Features +//! - Fast in-memory indexer and SVM via [LiteSVM](https://github.com/LiteSVM/LiteSVM) +//! - Supports custom programs +//! - Built-in prover +//! +//! **Use `light-program-test` when:** +//! - You need fast test execution +//! - You write unit/integration tests for your program or client code +//! +//! **Use `solana-test-validator` when:** +//! - You need RPC methods or external tools that are incompatible with LiteSVM +//! - Testing against real validator behavior +//! +//! ## Configuration Options +//! +//! ### `with_prover: bool` +//! - `true`: Starts a prover server in the background for generating validity proofs +//! - `false`: Runs without prover (faster for tests that don't need proofs, or repeated test runs to reduce startup time) +//! +//! ### `additional_programs: Option>` +//! - Specify custom programs to deploy alongside the default Light Protocol programs +//! - Format: `vec![("program_name", program_id)]` +//! - Programs are loaded from built artifacts +//! +//! ## Prerequisites +//! +//! 1. **ZK Compression CLI**: Required to start the prover server and download Light Protocol programs +//! ```bash +//! npm i -g @lightprotocol/zk-compression-cli +//! ``` +//! - If programs are missing after CLI installation, run `light test-validator` once to download them +//! +//! 2. **Build programs**: Run `cargo test-sbf` to build program binaries and set the required +//! environment variables for locating program artifacts +//! +//! ## Prover Server +//! +//! The prover server runs on port 3001 when enabled. To manually stop it: +//! ```bash +//! # Find the process ID +//! lsof -i:3001 +//! # Kill the process +//! kill +//! ``` +//! +//! ## Examples +//! +//! ### V1 Trees +//! ```rust +//! use light_program_test::{LightProgramTest, ProgramTestConfig}; +//! use solana_sdk::signer::Signer; +//! +//! #[tokio::test] +//! async fn test_v1_compressed_account() { +//! // Initialize with v1 trees +//! let config = ProgramTestConfig::default(); +//! let mut rpc = LightProgramTest::new(config).await.unwrap(); +//! +//! let payer = Keypair::new(); +//! +//! // Get v1 tree info +//! let address_tree_info = rpc.get_address_tree_v1(); +//! let state_tree_info = rpc.get_random_state_tree_info(); +//! +//! // Airdrop for testing +//! rpc.airdrop_lamports(&payer.pubkey(), 1_000_000_000).await.unwrap(); +//! +//! // Query compressed accounts using Indexer trait +//! let accounts = rpc.indexer().unwrap() +//! .get_compressed_accounts_by_owner(&payer.pubkey()) +//! .await +//! .unwrap(); +//! +//! println!("Found {} compressed accounts", accounts.len()); +//! } +//! ``` +//! +//! ### V2 Trees +//! ```rust +//! use light_program_test::{LightProgramTest, ProgramTestConfig}; +//! use solana_sdk::signer::Signer; +//! +//! #[tokio::test] +//! async fn test_v2_compressed_account() { +//! // Initialize with v2 batched trees and custom program +//! let config = ProgramTestConfig::new_v2( +//! true, // with_prover +//! Some(vec![("my_program", my_program::ID)]) +//! ); +//! let mut rpc = LightProgramTest::new(config).await.unwrap(); +//! +//! let payer = Keypair::new(); +//! +//! // Get v2 tree pubkeys +//! let address_tree_info = rpc.get_address_tree_v2(); +//! let state_tree_info = rpc.get_random_state_tree_info(); +//! +//! +//! rpc.airdrop_lamports(&payer.pubkey(), 1_000_000_000).await.unwrap(); +//! +//! // Query using Indexer trait methods +//! let accounts = rpc.indexer().unwrap() +//! .get_compressed_accounts_by_owner(&payer.pubkey()) +//! .await +//! .unwrap(); +//! +//! println!("Found {} compressed accounts with v2 trees", accounts.len()); +//! } +//! ``` + pub mod accounts; pub mod indexer; pub mod program_test; diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 24ff7cfeb8..afc6d7b4b1 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -140,13 +140,6 @@ impl LightProgramTest { self.test_accounts.v1_state_trees[0] } - #[cfg(feature = "v2")] - pub fn get_state_merkle_tree_v2( - &self, - ) -> crate::accounts::test_accounts::StateMerkleTreeAccountsV2 { - self.test_accounts.v2_state_trees[0] - } - pub fn get_address_merkle_tree(&self) -> AddressMerkleTreeAccounts { self.test_accounts.v1_address_trees[0] } diff --git a/sdk-libs/program-test/src/program_test/rpc.rs b/sdk-libs/program-test/src/program_test/rpc.rs index a298a7ef8a..1838678819 100644 --- a/sdk-libs/program-test/src/program_test/rpc.rs +++ b/sdk-libs/program-test/src/program_test/rpc.rs @@ -23,8 +23,7 @@ use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, rent::Rent, - signature::{Keypair, Signature, Signer}, - system_instruction, + signature::{Keypair, Signature}, transaction::Transaction, }; use solana_transaction_status_client_types::TransactionStatus; @@ -84,41 +83,8 @@ impl Rpc for LightProgramTest { to: &Pubkey, lamports: u64, ) -> Result { - // Create a transfer instruction - let transfer_instruction = - system_instruction::transfer(&self.get_payer().pubkey(), to, lamports); - let latest_blockhash = self.get_latest_blockhash().await?.0; - - // Use the Rpc implementation of get_payer to avoid ambiguity - let payer = ::get_payer(self); - - // Create and sign a transaction - let transaction = Transaction::new_signed_with_payer( - &[transfer_instruction], - Some(&payer.pubkey()), - &vec![payer], - latest_blockhash, - ); - let sig = *transaction.signatures.first().unwrap(); - - // Send the transaction - let _res = self.context.send_transaction(transaction).map_err(|x| { - #[cfg(not(debug_assertions))] - { - if self.config.log_failed_tx { - println!("{}", x.meta.pretty_logs()); - } - } - RpcError::TransactionError(x.err) - })?; - #[cfg(debug_assertions)] - { - if std::env::var("RUST_BACKTRACE").is_ok() { - println!("{}", _res.pretty_logs()); - } - } - - Ok(sig) + let res = self.context.airdrop(to, lamports).map_err(|e| e.err)?; + Ok(res.signature) } async fn get_balance(&self, pubkey: &Pubkey) -> Result { @@ -300,17 +266,27 @@ impl Rpc for LightProgramTest { /// Gets a random active state tree. /// State trees are cached and have to be fetched or set. - fn get_random_state_tree_info(&self) -> TreeInfo { + fn get_random_state_tree_info(&self) -> Result { use rand::Rng; let mut rng = rand::thread_rng(); #[cfg(not(feature = "v2"))] - return self.test_accounts.v1_state_trees - [rng.gen_range(0..self.test_accounts.v1_state_trees.len())] - .into(); + { + if self.test_accounts.v1_state_trees.is_empty() { + return Err(RpcError::NoStateTreesAvailable); + } + Ok(self.test_accounts.v1_state_trees + [rng.gen_range(0..self.test_accounts.v1_state_trees.len())] + .into()) + } #[cfg(feature = "v2")] - return self.test_accounts.v2_state_trees - [rng.gen_range(0..self.test_accounts.v2_state_trees.len())] - .into(); + { + if self.test_accounts.v2_state_trees.is_empty() { + return Err(RpcError::NoStateTreesAvailable); + } + Ok(self.test_accounts.v2_state_trees + [rng.gen_range(0..self.test_accounts.v2_state_trees.len())] + .into()) + } } fn get_address_tree_v1(&self) -> TreeInfo { @@ -322,18 +298,20 @@ impl Rpc for LightProgramTest { tree_type: TreeType::AddressV1, } } - // fn get_address_tree_v2(&self) -> MerkleContext { - // MerkleContext { - // tree: pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK"), - // queue: pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK"), - // cpi_context: None, - // next_tree_info: None, - // tree_type: TreeType::AddressV2, - // } - // } } impl LightProgramTest { + #[cfg(feature = "v2")] + pub fn get_address_tree_v2(&self) -> TreeInfo { + TreeInfo { + tree: pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK"), + queue: pubkey!("EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK"), + cpi_context: None, + next_tree_info: None, + tree_type: TreeType::AddressV2, + } + } + async fn _send_transaction_with_batched_event( &mut self, transaction: Transaction, diff --git a/sdk-libs/program-test/src/utils/setup_light_programs.rs b/sdk-libs/program-test/src/utils/setup_light_programs.rs index 221fcb826a..839dd56b8d 100644 --- a/sdk-libs/program-test/src/utils/setup_light_programs.rs +++ b/sdk-libs/program-test/src/utils/setup_light_programs.rs @@ -1,5 +1,6 @@ use light_client::rpc::RpcError; -use light_sdk::utils::get_registered_program_pda; +use light_compressed_account::constants::REGISTERED_PROGRAM_PDA; +use light_registry::account_compression_cpi::sdk::get_registered_program_pda; use litesvm::LiteSVM; use solana_compute_budget::compute_budget::ComputeBudget; use solana_pubkey::Pubkey; @@ -72,7 +73,10 @@ pub fn setup_light_programs( { let path = format!("{}/light_system_program_pinocchio.so", light_bin_path); program_test - .add_program_from_file(light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, path.clone()) + .add_program_from_file( + light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID.into(), + path.clone(), + ) .inspect_err(|_| { println!( "Program light_system_program_pinocchio bin not found in {}", @@ -84,13 +88,16 @@ pub fn setup_light_programs( #[cfg(not(feature = "devenv"))] { let path = format!("{}/light_system_program.so", light_bin_path); - program_test.add_program_from_file(light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM, path)?; + program_test.add_program_from_file( + Pubkey::from(light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID), + path, + )?; } let registered_program = registered_program_test_account_system_program(); program_test .set_account( - get_registered_program_pda(&light_sdk::constants::PROGRAM_ID_LIGHT_SYSTEM), + Pubkey::new_from_array(REGISTERED_PROGRAM_PDA), registered_program, ) .map_err(|e| { diff --git a/sdk-libs/sdk-pinocchio/Cargo.toml b/sdk-libs/sdk-pinocchio/Cargo.toml new file mode 100644 index 0000000000..29526c8bd1 --- /dev/null +++ b/sdk-libs/sdk-pinocchio/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "light-sdk-pinocchio" +version = "0.12.0" +description = "Rust SDK for ZK Compression on Solana with Pinocchio features" +repository = "https://github.com/Lightprotocol/light-protocol" +license = "Apache-2.0" +edition = "2021" + +[features] +default = [] +v2 = ["light-sdk-types/v2"] +small_ix = ["light-sdk-types/small_ix"] + +[dependencies] +pinocchio = { workspace = true } +light-hasher = { workspace = true } +light-account-checks = { workspace = true, features = ["pinocchio"] } +light-macros = { workspace = true } +light-sdk-macros = { workspace = true } +light-sdk-types = { workspace = true } +light-zero-copy = { workspace = true } +borsh = { workspace = true } +thiserror = { workspace = true } +light-compressed-account = { workspace = true } +solana-pubkey = { workspace = true } diff --git a/sdk-libs/sdk-pinocchio/src/account.rs b/sdk-libs/sdk-pinocchio/src/account.rs new file mode 100644 index 0000000000..5d40bff7cb --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/account.rs @@ -0,0 +1,196 @@ +use std::ops::{Deref, DerefMut}; + +use light_compressed_account::{ + compressed_account::PackedMerkleContext, + instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo}, +}; +use light_hasher::{DataHasher, Poseidon}; +use light_sdk_types::instruction::account_meta::CompressedAccountMetaTrait; +use pinocchio::pubkey::Pubkey; + +use crate::{error::LightSdkError, BorshDeserialize, BorshSerialize, LightDiscriminator}; + +#[derive(Debug, PartialEq)] +pub struct LightAccount< + 'a, + A: BorshSerialize + BorshDeserialize + LightDiscriminator + DataHasher + Default, +> { + owner: &'a Pubkey, + pub account: A, + account_info: CompressedAccountInfo, +} + +impl<'a, A: BorshSerialize + BorshDeserialize + LightDiscriminator + DataHasher + Default> + LightAccount<'a, A> +{ + pub fn new_init( + owner: &'a Pubkey, + address: Option<[u8; 32]>, + output_state_tree_index: u8, + ) -> Self { + let output_account_info = OutAccountInfo { + output_merkle_tree_index: output_state_tree_index, + discriminator: A::LIGHT_DISCRIMINATOR, + ..Default::default() + }; + Self { + owner, + account: A::default(), + account_info: CompressedAccountInfo { + address, + input: None, + output: Some(output_account_info), + }, + } + } + + pub fn new_mut( + owner: &'a Pubkey, + input_account_meta: &impl CompressedAccountMetaTrait, + input_account: A, + ) -> Result { + let input_account_info = { + let input_data_hash = input_account.hash::()?; + let tree_info = input_account_meta.get_tree_info(); + InAccountInfo { + data_hash: input_data_hash, + lamports: input_account_meta.get_lamports().unwrap_or_default(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + root_index: input_account_meta.get_root_index().unwrap_or_default(), + discriminator: A::LIGHT_DISCRIMINATOR, + } + }; + let output_account_info = { + let output_merkle_tree_index = input_account_meta + .get_output_state_tree_index() + .ok_or(LightSdkError::OutputStateTreeIndexIsNone)?; + OutAccountInfo { + lamports: input_account_meta.get_lamports().unwrap_or_default(), + output_merkle_tree_index, + discriminator: A::LIGHT_DISCRIMINATOR, + ..Default::default() + } + }; + + Ok(Self { + owner, + account: input_account, + account_info: CompressedAccountInfo { + address: input_account_meta.get_address(), + input: Some(input_account_info), + output: Some(output_account_info), + }, + }) + } + + pub fn new_close( + owner: &'a Pubkey, + input_account_meta: &impl CompressedAccountMetaTrait, + input_account: A, + ) -> Result { + let input_account_info = { + let input_data_hash = input_account.hash::()?; + let tree_info = input_account_meta.get_tree_info(); + InAccountInfo { + data_hash: input_data_hash, + lamports: input_account_meta.get_lamports().unwrap_or_default(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + root_index: input_account_meta.get_root_index().unwrap_or_default(), + discriminator: A::LIGHT_DISCRIMINATOR, + } + }; + Ok(Self { + owner, + account: input_account, + account_info: CompressedAccountInfo { + address: input_account_meta.get_address(), + input: Some(input_account_info), + output: None, + }, + }) + } + + pub fn discriminator(&self) -> &[u8; 8] { + &A::LIGHT_DISCRIMINATOR + } + + pub fn lamports(&self) -> u64 { + if let Some(output) = self.account_info.output.as_ref() { + output.lamports + } else if let Some(input) = self.account_info.input.as_ref() { + input.lamports + } else { + 0 + } + } + + pub fn lamports_mut(&mut self) -> &mut u64 { + if let Some(output) = self.account_info.output.as_mut() { + &mut output.lamports + } else if let Some(input) = self.account_info.input.as_mut() { + &mut input.lamports + } else { + panic!("No lamports field available in account_info") + } + } + + pub fn address(&self) -> &Option<[u8; 32]> { + &self.account_info.address + } + + pub fn owner(&self) -> &Pubkey { + self.owner + } + + pub fn in_account_info(&self) -> &Option { + &self.account_info.input + } + + pub fn out_account_info(&mut self) -> &Option { + &self.account_info.output + } + + /// 1. Serializes the account data and sets the output data hash. + /// 2. Returns CompressedAccountInfo. + /// + /// Note this is an expensive operation + /// that should only be called once per instruction. + pub fn to_account_info(mut self) -> Result { + if let Some(output) = self.account_info.output.as_mut() { + output.data_hash = self.account.hash::()?; + output.data = self + .account + .try_to_vec() + .map_err(|_| LightSdkError::Borsh)?; + } + Ok(self.account_info) + } +} + +impl Deref + for LightAccount<'_, A> +{ + type Target = A; + + fn deref(&self) -> &Self::Target { + &self.account + } +} + +impl DerefMut + for LightAccount<'_, A> +{ + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.account + } +} diff --git a/sdk-libs/sdk-pinocchio/src/address.rs b/sdk-libs/sdk-pinocchio/src/address.rs new file mode 100644 index 0000000000..ebfbf955d6 --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/address.rs @@ -0,0 +1,47 @@ +use pinocchio::{account_info::AccountInfo, pubkey::Pubkey}; + +// Define data structures needed +#[derive(Clone, Debug, Default)] +pub struct NewAddressParams { + pub seed: [u8; 32], + pub address_queue_pubkey: [u8; 32], + pub address_merkle_tree_pubkey: [u8; 32], + pub address_merkle_tree_root_index: u16, +} + +pub fn unpack_new_address_params( + address_params: &crate::NewAddressParamsPacked, + remaining_accounts: &[AccountInfo], +) -> NewAddressParams { + let address_merkle_tree_pubkey = + remaining_accounts[address_params.address_merkle_tree_account_index as usize].key(); + let address_queue_pubkey = + remaining_accounts[address_params.address_queue_account_index as usize].key(); + + NewAddressParams { + seed: address_params.seed, + address_queue_pubkey: *address_queue_pubkey, + address_merkle_tree_pubkey: *address_merkle_tree_pubkey, + address_merkle_tree_root_index: address_params.address_merkle_tree_root_index, + } +} + +pub mod v1 { + use super::*; + + /// Derives a single address seed for a compressed account, based on the + /// provided multiple `seeds`, `program_id` and `merkle_tree_pubkey`. + pub fn derive_address_seed(seeds: &[&[u8]], program_id: &Pubkey) -> [u8; 32] { + light_sdk_types::address::v1::derive_address_seed(seeds, program_id) + } + + /// Derives an address from provided seeds. Returns that address and a singular + /// seed. + pub fn derive_address( + seeds: &[&[u8]], + merkle_tree_pubkey: &Pubkey, + program_id: &Pubkey, + ) -> ([u8; 32], [u8; 32]) { + light_sdk_types::address::v1::derive_address(seeds, merkle_tree_pubkey, program_id) + } +} diff --git a/sdk-libs/sdk-pinocchio/src/cpi/accounts.rs b/sdk-libs/sdk-pinocchio/src/cpi/accounts.rs new file mode 100644 index 0000000000..1ebe42c28c --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/cpi/accounts.rs @@ -0,0 +1,103 @@ +use light_sdk_types::{CpiAccounts as GenericCpiAccounts, SYSTEM_ACCOUNTS_LEN}; +pub use light_sdk_types::{CpiAccountsConfig, CpiSigner}; +use pinocchio::{account_info::AccountInfo, instruction::AccountMeta}; + +use crate::error::{LightSdkError, Result}; + +pub type CpiAccounts<'a> = GenericCpiAccounts<'a, AccountInfo>; + +pub fn to_account_metas<'a>(cpi_accounts: &CpiAccounts<'a>) -> Result>> { + let mut account_metas = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); + account_metas.push(AccountMeta::writable_signer(cpi_accounts.fee_payer().key())); + account_metas.push(AccountMeta::readonly_signer( + cpi_accounts.authority()?.key(), + )); + + account_metas.push(AccountMeta::readonly( + cpi_accounts.registered_program_pda()?.key(), + )); + account_metas.push(AccountMeta::readonly(cpi_accounts.noop_program()?.key())); + account_metas.push(AccountMeta::readonly( + cpi_accounts.account_compression_authority()?.key(), + )); + account_metas.push(AccountMeta::readonly( + cpi_accounts.account_compression_program()?.key(), + )); + account_metas.push(AccountMeta::readonly( + cpi_accounts.invoking_program()?.key(), + )); + let mut current_index = 7; + let light_system_program_key = cpi_accounts.light_system_program()?.key(); + + if !cpi_accounts.config().sol_pool_pda { + account_metas.push(AccountMeta::readonly(light_system_program_key)); + } else { + let account = cpi_accounts.get_account_info(current_index)?; + account_metas.push(AccountMeta::writable(account.key())); + current_index += 1; + } + + if !cpi_accounts.config().sol_compression_recipient { + account_metas.push(AccountMeta::readonly(light_system_program_key)); + } else { + let account = cpi_accounts.get_account_info(current_index)?; + account_metas.push(AccountMeta::writable(account.key())); + current_index += 1; + } + + // System program - use default (all zeros) + account_metas.push(AccountMeta::readonly(&[0u8; 32])); + current_index += 1; + + if !cpi_accounts.config().cpi_context { + account_metas.push(AccountMeta::readonly(light_system_program_key)); + } else { + let account = cpi_accounts.get_account_info(current_index)?; + account_metas.push(AccountMeta::writable(account.key())); + current_index += 1; + } + + // Add remaining tree accounts + let tree_accounts = cpi_accounts + .account_infos() + .get(current_index..) + .ok_or(LightSdkError::CpiAccountsIndexOutOfBounds(current_index))?; + tree_accounts.iter().for_each(|acc| { + let account_meta = if acc.is_writable() { + AccountMeta::writable(acc.key()) + } else { + AccountMeta::readonly(acc.key()) + }; + account_metas.push(account_meta); + }); + + Ok(account_metas) +} + +pub fn to_account_infos_for_invoke<'a>( + cpi_accounts: &CpiAccounts<'a>, +) -> Result> { + let mut account_infos = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); + account_infos.push(cpi_accounts.fee_payer()); + // Skip the first account (light_system_program) and add the rest + cpi_accounts.account_infos()[1..] + .iter() + .for_each(|acc| account_infos.push(acc)); + let mut current_index = 7; + if !cpi_accounts.config().sol_pool_pda { + account_infos.insert(current_index, cpi_accounts.light_system_program()?); + } + current_index += 1; + + if !cpi_accounts.config().sol_compression_recipient { + account_infos.insert(current_index, cpi_accounts.light_system_program()?); + } + current_index += 1; + // system program + current_index += 1; + + if !cpi_accounts.config().cpi_context { + account_infos.insert(current_index, cpi_accounts.light_system_program()?); + } + Ok(account_infos) +} diff --git a/sdk-libs/sdk-pinocchio/src/cpi/accounts_small.rs b/sdk-libs/sdk-pinocchio/src/cpi/accounts_small.rs new file mode 100644 index 0000000000..6f59d524b0 --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/cpi/accounts_small.rs @@ -0,0 +1,67 @@ +use light_sdk_types::{ + CompressionCpiAccountIndexSmall, CpiAccountsSmall as GenericCpiAccountsSmall, + PROGRAM_ACCOUNTS_LEN, +}; +use pinocchio::{account_info::AccountInfo, instruction::AccountMeta}; + +use crate::error::Result; + +pub type CpiAccountsSmall<'a> = GenericCpiAccountsSmall<'a, AccountInfo>; + +pub fn to_account_metas_small<'a>( + cpi_accounts: &CpiAccountsSmall<'a>, +) -> Result>> { + let mut account_metas = + Vec::with_capacity(1 + cpi_accounts.account_infos().len() - PROGRAM_ACCOUNTS_LEN); + + account_metas.push(AccountMeta::writable_signer(cpi_accounts.fee_payer().key())); + account_metas.push(AccountMeta::readonly_signer( + cpi_accounts.authority()?.key(), + )); + + account_metas.push(AccountMeta::readonly( + cpi_accounts.registered_program_pda()?.key(), + )); + account_metas.push(AccountMeta::readonly( + cpi_accounts.account_compression_authority()?.key(), + )); + + let accounts = cpi_accounts.account_infos(); + let mut index = CompressionCpiAccountIndexSmall::SolPoolPda as usize; + + if cpi_accounts.config().sol_pool_pda { + let account = cpi_accounts.get_account_info(index)?; + account_metas.push(AccountMeta::writable(account.key())); + index += 1; + } + + if cpi_accounts.config().sol_compression_recipient { + let account = cpi_accounts.get_account_info(index)?; + account_metas.push(AccountMeta::writable(account.key())); + index += 1; + } + + if cpi_accounts.config().cpi_context { + let account = cpi_accounts.get_account_info(index)?; + account_metas.push(AccountMeta::writable(account.key())); + index += 1; + } + + // Add remaining tree accounts + let tree_accounts = + accounts + .get(index..) + .ok_or(crate::error::LightSdkError::CpiAccountsIndexOutOfBounds( + index, + ))?; + tree_accounts.iter().for_each(|acc| { + let account_meta = if acc.is_writable() { + AccountMeta::writable(acc.key()) + } else { + AccountMeta::readonly(acc.key()) + }; + account_metas.push(account_meta); + }); + + Ok(account_metas) +} diff --git a/sdk-libs/sdk-pinocchio/src/cpi/invoke.rs b/sdk-libs/sdk-pinocchio/src/cpi/invoke.rs new file mode 100644 index 0000000000..150413680f --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/cpi/invoke.rs @@ -0,0 +1,202 @@ +use light_compressed_account::{ + compressed_account::{ + CompressedAccount, CompressedAccountData, PackedCompressedAccountWithMerkleContext, + }, + discriminators::DISCRIMINATOR_INVOKE_CPI, + instruction_data::{ + cpi_context::CompressedCpiContext, + data::{NewAddressParamsPacked, OutputCompressedAccountWithPackedContext}, + invoke_cpi::InstructionDataInvokeCpi, + with_account_info::CompressedAccountInfo, + }, +}; +use light_sdk_types::constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}; +use pinocchio::{cpi::slice_invoke_signed, msg, pubkey::Pubkey}; + +use crate::{ + cpi::CpiAccounts, + error::{LightSdkError, Result}, + BorshSerialize, ValidityProof, +}; + +// Trait to provide the missing methods for CompressedAccountInfo +pub trait CompressedAccountInfoExt { + fn input_compressed_account( + &self, + owner: Pubkey, + ) -> Result>; + fn output_compressed_account( + &self, + owner: Pubkey, + ) -> Result>; +} + +impl CompressedAccountInfoExt for CompressedAccountInfo { + fn input_compressed_account( + &self, + owner: Pubkey, + ) -> Result> { + match self.input.as_ref() { + Some(input) => { + let data = Some(CompressedAccountData { + discriminator: input.discriminator, + data: Vec::new(), + data_hash: input.data_hash, + }); + Ok(Some(PackedCompressedAccountWithMerkleContext { + compressed_account: CompressedAccount { + owner: owner.into(), + lamports: input.lamports, + address: self.address, + data, + }, + merkle_context: input.merkle_context, + root_index: input.root_index, + read_only: false, + })) + } + None => Ok(None), + } + } + + fn output_compressed_account( + &self, + owner: Pubkey, + ) -> Result> { + match self.output.as_ref() { + Some(output) => { + let data = Some(CompressedAccountData { + discriminator: output.discriminator, + data: output.data.clone(), + data_hash: output.data_hash, + }); + Ok(Some(OutputCompressedAccountWithPackedContext { + compressed_account: CompressedAccount { + owner: owner.into(), + lamports: output.lamports, + address: self.address, + data, + }, + merkle_tree_index: output.output_merkle_tree_index, + })) + } + None => Ok(None), + } + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct CpiInputs { + pub proof: ValidityProof, + pub account_infos: Option>, + pub new_addresses: Option>, + pub compress_or_decompress_lamports: Option, + pub is_compress: bool, + pub cpi_context: Option, +} + +impl CpiInputs { + pub fn new(proof: ValidityProof, account_infos: Vec) -> Self { + Self { + proof, + account_infos: Some(account_infos), + ..Default::default() + } + } + + pub fn new_with_address( + proof: ValidityProof, + account_infos: Vec, + new_addresses: Vec, + ) -> Self { + Self { + proof, + account_infos: Some(account_infos), + new_addresses: Some(new_addresses), + ..Default::default() + } + } + + pub fn invoke_light_system_program(self, cpi_accounts: CpiAccounts) -> Result<()> { + light_system_program_instruction_invoke_cpi(self, &cpi_accounts) + } +} + +pub fn light_system_program_instruction_invoke_cpi( + cpi_inputs: CpiInputs, + cpi_accounts: &CpiAccounts, +) -> Result<()> { + let owner = *cpi_accounts.invoking_program()?.key(); + let (input_compressed_accounts_with_merkle_context, output_compressed_accounts) = + if let Some(account_infos) = cpi_inputs.account_infos.as_ref() { + let mut input_compressed_accounts_with_merkle_context = + Vec::with_capacity(account_infos.len()); + let mut output_compressed_accounts = Vec::with_capacity(account_infos.len()); + for account_info in account_infos.iter() { + if let Some(input_account) = + CompressedAccountInfoExt::input_compressed_account(account_info, owner)? + { + input_compressed_accounts_with_merkle_context.push(input_account); + } + if let Some(output_account) = + CompressedAccountInfoExt::output_compressed_account(account_info, owner)? + { + output_compressed_accounts.push(output_account); + } + } + ( + input_compressed_accounts_with_merkle_context, + output_compressed_accounts, + ) + } else { + (vec![], vec![]) + }; + + let inputs = InstructionDataInvokeCpi { + proof: cpi_inputs.proof.0, + new_address_params: cpi_inputs.new_addresses.unwrap_or_default(), + relay_fee: None, + input_compressed_accounts_with_merkle_context, + output_compressed_accounts, + compress_or_decompress_lamports: cpi_inputs.compress_or_decompress_lamports, + is_compress: cpi_inputs.is_compress, + cpi_context: cpi_inputs.cpi_context, + }; + let inputs = inputs.try_to_vec().map_err(|_| LightSdkError::Borsh)?; + + let mut data = Vec::with_capacity(8 + 4 + inputs.len()); + data.extend_from_slice(&DISCRIMINATOR_INVOKE_CPI); + data.extend_from_slice(&(inputs.len() as u32).to_le_bytes()); + data.extend(inputs); + + let account_metas: Vec = + crate::cpi::accounts::to_account_metas(cpi_accounts)?; + + // Create instruction with owned data and immediately invoke it + use pinocchio::instruction::{Instruction, Seed, Signer}; + + // Use the precomputed CPI signer and bump from the config + let bump = cpi_accounts.bump(); + let bump_seed = [bump]; + let seed_array = [ + Seed::from(CPI_AUTHORITY_PDA_SEED), + Seed::from(bump_seed.as_slice()), + ]; + let signer = Signer::from(&seed_array); + + let instruction = Instruction { + program_id: &Pubkey::from(LIGHT_SYSTEM_PROGRAM_ID), + accounts: &account_metas, + data: &data, + }; + let account_infos = crate::cpi::accounts::to_account_infos_for_invoke(cpi_accounts)?; + + match slice_invoke_signed(&instruction, &account_infos, &[signer]) { + Ok(()) => {} + Err(e) => { + msg!(format!("slice_invoke_signed failed: {:?}", e).as_str()); + return Err(LightSdkError::ProgramError(e)); + } + } + Ok(()) +} diff --git a/sdk-libs/sdk-pinocchio/src/cpi/mod.rs b/sdk-libs/sdk-pinocchio/src/cpi/mod.rs new file mode 100644 index 0000000000..d255f3dc19 --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/cpi/mod.rs @@ -0,0 +1,9 @@ +pub mod accounts; +#[cfg(feature = "small_ix")] +pub mod accounts_small; +pub mod invoke; + +pub use accounts::*; +#[cfg(feature = "small_ix")] +pub use accounts_small::*; +pub use invoke::*; diff --git a/sdk-libs/sdk-pinocchio/src/error.rs b/sdk-libs/sdk-pinocchio/src/error.rs new file mode 100644 index 0000000000..89663d599b --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/error.rs @@ -0,0 +1,156 @@ +use light_hasher::HasherError; +pub use light_sdk_types::error::LightSdkTypesError; +use light_zero_copy::errors::ZeroCopyError; +use pinocchio::program_error::ProgramError; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error, PartialEq)] +pub enum LightSdkError { + #[error("Constraint violation")] + ConstraintViolation, + #[error("Invalid light-system-program ID")] + InvalidLightSystemProgram, + #[error("Expected accounts in the instruction")] + ExpectedAccounts, + #[error("Expected address Merkle context to be provided")] + ExpectedAddressTreeInfo, + #[error("Expected address root index to be provided")] + ExpectedAddressRootIndex, + #[error("Accounts with a specified input are expected to have data")] + ExpectedData, + #[error("Accounts with specified data are expected to have a discriminator")] + ExpectedDiscriminator, + #[error("Accounts with specified data are expected to have a hash")] + ExpectedHash, + #[error("Expected the `{0}` light account to be provided")] + ExpectedLightSystemAccount(String), + #[error("`mut` and `close` accounts are expected to have a Merkle context")] + ExpectedMerkleContext, + #[error("Expected root index to be provided")] + ExpectedRootIndex, + #[error("Cannot transfer lamports from an account without input")] + TransferFromNoInput, + #[error("Cannot transfer from an account without lamports")] + TransferFromNoLamports, + #[error("Account, from which a transfer was attempted, has insufficient amount of lamports")] + TransferFromInsufficientLamports, + #[error("Integer overflow resulting from too large resulting amount")] + TransferIntegerOverflow, + #[error("Borsh error.")] + Borsh, + #[error("Fewer accounts than number of system accounts.")] + FewerAccountsThanSystemAccounts, + #[error("InvalidCpiSignerAccount")] + InvalidCpiSignerAccount, + #[error("Missing meta field: {0}")] + MissingField(String), + #[error("Output state tree index is none. Use an CompressedAccountMeta type with output tree index to initialize or update accounts.")] + OutputStateTreeIndexIsNone, + #[error("Address is none during initialization")] + InitAddressIsNone, + #[error("Address is none during initialization with address")] + InitWithAddressIsNone, + #[error("Output is none during initialization with address")] + InitWithAddressOutputIsNone, + #[error("Address is none during meta mutation")] + MetaMutAddressIsNone, + #[error("Input is none during meta mutation")] + MetaMutInputIsNone, + #[error("Output lamports is none during meta mutation")] + MetaMutOutputLamportsIsNone, + #[error("Output is none during meta mutation")] + MetaMutOutputIsNone, + #[error("Address is none during meta close")] + MetaCloseAddressIsNone, + #[error("Input is none during meta close")] + MetaCloseInputIsNone, + #[error("CPI accounts index out of bounds: {0}")] + CpiAccountsIndexOutOfBounds(usize), + #[error(transparent)] + Hasher(#[from] HasherError), + #[error(transparent)] + ZeroCopy(#[from] ZeroCopyError), + #[error("Program error: {0:?}")] + ProgramError(ProgramError), +} + +impl From for LightSdkError { + fn from(error: ProgramError) -> Self { + LightSdkError::ProgramError(error) + } +} + +impl From for ProgramError { + fn from(e: LightSdkError) -> Self { + ProgramError::Custom(e.into()) + } +} + +impl From for LightSdkError { + fn from(e: LightSdkTypesError) -> Self { + match e { + LightSdkTypesError::InitAddressIsNone => LightSdkError::InitAddressIsNone, + LightSdkTypesError::InitWithAddressIsNone => LightSdkError::InitWithAddressIsNone, + LightSdkTypesError::InitWithAddressOutputIsNone => { + LightSdkError::InitWithAddressOutputIsNone + } + LightSdkTypesError::MetaMutAddressIsNone => LightSdkError::MetaMutAddressIsNone, + LightSdkTypesError::MetaMutInputIsNone => LightSdkError::MetaMutInputIsNone, + LightSdkTypesError::MetaMutOutputLamportsIsNone => { + LightSdkError::MetaMutOutputLamportsIsNone + } + LightSdkTypesError::MetaMutOutputIsNone => LightSdkError::MetaMutOutputIsNone, + LightSdkTypesError::MetaCloseAddressIsNone => LightSdkError::MetaCloseAddressIsNone, + LightSdkTypesError::MetaCloseInputIsNone => LightSdkError::MetaCloseInputIsNone, + LightSdkTypesError::Hasher(e) => LightSdkError::Hasher(e), + LightSdkTypesError::FewerAccountsThanSystemAccounts => { + LightSdkError::FewerAccountsThanSystemAccounts + } + LightSdkTypesError::CpiAccountsIndexOutOfBounds(index) => { + LightSdkError::CpiAccountsIndexOutOfBounds(index) + } + } + } +} + +impl From for u32 { + fn from(e: LightSdkError) -> Self { + match e { + LightSdkError::ConstraintViolation => 16001, + LightSdkError::InvalidLightSystemProgram => 16002, + LightSdkError::ExpectedAccounts => 16003, + LightSdkError::ExpectedAddressTreeInfo => 16004, + LightSdkError::ExpectedAddressRootIndex => 16005, + LightSdkError::ExpectedData => 16006, + LightSdkError::ExpectedDiscriminator => 16007, + LightSdkError::ExpectedHash => 16008, + LightSdkError::ExpectedLightSystemAccount(_) => 16009, + LightSdkError::ExpectedMerkleContext => 16010, + LightSdkError::ExpectedRootIndex => 16011, + LightSdkError::TransferFromNoInput => 16012, + LightSdkError::TransferFromNoLamports => 16013, + LightSdkError::TransferFromInsufficientLamports => 16014, + LightSdkError::TransferIntegerOverflow => 16015, + LightSdkError::Borsh => 16016, + LightSdkError::FewerAccountsThanSystemAccounts => 16017, + LightSdkError::InvalidCpiSignerAccount => 16018, + LightSdkError::MissingField(_) => 16019, + LightSdkError::OutputStateTreeIndexIsNone => 16020, + LightSdkError::InitAddressIsNone => 16021, + LightSdkError::InitWithAddressIsNone => 16022, + LightSdkError::InitWithAddressOutputIsNone => 16023, + LightSdkError::MetaMutAddressIsNone => 16024, + LightSdkError::MetaMutInputIsNone => 16025, + LightSdkError::MetaMutOutputLamportsIsNone => 16026, + LightSdkError::MetaMutOutputIsNone => 16027, + LightSdkError::MetaCloseAddressIsNone => 16028, + LightSdkError::MetaCloseInputIsNone => 16029, + LightSdkError::CpiAccountsIndexOutOfBounds(_) => 16031, + LightSdkError::Hasher(e) => e.into(), + LightSdkError::ZeroCopy(e) => e.into(), + LightSdkError::ProgramError(e) => u64::from(e) as u32, + } + } +} diff --git a/sdk-libs/sdk-pinocchio/src/instruction/mod.rs b/sdk-libs/sdk-pinocchio/src/instruction/mod.rs new file mode 100644 index 0000000000..b28b45fcba --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/instruction/mod.rs @@ -0,0 +1 @@ +pub use light_sdk_types::instruction::*; diff --git a/sdk-libs/sdk-pinocchio/src/lib.rs b/sdk-libs/sdk-pinocchio/src/lib.rs new file mode 100644 index 0000000000..24626ead6b --- /dev/null +++ b/sdk-libs/sdk-pinocchio/src/lib.rs @@ -0,0 +1,17 @@ +pub mod account; +pub mod address; +pub mod cpi; +pub mod error; +pub mod instruction; + +pub use account::LightAccount; +pub use borsh::{BorshDeserialize, BorshSerialize}; +pub use cpi::{CpiAccounts, CpiAccountsConfig, CpiInputs}; +pub use light_account_checks::discriminator::Discriminator as LightDiscriminator; +pub use light_compressed_account::{ + self, + instruction_data::{compressed_proof::ValidityProof, data::*}, +}; +pub use light_hasher; +pub use light_sdk_macros::{derive_light_cpi_signer, LightDiscriminator, LightHasher}; +pub use light_sdk_types::{self, constants, CpiSigner}; diff --git a/sdk-libs/sdk-types/Cargo.toml b/sdk-libs/sdk-types/Cargo.toml new file mode 100644 index 0000000000..9116b15a3f --- /dev/null +++ b/sdk-libs/sdk-types/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "light-sdk-types" +version = "0.9.1" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/lightprotocol/light-protocol" +description = "Core types for Light Protocol SDK" + +[features] +anchor = ["anchor-lang", "light-compressed-account/anchor"] +v2 = [] +small_ix = [] + +[dependencies] +anchor-lang = { workspace = true, optional = true } +# Light Protocol dependencies +light-account-checks = { workspace = true } +light-hasher = { workspace = true } +light-compressed-account = { workspace = true } +light-macros = { workspace = true } +light-zero-copy = { workspace = true } + +# External dependencies +borsh = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +solana-pubkey = { workspace = true } diff --git a/sdk-libs/sdk-types/src/address.rs b/sdk-libs/sdk-types/src/address.rs new file mode 100644 index 0000000000..a1394fbb81 --- /dev/null +++ b/sdk-libs/sdk-types/src/address.rs @@ -0,0 +1,160 @@ +pub mod v1 { + use light_hasher::{hash_to_field_size::hashv_to_bn254_field_size_be, Hasher, Keccak}; + + /// Derives a single address seed for a compressed account, based on the + /// provided multiple `seeds`, `program_id` and `merkle_tree_pubkey`. + /// + /// # Examples + /// + /// ```ignore + /// use light_sdk::{address::derive_address, pubkey}; + /// + /// let address = derive_address( + /// &[b"my_compressed_account"], + /// &crate::ID, + /// ); + /// ``` + pub fn derive_address_seed(seeds: &[&[u8]], program_id: &[u8; 32]) -> [u8; 32] { + let mut inputs = Vec::with_capacity(seeds.len() + 1); + + inputs.push(program_id.as_slice()); + + inputs.extend(seeds); + + let seed = hashv_to_bn254_field_size_be_legacy(inputs.as_slice()); + seed + } + + fn hashv_to_bn254_field_size_be_legacy(bytes: &[&[u8]]) -> [u8; 32] { + let mut hashed_value: [u8; 32] = Keccak::hashv(bytes).unwrap(); + // Truncates to 31 bytes so that value is less than bn254 Fr modulo + // field size. + hashed_value[0] = 0; + hashed_value + } + + /// Derives an address for a compressed account, based on the provided singular + /// `seed` and `merkle_tree_pubkey`: + pub(crate) fn derive_address_from_seed( + address_seed: &[u8; 32], + merkle_tree_pubkey: &[u8; 32], + ) -> [u8; 32] { + let input = [merkle_tree_pubkey.as_slice(), address_seed.as_slice()]; + hashv_to_bn254_field_size_be(input.as_slice()) + } + + /// Derives an address from provided seeds. Returns that address and a singular + /// seed. + /// + /// # Examples + /// + /// ```ignore + /// use light_sdk::{address::derive_address, pubkey}; + /// + /// let address_tree_info = { + /// address_merkle_tree_pubkey: pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"), + /// address_queue_pubkey: pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"), + /// }; + /// let (address, address_seed) = derive_address( + /// &[b"my_compressed_account"], + /// &address_tree_info, + /// &crate::ID, + /// ); + /// ``` + pub fn derive_address( + seeds: &[&[u8]], + merkle_tree_pubkey: &[u8; 32], + program_id: &[u8; 32], + ) -> ([u8; 32], [u8; 32]) { + let address_seed = derive_address_seed(seeds, program_id); + let address = derive_address_from_seed(&address_seed, merkle_tree_pubkey); + + (address, address_seed) + } +} + +#[cfg(test)] +mod test { + use super::v1::*; + + #[allow(dead_code)] + #[derive(Debug)] + struct AddressTreeInfo { + pub address_merkle_tree_pubkey: [u8; 32], + pub address_queue_pubkey: [u8; 32], + } + + #[test] + fn test_derive_address_seed() { + use light_macros::pubkey_array; + let program_id = pubkey_array!("7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz"); + + let address_seed = derive_address_seed(&[b"foo", b"bar"], &program_id); + assert_eq!( + address_seed, + [ + 0, 246, 150, 3, 192, 95, 53, 123, 56, 139, 206, 179, 253, 133, 115, 103, 120, 155, + 251, 72, 250, 47, 117, 217, 118, 59, 174, 207, 49, 101, 201, 110 + ] + ); + + let address_seed = derive_address_seed(&[b"ayy", b"lmao"], &program_id); + assert_eq!( + address_seed, + [ + 0, 202, 44, 25, 221, 74, 144, 92, 69, 168, 38, 19, 206, 208, 29, 162, 53, 27, 120, + 214, 152, 116, 15, 107, 212, 168, 33, 121, 187, 10, 76, 233 + ] + ); + } + + #[test] + fn test_derive_address() { + use light_macros::pubkey_array; + let address_tree_info = AddressTreeInfo { + address_merkle_tree_pubkey: [0; 32], + address_queue_pubkey: [0; 32], + }; + let program_id = pubkey_array!("7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz"); + + let seeds: &[&[u8]] = &[b"foo", b"bar"]; + let expected_address_seed = [ + 0, 246, 150, 3, 192, 95, 53, 123, 56, 139, 206, 179, 253, 133, 115, 103, 120, 155, 251, + 72, 250, 47, 117, 217, 118, 59, 174, 207, 49, 101, 201, 110, + ]; + let expected_address = [ + 0, 141, 60, 24, 250, 156, 15, 250, 237, 196, 171, 243, 182, 10, 8, 66, 147, 57, 27, + 209, 222, 86, 109, 234, 161, 219, 142, 43, 121, 104, 16, 63, + ]; + + let address_seed = derive_address_seed(seeds, &program_id); + assert_eq!(address_seed, expected_address_seed); + let (address, address_seed) = derive_address( + seeds, + &address_tree_info.address_merkle_tree_pubkey, + &program_id, + ); + assert_eq!(address_seed, expected_address_seed); + assert_eq!(address, expected_address); + + let seeds: &[&[u8]] = &[b"ayy", b"lmao"]; + let expected_address_seed = [ + 0, 202, 44, 25, 221, 74, 144, 92, 69, 168, 38, 19, 206, 208, 29, 162, 53, 27, 120, 214, + 152, 116, 15, 107, 212, 168, 33, 121, 187, 10, 76, 233, + ]; + let expected_address = [ + 0, 104, 207, 102, 176, 61, 126, 178, 11, 174, 213, 195, 17, 36, 71, 95, 0, 231, 179, + 87, 218, 195, 114, 84, 47, 97, 176, 93, 106, 175, 72, 115, + ]; + + let address_seed = derive_address_seed(seeds, &program_id); + assert_eq!(address_seed, expected_address_seed); + let (address, address_seed) = derive_address( + seeds, + &address_tree_info.address_merkle_tree_pubkey, + &program_id, + ); + assert_eq!(address_seed, expected_address_seed); + assert_eq!(address, expected_address); + } +} diff --git a/sdk-libs/sdk/src/constants.rs b/sdk-libs/sdk-types/src/constants.rs similarity index 50% rename from sdk-libs/sdk/src/constants.rs rename to sdk-libs/sdk-types/src/constants.rs index 329ddee860..455cbdfd22 100644 --- a/sdk-libs/sdk/src/constants.rs +++ b/sdk-libs/sdk-types/src/constants.rs @@ -1,16 +1,20 @@ -use crate::{pubkey, Pubkey}; - -/// Seed of the CPI authority. -pub const CPI_AUTHORITY_PDA_SEED: &[u8] = b"cpi_authority"; +use light_macros::pubkey_array; /// ID of the account-compression program. -pub const PROGRAM_ID_ACCOUNT_COMPRESSION: Pubkey = - pubkey!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); -pub const PROGRAM_ID_NOOP: Pubkey = pubkey!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); +pub const ACCOUNT_COMPRESSION_PROGRAM_ID: [u8; 32] = + pubkey_array!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); /// ID of the light-system program. -pub const PROGRAM_ID_LIGHT_SYSTEM: Pubkey = pubkey!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); +pub const LIGHT_SYSTEM_PROGRAM_ID: [u8; 32] = + pubkey_array!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); +pub const REGISTERED_PROGRAM_PDA: [u8; 32] = + pubkey_array!("35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh"); /// ID of the light-compressed-token program. -pub const PROGRAM_ID_LIGHT_TOKEN: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); +pub const C_TOKEN_PROGRAM_ID: [u8; 32] = + pubkey_array!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +/// Seed of the CPI authority. +pub const CPI_AUTHORITY_PDA_SEED: &[u8] = b"cpi_authority"; +pub const NOOP_PROGRAM_ID: [u8; 32] = pubkey_array!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); pub const STATE_MERKLE_TREE_HEIGHT: usize = 26; pub const STATE_MERKLE_TREE_CHANGELOG: usize = 1400; @@ -25,5 +29,5 @@ pub const ADDRESS_MERKLE_TREE_INDEXED_CHANGELOG: usize = 1400; pub const TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [2, 0, 0, 0, 0, 0, 0, 0]; -pub const ADDRESS_TREE_V1: Pubkey = pubkey!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"); -pub const ADDRESS_QUEUE_V1: Pubkey = pubkey!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"); +pub const ADDRESS_TREE_V1: [u8; 32] = pubkey_array!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"); +pub const ADDRESS_QUEUE_V1: [u8; 32] = pubkey_array!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"); diff --git a/sdk-libs/sdk-types/src/cpi_accounts.rs b/sdk-libs/sdk-types/src/cpi_accounts.rs new file mode 100644 index 0000000000..c50874de69 --- /dev/null +++ b/sdk-libs/sdk-types/src/cpi_accounts.rs @@ -0,0 +1,230 @@ +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +use light_account_checks::AccountInfoTrait; + +use crate::{ + error::{LightSdkTypesError, Result}, + CpiSigner, +}; + +#[derive(Debug, Copy, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct CpiAccountsConfig { + pub cpi_context: bool, + pub sol_compression_recipient: bool, + pub sol_pool_pda: bool, + pub cpi_signer: CpiSigner, +} + +impl CpiAccountsConfig { + pub const fn new(cpi_signer: CpiSigner) -> Self { + Self { + cpi_context: false, + sol_compression_recipient: false, + sol_pool_pda: false, + cpi_signer, + } + } + + pub const fn new_with_cpi_context(cpi_signer: CpiSigner) -> Self { + Self { + cpi_context: true, + sol_compression_recipient: false, + sol_pool_pda: false, + cpi_signer, + } + } + + pub fn cpi_signer(&self) -> [u8; 32] { + self.cpi_signer.cpi_signer + } + + pub fn bump(&self) -> u8 { + self.cpi_signer.bump + } +} + +#[repr(usize)] +pub enum CompressionCpiAccountIndex { + LightSystemProgram, + Authority, + RegisteredProgramPda, + NoopProgram, + AccountCompressionAuthority, + AccountCompressionProgram, + InvokingProgram, + SolPoolPda, + DecompressionRecipient, + SystemProgram, + CpiContext, +} + +pub const SYSTEM_ACCOUNTS_LEN: usize = 11; + +pub struct CpiAccounts<'a, T: AccountInfoTrait> { + fee_payer: &'a T, + accounts: &'a [T], + config: CpiAccountsConfig, +} + +impl<'a, T: AccountInfoTrait> CpiAccounts<'a, T> { + pub fn new(fee_payer: &'a T, accounts: &'a [T], cpi_signer: CpiSigner) -> Self { + Self { + fee_payer, + accounts, + config: CpiAccountsConfig::new(cpi_signer), + } + } + + pub fn new_with_config(fee_payer: &'a T, accounts: &'a [T], config: CpiAccountsConfig) -> Self { + Self { + fee_payer, + accounts, + config, + } + } + + pub fn fee_payer(&self) -> &'a T { + self.fee_payer + } + + pub fn light_system_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::LightSystemProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn authority(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::Authority as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn invoking_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::InvokingProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn registered_program_pda(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::RegisteredProgramPda as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn noop_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::NoopProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn account_compression_authority(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::AccountCompressionAuthority as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn account_compression_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::AccountCompressionProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn sol_pool_pda(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::SolPoolPda as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn decompression_recipient(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::DecompressionRecipient as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn system_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::SystemProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn cpi_context(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::CpiContext as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn self_program_id(&self) -> T::Pubkey { + T::pubkey_from_bytes(self.config.cpi_signer.program_id) + } + + pub fn bump(&self) -> u8 { + self.config.cpi_signer.bump + } + + pub fn config(&self) -> &CpiAccountsConfig { + &self.config + } + + pub fn system_accounts_len(&self) -> usize { + let mut len = SYSTEM_ACCOUNTS_LEN; + if !self.config.sol_pool_pda { + len -= 1; + } + if !self.config.sol_compression_recipient { + len -= 1; + } + if !self.config.cpi_context { + len -= 1; + } + len + } + + pub fn account_infos(&self) -> &'a [T] { + self.accounts + } + + pub fn get_account_info(&self, index: usize) -> Result<&'a T> { + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn tree_accounts(&self) -> Result<&'a [T]> { + let system_len = self.system_accounts_len(); + self.accounts + .get(system_len..) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(system_len)) + } + + pub fn get_tree_account_info(&self, tree_index: usize) -> Result<&'a T> { + let tree_accounts = self.tree_accounts()?; + tree_accounts + .get(tree_index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + self.system_accounts_len() + tree_index, + )) + } + + /// Create a vector of account info references + pub fn to_account_infos(&self) -> Vec<&'a T> { + let mut account_infos = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); + account_infos.push(self.fee_payer()); + self.account_infos()[1..] + .iter() + .for_each(|acc| account_infos.push(acc)); + account_infos + } +} diff --git a/sdk-libs/sdk-types/src/cpi_accounts_small.rs b/sdk-libs/sdk-types/src/cpi_accounts_small.rs new file mode 100644 index 0000000000..517b84a4a6 --- /dev/null +++ b/sdk-libs/sdk-types/src/cpi_accounts_small.rs @@ -0,0 +1,157 @@ +use light_account_checks::AccountInfoTrait; + +use crate::{ + error::{LightSdkTypesError, Result}, + CpiAccountsConfig, CpiSigner, +}; + +#[repr(usize)] +pub enum CompressionCpiAccountIndexSmall { + LightSystemProgram, // Only exposed to outer instruction + AccountCompressionProgram, // Only exposed to outer instruction + SystemProgram, // Only exposed to outer instruction + Authority, // Cpi authority of the custom program, used to invoke the light system program. + RegisteredProgramPda, + AccountCompressionAuthority, + SolPoolPda, // Optional + DecompressionRecipient, // Optional + CpiContext, // Optional +} + +pub const PROGRAM_ACCOUNTS_LEN: usize = 3; +// 6 + 3 program ids, fee payer is extra. +pub const SMALL_SYSTEM_ACCOUNTS_LEN: usize = 9; + +pub struct CpiAccountsSmall<'a, T: AccountInfoTrait> { + fee_payer: &'a T, + accounts: &'a [T], + config: CpiAccountsConfig, +} + +impl<'a, T: AccountInfoTrait> CpiAccountsSmall<'a, T> { + pub fn new(fee_payer: &'a T, accounts: &'a [T], cpi_signer: CpiSigner) -> Self { + Self { + fee_payer, + accounts, + config: CpiAccountsConfig::new(cpi_signer), + } + } + + pub fn new_with_config(fee_payer: &'a T, accounts: &'a [T], config: CpiAccountsConfig) -> Self { + Self { + fee_payer, + accounts, + config, + } + } + + pub fn fee_payer(&self) -> &'a T { + self.fee_payer + } + + pub fn authority(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::Authority as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn registered_program_pda(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::RegisteredProgramPda as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn account_compression_authority(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::AccountCompressionAuthority as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn sol_pool_pda(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::SolPoolPda as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn decompression_recipient(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::DecompressionRecipient as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn cpi_context(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::CpiContext as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn self_program_id(&self) -> T::Pubkey { + T::pubkey_from_bytes(self.config.cpi_signer.program_id) + } + + pub fn config(&self) -> &CpiAccountsConfig { + &self.config + } + + pub fn system_accounts_end_offset(&self) -> usize { + let mut len = SMALL_SYSTEM_ACCOUNTS_LEN; + if !self.config.sol_pool_pda { + len -= 1; + } + if !self.config.sol_compression_recipient { + len -= 1; + } + if !self.config.cpi_context { + len -= 1; + } + len + } + + pub fn account_infos(&self) -> &'a [T] { + self.accounts + } + + pub fn get_account_info(&self, index: usize) -> Result<&'a T> { + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn tree_accounts(&self) -> Result<&'a [T]> { + let system_offset = self.system_accounts_end_offset(); + self.accounts + .get(system_offset..) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + system_offset, + )) + } + + pub fn get_tree_account_info(&self, tree_index: usize) -> Result<&'a T> { + let tree_accounts = self.tree_accounts()?; + tree_accounts + .get(tree_index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + self.system_accounts_end_offset() + tree_index, + )) + } + + /// Create a vector of account info references + pub fn to_account_infos(&self) -> Vec<&'a T> { + let mut account_infos = Vec::with_capacity(1 + self.accounts.len() - PROGRAM_ACCOUNTS_LEN); + account_infos.push(self.fee_payer()); + self.accounts[PROGRAM_ACCOUNTS_LEN..] + .iter() + .for_each(|acc| account_infos.push(acc)); + account_infos + } + + pub fn account_infos_slice(&self) -> &[T] { + &self.accounts[PROGRAM_ACCOUNTS_LEN..] + } +} diff --git a/sdk-libs/sdk-types/src/error.rs b/sdk-libs/sdk-types/src/error.rs new file mode 100644 index 0000000000..6ce017258a --- /dev/null +++ b/sdk-libs/sdk-types/src/error.rs @@ -0,0 +1,51 @@ +use light_hasher::HasherError; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error, PartialEq)] +pub enum LightSdkTypesError { + #[error("Address is none during initialization")] + InitAddressIsNone, + #[error("Address is none during initialization with address")] + InitWithAddressIsNone, + #[error("Output is none during initialization with address")] + InitWithAddressOutputIsNone, + #[error("Address is none during meta mutation")] + MetaMutAddressIsNone, + #[error("Input is none during meta mutation")] + MetaMutInputIsNone, + #[error("Output lamports is none during meta mutation")] + MetaMutOutputLamportsIsNone, + #[error("Output is none during meta mutation")] + MetaMutOutputIsNone, + #[error("Address is none during meta close")] + MetaCloseAddressIsNone, + #[error("Input is none during meta close")] + MetaCloseInputIsNone, + #[error("Fewer accounts than system accounts")] + FewerAccountsThanSystemAccounts, + #[error("CPI accounts index out of bounds: {0}")] + CpiAccountsIndexOutOfBounds(usize), + #[error(transparent)] + Hasher(#[from] HasherError), +} + +impl From for u32 { + fn from(e: LightSdkTypesError) -> Self { + match e { + LightSdkTypesError::InitAddressIsNone => 14021, + LightSdkTypesError::InitWithAddressIsNone => 14022, + LightSdkTypesError::InitWithAddressOutputIsNone => 14023, + LightSdkTypesError::MetaMutAddressIsNone => 14024, + LightSdkTypesError::MetaMutInputIsNone => 14025, + LightSdkTypesError::MetaMutOutputLamportsIsNone => 14026, + LightSdkTypesError::MetaMutOutputIsNone => 14027, + LightSdkTypesError::MetaCloseAddressIsNone => 14028, + LightSdkTypesError::MetaCloseInputIsNone => 14029, + LightSdkTypesError::FewerAccountsThanSystemAccounts => 14017, + LightSdkTypesError::CpiAccountsIndexOutOfBounds(_) => 14031, + LightSdkTypesError::Hasher(e) => e.into(), + } + } +} diff --git a/sdk-libs/sdk/src/account_info.rs b/sdk-libs/sdk-types/src/instruction/account_info.rs similarity index 75% rename from sdk-libs/sdk/src/account_info.rs rename to sdk-libs/sdk-types/src/instruction/account_info.rs index 6687cdffc8..d420efb249 100644 --- a/sdk-libs/sdk/src/account_info.rs +++ b/sdk-libs/sdk-types/src/instruction/account_info.rs @@ -7,10 +7,11 @@ use light_compressed_account::{ data::OutputCompressedAccountWithPackedContext, with_account_info::{CompressedAccountInfo, InAccountInfo}, }, - CompressedAccountError, + Pubkey, }; -use crate::{error::LightSdkError, instruction::account_meta::CompressedAccountMetaTrait, msg}; +use super::account_meta::CompressedAccountMetaTrait; +use crate::error::LightSdkTypesError; pub trait InAccountInfoTrait { fn input_meta( @@ -46,13 +47,13 @@ impl InAccountInfoTrait for InAccountInfo { } } -pub trait AccountInfoTrait { +pub trait CompressedAccountInfoTrait { fn init( &mut self, discriminator: [u8; 8], address: Option<[u8; 32]>, output_state_tree_index: u8, - ) -> Result<(), CompressedAccountError>; + ) -> Result<(), LightSdkTypesError>; fn meta_mut( &mut self, @@ -60,25 +61,25 @@ pub trait AccountInfoTrait { input_data_hash: [u8; 32], discriminator: [u8; 8], output_state_tree_index: u8, - ) -> Result<(), CompressedAccountError>; + ) -> Result<(), LightSdkTypesError>; fn meta_close( &mut self, input_account_meta: &M, input_data_hash: [u8; 32], discriminator: [u8; 8], - ) -> Result<(), CompressedAccountError>; + ) -> Result<(), LightSdkTypesError>; fn input_compressed_account( &self, - owner: crate::Pubkey, - ) -> Result, LightSdkError>; + owner: Pubkey, + ) -> Result, LightSdkTypesError>; fn output_compressed_account( &self, - owner: crate::Pubkey, - ) -> Result, LightSdkError>; + owner: Pubkey, + ) -> Result, LightSdkTypesError>; } -impl AccountInfoTrait for CompressedAccountInfo { +impl CompressedAccountInfoTrait for CompressedAccountInfo { /// Initializes a compressed account info with address. /// 1. The account is zeroed, data has to be added in a separate step. /// 2. Once data is added the data hash has to be added. @@ -87,24 +88,21 @@ impl AccountInfoTrait for CompressedAccountInfo { discriminator: [u8; 8], address: Option<[u8; 32]>, output_state_tree_index: u8, - ) -> Result<(), CompressedAccountError> { + ) -> Result<(), LightSdkTypesError> { if let Some(self_address) = self.address.as_mut() { if let Some(address) = address { self_address.copy_from_slice(&address); } else { - msg!("init: address is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::InitAddressIsNone); } } else { - msg!("init_with_address: address is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::InitWithAddressIsNone); } if let Some(output) = self.output.as_mut() { output.output_merkle_tree_index = output_state_tree_index; output.discriminator = discriminator; } else { - msg!("init_with_address: output is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::InitWithAddressOutputIsNone); } Ok(()) } @@ -115,24 +113,21 @@ impl AccountInfoTrait for CompressedAccountInfo { input_data_hash: [u8; 32], discriminator: [u8; 8], output_state_tree_index: u8, - ) -> Result<(), CompressedAccountError> { + ) -> Result<(), LightSdkTypesError> { if let Some(self_address) = self.address.as_mut() { if let Some(address) = input_account_meta.get_address().as_ref() { *self_address = *address; } else { - msg!("from_z_meta_mut: address is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaMutAddressIsNone); } } else { - msg!("from_z_meta_mut: address is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaMutAddressIsNone); } if let Some(input) = self.input.as_mut() { input.input_meta(input_account_meta, input_data_hash, discriminator); } else { - msg!("from_z_meta_mut: input is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaMutInputIsNone); } if let Some(output) = self.output.as_mut() { @@ -142,12 +137,10 @@ impl AccountInfoTrait for CompressedAccountInfo { if let Some(input_lamports) = input_account_meta.get_lamports() { output.lamports = input_lamports; } else { - msg!("from_z_meta_mut: output lamports is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaMutOutputLamportsIsNone); } } else { - msg!("from_z_meta_mut: output is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaMutOutputIsNone); } Ok(()) } @@ -157,24 +150,21 @@ impl AccountInfoTrait for CompressedAccountInfo { input_account_meta: &M, input_data_hash: [u8; 32], discriminator: [u8; 8], - ) -> Result<(), CompressedAccountError> { + ) -> Result<(), LightSdkTypesError> { if let Some(self_address) = self.address.as_mut() { if let Some(address) = input_account_meta.get_address() { self_address.copy_from_slice(&address); } else { - msg!("from_z_meta_mut: address is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaCloseAddressIsNone); } } else { - msg!("from_z_meta_mut: address is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaCloseAddressIsNone); } if let Some(input) = self.input.as_mut() { input.input_meta(input_account_meta, input_data_hash, discriminator); } else { - msg!("from_z_meta_mut: input is none"); - return Err(CompressedAccountError::InvalidAccountSize); + return Err(LightSdkTypesError::MetaCloseInputIsNone); } Ok(()) @@ -182,8 +172,8 @@ impl AccountInfoTrait for CompressedAccountInfo { fn input_compressed_account( &self, - owner: crate::Pubkey, - ) -> Result, LightSdkError> { + owner: Pubkey, + ) -> Result, LightSdkTypesError> { match self.input.as_ref() { Some(input) => { let data = Some(CompressedAccountData { @@ -209,8 +199,8 @@ impl AccountInfoTrait for CompressedAccountInfo { fn output_compressed_account( &self, - owner: crate::Pubkey, - ) -> Result, LightSdkError> { + owner: Pubkey, + ) -> Result, LightSdkTypesError> { match self.output.as_ref() { Some(output) => { let data = Some(CompressedAccountData { diff --git a/sdk-libs/sdk/src/instruction/account_meta.rs b/sdk-libs/sdk-types/src/instruction/account_meta.rs similarity index 98% rename from sdk-libs/sdk/src/instruction/account_meta.rs rename to sdk-libs/sdk-types/src/instruction/account_meta.rs index a0929f5fcb..49c4ff2336 100644 --- a/sdk-libs/sdk/src/instruction/account_meta.rs +++ b/sdk-libs/sdk-types/src/instruction/account_meta.rs @@ -1,4 +1,5 @@ -use crate::{instruction::tree_info::PackedStateTreeInfo, AnchorDeserialize, AnchorSerialize}; +use super::tree_info::PackedStateTreeInfo; +use crate::{AnchorDeserialize, AnchorSerialize}; /// CompressedAccountMeta (context, address, root_index, output_state_tree_index) /// CompressedAccountMetaNoLamportsNoAddress (context, root_index, output_state_tree_index) diff --git a/sdk-libs/sdk-types/src/instruction/mod.rs b/sdk-libs/sdk-types/src/instruction/mod.rs new file mode 100644 index 0000000000..b2d2019e94 --- /dev/null +++ b/sdk-libs/sdk-types/src/instruction/mod.rs @@ -0,0 +1,4 @@ +pub mod account_info; +pub mod account_meta; +mod tree_info; +pub use tree_info::*; diff --git a/sdk-libs/sdk-types/src/instruction/tree_info.rs b/sdk-libs/sdk-types/src/instruction/tree_info.rs new file mode 100644 index 0000000000..8cdcc7fed0 --- /dev/null +++ b/sdk-libs/sdk-types/src/instruction/tree_info.rs @@ -0,0 +1,40 @@ +use light_account_checks::AccountInfoTrait; +use light_compressed_account::instruction_data::data::NewAddressParamsPacked; + +use crate::{AnchorDeserialize, AnchorSerialize, CpiAccounts}; + +#[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] +pub struct PackedStateTreeInfo { + pub root_index: u16, + pub prove_by_index: bool, + pub merkle_tree_pubkey_index: u8, + pub queue_pubkey_index: u8, + pub leaf_index: u32, +} + +#[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] +pub struct PackedAddressTreeInfo { + pub address_merkle_tree_pubkey_index: u8, + pub address_queue_pubkey_index: u8, + pub root_index: u16, +} + +impl PackedAddressTreeInfo { + pub fn into_new_address_params_packed(self, seed: [u8; 32]) -> NewAddressParamsPacked { + NewAddressParamsPacked { + address_merkle_tree_account_index: self.address_merkle_tree_pubkey_index, + address_queue_account_index: self.address_queue_pubkey_index, + address_merkle_tree_root_index: self.root_index, + seed, + } + } + + pub fn get_tree_pubkey( + &self, + cpi_accounts: &CpiAccounts<'_, T>, + ) -> Result { + let account = + cpi_accounts.get_tree_account_info(self.address_merkle_tree_pubkey_index as usize)?; + Ok(account.pubkey()) + } +} diff --git a/sdk-libs/sdk-types/src/lib.rs b/sdk-libs/sdk-types/src/lib.rs new file mode 100644 index 0000000000..015c8a8e6c --- /dev/null +++ b/sdk-libs/sdk-types/src/lib.rs @@ -0,0 +1,28 @@ +pub mod address; +pub mod constants; +pub mod cpi_accounts; +#[cfg(feature = "small_ix")] +pub mod cpi_accounts_small; +pub mod error; +pub mod instruction; + +// Re-exports +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use constants::*; +pub use cpi_accounts::*; +#[cfg(feature = "small_ix")] +pub use cpi_accounts_small::{ + CompressionCpiAccountIndexSmall, CpiAccountsSmall, PROGRAM_ACCOUNTS_LEN, + SMALL_SYSTEM_ACCOUNTS_LEN, +}; + +/// Configuration struct containing program ID, CPI signer, and bump for Light Protocol +#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] +pub struct CpiSigner { + pub program_id: [u8; 32], + pub cpi_signer: [u8; 32], + pub bump: u8, +} diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index d40492c813..9adbf95435 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -11,31 +11,23 @@ crate-type = ["cdylib", "lib"] name = "light_sdk" [features] -solana = [ - "solana-cpi", - "solana-instruction", - "solana-pubkey", - "solana-pubkey/borsh", - "solana-pubkey/curve25519", - "solana-pubkey/sha2", - "solana-account-info", - "solana-msg", - "solana-program-error", - "borsh", - "light-compressed-account/solana", -] -default = ["solana"] +default = ["borsh"] idl-build = ["anchor-lang/idl-build"] -anchor = ["anchor-lang", "light-compressed-account/anchor"] -v2 = [] +anchor = [ + "anchor-lang", + "light-compressed-account/anchor", + "light-sdk-types/anchor", +] +v2 = ["light-sdk-types/v2"] +small_ix = ["light-sdk-types/small_ix"] [dependencies] -solana-pubkey = { workspace = true, optional = true } -solana-account-info = { workspace = true, optional = true } -solana-msg = { workspace = true, optional = true } -solana-cpi = { workspace = true, optional = true } -solana-program-error = { workspace = true, optional = true } -solana-instruction = { workspace = true, optional = true } +solana-pubkey = { workspace = true, features = ["borsh", "sha2", "curve25519"] } +solana-account-info = { workspace = true } +solana-msg = { workspace = true } +solana-cpi = { workspace = true } +solana-program-error = { workspace = true } +solana-instruction = { workspace = true } anchor-lang = { workspace = true, optional = true } num-bigint = { workspace = true } @@ -45,11 +37,13 @@ borsh = { workspace = true, optional = true } thiserror = { workspace = true } light-sdk-macros = { workspace = true } +light-sdk-types = { workspace = true } light-macros = { workspace = true } light-compressed-account = { workspace = true } light-hasher = { workspace = true } -light-account-checks = { workspace = true } +light-account-checks = { workspace = true, features = ["solana"] } +light-zero-copy = { workspace = true } [dev-dependencies] num-bigint = { workspace = true } -light-compressed-account = { workspace = true , features = ["new-unique"]} +light-compressed-account = { workspace = true, features = ["new-unique"] } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 29da44c72c..8206696040 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -1,15 +1,83 @@ +//! # Light Account +//! +//! LightAccount is a wrapper around a compressed account similar to anchor Account. +//! LightAccount abstracts hashing of compressed account data, +//! and wraps the compressed account data so that it is easy to use. +//! +//! Data structs used with LightAccount must implement the traits: +//! - DataHasher +//! - LightDiscriminator +//! - BorshSerialize, BorshDeserialize +//! - Debug, Default, Clone +//! +//! ### Account Data Hashing +//! The LightHasher derives a hashing scheme from the compressed account layout. +//! Alternatively, DataHasher can be implemented manually. +//! +//! Constraints: +//! - Poseidon hashes can only take up to 12 inputs +//! -> use nested structs for structs with more than 12 fields. +//! - Poseidon hashes inputs must be less than bn254 field size (254 bits). +//! hash_to_field_size methods in light hasher can be used to hash data longer than 253 bits. +//! -> use the `#[hash]` attribute for fields with data types greater than 31 bytes eg Pubkeys. +//! +//! ### Compressed account with LightHasher and LightDiscriminator +//! ``` +//! use light_sdk::{LightHasher, LightDiscriminator}; +//! use solana_pubkey::Pubkey; +//! #[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] +//! pub struct CounterAccount { +//! #[hash] +//! pub owner: Pubkey, +//! pub counter: u64, +//! } +//! ``` +//! +//! +//! ### Create compressed account +//! ```ignore +//! let mut my_compressed_account = LightAccount::<'_, CounterAccount>::new_init( +//! &crate::ID, +//! // Address +//! Some(address), +//! output_tree_index, +//! ); +//! // Set data: +//! my_compressed_account.owner = ctx.accounts.signer.key(); +//! ``` +//! ### Update compressed account +//! ```ignore +//! let mut my_compressed_account = LightAccount::<'_, CounterAccount>::new_mut( +//! &crate::ID, +//! &account_meta, +//! my_compressed_account, +//! ); +//! // Increment counter. +//! my_compressed_account.counter += 1; +//! ``` +//! ### Close compressed account +//! ```ignore +//! let mut my_compressed_account = LightAccount::<'_, CounterAccount>::new_close( +//! &crate::ID, +//! &account_meta_close, +//! my_compressed_account, +//! ); +//! ``` +// TODO: add example for manual hashing + use std::ops::{Deref, DerefMut}; use light_compressed_account::{ compressed_account::PackedMerkleContext, instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo}, }; -use light_hasher::{DataHasher, Poseidon}; +use light_sdk_types::instruction::account_meta::CompressedAccountMetaTrait; use solana_pubkey::Pubkey; use crate::{ - error::LightSdkError, instruction::account_meta::CompressedAccountMetaTrait, AnchorDeserialize, - AnchorSerialize, LightDiscriminator, + error::LightSdkError, + light_hasher::{DataHasher, Poseidon}, + AnchorDeserialize, AnchorSerialize, LightDiscriminator, }; #[derive(Debug, PartialEq)] diff --git a/sdk-libs/sdk/src/address.rs b/sdk-libs/sdk/src/address.rs index 3adb5043f7..fbc9cd8530 100644 --- a/sdk-libs/sdk/src/address.rs +++ b/sdk-libs/sdk/src/address.rs @@ -1,63 +1,50 @@ -use light_compressed_account::instruction_data::data::{ - NewAddressParams, NewAddressParamsPacked as PackedNewAddressParams, +//! ## Addresses +//! Address seed is 32 bytes. Multiple seeds are hashed +//! into a single 32 bytes seed that is passed into the light system program for address creation. +//! Addresses are created independently from compressed accounts. +//! This means that an address can be used in a compressed account but does not have to be used. +//! +//! ### Address uniqueness +//! Every address can only be created once per address tree. +//! Addresses over all address trees are unique but +//! address seeds can be reused in different address trees. +//! If your program security requires global address uniqueness over all address trees, +//! the used address Merkle tree must be checked. +//! If your program just requires addresses to identify accounts but not uniqueness over all address trees +//! the used address Merkle tree does not need to be checked. +//! +//! +//! ### Create address example +//! ```ignore +//! let packed_address_tree_info = instruction_data.address_tree_info; +//! let tree_accounts = cpi_accounts.tree_accounts(); +//! +//! let address_tree_pubkey = tree_accounts[address_tree_info +//! .address_merkle_tree_pubkey_index as usize] +//! .key(); +//! +//! let (address, address_seed) = derive_address( +//! &[b"counter"], +//! &address_tree_pubkey, +//! &crate::ID, +//! ); +//! +//! // Used in cpi to light-system program +//! // to insert the new address into the address merkle tree. +//! let new_address_params = packed_address_tree_info +//! .into_new_address_params_packed(address_seed); +//! ``` + +pub use light_compressed_account::instruction_data::data::NewAddressParams; +/// Struct passed into the light system program cpi to create a new address. +pub use light_compressed_account::instruction_data::data::NewAddressParamsPacked as PackedNewAddressParams; +#[cfg(feature = "v2")] +pub use light_compressed_account::instruction_data::data::{ + NewAddressParamsAssigned, NewAddressParamsAssignedPacked, PackedReadOnlyAddress, + ReadOnlyAddress, }; -use crate::{ - instruction::{pack_accounts::PackedAccounts, tree_info::AddressTreeInfo}, - AccountInfo, -}; - -pub struct AddressWithMerkleContext { - pub address: [u8; 32], - pub address_tree_info: AddressTreeInfo, -} - -pub fn pack_new_addresses_params( - addresses_params: &[NewAddressParams], - remaining_accounts: &mut PackedAccounts, -) -> Vec { - addresses_params - .iter() - .map(|x| { - let address_queue_account_index = - remaining_accounts.insert_or_get(x.address_queue_pubkey.to_bytes().into()); - let address_merkle_tree_account_index = - remaining_accounts.insert_or_get(x.address_merkle_tree_pubkey.to_bytes().into()); - PackedNewAddressParams { - seed: x.seed, - address_queue_account_index, - address_merkle_tree_account_index, - address_merkle_tree_root_index: x.address_merkle_tree_root_index, - } - }) - .collect::>() -} - -pub fn pack_new_address_params( - address_params: NewAddressParams, - remaining_accounts: &mut PackedAccounts, -) -> PackedNewAddressParams { - pack_new_addresses_params(&[address_params], remaining_accounts)[0] -} - -pub fn unpack_new_address_params( - address_params: &PackedNewAddressParams, - remaining_accounts: &[AccountInfo], -) -> NewAddressParams { - let address_merkle_tree_pubkey = - remaining_accounts[address_params.address_merkle_tree_account_index as usize].key; - let address_queue_pubkey = - remaining_accounts[address_params.address_queue_account_index as usize].key; - NewAddressParams { - seed: address_params.seed, - address_queue_pubkey: address_queue_pubkey.to_bytes().into(), - address_merkle_tree_pubkey: address_merkle_tree_pubkey.to_bytes().into(), - address_merkle_tree_root_index: address_params.address_merkle_tree_root_index, - } -} - pub mod v1 { - use light_hasher::{hash_to_field_size::hashv_to_bn254_field_size_be, Hasher, Keccak}; use crate::Pubkey; @@ -75,33 +62,7 @@ pub mod v1 { /// ); /// ``` pub fn derive_address_seed(seeds: &[&[u8]], program_id: &Pubkey) -> [u8; 32] { - let mut inputs = Vec::with_capacity(seeds.len() + 1); - - let program_id = program_id.to_bytes(); - inputs.push(program_id.as_slice()); - - inputs.extend(seeds); - - let seed = hashv_to_bn254_field_size_be_legacy(inputs.as_slice()); - seed - } - - fn hashv_to_bn254_field_size_be_legacy(bytes: &[&[u8]]) -> [u8; 32] { - let mut hashed_value: [u8; 32] = Keccak::hashv(bytes).unwrap(); - // Truncates to 31 bytes so that value is less than bn254 Fr modulo - // field size. - hashed_value[0] = 0; - hashed_value - } - - /// Derives an address for a compressed account, based on the provided singular - /// `seed` and `merkle_tree_pubkey`: - pub(crate) fn derive_address_from_seed( - address_seed: &[u8; 32], - merkle_tree_pubkey: &Pubkey, - ) -> [u8; 32] { - let input = [merkle_tree_pubkey.to_bytes(), *address_seed].concat(); - hashv_to_bn254_field_size_be(&[input.as_slice()]) + light_sdk_types::address::v1::derive_address_seed(seeds, &program_id.to_bytes()) } /// Derives an address from provided seeds. Returns that address and a singular @@ -127,10 +88,11 @@ pub mod v1 { merkle_tree_pubkey: &Pubkey, program_id: &Pubkey, ) -> ([u8; 32], [u8; 32]) { - let address_seed = derive_address_seed(seeds, program_id); - let address = derive_address_from_seed(&address_seed, merkle_tree_pubkey); - - (address, address_seed) + light_sdk_types::address::v1::derive_address( + seeds, + &merkle_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ) } } @@ -138,7 +100,8 @@ pub mod v1 { mod test { use solana_pubkey::pubkey; - use super::{v1::*, *}; + use super::v1::*; + use crate::instruction::AddressTreeInfo; #[test] fn test_derive_address_seed() { @@ -166,8 +129,8 @@ mod test { #[test] fn test_derive_address() { let address_tree_info = AddressTreeInfo { - address_merkle_tree_pubkey: pubkey!("11111111111111111111111111111111"), - address_queue_pubkey: pubkey!("22222222222222222222222222222222222222222222"), + tree: pubkey!("11111111111111111111111111111111"), + queue: pubkey!("22222222222222222222222222222222222222222222"), }; let program_id = pubkey!("7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz"); @@ -180,14 +143,7 @@ mod test { let address_seed = derive_address_seed(seeds, &program_id); assert_eq!(address_seed, expected_address_seed); - let address = - derive_address_from_seed(&address_seed, &address_tree_info.address_merkle_tree_pubkey); - assert_eq!(address, expected_address.to_bytes()); - let (address, address_seed) = derive_address( - seeds, - &address_tree_info.address_merkle_tree_pubkey, - &program_id, - ); + let (address, address_seed) = derive_address(seeds, &address_tree_info.tree, &program_id); assert_eq!(address_seed, expected_address_seed); assert_eq!(address, expected_address.to_bytes()); @@ -200,14 +156,7 @@ mod test { let address_seed = derive_address_seed(seeds, &program_id); assert_eq!(address_seed, expected_address_seed); - let address = - derive_address_from_seed(&address_seed, &address_tree_info.address_merkle_tree_pubkey); - assert_eq!(address, expected_address.to_bytes()); - let (address, address_seed) = derive_address( - seeds, - &address_tree_info.address_merkle_tree_pubkey, - &program_id, - ); + let (address, address_seed) = derive_address(seeds, &address_tree_info.tree, &program_id); assert_eq!(address_seed, expected_address_seed); assert_eq!(address, expected_address.to_bytes()); } diff --git a/sdk-libs/sdk/src/cpi/accounts.rs b/sdk-libs/sdk/src/cpi/accounts.rs index d6046c7621..d1bddcb78f 100644 --- a/sdk-libs/sdk/src/cpi/accounts.rs +++ b/sdk-libs/sdk/src/cpi/accounts.rs @@ -1,263 +1,109 @@ +pub use light_sdk_types::CpiAccountsConfig; +use light_sdk_types::{CpiAccounts as GenericCpiAccounts, SYSTEM_ACCOUNTS_LEN}; + use crate::{ error::{LightSdkError, Result}, - AccountInfo, AccountMeta, AnchorDeserialize, AnchorSerialize, Pubkey, + AccountInfo, AccountMeta, Pubkey, }; -#[derive(Debug, Default, Copy, Clone, AnchorSerialize, AnchorDeserialize)] -pub struct CpiAccountsConfig { - pub self_program: Pubkey, - pub cpi_context: bool, - pub sol_compression_recipient: bool, - pub sol_pool_pda: bool, -} - -impl CpiAccountsConfig { - pub fn new(self_program: Pubkey) -> Self { - Self { - self_program, - cpi_context: false, - sol_compression_recipient: false, - sol_pool_pda: false, - } - } - - pub fn new_with_cpi_context(self_program: Pubkey) -> Self { - Self { - self_program, - cpi_context: true, - sol_compression_recipient: false, - sol_pool_pda: false, - } - } -} - -#[repr(usize)] -pub enum CompressionCpiAccountIndex { - LightSystemProgram, - Authority, - RegisteredProgramPda, - NoopProgram, - AccountCompressionAuthority, - AccountCompressionProgram, - InvokingProgram, - SolPoolPda, - DecompressionRecipent, - SystemProgram, - CpiContext, -} - -pub const SYSTEM_ACCOUNTS_LEN: usize = 11; - -// TODO: add unit tests -pub struct CpiAccounts<'c, 'info> { - fee_payer: &'c AccountInfo<'info>, - accounts: &'c [AccountInfo<'info>], - config: CpiAccountsConfig, -} - -impl<'c, 'info> CpiAccounts<'c, 'info> { - pub fn new( - fee_payer: &'c AccountInfo<'info>, - accounts: &'c [AccountInfo<'info>], - program_id: Pubkey, - ) -> Result { - let new = Self { - fee_payer, - accounts, - config: CpiAccountsConfig { - self_program: program_id, - ..Default::default() - }, - }; - if accounts.len() < new.system_accounts_len() { - crate::msg!("accounts len {}", accounts.len()); - return Err(LightSdkError::FewerAccountsThanSystemAccounts); - } - Ok(new) - } - - pub fn new_with_config( - fee_payer: &'c AccountInfo<'info>, - accounts: &'c [AccountInfo<'info>], - config: CpiAccountsConfig, - ) -> Result { - let new = Self { - fee_payer, - accounts, - config, - }; - if accounts.len() < new.system_accounts_len() { - crate::msg!("accounts len {}", accounts.len()); - return Err(LightSdkError::FewerAccountsThanSystemAccounts); - } - Ok(new) - } - - pub fn fee_payer(&self) -> &'c AccountInfo<'info> { - self.fee_payer - } - - pub fn light_system_program(&self) -> &'c AccountInfo<'info> { - // PANICS: We are sure about the bounds of the slice. - self.accounts - .get(CompressionCpiAccountIndex::LightSystemProgram as usize) - .unwrap() - } - - pub fn authority(&self) -> &'c AccountInfo<'info> { - // PANICS: We are sure about the bounds of the slice. - self.accounts - .get(CompressionCpiAccountIndex::Authority as usize) - .unwrap() - } - - pub fn invoking_program(&self) -> &'c AccountInfo<'info> { - // PANICS: We are sure about the bounds of the slice. - self.accounts - .get(CompressionCpiAccountIndex::InvokingProgram as usize) - .unwrap() - } - - pub fn self_program_id(&self) -> &Pubkey { - &self.config.self_program - } - - pub fn to_account_infos(&self) -> Vec> { - let mut account_infos = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); - account_infos.push(self.fee_payer.clone()); - self.accounts[1..] - .iter() - .for_each(|acc| account_infos.push(acc.clone())); - account_infos - } - - pub fn to_account_metas(&self) -> Vec { - let mut account_metas = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); +pub type CpiAccounts<'c, 'info> = GenericCpiAccounts<'c, AccountInfo<'info>>; + +pub fn to_account_metas(cpi_accounts: CpiAccounts<'_, '_>) -> Result> { + let mut account_metas = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.fee_payer().key, + is_signer: true, + is_writable: true, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.authority()?.key, + is_signer: true, + is_writable: false, + }); + + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.registered_program_pda()?.key, + is_signer: false, + is_writable: false, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.noop_program()?.key, + is_signer: false, + is_writable: false, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.account_compression_authority()?.key, + is_signer: false, + is_writable: false, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.account_compression_program()?.key, + is_signer: false, + is_writable: false, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.invoking_program()?.key, + is_signer: false, + is_writable: false, + }); + let mut current_index = 7; + let anchor_none_account_meta = AccountMeta { + pubkey: *cpi_accounts.light_system_program()?.key, + is_signer: false, + is_writable: false, + }; + if !cpi_accounts.config().sol_pool_pda { + account_metas.push(anchor_none_account_meta.clone()); + } else { + let account = cpi_accounts.get_account_info(current_index)?; account_metas.push(AccountMeta { - pubkey: *self.fee_payer.key, - is_signer: true, + pubkey: *account.key, + is_signer: false, is_writable: true, }); - account_metas.push(AccountMeta { - pubkey: *self.authority().key, - is_signer: true, - is_writable: false, - }); + current_index += 1; + } + if !cpi_accounts.config().sol_compression_recipient { + account_metas.push(anchor_none_account_meta.clone()); + } else { + let account = cpi_accounts.get_account_info(current_index)?; account_metas.push(AccountMeta { - pubkey: *self.accounts[CompressionCpiAccountIndex::RegisteredProgramPda as usize].key, - is_signer: false, - is_writable: false, - }); - account_metas.push(AccountMeta { - pubkey: *self.accounts[CompressionCpiAccountIndex::NoopProgram as usize].key, - is_signer: false, - is_writable: false, - }); - account_metas.push(AccountMeta { - pubkey: *self.accounts - [CompressionCpiAccountIndex::AccountCompressionAuthority as usize] - .key, - is_signer: false, - is_writable: false, - }); - account_metas.push(AccountMeta { - pubkey: *self.accounts[CompressionCpiAccountIndex::AccountCompressionProgram as usize] - .key, + pubkey: *account.key, is_signer: false, - is_writable: false, + is_writable: true, }); + current_index += 1; + } + // System program + account_metas.push(AccountMeta { + pubkey: Pubkey::default(), + is_signer: false, + is_writable: false, + }); + current_index += 1; + + if !cpi_accounts.config().cpi_context { + account_metas.push(anchor_none_account_meta); + } else { + let account = cpi_accounts.get_account_info(current_index)?; account_metas.push(AccountMeta { - pubkey: *self.accounts[CompressionCpiAccountIndex::InvokingProgram as usize].key, + pubkey: *account.key, is_signer: false, - is_writable: false, + is_writable: true, }); - let mut current_index = 7; - if !self.config.sol_pool_pda { - account_metas.push(AccountMeta { - pubkey: *self.light_system_program().key, - is_signer: false, - is_writable: false, - }); - } else { - account_metas.push(AccountMeta { - pubkey: *self.accounts[current_index].key, - is_signer: false, - is_writable: true, - }); - current_index += 1; - } - - if !self.config.sol_compression_recipient { - account_metas.push(AccountMeta { - pubkey: *self.light_system_program().key, - is_signer: false, - is_writable: false, - }); - } else { - account_metas.push(AccountMeta { - pubkey: *self.accounts[current_index].key, - is_signer: false, - is_writable: true, - }); - current_index += 1; - } - // System program + current_index += 1; + } + let tree_accounts = cpi_accounts + .account_infos() + .get(current_index..) + .ok_or(LightSdkError::CpiAccountsIndexOutOfBounds(current_index))?; + tree_accounts.iter().for_each(|acc| { account_metas.push(AccountMeta { - pubkey: Pubkey::default(), + pubkey: *acc.key, is_signer: false, - is_writable: false, - }); - current_index += 1; - - if !self.config.cpi_context { - account_metas.push(AccountMeta { - pubkey: *self.light_system_program().key, - is_signer: false, - is_writable: false, - }); - } else { - account_metas.push(AccountMeta { - pubkey: *self.accounts[current_index].key, - is_signer: false, - is_writable: true, - }); - current_index += 1; - } - //self.system_accounts_len() - self.accounts[current_index..].iter().for_each(|acc| { - account_metas.push(AccountMeta { - pubkey: *acc.key, - is_signer: false, - is_writable: true, - }); + is_writable: acc.is_writable, }); - account_metas - } - - pub fn config(&self) -> &CpiAccountsConfig { - &self.config - } - - pub fn system_accounts_len(&self) -> usize { - let mut len = SYSTEM_ACCOUNTS_LEN; - if !self.config.sol_pool_pda { - len -= 1; - } - if !self.config.sol_compression_recipient { - len -= 1; - } - if !self.config.cpi_context { - len -= 1; - } - len - } - - pub fn account_infos(&self) -> &'c [AccountInfo<'info>] { - self.accounts - } - - pub fn tree_accounts(&self) -> &'c [AccountInfo<'info>] { - &self.accounts[self.system_accounts_len()..] - } + }); + Ok(account_metas) } diff --git a/sdk-libs/sdk/src/cpi/accounts_small_ix.rs b/sdk-libs/sdk/src/cpi/accounts_small_ix.rs index edc5d916c5..c4e4f7c144 100644 --- a/sdk-libs/sdk/src/cpi/accounts_small_ix.rs +++ b/sdk-libs/sdk/src/cpi/accounts_small_ix.rs @@ -1,185 +1,85 @@ -#![cfg(feature = "v2")] -use crate::{cpi::CpiAccountsConfig, error::Result, msg, AccountInfo, AccountMeta, Pubkey}; - -#[repr(usize)] -pub enum CompressionCpiAccountIndexSmall { - LightSystemProgram, // Only exposed to outer instruction - AccountCompressionProgram, // Only exposed to outer instruction - SystemProgram, // Only exposed to outer instruction - Authority, // Cpi authority of the custom program, used to invoke the light system program. - RegisteredProgramPda, - AccountCompressionAuthority, - SolPoolPda, // Optional - DecompressionRecipent, // Optional - CpiContext, // Optional -} - -pub const PROGRAM_ACCOUNTS_LEN: usize = 3; -// 6 + 3 program ids, fee payer is extra. -pub const SMALL_SYSTEM_ACCOUNTS_LEN: usize = 9; - -// TODO: add unit tests -pub struct CpiAccountsSmall<'c, 'info> { - fee_payer: &'c AccountInfo<'info>, - accounts: &'c [AccountInfo<'info>], - config: CpiAccountsConfig, -} - -impl<'c, 'info> CpiAccountsSmall<'c, 'info> { - // TODO: consider to pass num of trees to split remaining accounts - pub fn new( - fee_payer: &'c AccountInfo<'info>, - accounts: &'c [AccountInfo<'info>], - program_id: Pubkey, - ) -> Result { - // if accounts.len() < SYSTEM_ACCOUNTS_LEN { - // msg!("accounts len {}", accounts.len()); - // return Err(LightSdkError::FewerAccountsThanSystemAccounts); - // } - Ok(Self { - fee_payer, - accounts, - config: CpiAccountsConfig { - self_program: program_id, - ..Default::default() - }, - }) - } - - pub fn new_with_config( - fee_payer: &'c AccountInfo<'info>, - accounts: &'c [AccountInfo<'info>], - config: CpiAccountsConfig, - ) -> Result { - msg!("config {:?}", config); - // if accounts.len() < SYSTEM_ACCOUNTS_LEN { - // msg!("accounts len {}", accounts.len()); - // return Err(LightSdkError::FewerAccountsThanSystemAccounts); - // } - Ok(Self { - fee_payer, - accounts, - config, - }) - } - - pub fn fee_payer(&self) -> &'c AccountInfo<'info> { - self.fee_payer - } - - pub fn authority(&self) -> &'c AccountInfo<'info> { - self.accounts - .get(CompressionCpiAccountIndexSmall::Authority as usize) - .unwrap() - } - - pub fn self_program_id(&self) -> &Pubkey { - &self.config.self_program - } - - /// Account infos for cpi to light system program. - pub fn to_account_infos(&self) -> Vec> { - // TODO: do a version with a const array instead of vector. - let mut account_infos = Vec::with_capacity(1 + self.accounts.len() - PROGRAM_ACCOUNTS_LEN); - account_infos.push(self.fee_payer.clone()); - self.accounts[PROGRAM_ACCOUNTS_LEN..] - .iter() - .for_each(|acc| account_infos.push(acc.clone())); - account_infos - } - - /// Account metas for cpi to light system program. - pub fn to_account_metas(&self) -> Vec { - // TODO: do a version with a const array instead of vector. - let mut account_metas = Vec::with_capacity(1 + self.accounts.len() - PROGRAM_ACCOUNTS_LEN); - +use light_sdk_types::{ + CompressionCpiAccountIndexSmall, CpiAccountsSmall as GenericCpiAccountsSmall, + PROGRAM_ACCOUNTS_LEN, +}; + +use crate::{error::Result, AccountInfo, AccountMeta}; + +pub type CpiAccountsSmall<'c, 'info> = GenericCpiAccountsSmall<'c, AccountInfo<'info>>; + +pub fn to_account_metas_small(cpi_accounts: CpiAccountsSmall<'_, '_>) -> Result> { + // TODO: do a version with a const array instead of vector. + let mut account_metas = + Vec::with_capacity(1 + cpi_accounts.account_infos().len() - PROGRAM_ACCOUNTS_LEN); + + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.fee_payer().key, + is_signer: true, + is_writable: true, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.authority()?.key, + is_signer: true, + is_writable: false, + }); + + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.registered_program_pda()?.key, + is_signer: false, + is_writable: false, + }); + account_metas.push(AccountMeta { + pubkey: *cpi_accounts.account_compression_authority()?.key, + is_signer: false, + is_writable: false, + }); + + let accounts = cpi_accounts.account_infos(); + let mut index = CompressionCpiAccountIndexSmall::SolPoolPda as usize; + + if cpi_accounts.config().sol_pool_pda { + let account = cpi_accounts.get_account_info(index)?; account_metas.push(AccountMeta { - pubkey: *self.fee_payer.key, - is_signer: true, + pubkey: *account.key, + is_signer: false, is_writable: true, }); + index += 1; + } + + if cpi_accounts.config().sol_compression_recipient { + let account = cpi_accounts.get_account_info(index)?; account_metas.push(AccountMeta { - pubkey: *self.authority().key, - is_signer: true, - is_writable: false, + pubkey: *account.key, + is_signer: false, + is_writable: true, }); + index += 1; + } + if cpi_accounts.config().cpi_context { + let account = cpi_accounts.get_account_info(index)?; account_metas.push(AccountMeta { - pubkey: *self.accounts[CompressionCpiAccountIndexSmall::RegisteredProgramPda as usize] - .key, + pubkey: *account.key, is_signer: false, - is_writable: false, + is_writable: true, }); + index += 1; + } + assert_eq!(cpi_accounts.system_accounts_end_offset(), index); + + let tree_accounts = + accounts + .get(index..) + .ok_or(crate::error::LightSdkError::CpiAccountsIndexOutOfBounds( + index, + ))?; + tree_accounts.iter().for_each(|acc| { account_metas.push(AccountMeta { - pubkey: *self.accounts - [CompressionCpiAccountIndexSmall::AccountCompressionAuthority as usize] - .key, + pubkey: *acc.key, is_signer: false, - is_writable: false, - }); - - let mut index = CompressionCpiAccountIndexSmall::SolPoolPda as usize; - if self.config.sol_pool_pda { - account_metas.push(AccountMeta { - pubkey: *self.accounts[index].key, - is_signer: false, - is_writable: true, - }); - index += 1; - } - - if self.config.sol_compression_recipient { - account_metas.push(AccountMeta { - pubkey: *self.accounts[index].key, - is_signer: false, - is_writable: true, - }); - index += 1; - } - - if self.config.cpi_context { - account_metas.push(AccountMeta { - pubkey: *self.accounts[index].key, - is_signer: false, - is_writable: true, - }); - index += 1; - } - assert_eq!(self.system_accounts_end_offset(), index); - - self.accounts[index..].iter().for_each(|acc| { - account_metas.push(AccountMeta { - pubkey: *acc.key, - is_signer: false, - is_writable: true, - }); + is_writable: true, }); - account_metas - } - - pub fn config(&self) -> &CpiAccountsConfig { - &self.config - } - - pub fn system_accounts_end_offset(&self) -> usize { - let mut len = SMALL_SYSTEM_ACCOUNTS_LEN; - if !self.config.sol_pool_pda { - len -= 1; - } - if !self.config.sol_compression_recipient { - len -= 1; - } - if !self.config.cpi_context { - len -= 1; - } - len - } - - pub fn account_infos(&self) -> &'c [AccountInfo<'info>] { - self.accounts - } - - pub fn tree_accounts(&self) -> &'c [AccountInfo<'info>] { - &self.accounts[self.system_accounts_end_offset()..] - } + }); + Ok(account_metas) } diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 8387226caf..2bd9d842d4 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -7,13 +7,13 @@ use light_compressed_account::{ with_account_info::CompressedAccountInfo, }, }; +use light_sdk_types::constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}; use crate::{ - account_info::AccountInfoTrait, - cpi::CpiAccounts, + cpi::{to_account_metas, CpiAccounts}, error::{LightSdkError, Result}, - find_cpi_signer_macro, invoke_signed, AccountInfo, AccountMeta, AnchorSerialize, Instruction, - Pubkey, ValidityProof, CPI_AUTHORITY_PDA_SEED, PROGRAM_ID_LIGHT_SYSTEM, + instruction::{account_info::CompressedAccountInfoTrait, ValidityProof}, + invoke_signed, AccountInfo, AccountMeta, AnchorSerialize, Instruction, }; #[derive(Debug, Default, PartialEq, Clone)] @@ -51,31 +51,33 @@ impl CpiInputs { } pub fn invoke_light_system_program(self, cpi_accounts: CpiAccounts) -> Result<()> { - let instruction = create_light_system_progam_instruction_invoke_cpi(self, &cpi_accounts)?; - - invoke_light_system_program( - cpi_accounts.self_program_id(), - cpi_accounts.to_account_infos().as_slice(), - instruction, - ) + let bump = cpi_accounts.bump(); + let account_info_refs = cpi_accounts.to_account_infos(); + let instruction = create_light_system_progam_instruction_invoke_cpi(self, cpi_accounts)?; + let account_infos: Vec = account_info_refs.into_iter().cloned().collect(); + invoke_light_system_program(account_infos.as_slice(), instruction, bump) } } pub fn create_light_system_progam_instruction_invoke_cpi( cpi_inputs: CpiInputs, - cpi_accounts: &CpiAccounts, + cpi_accounts: CpiAccounts, ) -> Result { - let owner = *cpi_accounts.invoking_program().key; + let owner = *cpi_accounts.invoking_program()?.key; let (input_compressed_accounts_with_merkle_context, output_compressed_accounts) = if let Some(account_infos) = cpi_inputs.account_infos.as_ref() { let mut input_compressed_accounts_with_merkle_context = Vec::with_capacity(account_infos.len()); let mut output_compressed_accounts = Vec::with_capacity(account_infos.len()); for account_info in account_infos.iter() { - if let Some(input_account) = account_info.input_compressed_account(owner)? { + if let Some(input_account) = + account_info.input_compressed_account(owner.to_bytes().into())? + { input_compressed_accounts_with_merkle_context.push(input_account); } - if let Some(output_account) = account_info.output_compressed_account(owner)? { + if let Some(output_account) = + account_info.output_compressed_account(owner.to_bytes().into())? + { output_compressed_accounts.push(output_account); } } @@ -112,9 +114,9 @@ pub fn create_light_system_progam_instruction_invoke_cpi( data.extend_from_slice(&(inputs.len() as u32).to_le_bytes()); data.extend(inputs); - let account_metas: Vec = cpi_accounts.to_account_metas(); + let account_metas: Vec = to_account_metas(cpi_accounts)?; Ok(Instruction { - program_id: PROGRAM_ID_LIGHT_SYSTEM, + program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), accounts: account_metas, data, }) @@ -123,7 +125,7 @@ pub fn create_light_system_progam_instruction_invoke_cpi( /// Invokes the light system program to verify and apply a zk-compressed state /// transition. Serializes CPI instruction data, configures necessary accounts, /// and executes the CPI. -pub fn verify_borsh(light_system_accounts: &CpiAccounts, inputs: &T) -> Result<()> +pub fn verify_borsh(light_system_accounts: CpiAccounts, inputs: &T) -> Result<()> where T: AnchorSerialize, { @@ -133,28 +135,25 @@ where data.extend_from_slice(&light_compressed_account::discriminators::DISCRIMINATOR_INVOKE_CPI); data.extend_from_slice(&(inputs.len() as u32).to_le_bytes()); data.extend(inputs); - let account_infos: Vec = light_system_accounts.to_account_infos(); + let account_info_refs = light_system_accounts.to_account_infos(); + let account_infos: Vec = account_info_refs.into_iter().cloned().collect(); - let account_metas: Vec = light_system_accounts.to_account_metas(); + let bump = light_system_accounts.bump(); + let account_metas: Vec = to_account_metas(light_system_accounts)?; let instruction = Instruction { - program_id: PROGRAM_ID_LIGHT_SYSTEM, + program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), accounts: account_metas, data, }; - invoke_light_system_program( - light_system_accounts.self_program_id(), - account_infos.as_slice(), - instruction, - ) + invoke_light_system_program(account_infos.as_slice(), instruction, bump) } #[inline(always)] pub fn invoke_light_system_program( - invoking_program_id: &Pubkey, account_infos: &[AccountInfo], instruction: Instruction, + bump: u8, ) -> Result<()> { - let (_authority, bump) = find_cpi_signer_macro!(invoking_program_id); let signer_seeds = [CPI_AUTHORITY_PDA_SEED, &[bump]]; // TODO: restore but not a priority it is a convenience check diff --git a/sdk-libs/sdk/src/cpi/mod.rs b/sdk-libs/sdk/src/cpi/mod.rs index fb5d23bea7..e1329328df 100644 --- a/sdk-libs/sdk/src/cpi/mod.rs +++ b/sdk-libs/sdk/src/cpi/mod.rs @@ -1,8 +1,62 @@ +//! +//! +//! To create, update, or close compressed accounts, +//! programs need to invoke the light system program via cross program invocation (cpi). +//! +//! ```ignore +//! declare_id!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); +//! pub const LIGHT_CPI_SIGNER: CpiSigner = +//! derive_light_cpi_signer!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); +//! +//! let light_cpi_accounts = CpiAccounts::new( +//! ctx.accounts.fee_payer.as_ref(), +//! ctx.remaining_accounts, +//! crate::LIGHT_CPI_SIGNER, +//! ) +//! .map_err(ProgramError::from)?; +//! +//! let (address, address_seed) = derive_address( +//! &[b"compressed", name.as_bytes()], +//! &address_tree_info.get_tree_pubkey(&light_cpi_accounts)?, +//! &crate::ID, +//! ); +//! let new_address_params = address_tree_info.into_new_address_params_packed(address_seed); +//! +//! let mut my_compressed_account = LightAccount::<'_, MyCompressedAccount>::new_init( +//! &crate::ID, +//! Some(address), +//! output_tree_index, +//! ); +//! +//! my_compressed_account.name = name; +//! my_compressed_account.nested = NestedData::default(); +//! +//! let cpi_inputs = CpiInputs::new_with_address( +//! proof, +//! // add compressed accounts to create, update or close here +//! vec![my_compressed_account +//! .to_account_info() +//! .map_err(ProgramError::from)?], +//! // add new addresses here +//! // (existing addresses are part of the account info and must not be added here) +//! vec![new_address_params], +//! ); +//! +//! cpi_inputs +//! .invoke_light_system_program(light_cpi_accounts) +//! .map_err(ProgramError::from)?; +//! ``` + mod accounts; +#[cfg(feature = "small_ix")] mod accounts_small_ix; mod invoke; pub use accounts::*; -#[cfg(feature = "v2")] +#[cfg(feature = "small_ix")] pub use accounts_small_ix::*; pub use invoke::*; +/// Derives cpi signer and bump to invoke the light system program at compile time. +pub use light_sdk_macros::derive_light_cpi_signer; +/// Contains program id, derived cpi signer, and bump, +pub use light_sdk_types::CpiSigner; diff --git a/sdk-libs/sdk/src/error.rs b/sdk-libs/sdk/src/error.rs index 68806ca875..51c6ae3e5b 100644 --- a/sdk-libs/sdk/src/error.rs +++ b/sdk-libs/sdk/src/error.rs @@ -1,4 +1,6 @@ use light_hasher::HasherError; +use light_sdk_types::error::LightSdkTypesError; +use light_zero_copy::errors::ZeroCopyError; use thiserror::Error; use crate::ProgramError; @@ -47,43 +49,103 @@ pub enum LightSdkError { MissingField(String), #[error("Output state tree index is none. Use an CompressedAccountMeta type with output tree index to initialize or update accounts.")] OutputStateTreeIndexIsNone, + #[error("Address is none during initialization")] + InitAddressIsNone, + #[error("Address is none during initialization with address")] + InitWithAddressIsNone, + #[error("Output is none during initialization with address")] + InitWithAddressOutputIsNone, + #[error("Address is none during meta mutation")] + MetaMutAddressIsNone, + #[error("Input is none during meta mutation")] + MetaMutInputIsNone, + #[error("Output lamports is none during meta mutation")] + MetaMutOutputLamportsIsNone, + #[error("Output is none during meta mutation")] + MetaMutOutputIsNone, + #[error("Address is none during meta close")] + MetaCloseAddressIsNone, + #[error("Input is none during meta close")] + MetaCloseInputIsNone, + #[error("CPI accounts index out of bounds: {0}")] + CpiAccountsIndexOutOfBounds(usize), #[error(transparent)] Hasher(#[from] HasherError), + #[error(transparent)] + ZeroCopy(#[from] ZeroCopyError), #[error("Program error: {0}")] ProgramError(#[from] ProgramError), } -impl From for u32 { +impl From for ProgramError { fn from(e: LightSdkError) -> Self { + ProgramError::Custom(e.into()) + } +} + +impl From for LightSdkError { + fn from(e: LightSdkTypesError) -> Self { match e { - LightSdkError::ConstraintViolation => 14001, - LightSdkError::InvalidLightSystemProgram => 14002, - LightSdkError::ExpectedAccounts => 14003, - LightSdkError::ExpectedAddressTreeInfo => 14004, - LightSdkError::ExpectedAddressRootIndex => 14005, - LightSdkError::ExpectedData => 14006, - LightSdkError::ExpectedDiscriminator => 14007, - LightSdkError::ExpectedHash => 14008, - LightSdkError::ExpectedLightSystemAccount(_) => 14009, - LightSdkError::ExpectedMerkleContext => 14010, - LightSdkError::ExpectedRootIndex => 14011, - LightSdkError::TransferFromNoInput => 14012, - LightSdkError::TransferFromNoLamports => 14013, - LightSdkError::TransferFromInsufficientLamports => 14014, - LightSdkError::TransferIntegerOverflow => 14015, - LightSdkError::Borsh => 14016, - LightSdkError::FewerAccountsThanSystemAccounts => 14017, - LightSdkError::InvalidCpiSignerAccount => 14018, - LightSdkError::MissingField(_) => 14019, - LightSdkError::OutputStateTreeIndexIsNone => 14020, - LightSdkError::Hasher(e) => e.into(), - LightSdkError::ProgramError(e) => u32::try_from(u64::from(e)).unwrap(), + LightSdkTypesError::InitAddressIsNone => LightSdkError::InitAddressIsNone, + LightSdkTypesError::InitWithAddressIsNone => LightSdkError::InitWithAddressIsNone, + LightSdkTypesError::InitWithAddressOutputIsNone => { + LightSdkError::InitWithAddressOutputIsNone + } + LightSdkTypesError::MetaMutAddressIsNone => LightSdkError::MetaMutAddressIsNone, + LightSdkTypesError::MetaMutInputIsNone => LightSdkError::MetaMutInputIsNone, + LightSdkTypesError::MetaMutOutputLamportsIsNone => { + LightSdkError::MetaMutOutputLamportsIsNone + } + LightSdkTypesError::MetaMutOutputIsNone => LightSdkError::MetaMutOutputIsNone, + LightSdkTypesError::MetaCloseAddressIsNone => LightSdkError::MetaCloseAddressIsNone, + LightSdkTypesError::MetaCloseInputIsNone => LightSdkError::MetaCloseInputIsNone, + LightSdkTypesError::FewerAccountsThanSystemAccounts => { + LightSdkError::FewerAccountsThanSystemAccounts + } + LightSdkTypesError::CpiAccountsIndexOutOfBounds(index) => { + LightSdkError::CpiAccountsIndexOutOfBounds(index) + } + LightSdkTypesError::Hasher(e) => LightSdkError::Hasher(e), } } } -impl From for ProgramError { +impl From for u32 { fn from(e: LightSdkError) -> Self { - ProgramError::Custom(e.into()) + match e { + LightSdkError::ConstraintViolation => 16001, + LightSdkError::InvalidLightSystemProgram => 16002, + LightSdkError::ExpectedAccounts => 16003, + LightSdkError::ExpectedAddressTreeInfo => 16004, + LightSdkError::ExpectedAddressRootIndex => 16005, + LightSdkError::ExpectedData => 16006, + LightSdkError::ExpectedDiscriminator => 16007, + LightSdkError::ExpectedHash => 16008, + LightSdkError::ExpectedLightSystemAccount(_) => 16009, + LightSdkError::ExpectedMerkleContext => 16010, + LightSdkError::ExpectedRootIndex => 16011, + LightSdkError::TransferFromNoInput => 16012, + LightSdkError::TransferFromNoLamports => 16013, + LightSdkError::TransferFromInsufficientLamports => 16014, + LightSdkError::TransferIntegerOverflow => 16015, + LightSdkError::Borsh => 16016, + LightSdkError::FewerAccountsThanSystemAccounts => 16017, + LightSdkError::InvalidCpiSignerAccount => 16018, + LightSdkError::MissingField(_) => 16019, + LightSdkError::OutputStateTreeIndexIsNone => 16020, + LightSdkError::InitAddressIsNone => 16021, + LightSdkError::InitWithAddressIsNone => 16022, + LightSdkError::InitWithAddressOutputIsNone => 16023, + LightSdkError::MetaMutAddressIsNone => 16024, + LightSdkError::MetaMutInputIsNone => 16025, + LightSdkError::MetaMutOutputLamportsIsNone => 16026, + LightSdkError::MetaMutOutputIsNone => 16027, + LightSdkError::MetaCloseAddressIsNone => 16028, + LightSdkError::MetaCloseInputIsNone => 16029, + LightSdkError::CpiAccountsIndexOutOfBounds(_) => 16031, + LightSdkError::Hasher(e) => e.into(), + LightSdkError::ZeroCopy(e) => e.into(), + LightSdkError::ProgramError(e) => u64::from(e) as u32, + } } } diff --git a/sdk-libs/sdk/src/instruction/mod.rs b/sdk-libs/sdk/src/instruction/mod.rs index b2faa2566f..49cd82bd60 100644 --- a/sdk-libs/sdk/src/instruction/mod.rs +++ b/sdk-libs/sdk/src/instruction/mod.rs @@ -1,4 +1,184 @@ -pub mod account_meta; -pub mod accounts; -pub mod pack_accounts; -pub mod tree_info; +//! # Instruction data with AccountMeta (in Solana program) +//! ### Example instruction data to create new compressed account with address: +//! ```ignore +//! #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +//! pub struct CreatePdaInstructionData { +//! /// Prove validity of the new address. +//! pub proof: ValidityProof, +//! pub address_tree_info: PackedAddressTreeInfo, +//! /// Index of Merkle tree in the account array (remaining accounts in anchor). +//! pub output_merkle_tree_index: u8, +//! /// Arbitrary data of the new account. +//! pub data: [u8; 31], +//! /// Account offsets for convenience (can be hardcoded). +//! pub system_accounts_offset: u8, +//! pub tree_accounts_offset: u8, +//! } +//! ``` +//! +//! +//! ### Example instruction data to update a compressed account: +//! ```ignore +//! #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +//!pub struct UpdatePdaInstructionData { +//! /// Prove validity of the existing compressed account state. +//! pub proof: ValidityProof, +//! /// Data and metadata of the compressed account. +//! pub my_compressed_account: UpdateMyCompressedAccount, +//! /// Arbitrary new data the compressed account will be updated with. +//! pub new_data: [u8; 31], +//! /// Account offsets for convenience (can be hardcoded). +//! pub system_accounts_offset: u8, +//! } +//! +//! #[derive(Clone, Debug, Default, BorshDeserialize, BorshSerialize)] +//! pub struct UpdateMyCompressedAccount { +//! /// Metadata of the compressed account. +//! pub meta: CompressedAccountMeta, +//! /// Data of the compressed account. +//! pub data: [u8; 31], +//! } +//! ``` +//! ### Example anchor instruction data to create new compressed account with address: +//! ```ignore +//! pub fn create_compressed_account<'info>( +//! ctx: Context<'_, '_, '_, 'info, WithNestedData<'info>>, +//! /// Prove validity of the new address. +//! proof: ValidityProof, +//! address_tree_info: PackedAddressTreeInfo, +//! /// Index of Merkle tree in remaining accounts. +//! output_tree_index: u8, +//! /// Arbitrary data of the new account. +//! name: String, +//! ) -> Result<()>; +//! ``` +//! ### Example anchor instruction data to update a compressed account: +//! ```ignore +//! pub fn update_compressed_account<'info>( +//! ctx: Context<'_, '_, '_, 'info, UpdateNestedData<'info>>, +//! /// Prove validity of the existing compressed account state. +//! proof: ValidityProof, +//! /// Data of the compressed account. +//! my_compressed_account: MyCompressedAccount, +//! /// Metadata of the compressed account. +//! account_meta: CompressedAccountMeta, +//! /// Arbitrary new data the compressed account will be updated with. +//! nested_data: NestedData, +//! ) -> Result<()>; +//! ``` +//! ### Example instruction data to update a compressed account: +//! # Create instruction with packed accounts (in client) +//! +//! ### Create instruction to create 1 compressed account and address +//! ```ignore +//! let config = +//! ProgramTestConfig::new_v2(true, Some(vec![("sdk_anchor_test", sdk_anchor_test::ID)])); +//! let mut rpc = LightProgramTest::new(config).await.unwrap(); +//! let name = "test"; +//! let address_tree_info = rpc.get_address_tree_v1(); +//! let (address, _) = derive_address( +//! &[b"compressed", name.as_bytes()], +//! &address_tree_info.tree, +//! &sdk_anchor_test::ID, +//! ); +//! let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); +//! let mut remaining_accounts = PackedAccounts::default(); +//! remaining_accounts.add_system_accounts(config); +//! +//! let address_merkle_tree_info = rpc.get_address_tree_v1(); +//! +//! let rpc_result = rpc +//! .get_validity_proof( +//! vec![], +//! vec![AddressWithTree { +//! address: *address, +//! tree: address_merkle_tree_info.tree, +//! }], +//! None, +//! ) +//! .await? +//! .value; +//! let packed_accounts = rpc_result.pack_tree_infos(&mut remaining_accounts); +//! +//! let output_tree_index = rpc +//! .get_random_state_tree_info() +//! .pack_output_tree_index(&mut remaining_accounts) +//! .unwrap(); +//! +//! let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); +//! +//! let instruction_data = sdk_anchor_test::instruction::WithNestedData { +//! proof: rpc_result.proof, +//! address_tree_info: packed_accounts.address_trees[0], +//! name, +//! output_tree_index, +//! }; +//! +//! let accounts = sdk_anchor_test::accounts::WithNestedData { +//! signer: payer.pubkey(), +//! }; +//! +//! let instruction = Instruction { +//! program_id: sdk_anchor_test::ID, +//! accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), +//! data: instruction_data.data(), +//! }; +//! ``` +//! +//! ### Create instruction to create 1 compressed account and address (anchor) +//! ```ignore +//! let mut remaining_accounts = PackedAccounts::default(); +//! +//! let config = SystemAccountMetaConfig::new(sdk_anchor_test::ID); +//! remaining_accounts.add_system_accounts(config); +//! let hash = compressed_account.hash; +//! +//! let rpc_result = rpc +//! .get_validity_proof(vec![hash], vec![], None) +//! .await? +//! .value; +//! +//! let packed_tree_accounts = rpc_result +//! .pack_tree_infos(&mut remaining_accounts) +//! .state_trees +//! .unwrap(); +//! +//! let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); +//! +//! let my_compressed_account = MyCompressedAccount::deserialize( +//! &mut compressed_account.data.as_mut().unwrap().data.as_slice(), +//! ) +//! .unwrap(); +//! let instruction_data = sdk_anchor_test::instruction::UpdateNestedData { +//! proof: rpc_result.proof, +//! my_compressed_account, +//! account_meta: CompressedAccountMeta { +//! tree_info: packed_tree_accounts.packed_tree_infos[0], +//! address: compressed_account.address.unwrap(), +//! output_state_tree_index: packed_tree_accounts.output_tree_index, +//! }, +//! nested_data, +//! }; +//! +//! let accounts = sdk_anchor_test::accounts::UpdateNestedData { +//! signer: payer.pubkey(), +//! }; +//! +//! let instruction = Instruction { +//! program_id: sdk_anchor_test::ID, +//! accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(), +//! data: instruction_data.data(), +//! }; +//! ``` +// TODO: link to examples + +mod pack_accounts; +mod system_accounts; +mod tree_info; + +/// Zero-knowledge proof to prove the validity of existing compressed accounts and new addresses. +pub use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +pub use light_sdk_types::instruction::*; +pub use pack_accounts::*; +pub use system_accounts::*; +pub use tree_info::*; diff --git a/sdk-libs/sdk/src/instruction/pack_accounts.rs b/sdk-libs/sdk/src/instruction/pack_accounts.rs index ea5a951ddf..830ebe98a1 100644 --- a/sdk-libs/sdk/src/instruction/pack_accounts.rs +++ b/sdk-libs/sdk/src/instruction/pack_accounts.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::{ - instruction::accounts::{get_light_system_account_metas, SystemAccountMetaConfig}, + instruction::system_accounts::{get_light_system_account_metas, SystemAccountMetaConfig}, AccountMeta, Pubkey, }; @@ -102,7 +102,7 @@ impl PackedAccounts { } /// Converts the collection of accounts to a vector of - /// [`AccountMeta`](solana_sdk::instruction::AccountMeta), which can be used + /// [`AccountMeta`](solana_instruction::AccountMeta), which can be used /// as remaining accounts in instructions or CPI calls. pub fn to_account_metas(&self) -> (Vec, usize, usize) { let packed_accounts = self.hash_set_accounts_to_metas(); diff --git a/sdk-libs/sdk/src/instruction/accounts.rs b/sdk-libs/sdk/src/instruction/system_accounts.rs similarity index 88% rename from sdk-libs/sdk/src/instruction/accounts.rs rename to sdk-libs/sdk/src/instruction/system_accounts.rs index 3ff9f58418..8859068603 100644 --- a/sdk-libs/sdk/src/instruction/accounts.rs +++ b/sdk-libs/sdk/src/instruction/system_accounts.rs @@ -1,8 +1,10 @@ -use crate::{ - find_cpi_signer_macro, AccountMeta, Pubkey, CPI_AUTHORITY_PDA_SEED, - PROGRAM_ID_ACCOUNT_COMPRESSION, PROGRAM_ID_LIGHT_SYSTEM, PROGRAM_ID_NOOP, +use light_sdk_types::constants::{ + ACCOUNT_COMPRESSION_PROGRAM_ID, CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID, + NOOP_PROGRAM_ID, }; +use crate::{find_cpi_signer_macro, AccountMeta, Pubkey}; + #[derive(Debug, Default, Copy, Clone)] pub struct SystemAccountMetaConfig { pub self_program: Pubkey, @@ -45,20 +47,20 @@ pub struct SystemAccountPubkeys { impl Default for SystemAccountPubkeys { fn default() -> Self { Self { - light_sytem_program: PROGRAM_ID_LIGHT_SYSTEM, + light_sytem_program: Pubkey::from(LIGHT_SYSTEM_PROGRAM_ID), system_program: Pubkey::default(), - account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, + account_compression_program: Pubkey::from(ACCOUNT_COMPRESSION_PROGRAM_ID), account_compression_authority: Pubkey::find_program_address( &[CPI_AUTHORITY_PDA_SEED], - &PROGRAM_ID_LIGHT_SYSTEM, + &Pubkey::from(LIGHT_SYSTEM_PROGRAM_ID), ) .0, registered_program_pda: Pubkey::find_program_address( - &[PROGRAM_ID_LIGHT_SYSTEM.to_bytes().as_slice()], - &PROGRAM_ID_ACCOUNT_COMPRESSION, + &[LIGHT_SYSTEM_PROGRAM_ID.as_slice()], + &Pubkey::from(ACCOUNT_COMPRESSION_PROGRAM_ID), ) .0, - noop_program: PROGRAM_ID_NOOP, + noop_program: Pubkey::from(NOOP_PROGRAM_ID), // TODO: add correct pubkey sol_pool_pda: Pubkey::default(), } diff --git a/sdk-libs/sdk/src/instruction/tree_info.rs b/sdk-libs/sdk/src/instruction/tree_info.rs index 59fbbb378d..0281d824df 100644 --- a/sdk-libs/sdk/src/instruction/tree_info.rs +++ b/sdk-libs/sdk/src/instruction/tree_info.rs @@ -1,40 +1,13 @@ pub use light_compressed_account::compressed_account::{MerkleContext, PackedMerkleContext}; -use light_compressed_account::instruction_data::data::NewAddressParamsPacked; +use light_sdk_types::instruction::PackedAddressTreeInfo; -use super::pack_accounts::PackedAccounts; +use super::PackedAccounts; use crate::{AccountInfo, AnchorDeserialize, AnchorSerialize, Pubkey}; -#[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] -pub struct PackedStateTreeInfo { - pub root_index: u16, - pub prove_by_index: bool, - pub merkle_tree_pubkey_index: u8, - pub queue_pubkey_index: u8, - pub leaf_index: u32, -} - #[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] pub struct AddressTreeInfo { - pub address_merkle_tree_pubkey: Pubkey, - pub address_queue_pubkey: Pubkey, -} - -#[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] -pub struct PackedAddressTreeInfo { - pub address_merkle_tree_pubkey_index: u8, - pub address_queue_pubkey_index: u8, - pub root_index: u16, -} - -impl PackedAddressTreeInfo { - pub fn into_new_address_params_packed(self, seed: [u8; 32]) -> NewAddressParamsPacked { - NewAddressParamsPacked { - address_merkle_tree_account_index: self.address_merkle_tree_pubkey_index, - address_queue_account_index: self.address_queue_pubkey_index, - address_merkle_tree_root_index: self.root_index, - seed, - } - } + pub tree: Pubkey, + pub queue: Pubkey, } #[deprecated(since = "0.13.0", note = "please use PackedStateTreeInfo")] @@ -95,13 +68,9 @@ pub fn pack_address_tree_info( remaining_accounts: &mut PackedAccounts, root_index: u16, ) -> PackedAddressTreeInfo { - let AddressTreeInfo { - address_merkle_tree_pubkey, - address_queue_pubkey, - } = address_tree_info; - let address_merkle_tree_pubkey_index = - remaining_accounts.insert_or_get(*address_merkle_tree_pubkey); - let address_queue_pubkey_index = remaining_accounts.insert_or_get(*address_queue_pubkey); + let AddressTreeInfo { tree, queue } = address_tree_info; + let address_merkle_tree_pubkey_index = remaining_accounts.insert_or_get(*tree); + let address_queue_pubkey_index = remaining_accounts.insert_or_get(*queue); PackedAddressTreeInfo { address_merkle_tree_pubkey_index, @@ -120,8 +89,8 @@ pub fn unpack_address_tree_infos( *remaining_accounts[x.address_merkle_tree_pubkey_index as usize].key; let address_queue_pubkey = *remaining_accounts[x.address_queue_pubkey_index as usize].key; result.push(AddressTreeInfo { - address_merkle_tree_pubkey, - address_queue_pubkey, + tree: address_merkle_tree_pubkey, + queue: address_queue_pubkey, }); } result @@ -230,8 +199,8 @@ mod test { let mut remaining_accounts = PackedAccounts::default(); let address_tree_info = AddressTreeInfo { - address_merkle_tree_pubkey: Pubkey::new_unique(), - address_queue_pubkey: Pubkey::new_unique(), + tree: Pubkey::new_unique(), + queue: Pubkey::new_unique(), }; let packed_address_tree_info = @@ -252,16 +221,16 @@ mod test { use solana_pubkey::Pubkey; let address_tree_infos = [ AddressTreeInfo { - address_merkle_tree_pubkey: Pubkey::new_unique(), - address_queue_pubkey: Pubkey::new_unique(), + tree: Pubkey::new_unique(), + queue: Pubkey::new_unique(), }, AddressTreeInfo { - address_merkle_tree_pubkey: Pubkey::new_unique(), - address_queue_pubkey: Pubkey::new_unique(), + tree: Pubkey::new_unique(), + queue: Pubkey::new_unique(), }, AddressTreeInfo { - address_merkle_tree_pubkey: Pubkey::new_unique(), - address_queue_pubkey: Pubkey::new_unique(), + tree: Pubkey::new_unique(), + queue: Pubkey::new_unique(), }, ]; diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index bdf746d760..b8eef1be97 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -1,13 +1,118 @@ +//! +//! # Core Functionality +//! 1. Instruction +//! - `AccountMeta` - Compressed account metadata structs for instruction data. +//! - `PackedAccounts` - Abstraction to prepare accounts offchain for instructions with compressed accounts. +//! 2. Program logic +//! - `LightAccount` - Compressed account abstraction similar to anchor Account. +//! - `derive_address` - Create a compressed account address. +//! - `LightHasher` - DeriveMacro to derive a hashing scheme from a struct layout. +//! - `LightDiscriminator` - DeriveMacro to derive a compressed account discriminator. +//! 3. Cpi +//! - `CpiAccounts` - Prepare accounts to cpi the light system program. +//! - `CpiInputs` - Prepare instruction data to cpi the light system program. +//! - `invoke_light_system_program` - Invoke the light system program via cpi. +//! +//! +//! # Features +//! 1. `anchor` - Derives AnchorSerialize, AnchorDeserialize instead of BorshSerialize, BorshDeserialize. +//! +//! 2. `v2` - light protocol program v2 are currently in audit and only available on local host and with light-program-test. +//! Deploy on devnet and mainnet only without v2 features enabled. +//! +//! ### Example Solana program code to create a compressed account +//! ```ignore +//! use anchor_lang::{prelude::*, Discriminator}; +//! use light_sdk::{ +//! account::LightAccount, +//! address::v1::derive_address, +//! cpi::{CpiAccounts, CpiInputs}, +//! derive_light_cpi_signer, +//! instruction::{account_meta::CompressedAccountMeta, PackedAddressTreeInfo}, +//! CpiSigner, LightDiscriminator, LightHasher, ValidityProof, +//! }; +//! +//! declare_id!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); +//! +//! pub const LIGHT_CPI_SIGNER: CpiSigner = +//! derive_light_cpi_signer!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); +//! +//! #[program] +//! pub mod counter { +//! +//! use super::*; +//! +//! pub fn create_compressed_account<'info>( +//! ctx: Context<'_, '_, '_, 'info, CreateCompressedAccount<'info>>, +//! proof: ValidityProof, +//! address_tree_info: PackedAddressTreeInfo, +//! output_tree_index: u8, +//! ) -> Result<()> { +//! let light_cpi_accounts = CpiAccounts::new( +//! ctx.accounts.fee_payer.as_ref(), +//! ctx.remaining_accounts, +//! crate::LIGHT_CPI_SIGNER, +//! ) +//! .map_err(ProgramError::from)?; +//! +//! let (address, address_seed) = derive_address( +//! &[b"counter", ctx.accounts.fee_payer.key().as_ref()], +//! &address_tree_info.get_tree_pubkey(&light_cpi_accounts)?, +//! &crate::ID, +//! ); +//! let new_address_params = address_tree_info +//! .into_new_address_params_packed(address_seed); +//! +//! let mut my_compressed_account = LightAccount::<'_, CounterAccount>::new_init( +//! &crate::ID, +//! Some(address), +//! output_tree_index, +//! ); +//! +//! my_compressed_account.owner = ctx.accounts.fee_payer.key(); +//! +//! let cpi_inputs = CpiInputs::new_with_address( +//! proof, +//! vec![my_compressed_account +//! .to_account_info() +//! .map_err(ProgramError::from)?], +//! vec![new_address_params], +//! ); +//! +//! cpi_inputs +//! .invoke_light_system_program(light_cpi_accounts) +//! .map_err(ProgramError::from)?; +//! +//! Ok(()) +//! } +//! } +//! +//! #[derive(Accounts)] +//! pub struct CreateCompressedAccount<'info> { +//! #[account(mut)] +//! pub fee_payer: Signer<'info>, +//! } +//! +//! #[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)] +//!pub struct CounterAccount { +//! #[hash] +//! pub owner: Pubkey, +//! pub counter: u64 +//!} +//! ``` + +/// Compressed account abstraction similar to anchor Account. pub mod account; -pub mod account_info; +/// Functions to derive compressed account addresses. pub mod address; -pub mod constants; -pub use constants::*; +/// Utilities to invoke the light-system-program via cpi. pub mod cpi; pub mod error; +/// Utilities to build instructions for programs with compressed accounts. pub mod instruction; pub mod legacy; pub mod token; +/// Transfer compressed sol between compressed accounts. pub mod transfer; pub mod utils; @@ -15,53 +120,14 @@ pub mod utils; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -pub use light_account_checks::{discriminator::Discriminator as LightDiscriminator, *}; -use light_compressed_account::instruction_data::compressed_proof::CompressedProof; -pub use light_compressed_account::{self, instruction_data::data::*}; -pub use light_hasher::*; -pub use light_sdk_macros::*; +pub use light_account_checks::{self, discriminator::Discriminator as LightDiscriminator}; +pub use light_hasher; +pub use light_sdk_macros::{ + derive_light_cpi_signer, light_system_accounts, LightDiscriminator, LightHasher, LightTraits, +}; +pub use light_sdk_types::constants; use solana_account_info::AccountInfo; use solana_cpi::invoke_signed; use solana_instruction::{AccountMeta, Instruction}; -use solana_msg::msg; use solana_program_error::ProgramError; -use solana_pubkey::{pubkey, Pubkey}; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] -pub struct ValidityProof(pub Option); - -impl ValidityProof { - pub fn new(proof: Option) -> Self { - Self(proof) - } -} - -impl From for ValidityProof { - fn from(proof: CompressedProof) -> Self { - Self(Some(proof)) - } -} - -impl From> for ValidityProof { - fn from(proof: Option) -> Self { - Self(proof) - } -} -impl From<&CompressedProof> for ValidityProof { - fn from(proof: &CompressedProof) -> Self { - Self(Some(*proof)) - } -} - -impl From<&Option> for ValidityProof { - fn from(proof: &Option) -> Self { - Self(*proof) - } -} - -#[allow(clippy::from_over_into)] -impl Into> for ValidityProof { - fn into(self) -> Option { - self.0 - } -} +use solana_pubkey::Pubkey; diff --git a/sdk-libs/sdk/src/utils.rs b/sdk-libs/sdk/src/utils.rs index 62936ab5f1..70dea91527 100644 --- a/sdk-libs/sdk/src/utils.rs +++ b/sdk-libs/sdk/src/utils.rs @@ -1,21 +1,5 @@ -use crate::{Pubkey, CPI_AUTHORITY_PDA_SEED, PROGRAM_ID_ACCOUNT_COMPRESSION}; - -pub fn get_registered_program_pda(program_id: &Pubkey) -> Pubkey { - Pubkey::find_program_address( - &[program_id.to_bytes().as_slice()], - &PROGRAM_ID_ACCOUNT_COMPRESSION, - ) - .0 -} - -pub fn find_cpi_signer(program_id: &Pubkey) -> Pubkey { - Pubkey::find_program_address([CPI_AUTHORITY_PDA_SEED].as_slice(), program_id).0 -} - -pub fn get_cpi_authority_pda(program_id: &Pubkey) -> Pubkey { - Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], program_id).0 -} - +#[allow(unused_imports)] +use crate::constants::CPI_AUTHORITY_PDA_SEED; #[macro_export] macro_rules! find_cpi_signer_macro { ($program_id:expr) => {