From b3a82991c9cde6e6646811b3b57a43a53ada3efe Mon Sep 17 00:00:00 2001 From: Michal Rostecki Date: Wed, 11 Sep 2024 09:45:27 +0000 Subject: [PATCH] refactor: Remove hard dependency on `light-system-program` in SDK First of all, add more types to the SDK, which are going to be used by third-party applications, which were until now defined in program crates: - `address` - `NewAddressParams` - `NewAddressParamsPacked` - `compressed_account` - `CompressedAccount` - `CompressedAccountData` - `CompressedAccountWithMerkleContext` - `PackedCompressedAccountWithMerkleContext` - `OutputCompressedAccountWithPackedContext` - `event` - `MerkleTreeSequenceNumber` - `PublicTransactionEvent` - `merkle_context` - `QueueIndex` - `MerkleContext` - `PackedMerkleContext` - `proof` - `CompressedProof` - `ProofRpcResult` - `verify` - `CompressedCpiContext` - `InstructionDataInvokeCpi` Hide the imports from `light_system_program` behind a feature flag. The long-term plan is to remove them all together. Provide a dependency-free, tinier implementation of `TestIndexer` in the `light-client` crate. --- Cargo.lock | 19 +- Cargo.toml | 17 + client/Cargo.toml | 18 + client/src/indexer/mod.rs | 111 +++- client/src/indexer/test_indexer.rs | 483 ++++++++++++++++++ client/src/lib.rs | 1 + client/src/rpc/merkle_tree.rs | 53 ++ client/src/rpc/mod.rs | 2 + client/src/rpc/test_rpc.rs | 310 +++++++++++ client/src/rpc_pool.rs | 3 +- .../programs/name-service/Cargo.toml | 23 +- .../programs/name-service/src/lib.rs | 6 +- .../programs/name-service/tests/test.rs | 119 +++-- .../programs/token-escrow/Cargo.toml | 2 +- .../src/escrow_with_compressed_pda/escrow.rs | 5 +- forester-utils/Cargo.toml | 4 +- macros/light-sdk-macros/src/accounts.rs | 17 +- macros/light-sdk-macros/src/lib.rs | 8 +- macros/light-sdk-macros/src/program.rs | 8 +- macros/light-sdk-macros/src/traits.rs | 43 +- sdk/Cargo.toml | 18 +- sdk/src/address.rs | 72 ++- sdk/src/compressed_account.rs | 178 +++++-- sdk/src/constants.rs | 27 + sdk/src/context.rs | 7 +- sdk/src/event.rs | 24 + sdk/src/legacy.rs | 36 ++ sdk/src/lib.rs | 7 + sdk/src/merkle_context.rs | 31 +- sdk/src/proof.rs | 49 ++ sdk/src/state.rs | 49 ++ sdk/src/token.rs | 34 ++ sdk/src/traits.rs | 14 +- sdk/src/utils.rs | 15 +- sdk/src/verify.rs | 273 +++++++--- test-utils/src/test_env.rs | 75 +++ 36 files changed, 1871 insertions(+), 290 deletions(-) create mode 100644 client/src/indexer/test_indexer.rs create mode 100644 client/src/rpc/merkle_tree.rs create mode 100644 client/src/rpc/test_rpc.rs create mode 100644 sdk/src/constants.rs create mode 100644 sdk/src/event.rs create mode 100644 sdk/src/legacy.rs create mode 100644 sdk/src/proof.rs create mode 100644 sdk/src/state.rs create mode 100644 sdk/src/token.rs diff --git a/Cargo.lock b/Cargo.lock index 81b5a73cc6..954cdbe65a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3501,10 +3501,19 @@ dependencies = [ "async-trait", "bb8", "borsh 0.10.3", + "light-concurrent-merkle-tree", + "light-indexed-merkle-tree", + "light-merkle-tree-reference", + "light-prover-client", + "light-sdk", "log", + "num-bigint 0.4.6", + "num-traits", + "reqwest 0.11.27", "solana-banks-client", "solana-client", "solana-program", + "solana-program-test", "solana-sdk", "solana-transaction-status", "thiserror", @@ -3730,6 +3739,7 @@ dependencies = [ "serde_json", "solana-banks-interface", "solana-cli-output", + "solana-program", "solana-program-test", "solana-sdk", "tokio", @@ -4105,16 +4115,13 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" name = "name-service" version = "0.6.0" dependencies = [ - "account-compression", "anchor-lang", "borsh 0.10.3", - "light-compressed-token", + "light-client", "light-hasher", - "light-heap", "light-macros", "light-sdk", "light-sdk-macros", - "light-system-program", "light-test-utils", "light-utils", "light-verifier", @@ -8137,9 +8144,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] diff --git a/Cargo.toml b/Cargo.toml index 6ad793d7d3..46a8f6d6bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,8 +71,25 @@ thiserror = "1.0" # Light Protocol light-client = { path = "client", version = "0.8.0" } +light-concurrent-merkle-tree = { path = "merkle-tree/concurrent", version = "1.0.0" } +light-hasher = { path = "merkle-tree/hasher", version = "1.0.0" } +light-indexed-merkle-tree = { path = "merkle-tree/indexed", version = "1.0.0" } +light-macros = { path = "macros/light", version = "1.0.0" } +light-merkle-tree-reference = { path = "merkle-tree/reference", version = "1.0.0" } +light-prover-client = { path = "circuit-lib/light-prover-client", version = "1.0.0" } +light-sdk = { path = "sdk", version = "0.8.0" } +light-sdk-macros = { path = "macros/light-sdk-macros", version = "0.1.0" } +light-utils = { path = "utils", version = "1.0.0" } +light-verifier = { path = "circuit-lib/verifier", version = "1.0.0" } photon-api = { path = "photon-api" } +# Math and crypto +num-bigint = "0.4.6" +num-traits = "0.2.19" + +# HTTP client +reqwest = "0.11.26" + [patch.crates-io] "solana-account-decoder" = { git = "https://github.com/lightprotocol/agave", branch = "v1.18.22-enforce-cpi-tracking" } "solana-accounts-db" = { git = "https://github.com/lightprotocol/agave", branch = "v1.18.22-enforce-cpi-tracking" } diff --git a/client/Cargo.toml b/client/Cargo.toml index e34da5213c..55b0d7f539 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,6 +11,7 @@ description = "Client library for Light Protocol" solana-banks-client = { workspace = true } solana-client = { workspace = true } solana-program = { workspace = true } +solana-program-test = { workspace = true } solana-sdk = { workspace = true } solana-transaction-status = { workspace = true } @@ -22,5 +23,22 @@ tokio = { workspace = true } async-trait = { workspace = true } bb8 = { workspace = true } +# Logging log = { workspace = true } + +# Error handling thiserror = { workspace = true } + +# Light Protocol +light-concurrent-merkle-tree = { workspace = true } +light-indexed-merkle-tree = { workspace = true } +light-merkle-tree-reference = { workspace = true } +light-prover-client = { workspace = true } +light-sdk = { workspace = true } + +# Math and crypto +num-bigint = { workspace = true } +num-traits = { workspace = true } + +# HTTP client +reqwest = { workspace = true } diff --git a/client/src/indexer/mod.rs b/client/src/indexer/mod.rs index 9499eca14c..61ddf0b3b3 100644 --- a/client/src/indexer/mod.rs +++ b/client/src/indexer/mod.rs @@ -1 +1,110 @@ -pub trait Indexer: Sync + Send + Debug + 'static {} +use std::{fmt::Debug, future::Future}; + +use light_concurrent_merkle_tree::light_hasher::Poseidon; +use light_indexed_merkle_tree::{ + array::{IndexedArray, IndexedElement}, + reference::IndexedMerkleTree, +}; +use light_merkle_tree_reference::MerkleTree; +use light_sdk::{ + compressed_account::CompressedAccountWithMerkleContext, event::PublicTransactionEvent, + proof::ProofRpcResult, token::TokenDataWithMerkleContext, +}; +use num_bigint::BigUint; +use solana_sdk::pubkey::Pubkey; +use thiserror::Error; + +use crate::rpc::RpcConnection; + +pub mod test_indexer; + +#[derive(Error, Debug)] +pub enum IndexerError { + #[error("RPC Error: {0}")] + RpcError(#[from] solana_client::client_error::ClientError), + #[error("failed to deserialize account data")] + DeserializeError(#[from] solana_sdk::program_error::ProgramError), + #[error("failed to copy merkle tree")] + CopyMerkleTreeError(#[from] std::io::Error), + #[error("error: {0:?}")] + Custom(String), + #[error("unknown error")] + Unknown, +} + +pub trait Indexer: Sync + Send + Debug + 'static { + fn add_event_and_compressed_accounts( + &mut self, + event: &PublicTransactionEvent, + ) -> ( + Vec, + Vec, + ); + + fn create_proof_for_compressed_accounts( + &mut self, + compressed_accounts: Option<&[[u8; 32]]>, + state_merkle_tree_pubkeys: Option<&[Pubkey]>, + new_addresses: Option<&[[u8; 32]]>, + address_merkle_tree_pubkeys: Option>, + rpc: &mut R, + ) -> impl Future; + + fn get_compressed_accounts_by_owner( + &self, + owner: &Pubkey, + ) -> Vec; +} + +#[derive(Debug, Clone)] +pub struct MerkleProof { + pub hash: String, + pub leaf_index: u64, + pub merkle_tree: String, + pub proof: Vec<[u8; 32]>, + pub root_seq: u64, +} + +// For consistency with the Photon API. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct NewAddressProofWithContext { + pub merkle_tree: [u8; 32], + pub root: [u8; 32], + pub root_seq: u64, + pub low_address_index: u64, + pub low_address_value: [u8; 32], + pub low_address_next_index: u64, + pub low_address_next_value: [u8; 32], + pub low_address_proof: [[u8; 32]; 16], + pub new_low_element: Option>, + pub new_element: Option>, + pub new_element_next_value: Option, +} + +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] +pub struct StateMerkleTreeAccounts { + pub merkle_tree: Pubkey, + pub nullifier_queue: Pubkey, + pub cpi_context: Pubkey, +} + +#[derive(Debug, Clone, Copy)] +pub struct AddressMerkleTreeAccounts { + pub merkle_tree: Pubkey, + pub queue: Pubkey, +} + +#[derive(Debug, Clone)] +pub struct StateMerkleTreeBundle { + pub rollover_fee: u64, + pub merkle_tree: Box>, + pub accounts: StateMerkleTreeAccounts, +} + +#[derive(Debug, Clone)] +pub struct AddressMerkleTreeBundle { + pub rollover_fee: u64, + pub merkle_tree: Box>, + pub indexed_array: Box>, + pub accounts: AddressMerkleTreeAccounts, +} diff --git a/client/src/indexer/test_indexer.rs b/client/src/indexer/test_indexer.rs new file mode 100644 index 0000000000..be5d6d4757 --- /dev/null +++ b/client/src/indexer/test_indexer.rs @@ -0,0 +1,483 @@ +use std::{marker::PhantomData, time::Duration}; + +use borsh::BorshDeserialize; +use light_concurrent_merkle_tree::light_hasher::Poseidon; +use light_indexed_merkle_tree::{array::IndexedArray, reference::IndexedMerkleTree}; +use light_merkle_tree_reference::MerkleTree; +use light_prover_client::{ + gnark::{ + combined_json_formatter::CombinedJsonStruct, + constants::{PROVE_PATH, SERVER_ADDRESS}, + helpers::{spawn_prover, ProofType}, + inclusion_json_formatter::BatchInclusionJsonStruct, + non_inclusion_json_formatter::BatchNonInclusionJsonStruct, + proof_helpers::{compress_proof, deserialize_gnark_proof_json, proof_from_json_struct}, + }, + inclusion::merkle_inclusion_proof_inputs::{InclusionMerkleProofInputs, InclusionProofInputs}, + non_inclusion::merkle_non_inclusion_proof_inputs::{ + get_non_inclusion_proof_inputs, NonInclusionProofInputs, + }, +}; +use light_sdk::{ + compressed_account::CompressedAccountWithMerkleContext, + event::PublicTransactionEvent, + merkle_context::MerkleContext, + proof::{CompressedProof, ProofRpcResult}, + token::{TokenData, TokenDataWithMerkleContext}, + ADDRESS_MERKLE_TREE_CANOPY_DEPTH, ADDRESS_MERKLE_TREE_HEIGHT, PROGRAM_ID_LIGHT_SYSTEM, + STATE_MERKLE_TREE_CANOPY_DEPTH, STATE_MERKLE_TREE_HEIGHT, + TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, +}; +use log::warn; +use num_bigint::BigInt; +use num_traits::FromBytes; +use reqwest::Client; +use solana_sdk::pubkey::Pubkey; + +use crate::{ + indexer::Indexer, + rpc::{merkle_tree::MerkleTreeExt, RpcConnection}, + transaction_params::FeeConfig, +}; + +use super::{ + AddressMerkleTreeAccounts, AddressMerkleTreeBundle, StateMerkleTreeAccounts, + StateMerkleTreeBundle, +}; + +#[derive(Debug)] +pub struct TestIndexer +where + R: RpcConnection + MerkleTreeExt, +{ + pub state_merkle_trees: Vec, + pub address_merkle_trees: Vec, + pub compressed_accounts: Vec, + pub nullified_compressed_accounts: Vec, + pub token_compressed_accounts: Vec, + pub token_nullified_compressed_accounts: Vec, + pub events: Vec, + proof_types: Vec, + _rpc: PhantomData, +} + +impl Indexer for TestIndexer +where + R: RpcConnection + MerkleTreeExt, +{ + fn add_event_and_compressed_accounts( + &mut self, + event: &PublicTransactionEvent, + ) -> ( + Vec, + Vec, + ) { + for hash in event.input_compressed_account_hashes.iter() { + let index = self.compressed_accounts.iter().position(|x| { + x.compressed_account + .hash::( + &x.merkle_context.merkle_tree_pubkey, + &x.merkle_context.leaf_index, + ) + .unwrap() + == *hash + }); + if let Some(index) = index { + self.nullified_compressed_accounts + .push(self.compressed_accounts[index].clone()); + self.compressed_accounts.remove(index); + continue; + }; + if index.is_none() { + let index = self + .token_compressed_accounts + .iter() + .position(|x| { + x.compressed_account + .compressed_account + .hash::( + &x.compressed_account.merkle_context.merkle_tree_pubkey, + &x.compressed_account.merkle_context.leaf_index, + ) + .unwrap() + == *hash + }) + .expect("input compressed account not found"); + self.token_nullified_compressed_accounts + .push(self.token_compressed_accounts[index].clone()); + self.token_compressed_accounts.remove(index); + } + } + + let mut compressed_accounts = Vec::new(); + let mut token_compressed_accounts = Vec::new(); + for (i, compressed_account) in event.output_compressed_accounts.iter().enumerate() { + let nullifier_queue_pubkey = self + .state_merkle_trees + .iter() + .find(|x| { + x.accounts.merkle_tree + == event.pubkey_array + [event.output_compressed_accounts[i].merkle_tree_index as usize] + }) + .unwrap() + .accounts + .nullifier_queue; + // if data is some, try to deserialize token data, if it fails, add to compressed_accounts + // if data is none add to compressed_accounts + // new accounts are inserted in front so that the newest accounts are found first + match compressed_account.compressed_account.data.as_ref() { + Some(data) => { + if compressed_account.compressed_account.owner == PROGRAM_ID_LIGHT_SYSTEM + && data.discriminator == TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR + { + if let Ok(token_data) = TokenData::deserialize(&mut data.data.as_slice()) { + let token_account = TokenDataWithMerkleContext { + token_data, + compressed_account: CompressedAccountWithMerkleContext { + compressed_account: compressed_account + .compressed_account + .clone(), + merkle_context: MerkleContext { + leaf_index: event.output_leaf_indices[i], + merkle_tree_pubkey: event.pubkey_array[event + .output_compressed_accounts[i] + .merkle_tree_index + as usize], + nullifier_queue_pubkey, + queue_index: None, + }, + }, + }; + token_compressed_accounts.push(token_account.clone()); + self.token_compressed_accounts.insert(0, token_account); + } + } else { + let compressed_account = CompressedAccountWithMerkleContext { + compressed_account: compressed_account.compressed_account.clone(), + merkle_context: MerkleContext { + leaf_index: event.output_leaf_indices[i], + merkle_tree_pubkey: event.pubkey_array[event + .output_compressed_accounts[i] + .merkle_tree_index + as usize], + nullifier_queue_pubkey, + queue_index: None, + }, + }; + compressed_accounts.push(compressed_account.clone()); + self.compressed_accounts.insert(0, compressed_account); + } + } + None => { + let compressed_account = CompressedAccountWithMerkleContext { + compressed_account: compressed_account.compressed_account.clone(), + merkle_context: MerkleContext { + leaf_index: event.output_leaf_indices[i], + merkle_tree_pubkey: event.pubkey_array + [event.output_compressed_accounts[i].merkle_tree_index as usize], + nullifier_queue_pubkey, + queue_index: None, + }, + }; + compressed_accounts.push(compressed_account.clone()); + self.compressed_accounts.insert(0, compressed_account); + } + }; + let merkle_tree = &mut self + .state_merkle_trees + .iter_mut() + .find(|x| { + x.accounts.merkle_tree + == event.pubkey_array + [event.output_compressed_accounts[i].merkle_tree_index as usize] + }) + .unwrap() + .merkle_tree; + merkle_tree + .append( + &compressed_account + .compressed_account + .hash::( + &event.pubkey_array + [event.output_compressed_accounts[i].merkle_tree_index as usize], + &event.output_leaf_indices[i], + ) + .unwrap(), + ) + .expect("insert failed"); + } + + self.events.push(event.clone()); + (compressed_accounts, token_compressed_accounts) + } + + async fn create_proof_for_compressed_accounts( + &mut self, + compressed_accounts: Option<&[[u8; 32]]>, + state_merkle_tree_pubkeys: Option<&[solana_sdk::pubkey::Pubkey]>, + new_addresses: Option<&[[u8; 32]]>, + address_merkle_tree_pubkeys: Option>, + rpc: &mut R, + ) -> ProofRpcResult { + if compressed_accounts.is_some() + && ![1usize, 2usize, 3usize, 4usize, 8usize] + .contains(&compressed_accounts.unwrap().len()) + { + panic!( + "compressed_accounts must be of length 1, 2, 3, 4 or 8 != {}", + compressed_accounts.unwrap().len() + ) + } + if new_addresses.is_some() && ![1usize, 2usize].contains(&new_addresses.unwrap().len()) { + panic!("new_addresses must be of length 1, 2") + } + let client = Client::new(); + let (root_indices, address_root_indices, json_payload) = + match (compressed_accounts, new_addresses) { + (Some(accounts), None) => { + let (payload, indices) = self + .process_inclusion_proofs(state_merkle_tree_pubkeys.unwrap(), accounts, rpc) + .await; + (indices, Vec::new(), payload.to_string()) + } + (None, Some(addresses)) => { + let (payload, indices) = self + .process_non_inclusion_proofs( + address_merkle_tree_pubkeys.unwrap().as_slice(), + addresses, + rpc, + ) + .await; + (Vec::::new(), indices, payload.to_string()) + } + (Some(accounts), Some(addresses)) => { + let (inclusion_payload, inclusion_indices) = self + .process_inclusion_proofs(state_merkle_tree_pubkeys.unwrap(), accounts, rpc) + .await; + let (non_inclusion_payload, non_inclusion_indices) = self + .process_non_inclusion_proofs( + address_merkle_tree_pubkeys.unwrap().as_slice(), + addresses, + rpc, + ) + .await; + + let combined_payload = CombinedJsonStruct { + inclusion: inclusion_payload.inputs, + non_inclusion: non_inclusion_payload.inputs, + } + .to_string(); + (inclusion_indices, non_inclusion_indices, combined_payload) + } + _ => { + panic!("At least one of compressed_accounts or new_addresses must be provided") + } + }; + + let mut retries = 3; + while retries > 0 { + let response_result = client + .post(&format!("{}{}", SERVER_ADDRESS, PROVE_PATH)) + .header("Content-Type", "text/plain; charset=utf-8") + .body(json_payload.clone()) + .send() + .await + .expect("Failed to execute request."); + if response_result.status().is_success() { + let body = response_result.text().await.unwrap(); + let proof_json = deserialize_gnark_proof_json(&body).unwrap(); + let (proof_a, proof_b, proof_c) = proof_from_json_struct(proof_json); + let (proof_a, proof_b, proof_c) = compress_proof(&proof_a, &proof_b, &proof_c); + return ProofRpcResult { + root_indices, + address_root_indices, + proof: CompressedProof { + a: proof_a, + b: proof_b, + c: proof_c, + }, + }; + } else { + warn!("Error: {}", response_result.text().await.unwrap()); + tokio::time::sleep(Duration::from_secs(1)).await; + spawn_prover(true, self.proof_types.as_slice()).await; + retries -= 1; + } + } + panic!("Failed to get proof from server"); + } + + /// Returns compressed accounts owned by the given `owner`. + fn get_compressed_accounts_by_owner( + &self, + owner: &Pubkey, + ) -> Vec { + self.compressed_accounts + .iter() + .filter(|x| x.compressed_account.owner == *owner) + .cloned() + .collect() + } +} + +impl TestIndexer +where + R: RpcConnection + MerkleTreeExt, +{ + pub async fn new( + state_merkle_tree_accounts: &[StateMerkleTreeAccounts], + address_merkle_tree_accounts: &[AddressMerkleTreeAccounts], + inclusion: bool, + non_inclusion: bool, + ) -> Self { + let state_merkle_trees = state_merkle_tree_accounts + .iter() + .map(|accounts| { + let merkle_tree = Box::new(MerkleTree::::new( + STATE_MERKLE_TREE_HEIGHT, + STATE_MERKLE_TREE_CANOPY_DEPTH, + )); + StateMerkleTreeBundle { + accounts: *accounts, + merkle_tree, + rollover_fee: FeeConfig::default().state_merkle_tree_rollover, + } + }) + .collect::>(); + + let address_merkle_trees = address_merkle_tree_accounts + .iter() + .map(|accounts| Self::add_address_merkle_tree_bundle(accounts)) + .collect::>(); + + let mut proof_types = vec![]; + if inclusion { + proof_types.push(ProofType::Inclusion); + } + if non_inclusion { + proof_types.push(ProofType::NonInclusion); + } + if !proof_types.is_empty() { + spawn_prover(true, proof_types.as_slice()).await; + } + + Self { + state_merkle_trees, + address_merkle_trees, + compressed_accounts: Vec::new(), + nullified_compressed_accounts: Vec::new(), + token_compressed_accounts: Vec::new(), + token_nullified_compressed_accounts: Vec::new(), + events: Vec::new(), + proof_types, + _rpc: PhantomData, + } + } + + pub fn add_address_merkle_tree_bundle( + accounts: &AddressMerkleTreeAccounts, + // TODO: add config here + ) -> AddressMerkleTreeBundle { + let mut merkle_tree = Box::new( + IndexedMerkleTree::::new( + ADDRESS_MERKLE_TREE_HEIGHT, + ADDRESS_MERKLE_TREE_CANOPY_DEPTH, + ) + .unwrap(), + ); + merkle_tree.init().unwrap(); + let mut indexed_array = Box::>::default(); + indexed_array.init().unwrap(); + AddressMerkleTreeBundle { + merkle_tree, + indexed_array, + accounts: *accounts, + rollover_fee: FeeConfig::default().address_queue_rollover, + } + } + + async fn process_inclusion_proofs( + &self, + merkle_tree_pubkeys: &[Pubkey], + accounts: &[[u8; 32]], + rpc: &mut R, + ) -> (BatchInclusionJsonStruct, Vec) { + let mut inclusion_proofs = Vec::new(); + let mut root_indices = Vec::new(); + + for (i, account) in accounts.iter().enumerate() { + let merkle_tree = &self + .state_merkle_trees + .iter() + .find(|x| x.accounts.merkle_tree == merkle_tree_pubkeys[i]) + .unwrap() + .merkle_tree; + let leaf_index = merkle_tree.get_leaf_index(account).unwrap(); + let proof = merkle_tree.get_proof_of_leaf(leaf_index, true).unwrap(); + inclusion_proofs.push(InclusionMerkleProofInputs { + root: BigInt::from_be_bytes(merkle_tree.root().as_slice()), + leaf: BigInt::from_be_bytes(account), + path_index: BigInt::from_be_bytes(leaf_index.to_be_bytes().as_slice()), + path_elements: proof.iter().map(|x| BigInt::from_be_bytes(x)).collect(), + }); + let onchain_merkle_tree = rpc + .get_state_merkle_tree(merkle_tree_pubkeys[i]) + .await + .unwrap(); + + root_indices.push(onchain_merkle_tree.root_index() as u16); + } + + let inclusion_proof_inputs = InclusionProofInputs(inclusion_proofs.as_slice()); + let batch_inclusion_proof_inputs = + BatchInclusionJsonStruct::from_inclusion_proof_inputs(&inclusion_proof_inputs); + + (batch_inclusion_proof_inputs, root_indices) + } + + async fn process_non_inclusion_proofs( + &self, + address_merkle_tree_pubkeys: &[Pubkey], + addresses: &[[u8; 32]], + rpc: &mut R, + ) -> (BatchNonInclusionJsonStruct, Vec) { + let mut non_inclusion_proofs = Vec::new(); + let mut address_root_indices = Vec::new(); + for (i, address) in addresses.iter().enumerate() { + let address_tree = &self + .address_merkle_trees + .iter() + .find(|x| x.accounts.merkle_tree == address_merkle_tree_pubkeys[i]) + .unwrap(); + let proof_inputs = get_non_inclusion_proof_inputs( + address, + &address_tree.merkle_tree, + &address_tree.indexed_array, + ); + non_inclusion_proofs.push(proof_inputs); + let onchain_address_merkle_tree = rpc + .get_address_merkle_tree(address_merkle_tree_pubkeys[i]) + .await + .unwrap(); + address_root_indices.push(onchain_address_merkle_tree.root_index() as u16); + } + + let non_inclusion_proof_inputs = NonInclusionProofInputs(non_inclusion_proofs.as_slice()); + let batch_non_inclusion_proof_inputs = + BatchNonInclusionJsonStruct::from_non_inclusion_proof_inputs( + &non_inclusion_proof_inputs, + ); + (batch_non_inclusion_proof_inputs, address_root_indices) + } + + /// deserializes an event + /// adds the output_compressed_accounts to the compressed_accounts + /// removes the input_compressed_accounts from the compressed_accounts + /// adds the input_compressed_accounts to the nullified_compressed_accounts + /// deserialiazes token data from the output_compressed_accounts + /// adds the token_compressed_accounts to the token_compressed_accounts + pub fn add_compressed_accounts_with_token_data(&mut self, event: &PublicTransactionEvent) { + self.add_event_and_compressed_accounts(event); + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index c3b26435bf..955c9b1ee5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1,3 +1,4 @@ +pub mod indexer; pub mod rpc; pub mod rpc_pool; pub mod transaction_params; diff --git a/client/src/rpc/merkle_tree.rs b/client/src/rpc/merkle_tree.rs new file mode 100644 index 0000000000..45b800776d --- /dev/null +++ b/client/src/rpc/merkle_tree.rs @@ -0,0 +1,53 @@ +use std::mem; + +use async_trait::async_trait; +use light_concurrent_merkle_tree::{ + copy::ConcurrentMerkleTreeCopy, errors::ConcurrentMerkleTreeError, light_hasher::Poseidon, +}; +use light_indexed_merkle_tree::{copy::IndexedMerkleTreeCopy, errors::IndexedMerkleTreeError}; +use light_sdk::state::MerkleTreeMetadata; +use solana_sdk::pubkey::Pubkey; +use thiserror::Error; + +use super::{RpcConnection, RpcError}; + +#[derive(Error, Debug)] +pub enum MerkleTreeExtError { + #[error(transparent)] + Rpc(#[from] RpcError), + + #[error(transparent)] + ConcurrentMerkleTree(#[from] ConcurrentMerkleTreeError), + + #[error(transparent)] + IndexedMerkleTree(#[from] IndexedMerkleTreeError), +} + +/// Extension to the RPC connection which provides convenience utilities for +/// fetching Merkle trees. +#[async_trait] +pub trait MerkleTreeExt: RpcConnection { + async fn get_state_merkle_tree( + &mut self, + pubkey: Pubkey, + ) -> Result, MerkleTreeExtError> { + let account = self.get_account(pubkey).await?.unwrap(); + let tree = ConcurrentMerkleTreeCopy::from_bytes_copy( + &account.data[8 + mem::size_of::()..], + )?; + + Ok(tree) + } + + async fn get_address_merkle_tree( + &mut self, + pubkey: Pubkey, + ) -> Result, MerkleTreeExtError> { + let account = self.get_account(pubkey).await?.unwrap(); + let tree = IndexedMerkleTreeCopy::from_bytes_copy( + &account.data[8 + mem::size_of::()..], + )?; + + Ok(tree) + } +} diff --git a/client/src/rpc/mod.rs b/client/src/rpc/mod.rs index 2eab66981b..a8a9559adc 100644 --- a/client/src/rpc/mod.rs +++ b/client/src/rpc/mod.rs @@ -1,6 +1,8 @@ pub mod errors; +pub mod merkle_tree; pub mod rpc_connection; pub mod solana_rpc; +pub mod test_rpc; pub use errors::{assert_rpc_error, RpcError}; pub use rpc_connection::RpcConnection; diff --git a/client/src/rpc/test_rpc.rs b/client/src/rpc/test_rpc.rs new file mode 100644 index 0000000000..c39ffa7d9f --- /dev/null +++ b/client/src/rpc/test_rpc.rs @@ -0,0 +1,310 @@ +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use borsh::BorshDeserialize; +use solana_banks_client::BanksClientError; +use solana_program_test::ProgramTestContext; +use solana_sdk::{ + account::{Account, AccountSharedData}, + clock::Slot, + commitment_config::CommitmentConfig, + epoch_info::EpochInfo, + hash::Hash, + instruction::{Instruction, InstructionError}, + pubkey::Pubkey, + signature::{Keypair, Signature, Signer}, + system_instruction, + transaction::{Transaction, TransactionError}, +}; + +use crate::transaction_params::TransactionParams; + +use super::{merkle_tree::MerkleTreeExt, RpcConnection, RpcError}; + +pub struct ProgramTestRpcConnection { + pub context: ProgramTestContext, +} + +impl Debug for ProgramTestRpcConnection { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ProgramTestRpcConnection") + } +} + +#[async_trait] +impl RpcConnection for ProgramTestRpcConnection { + fn new(_url: U, _commitment_config: Option) -> Self + where + Self: Sized, + { + unimplemented!() + } + + fn get_payer(&self) -> &Keypair { + &self.context.payer + } + + fn get_url(&self) -> String { + unimplemented!("get_url doesn't make sense for ProgramTestRpcConnection") + } + + async fn health(&self) -> Result<(), RpcError> { + unimplemented!() + } + + async fn get_block_time(&self, _slot: u64) -> Result { + unimplemented!() + } + + async fn get_epoch_info(&self) -> Result { + unimplemented!() + } + + async fn get_program_accounts( + &self, + _program_id: &Pubkey, + ) -> Result, RpcError> { + unimplemented!("get_program_accounts") + } + + async fn process_transaction( + &mut self, + transaction: Transaction, + ) -> Result { + let sig = *transaction.signatures.first().unwrap(); + let result = self + .context + .banks_client + .process_transaction_with_metadata(transaction) + .await + .map_err(RpcError::from)?; + result.result.map_err(RpcError::TransactionError)?; + Ok(sig) + } + + async fn process_transaction_with_context( + &mut self, + transaction: Transaction, + ) -> Result<(Signature, Slot), RpcError> { + let sig = *transaction.signatures.first().unwrap(); + let result = self + .context + .banks_client + .process_transaction_with_metadata(transaction) + .await + .map_err(RpcError::from)?; + result.result.map_err(RpcError::TransactionError)?; + let slot = self.context.banks_client.get_root_slot().await?; + Ok((sig, slot)) + } + + async fn create_and_send_transaction_with_event( + &mut self, + instruction: &[Instruction], + payer: &Pubkey, + signers: &[&Keypair], + transaction_params: Option, + ) -> Result, RpcError> + where + T: BorshDeserialize + Send + Debug, + { + let pre_balance = self + .context + .banks_client + .get_account(*payer) + .await? + .unwrap() + .lamports; + + let transaction = Transaction::new_signed_with_payer( + instruction, + Some(payer), + signers, + self.context.get_new_latest_blockhash().await?, + ); + + let signature = transaction.signatures[0]; + // Simulate the transaction. Currently, in banks-client/server, only + // simulations are able to track CPIs. Therefore, simulating is the + // only way to retrieve the event. + let simulation_result = self + .context + .banks_client + .simulate_transaction(transaction.clone()) + .await?; + // Handle an error nested in the simulation result. + if let Some(Err(e)) = simulation_result.result { + let error = match e { + TransactionError::InstructionError(_, _) => RpcError::TransactionError(e), + _ => RpcError::from(BanksClientError::TransactionError(e)), + }; + return Err(error); + } + + // Retrieve the event. + let event = simulation_result + .simulation_details + .and_then(|details| details.inner_instructions) + .and_then(|instructions| { + instructions.iter().flatten().find_map(|inner_instruction| { + T::try_from_slice(inner_instruction.instruction.data.as_slice()).ok() + }) + }); + // If transaction was successful, execute it. + if let Some(Ok(())) = simulation_result.result { + let result = self + .context + .banks_client + .process_transaction(transaction) + .await; + if let Err(e) = result { + let error = RpcError::from(e); + return Err(error); + } + } + + // assert correct rollover fee and network_fee distribution + if let Some(transaction_params) = transaction_params { + let mut deduped_signers = signers.to_vec(); + deduped_signers.dedup(); + let post_balance = self.get_account(*payer).await?.unwrap().lamports; + + // a network_fee is charged if there are input compressed accounts or new addresses + let mut network_fee: i64 = 0; + if transaction_params.num_input_compressed_accounts != 0 { + network_fee += transaction_params.fee_config.network_fee as i64; + } + if transaction_params.num_new_addresses != 0 { + network_fee += transaction_params.fee_config.address_network_fee as i64; + } + let expected_post_balance = pre_balance as i64 + - i64::from(transaction_params.num_new_addresses) + * transaction_params.fee_config.address_queue_rollover as i64 + - i64::from(transaction_params.num_output_compressed_accounts) + * transaction_params.fee_config.state_merkle_tree_rollover as i64 + - transaction_params.compress + - transaction_params.fee_config.solana_network_fee * deduped_signers.len() as i64 + - network_fee; + + if post_balance as i64 != expected_post_balance { + println!("transaction_params: {:?}", transaction_params); + println!("pre_balance: {}", pre_balance); + println!("post_balance: {}", post_balance); + println!("expected post_balance: {}", expected_post_balance); + println!( + "diff post_balance: {}", + post_balance as i64 - expected_post_balance + ); + println!( + "rollover fee: {}", + transaction_params.fee_config.state_merkle_tree_rollover + ); + println!( + "address_network_fee: {}", + transaction_params.fee_config.address_network_fee + ); + println!("network_fee: {}", network_fee); + println!("num signers {}", deduped_signers.len()); + return Err(RpcError::from(BanksClientError::TransactionError( + TransactionError::InstructionError(0, InstructionError::Custom(11111)), + ))); + } + } + + let slot = self.context.banks_client.get_root_slot().await?; + let result = event.map(|event| (event, signature, slot)); + Ok(result) + } + + async fn confirm_transaction(&self, _transaction: Signature) -> Result { + Ok(true) + } + + async fn get_account(&mut self, address: Pubkey) -> Result, RpcError> { + self.context + .banks_client + .get_account(address) + .await + .map_err(RpcError::from) + } + + fn set_account(&mut self, address: &Pubkey, account: &AccountSharedData) { + self.context.set_account(address, account); + } + + async fn get_minimum_balance_for_rent_exemption( + &mut self, + data_len: usize, + ) -> Result { + let rent = self + .context + .banks_client + .get_rent() + .await + .map_err(RpcError::from); + + Ok(rent?.minimum_balance(data_len)) + } + + async fn airdrop_lamports( + &mut self, + to: &Pubkey, + lamports: u64, + ) -> Result { + // Create a transfer instruction + let transfer_instruction = + system_instruction::transfer(&self.context.payer.pubkey(), to, lamports); + let latest_blockhash = self.get_latest_blockhash().await.unwrap(); + // Create and sign a transaction + let transaction = Transaction::new_signed_with_payer( + &[transfer_instruction], + Some(&self.get_payer().pubkey()), + &vec![&self.get_payer()], + latest_blockhash, + ); + let sig = *transaction.signatures.first().unwrap(); + + // Send the transaction + self.context + .banks_client + .process_transaction(transaction) + .await?; + + Ok(sig) + } + + async fn get_balance(&mut self, pubkey: &Pubkey) -> Result { + self.context + .banks_client + .get_balance(*pubkey) + .await + .map_err(RpcError::from) + } + + async fn get_latest_blockhash(&mut self) -> Result { + self.context + .get_new_latest_blockhash() + .await + .map_err(|e| RpcError::from(BanksClientError::from(e))) + } + + async fn get_slot(&mut self) -> Result { + self.context + .banks_client + .get_root_slot() + .await + .map_err(RpcError::from) + } + + async fn warp_to_slot(&mut self, slot: Slot) -> Result<(), RpcError> { + self.context + .warp_to_slot(slot) + .map_err(|_| RpcError::InvalidWarpSlot) + } + + async fn send_transaction(&self, _transaction: &Transaction) -> Result { + unimplemented!("send transaction is unimplemented for ProgramTestRpcConnection") + } +} + +impl MerkleTreeExt for ProgramTestRpcConnection {} diff --git a/client/src/rpc_pool.rs b/client/src/rpc_pool.rs index 11ae112ae9..f1d01c2066 100644 --- a/client/src/rpc_pool.rs +++ b/client/src/rpc_pool.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use bb8::{Pool, PooledConnection}; use solana_sdk::commitment_config::CommitmentConfig; use std::time::Duration; @@ -32,7 +33,7 @@ impl SolanaConnectionManager { } } -#[async_trait::async_trait] +#[async_trait] impl bb8::ManageConnection for SolanaConnectionManager { type Connection = R; type Error = PoolError; diff --git a/examples/name-service/programs/name-service/Cargo.toml b/examples/name-service/programs/name-service/Cargo.toml index b7430160a7..6233011b85 100644 --- a/examples/name-service/programs/name-service/Cargo.toml +++ b/examples/name-service/programs/name-service/Cargo.toml @@ -15,29 +15,26 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap"] -default = ["custom-heap"] +default = ["idl-build"] test-sbf = [] bench-sbf = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] [dependencies] anchor-lang = { workspace = true, features = ["init-if-needed"] } -borsh = "0.10" -light-compressed-token = { path = "../../../../programs/compressed-token", version = "1.0.0", features = ["cpi"] } -light-system-program = { path = "../../../../programs/system", version = "1.0.0", features = ["cpi"]} -account-compression = { path = "../../../../programs/account-compression", version = "1.0.0", features = ["cpi"] } -light-hasher = { path = "../../../../merkle-tree/hasher", version = "1.0.0" } -light-heap = { path = "../../../../heap", version = "1.0.0", optional = true } -light-macros = { path = "../../../../macros/light", version = "1.0.0" } -light-sdk-macros = { path = "../../../../macros/light-sdk-macros", version = "0.1.0" } -light-sdk = { path = "../../../../sdk", version = "0.8.0" } -light-utils = { path = "../../../../utils", version = "1.0.0" } -light-verifier = { path = "../../../../circuit-lib/verifier", version = "1.0.0" } +borsh = { workspace = true } +light-hasher = { workspace = true, features = ["solana"] } +light-macros = { workspace = true } +light-sdk = { workspace = true } +light-sdk-macros = { workspace = true } +light-utils = { workspace = true } +light-verifier = { workspace = true } [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = { workspace = true } [dev-dependencies] +light-client = { workspace = true } light-test-utils = { path = "../../../../test-utils", version = "1.0.0" } solana-program-test = { workspace = true } tokio = "1.36.0" diff --git a/examples/name-service/programs/name-service/src/lib.rs b/examples/name-service/programs/name-service/src/lib.rs index 1f7c11990c..25c43e9802 100644 --- a/examples/name-service/programs/name-service/src/lib.rs +++ b/examples/name-service/programs/name-service/src/lib.rs @@ -4,11 +4,9 @@ use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use light_hasher::bytes::AsByteVec; use light_sdk::{ - compressed_account::LightAccount, - light_account, light_accounts, light_program, - merkle_context::{PackedAddressMerkleContext, PackedMerkleContext}, + compressed_account::LightAccount, light_account, light_accounts, light_program, + merkle_context::PackedAddressMerkleContext, }; -use light_system_program::invoke::processor::CompressedProof; declare_id!("7yucc7fL3JGbyMwg4neUaenNSdySS39hbAk89Ao3t1Hz"); diff --git a/examples/name-service/programs/name-service/tests/test.rs b/examples/name-service/programs/name-service/tests/test.rs index ac52dfb155..33ee8eb17e 100644 --- a/examples/name-service/programs/name-service/tests/test.rs +++ b/examples/name-service/programs/name-service/tests/test.rs @@ -2,19 +2,22 @@ use std::net::{Ipv4Addr, Ipv6Addr}; -use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; -use light_sdk::address::derive_address_seed; +use light_client::indexer::test_indexer::TestIndexer; +use light_client::indexer::{AddressMerkleTreeAccounts, Indexer, StateMerkleTreeAccounts}; +use light_client::rpc::merkle_tree::MerkleTreeExt; +use light_client::rpc::test_rpc::ProgramTestRpcConnection; +use light_sdk::address::{derive_address, derive_address_seed}; +use light_sdk::compressed_account::CompressedAccountWithMerkleContext; use light_sdk::merkle_context::{ pack_address_merkle_context, pack_merkle_context, AddressMerkleContext, MerkleContext, - PackedAddressMerkleContext, RemainingAccounts, + PackedAddressMerkleContext, PackedMerkleContext, RemainingAccounts, }; -use light_system_program::sdk::address::derive_address; -use light_system_program::sdk::compressed_account::CompressedAccountWithMerkleContext; -use light_test_utils::indexer::test_indexer::TestIndexer; -use light_test_utils::rpc::ProgramTestRpcConnection; -use light_test_utils::test_env::{setup_test_programs_with_accounts, EnvAccounts}; -use light_test_utils::{Indexer, RpcConnection, RpcError}; +use light_sdk::utils::get_cpi_authority_pda; +use light_sdk::verify::find_cpi_signer; +use light_sdk::{PROGRAM_ID_ACCOUNT_COMPRESSION, PROGRAM_ID_LIGHT_SYSTEM, PROGRAM_ID_NOOP}; +use light_test_utils::test_env::{setup_test_programs_with_accounts_v2, EnvAccounts}; +use light_test_utils::{RpcConnection, RpcError}; use name_service::{CustomError, NameRecord, RData}; use solana_sdk::instruction::{Instruction, InstructionError}; use solana_sdk::native_token::LAMPORTS_PER_SOL; @@ -22,26 +25,44 @@ use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signer}; use solana_sdk::transaction::{Transaction, TransactionError}; -fn find_cpi_signer() -> (Pubkey, u8) { - Pubkey::find_program_address([CPI_AUTHORITY_PDA_SEED].as_slice(), &name_service::ID) -} - #[tokio::test] async fn test_name_service() { - let (mut rpc, env) = setup_test_programs_with_accounts(Some(vec![( + let (mut rpc, env) = setup_test_programs_with_accounts_v2(Some(vec![( String::from("name_service"), name_service::ID, )])) .await; let payer = rpc.get_payer().insecure_clone(); - let mut test_indexer: TestIndexer = - TestIndexer::init_from_env(&payer, &env, true, true).await; + // let mut test_indexer: TestIndexer = + // TestIndexer::init_from_env(&payer, &env, true, true).await; + let mut test_indexer: TestIndexer = TestIndexer::new( + &[StateMerkleTreeAccounts { + merkle_tree: env.merkle_tree_pubkey, + nullifier_queue: env.nullifier_queue_pubkey, + cpi_context: env.cpi_context_account_pubkey, + }], + &[AddressMerkleTreeAccounts { + merkle_tree: env.address_merkle_tree_pubkey, + queue: env.address_merkle_tree_queue_pubkey, + }], + true, + true, + ) + .await; let name = "example.io"; let mut remaining_accounts = RemainingAccounts::default(); + let merkle_context = MerkleContext { + merkle_tree_pubkey: env.merkle_tree_pubkey, + nullifier_queue_pubkey: env.nullifier_queue_pubkey, + leaf_index: 0, + queue_index: None, + }; + let merkle_context = pack_merkle_context(merkle_context, &mut remaining_accounts); + let address_merkle_context = AddressMerkleContext { address_merkle_tree_pubkey: env.address_merkle_tree_pubkey, address_queue_pubkey: env.address_merkle_tree_queue_pubkey, @@ -52,17 +73,15 @@ async fn test_name_service() { &name_service::ID, &address_merkle_context, ); - println!("ADDRESS_SEED: {address_seed:?}"); - let address = derive_address(&env.address_merkle_tree_pubkey, &address_seed).unwrap(); + let address = derive_address(&address_seed, &address_merkle_context); let address_merkle_context = pack_address_merkle_context(address_merkle_context, &mut remaining_accounts); - let account_compression_authority = - light_system_program::utils::get_cpi_authority_pda(&light_system_program::ID); + let account_compression_authority = get_cpi_authority_pda(&PROGRAM_ID_LIGHT_SYSTEM); let registered_program_pda = Pubkey::find_program_address( - &[light_system_program::ID.to_bytes().as_slice()], - &account_compression::ID, + &[PROGRAM_ID_LIGHT_SYSTEM.to_bytes().as_slice()], + &PROGRAM_ID_ACCOUNT_COMPRESSION, ) .0; @@ -77,6 +96,7 @@ async fn test_name_service() { &mut remaining_accounts, &payer, &address, + &merkle_context, &address_merkle_context, &account_compression_authority, ®istered_program_pda, @@ -193,7 +213,7 @@ async fn test_name_service() { .unwrap(); } -async fn create_record( +async fn create_record( name: &str, rdata: &RData, rpc: &mut R, @@ -202,10 +222,13 @@ async fn create_record( remaining_accounts: &mut RemainingAccounts, payer: &Keypair, address: &[u8; 32], + merkle_context: &PackedMerkleContext, address_merkle_context: &PackedAddressMerkleContext, account_compression_authority: &Pubkey, registered_program_pda: &Pubkey, -) { +) where + R: RpcConnection + MerkleTreeExt, +{ let rpc_result = test_indexer .create_proof_for_compressed_accounts( None, @@ -216,18 +239,10 @@ async fn create_record( ) .await; - let merkle_context = MerkleContext { - merkle_tree_pubkey: env.merkle_tree_pubkey, - nullifier_queue_pubkey: env.nullifier_queue_pubkey, - leaf_index: 0, - queue_index: None, - }; - let merkle_context = pack_merkle_context(merkle_context, remaining_accounts); - let instruction_data = name_service::instruction::CreateRecord { inputs: Vec::new(), proof: rpc_result.proof, - merkle_context, + merkle_context: *merkle_context, merkle_tree_root_index: 0, address_merkle_context: *address_merkle_context, address_merkle_tree_root_index: rpc_result.address_root_indices[0], @@ -235,15 +250,15 @@ async fn create_record( rdata: rdata.clone(), }; - let (cpi_signer, _) = find_cpi_signer(); + let cpi_signer = find_cpi_signer(&name_service::ID); let accounts = name_service::accounts::CreateRecord { signer: payer.pubkey(), - light_system_program: light_system_program::ID, - account_compression_program: account_compression::ID, + light_system_program: PROGRAM_ID_LIGHT_SYSTEM, + account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, account_compression_authority: *account_compression_authority, registered_program_pda: *registered_program_pda, - noop_program: Pubkey::new_from_array(account_compression::utils::constants::NOOP_PUBKEY), + noop_program: PROGRAM_ID_NOOP, self_program: name_service::ID, cpi_signer, system_program: solana_sdk::system_program::id(), @@ -265,7 +280,7 @@ async fn create_record( test_indexer.add_compressed_accounts_with_token_data(&event.0); } -async fn update_record( +async fn update_record( rpc: &mut R, test_indexer: &mut TestIndexer, remaining_accounts: &mut RemainingAccounts, @@ -275,7 +290,10 @@ async fn update_record( address_merkle_context: &PackedAddressMerkleContext, account_compression_authority: &Pubkey, registered_program_pda: &Pubkey, -) -> Result<(), RpcError> { +) -> Result<(), RpcError> +where + R: RpcConnection + MerkleTreeExt, +{ let hash = compressed_account.hash().unwrap(); let merkle_tree_pubkey = compressed_account.merkle_context.merkle_tree_pubkey; @@ -310,15 +328,15 @@ async fn update_record( new_rdata: new_rdata.clone(), }; - let (cpi_signer, _) = find_cpi_signer(); + let cpi_signer = find_cpi_signer(&name_service::ID); let accounts = name_service::accounts::UpdateRecord { signer: payer.pubkey(), - light_system_program: light_system_program::ID, - account_compression_program: account_compression::ID, + light_system_program: PROGRAM_ID_LIGHT_SYSTEM, + account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, account_compression_authority: *account_compression_authority, registered_program_pda: *registered_program_pda, - noop_program: Pubkey::new_from_array(account_compression::utils::constants::NOOP_PUBKEY), + noop_program: PROGRAM_ID_NOOP, self_program: name_service::ID, cpi_signer, system_program: solana_sdk::system_program::id(), @@ -339,7 +357,7 @@ async fn update_record( Ok(()) } -async fn delete_record( +async fn delete_record( rpc: &mut R, test_indexer: &mut TestIndexer, remaining_accounts: &mut RemainingAccounts, @@ -348,7 +366,10 @@ async fn delete_record( address_merkle_context: &PackedAddressMerkleContext, account_compression_authority: &Pubkey, registered_program_pda: &Pubkey, -) -> Result<(), RpcError> { +) -> Result<(), RpcError> +where + R: RpcConnection + MerkleTreeExt, +{ let hash = compressed_account.hash().unwrap(); let merkle_tree_pubkey = compressed_account.merkle_context.merkle_tree_pubkey; @@ -382,15 +403,15 @@ async fn delete_record( address_merkle_tree_root_index: 0, }; - let (cpi_signer, _) = find_cpi_signer(); + let cpi_signer = find_cpi_signer(&name_service::ID); let accounts = name_service::accounts::DeleteRecord { signer: payer.pubkey(), - light_system_program: light_system_program::ID, - account_compression_program: account_compression::ID, + light_system_program: PROGRAM_ID_LIGHT_SYSTEM, + account_compression_program: PROGRAM_ID_ACCOUNT_COMPRESSION, account_compression_authority: *account_compression_authority, registered_program_pda: *registered_program_pda, - noop_program: Pubkey::new_from_array(account_compression::utils::constants::NOOP_PUBKEY), + noop_program: PROGRAM_ID_NOOP, self_program: name_service::ID, cpi_signer, system_program: solana_sdk::system_program::id(), diff --git a/examples/token-escrow/programs/token-escrow/Cargo.toml b/examples/token-escrow/programs/token-escrow/Cargo.toml index 72255f46c6..dada45c6e3 100644 --- a/examples/token-escrow/programs/token-escrow/Cargo.toml +++ b/examples/token-escrow/programs/token-escrow/Cargo.toml @@ -26,7 +26,7 @@ light-system-program = { path = "../../../../programs/system", version = "1.0.0" account-compression = { path = "../../../../programs/account-compression", version = "1.0.0", features = ["cpi"] } light-hasher = { path = "../../../../merkle-tree/hasher", version = "1.0.0" } light-verifier = { path = "../../../../circuit-lib/verifier", version = "1.0.0" } -light-sdk = { path = "../../../../sdk", version = "0.8.0", features = ["cpi"] } +light-sdk = { workspace = true, features = ["legacy"] } [target.'cfg(not(target_os = "solana"))'.dependencies] solana-sdk = { workspace = true } diff --git a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs index d92a5f0f7f..afd0c0f8b3 100644 --- a/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs +++ b/examples/token-escrow/programs/token-escrow/src/escrow_with_compressed_pda/escrow.rs @@ -10,11 +10,10 @@ use light_compressed_token::{ }; use light_hasher::{errors::HasherError, DataHasher, Hasher, Poseidon}; use light_sdk::{ - light_system_accounts, utils::create_cpi_inputs_for_new_account, verify::verify, LightTraits, + legacy::create_cpi_inputs_for_new_account, light_system_accounts, verify::verify, LightTraits, }; use light_system_program::{ invoke::processor::CompressedProof, - invoke_cpi::account::CpiContextAccount, sdk::{ address::derive_address, compressed_account::{CompressedAccount, CompressedAccountData, PackedMerkleContext}, @@ -39,7 +38,7 @@ pub struct EscrowCompressedTokensWithCompressedPda<'info> { /// CHECK: #[cpi_context] #[account(mut)] - pub cpi_context_account: Account<'info, CpiContextAccount>, + pub cpi_context_account: AccountInfo<'info>, #[authority] #[account(seeds = [CPI_AUTHORITY_PDA_SEED], bump)] pub cpi_authority_pda: AccountInfo<'info>, diff --git a/forester-utils/Cargo.toml b/forester-utils/Cargo.toml index 7a2618022f..5886f0d121 100644 --- a/forester-utils/Cargo.toml +++ b/forester-utils/Cargo.toml @@ -45,8 +45,8 @@ thiserror = "1.0" log = "0.4" # Big numbers -num-bigint = "0.4.6" -num-traits = "0.2.19" +num-bigint = { workspace = true } +num-traits = { workspace = true } # HTTP client reqwest = "0.11.26" diff --git a/macros/light-sdk-macros/src/accounts.rs b/macros/light-sdk-macros/src/accounts.rs index 070950f40a..19299e08c3 100644 --- a/macros/light-sdk-macros/src/accounts.rs +++ b/macros/light-sdk-macros/src/accounts.rs @@ -22,21 +22,12 @@ pub(crate) fn process_light_system_accounts(input: ItemStruct) -> Result", - ), + ("light_system_program", "AccountInfo<'info>"), ("system_program", "Program<'info, System>"), - ( - "account_compression_program", - "Program<'info, ::account_compression::program::AccountCompression>", - ), + ("account_compression_program", "AccountInfo<'info>"), ]; let fields_to_add_check = [ - ( - "registered_program_pda", - "Account<'info, ::account_compression::RegisteredProgram>", - ), + ("registered_program_pda", "AccountInfo<'info>"), ("noop_program", "AccountInfo<'info>"), ("account_compression_authority", "AccountInfo<'info>"), ]; @@ -582,7 +573,7 @@ pub(crate) fn process_light_accounts_derive(input: ItemStruct) -> Result Vec<::light_sdk::compressed_account::NewAddressParamsPacked> { + fn new_address_params(&self) -> Vec<::light_sdk::address::NewAddressParamsPacked> { let mut new_address_params = Vec::new(); #(#new_address_params_calls)* new_address_params diff --git a/macros/light-sdk-macros/src/lib.rs b/macros/light-sdk-macros/src/lib.rs index e461311c99..0e186e2dee 100644 --- a/macros/light-sdk-macros/src/lib.rs +++ b/macros/light-sdk-macros/src/lib.rs @@ -108,12 +108,12 @@ pub fn light_accounts_derive(input: TokenStream) -> TokenStream { /// #[authority] /// pub user: AccountInfo<'info>, /// #[cpi_context] -/// pub cpi_context_account: Account<'info, CpiContextAccount>, -/// pub light_system_program: Program<'info, LightSystemProgram>, -/// pub registered_program_pda: Account<'info, RegisteredProgram>, +/// pub cpi_context_account: AccountInfo<'info>, +/// pub light_system_program: AccountInfo<'info>, +/// pub registered_program_pda: AccountInfo<'info>, /// pub noop_program: AccountInfo<'info>, /// pub account_compression_authority: AccountInfo<'info>, -/// pub account_compression_program: Program<'info, AccountCompression>, +/// pub account_compression_program: AccountInfo<'info>, /// pub system_program: Program<'info, System>, /// } /// ``` diff --git a/macros/light-sdk-macros/src/program.rs b/macros/light-sdk-macros/src/program.rs index 6af7304150..ed4dfa1ff2 100644 --- a/macros/light-sdk-macros/src/program.rs +++ b/macros/light-sdk-macros/src/program.rs @@ -188,14 +188,14 @@ impl VisitMut for LightProgramTransform { i.sig.inputs.insert(1, inputs_arg); // Inject Merkle context related arguments. - let proof_arg: FnArg = parse_quote! { proof: CompressedProof }; + let proof_arg: FnArg = parse_quote! { proof: ::light_sdk::proof::CompressedProof }; i.sig.inputs.insert(2, proof_arg); - let merkle_context_arg: FnArg = parse_quote! { merkle_context: PackedMerkleContext }; + let merkle_context_arg: FnArg = + parse_quote! { merkle_context: ::light_sdk::merkle_context::PackedMerkleContext }; i.sig.inputs.insert(3, merkle_context_arg); let merkle_tree_root_index_arg: FnArg = parse_quote! { merkle_tree_root_index: u16 }; i.sig.inputs.insert(4, merkle_tree_root_index_arg); - let address_merkle_context_arg: FnArg = - parse_quote! { address_merkle_context: PackedAddressMerkleContext }; + let address_merkle_context_arg: FnArg = parse_quote! { address_merkle_context: ::light_sdk::merkle_context::PackedAddressMerkleContext }; i.sig.inputs.insert(5, address_merkle_context_arg); let address_merkle_tree_root_index_arg: FnArg = parse_quote! { address_merkle_tree_root_index: u16 }; diff --git a/macros/light-sdk-macros/src/traits.rs b/macros/light-sdk-macros/src/traits.rs index 6c7261f425..8df282b64b 100644 --- a/macros/light-sdk-macros/src/traits.rs +++ b/macros/light-sdk-macros/src/traits.rs @@ -212,20 +212,14 @@ fn process_fields_and_attributes(name: &Ident, fields: FieldsNamed) -> TokenStre } } impl<'info> ::light_sdk::traits::LightSystemAccount<'info> for #name<'info> { - fn get_light_system_program(&self) -> &::anchor_lang::prelude::Program< - 'info, - ::light_system_program::program::LightSystemProgram - > { + fn get_light_system_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { &self.#light_system_program_field } } }; let invoke_accounts_impl = quote! { impl<'info> ::light_sdk::traits::InvokeAccounts<'info> for #name<'info> { - fn get_registered_program_pda(&self) -> &::anchor_lang::prelude::Account< - 'info, - ::account_compression::RegisteredProgram - > { + fn get_registered_program_pda(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { &self.#registered_program_pda_field } fn get_noop_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { @@ -234,10 +228,7 @@ fn process_fields_and_attributes(name: &Ident, fields: FieldsNamed) -> TokenStre fn get_account_compression_authority(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { &self.#account_compression_authority_field } - fn get_account_compression_program(&self) -> &::anchor_lang::prelude::Program< - 'info, - ::account_compression::program::AccountCompression - > { + fn get_account_compression_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { &self.#account_compression_program_field } fn get_system_program(&self) -> &::anchor_lang::prelude::Program<'info, System> { @@ -257,10 +248,7 @@ fn process_fields_and_attributes(name: &Ident, fields: FieldsNamed) -> TokenStre #invoke_accounts_impl impl<'info> ::light_sdk::traits::InvokeCpiContextAccount<'info> for #name<'info> { fn get_cpi_context_account(&self) -> Option< - &::anchor_lang::prelude::Account< - 'info, - ::light_system_program::invoke_cpi::account::CpiContextAccount - > + &::anchor_lang::prelude::AccountInfo<'info> > { None } @@ -272,10 +260,7 @@ fn process_fields_and_attributes(name: &Ident, fields: FieldsNamed) -> TokenStre #invoke_accounts_impl impl<'info> ::light_sdk::traits::InvokeCpiContextAccount<'info> for #name<'info> { fn get_cpi_context_account(&self) -> Option< - &::anchor_lang::prelude::Account< - 'info, - ::light_system_program::invoke_cpi::account::CpiContextAccount - > + &::anchor_lang::prelude::AccountInfo<'info> > { Some(&self.#cpi_context_account_field) } @@ -300,11 +285,11 @@ mod tests { pub payer: Signer<'info>, #[authority] pub user: AccountInfo<'info>, - pub light_system_program: Program<'info, LightSystemProgram>, - pub registered_program_pda: Account<'info, RegisteredProgram>, + pub light_system_program: AccountInfo<'info>, + pub registered_program_pda: AccountInfo<'info>, pub noop_program: AccountInfo<'info>, pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: Program<'info, AccountCompression>, + pub account_compression_program: AccountInfo<'info>, pub system_program: Program<'info, System>, } }; @@ -329,11 +314,11 @@ mod tests { pub payer: Signer<'info>, #[authority] pub user: AccountInfo<'info>, - pub light_system_program: Program<'info, LightSystemProgram>, - pub registered_program_pda: Account<'info, RegisteredProgram>, + pub light_system_program: AccountInfo<'info>, + pub registered_program_pda: AccountInfo<'info>, pub noop_program: AccountInfo<'info>, pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: Program<'info, AccountCompression>, + pub account_compression_program: AccountInfo<'info>, pub system_program: Program<'info, System>, } }; @@ -377,11 +362,11 @@ mod tests { pub my_program: Program<'info, MyProgram>, // Missing #[self_program] pub payer: Signer<'info>, // Missing #[fee_payer] pub user: AccountInfo<'info>, // Missing #[authority] - pub light_system_program: Program<'info, LightSystemProgram>, - pub registered_program_pda: Account<'info, RegisteredProgram>, + pub light_system_program: AccountInfo<'info>, + pub registered_program_pda: AccountInfo<'info>, pub noop_program: AccountInfo<'info>, pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: Program<'info, AccountCompression>, + pub account_compression_program: AccountInfo<'info>, pub system_program: Program<'info, System>, } }; diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index ceff897709..02a2343fe5 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -20,17 +20,28 @@ mem-profiling = [] default = ["custom-heap"] test-sbf = [] bench-sbf = [] +idl-build = ["anchor-lang/idl-build"] +legacy = ["account-compression", "light-system-program"] [dependencies] +# Solana +solana-program = { workspace = true } + +# Anchor +anchor-lang = { workspace = true } + +# Math and crypto +num-bigint = { workspace = true } + aligned-sized = { version = "1.0.0", path = "../macros/aligned-sized" } light-macros = { version = "1.0.0", path = "../macros/light" } light-sdk-macros = { version = "0.1.0", path = "../macros/light-sdk-macros" } -anchor-lang = { workspace = true } bytemuck = "1.17" light-hasher = { version = "1.0.0", path = "../merkle-tree/hasher" } light-heap = { version = "1.0.0", path = "../heap", optional = true } -account-compression = { version = "1.0.0", path = "../programs/account-compression", features = ["cpi"] } -light-system-program = { version = "1.0.0", path = "../programs/system", features = ["cpi"] } +light-indexed-merkle-tree = { workspace = true } +account-compression = { version = "1.0.0", path = "../programs/account-compression", features = ["cpi"], optional = true } +light-system-program = { version = "1.0.0", path = "../programs/system", features = ["cpi"], optional = true } light-concurrent-merkle-tree = { path = "../merkle-tree/concurrent", version = "1.0.0" } light-utils = { version = "1.0.0", path = "../utils" } groth16-solana = "0.0.3" @@ -44,7 +55,6 @@ solana-sdk = { workspace = true } solana-banks-interface = { workspace = true } solana-cli-output = { workspace = true } solana-program-test = { workspace = true } -solana-sdk = { workspace = true } serde_json = "1.0.114" reqwest = "0.12" tokio = { workspace = true } diff --git a/sdk/src/address.rs b/sdk/src/address.rs index fe6b988780..9e9bef1574 100644 --- a/sdk/src/address.rs +++ b/sdk/src/address.rs @@ -1,7 +1,60 @@ use anchor_lang::solana_program::pubkey::Pubkey; -use light_utils::hashv_to_bn254_field_size_be; +use borsh::{BorshDeserialize, BorshSerialize}; +use light_utils::{hash_to_bn254_field_size_be, hashv_to_bn254_field_size_be}; -use crate::merkle_context::AddressMerkleContext; +use crate::merkle_context::{AddressMerkleContext, RemainingAccounts}; + +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct NewAddressParams { + pub seed: [u8; 32], + pub address_queue_pubkey: Pubkey, + pub address_merkle_tree_pubkey: Pubkey, + pub address_merkle_tree_root_index: u16, +} + +#[derive(Debug, PartialEq, Default, Clone, Copy, BorshDeserialize, BorshSerialize)] +pub struct NewAddressParamsPacked { + pub seed: [u8; 32], + pub address_queue_account_index: u8, + pub address_merkle_tree_account_index: u8, + pub address_merkle_tree_root_index: u16, +} + +#[cfg(feature = "idl-build")] +impl anchor_lang::IdlBuild for NewAddressParamsPacked {} + +pub struct AddressWithMerkleContext { + pub address: [u8; 32], + pub address_merkle_context: AddressMerkleContext, +} + +pub fn pack_new_addresses_params( + addresses_params: &[NewAddressParams], + remaining_accounts: &mut RemainingAccounts, +) -> Vec { + addresses_params + .iter() + .map(|x| { + let address_queue_account_index = + remaining_accounts.insert_or_get(x.address_queue_pubkey); + let address_merkle_tree_account_index = + remaining_accounts.insert_or_get(x.address_merkle_tree_pubkey); + NewAddressParamsPacked { + 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 RemainingAccounts, +) -> NewAddressParamsPacked { + pack_new_addresses_params(&[address_params], remaining_accounts)[0] +} /// Derives a single address seed for a compressed account, based on the /// provided multiple `seeds`, `program_id` and `merkle_tree_pubkey`. @@ -35,3 +88,18 @@ pub fn derive_address_seed( let address = hashv_to_bn254_field_size_be(inputs.as_slice()); address } + +/// Derives an address for a compressed account, based on the provided singular +/// `seed` and `address_merkle_context`: +pub fn derive_address( + address_seed: &[u8; 32], + address_merkle_context: &AddressMerkleContext, +) -> [u8; 32] { + let merkle_tree_pubkey = address_merkle_context.address_merkle_tree_pubkey.to_bytes(); + let input = [merkle_tree_pubkey, *address_seed].concat(); + + // PANICS: Not being able to find the bump for truncating the hash is + // practically impossible. Quite frankly, we should just remove that error + // inside. + hash_to_bn254_field_size_be(input.as_slice()).unwrap().0 +} diff --git a/sdk/src/compressed_account.rs b/sdk/src/compressed_account.rs index 048da88379..63bc26f9e0 100644 --- a/sdk/src/compressed_account.rs +++ b/sdk/src/compressed_account.rs @@ -1,19 +1,18 @@ use std::ops::{Deref, DerefMut}; -use anchor_lang::prelude::{AccountInfo, Key, ProgramError, Pubkey, Result}; +use anchor_lang::prelude::{AccountInfo, ProgramError, Pubkey, Result}; use borsh::{BorshDeserialize, BorshSerialize}; -use light_hasher::{DataHasher, Discriminator, Poseidon}; -use light_system_program::sdk::address::derive_address; -pub use light_system_program::{ - sdk::compressed_account::{ - CompressedAccount, CompressedAccountData, CompressedAccountWithMerkleContext, - PackedCompressedAccountWithMerkleContext, PackedMerkleContext, +use light_hasher::{DataHasher, Discriminator, Hasher, Poseidon}; +// use light_system_program::sdk::address::derive_address; +use light_utils::hash_to_bn254_field_size_be; + +use crate::{ + address::{derive_address, NewAddressParamsPacked}, + merkle_context::{ + pack_merkle_context, MerkleContext, PackedAddressMerkleContext, PackedMerkleContext, + RemainingAccounts, }, - NewAddressParamsPacked, OutputCompressedAccountWithPackedContext, -}; - -use crate::merkle_context::{ - pack_merkle_context, PackedAddressMerkleContext, PackedMerkleOutputContext, RemainingAccounts, + program_merkle_context::unpack_address_merkle_context, }; pub trait LightAccounts: Sized { @@ -420,6 +419,114 @@ where } } +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct CompressedAccount { + pub owner: Pubkey, + pub lamports: u64, + pub address: Option<[u8; 32]>, + pub data: Option, +} + +/// Hashing scheme: +/// H(owner || leaf_index || merkle_tree_pubkey || lamports || address || data.discriminator || data.data_hash) +impl CompressedAccount { + pub fn hash_with_hashed_values( + &self, + &owner_hashed: &[u8; 32], + &merkle_tree_hashed: &[u8; 32], + leaf_index: &u32, + ) -> Result<[u8; 32]> { + let capacity = 3 + + std::cmp::min(self.lamports, 1) as usize + + self.address.is_some() as usize + + self.data.is_some() as usize * 2; + let mut vec: Vec<&[u8]> = Vec::with_capacity(capacity); + vec.push(owner_hashed.as_slice()); + + // leaf index and merkle tree pubkey are used to make every compressed account hash unique + let leaf_index = leaf_index.to_le_bytes(); + vec.push(leaf_index.as_slice()); + + vec.push(merkle_tree_hashed.as_slice()); + + // Lamports are only hashed if non-zero to safe CU + // For safety we prefix the lamports with 1 in 1 byte. + // Thus even if the discriminator has the same value as the lamports, the hash will be different. + let mut lamports_bytes = [1, 0, 0, 0, 0, 0, 0, 0, 0]; + if self.lamports != 0 { + lamports_bytes[1..].copy_from_slice(&self.lamports.to_le_bytes()); + vec.push(lamports_bytes.as_slice()); + } + + if self.address.is_some() { + vec.push(self.address.as_ref().unwrap().as_slice()); + } + + let mut discriminator_bytes = [2, 0, 0, 0, 0, 0, 0, 0, 0]; + if let Some(data) = &self.data { + discriminator_bytes[1..].copy_from_slice(&data.discriminator); + vec.push(&discriminator_bytes); + vec.push(&data.data_hash); + } + let hash = H::hashv(&vec).map_err(ProgramError::from)?; + Ok(hash) + } + + pub fn hash( + &self, + &merkle_tree_pubkey: &Pubkey, + leaf_index: &u32, + ) -> Result<[u8; 32]> { + self.hash_with_hashed_values::( + &hash_to_bn254_field_size_be(&self.owner.to_bytes()) + .unwrap() + .0, + &hash_to_bn254_field_size_be(&merkle_tree_pubkey.to_bytes()) + .unwrap() + .0, + leaf_index, + ) + } +} + +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct CompressedAccountData { + pub discriminator: [u8; 8], + pub data: Vec, + pub data_hash: [u8; 32], +} + +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct CompressedAccountWithMerkleContext { + pub compressed_account: CompressedAccount, + pub merkle_context: MerkleContext, +} + +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct PackedCompressedAccountWithMerkleContext { + pub compressed_account: CompressedAccount, + pub merkle_context: PackedMerkleContext, + /// Index of root used in inclusion validity proof. + pub root_index: u16, + /// Placeholder to mark accounts read-only unimplemented set to false. + pub read_only: bool, +} + +impl CompressedAccountWithMerkleContext { + pub fn hash(&self) -> Result<[u8; 32]> { + self.compressed_account.hash::( + &self.merkle_context.merkle_tree_pubkey, + &self.merkle_context.leaf_index, + ) + } +} + +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct OutputCompressedAccountWithPackedContext { + pub compressed_account: CompressedAccount, + pub merkle_tree_index: u8, +} + pub fn serialize_and_hash_account( account: &T, address_seed: &[u8; 32], @@ -438,11 +545,10 @@ where data_hash, }; - let address = derive_address( - &remaining_accounts[address_merkle_context.address_merkle_tree_pubkey_index as usize].key(), - address_seed, - ) - .map_err(|_| ProgramError::InvalidArgument)?; + let address_merkle_context = + unpack_address_merkle_context(*address_merkle_context, remaining_accounts); + let address = derive_address(address_seed, &address_merkle_context); + anchor_lang::prelude::msg!("ADDRESS: {:?}", address); let compressed_account = CompressedAccount { owner: *program_id, @@ -454,44 +560,6 @@ where Ok(compressed_account) } -pub fn new_compressed_account( - account: &T, - address_seed: &[u8; 32], - program_id: &Pubkey, - merkle_output_context: &PackedMerkleOutputContext, - address_merkle_context: &PackedAddressMerkleContext, - address_merkle_tree_root_index: u16, - remaining_accounts: &[AccountInfo], -) -> Result<( - OutputCompressedAccountWithPackedContext, - NewAddressParamsPacked, -)> -where - T: BorshSerialize + DataHasher + Discriminator, -{ - let compressed_account = serialize_and_hash_account( - account, - address_seed, - program_id, - address_merkle_context, - remaining_accounts, - )?; - - let compressed_account = OutputCompressedAccountWithPackedContext { - compressed_account, - merkle_tree_index: merkle_output_context.merkle_tree_pubkey_index, - }; - - let new_address_params = NewAddressParamsPacked { - seed: *address_seed, - address_merkle_tree_account_index: address_merkle_context.address_merkle_tree_pubkey_index, - address_queue_account_index: address_merkle_context.address_queue_pubkey_index, - address_merkle_tree_root_index, - }; - - Ok((compressed_account, new_address_params)) -} - pub fn input_compressed_account( account: &T, address_seed: &[u8; 32], diff --git a/sdk/src/constants.rs b/sdk/src/constants.rs new file mode 100644 index 0000000000..d995c01a51 --- /dev/null +++ b/sdk/src/constants.rs @@ -0,0 +1,27 @@ +use light_macros::pubkey; +use solana_program::pubkey::Pubkey; + +/// Seed of the CPI authority. +pub const CPI_AUTHORITY_PDA_SEED: &[u8] = b"cpi_authority"; + +/// ID of the account-compression program. +pub const PROGRAM_ID_ACCOUNT_COMPRESSION: Pubkey = + pubkey!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); +pub const PROGRAM_ID_NOOP: Pubkey = pubkey!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); +/// ID of the light-system program. +pub const PROGRAM_ID_LIGHT_SYSTEM: Pubkey = pubkey!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); +/// ID of the light-compressed-token program. +pub const PROGRAM_ID_LIGHT_TOKEN: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +pub const STATE_MERKLE_TREE_HEIGHT: usize = 26; +pub const STATE_MERKLE_TREE_CHANGELOG: usize = 1400; +pub const STATE_MERKLE_TREE_ROOTS: usize = 2400; +pub const STATE_MERKLE_TREE_CANOPY_DEPTH: usize = 10; + +pub const ADDRESS_MERKLE_TREE_HEIGHT: usize = 26; +pub const ADDRESS_MERKLE_TREE_CHANGELOG: usize = 1400; +pub const ADDRESS_MERKLE_TREE_ROOTS: usize = 2400; +pub const ADDRESS_MERKLE_TREE_CANOPY_DEPTH: usize = 10; +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]; diff --git a/sdk/src/context.rs b/sdk/src/context.rs index 6627f69fd0..5face8d40e 100644 --- a/sdk/src/context.rs +++ b/sdk/src/context.rs @@ -1,17 +1,17 @@ use std::ops::{Deref, DerefMut}; -use account_compression::utils::constants::CPI_AUTHORITY_PDA_SEED; use anchor_lang::{context::Context, prelude::Pubkey, Bumps, Key, Result}; -use light_system_program::{invoke::processor::CompressedProof, InstructionDataInvokeCpi}; use crate::{ compressed_account::LightAccounts, + constants::CPI_AUTHORITY_PDA_SEED, merkle_context::{PackedAddressMerkleContext, PackedMerkleContext}, + proof::CompressedProof, traits::{ InvokeAccounts, InvokeCpiAccounts, InvokeCpiContextAccount, LightSystemAccount, SignerAccounts, }, - verify::verify, + verify::{verify, InstructionDataInvokeCpi}, }; /// Provides non-argument inputs to the program, including light accounts and @@ -101,6 +101,7 @@ where ) .1; let signer_seeds = [CPI_AUTHORITY_PDA_SEED, &[bump]]; + anchor_lang::prelude::msg!("SIGNER_SEEDS: {:?}", signer_seeds); // TODO(vadorovsky): Remove. // self.light_accounts diff --git a/sdk/src/event.rs b/sdk/src/event.rs new file mode 100644 index 0000000000..dc491e7a12 --- /dev/null +++ b/sdk/src/event.rs @@ -0,0 +1,24 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::compressed_account::OutputCompressedAccountWithPackedContext; + +#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Default, PartialEq)] +pub struct MerkleTreeSequenceNumber { + pub pubkey: Pubkey, + pub seq: u64, +} + +#[derive(Debug, Clone, BorshDeserialize, BorshSerialize, Default, PartialEq)] +pub struct PublicTransactionEvent { + pub input_compressed_account_hashes: Vec<[u8; 32]>, + pub output_compressed_account_hashes: Vec<[u8; 32]>, + pub output_compressed_accounts: Vec, + pub output_leaf_indices: Vec, + pub sequence_numbers: Vec, + pub relay_fee: Option, + pub is_compress: bool, + pub compress_or_decompress_lamports: Option, + pub pubkey_array: Vec, + pub message: Option>, +} diff --git a/sdk/src/legacy.rs b/sdk/src/legacy.rs new file mode 100644 index 0000000000..1a609cc6c9 --- /dev/null +++ b/sdk/src/legacy.rs @@ -0,0 +1,36 @@ +#![cfg(feature = "legacy")] + +//! Legacy types re-imported from programs which should be removed as soon as +//! possible. + +pub use light_system_program::{ + invoke::processor::CompressedProof, + sdk::{ + compressed_account::{ + CompressedAccount, CompressedAccountData, CompressedAccountWithMerkleContext, + PackedCompressedAccountWithMerkleContext, PackedMerkleContext, QueueIndex, + }, + CompressedCpiContext, + }, + InstructionDataInvokeCpi, NewAddressParams, NewAddressParamsPacked, + OutputCompressedAccountWithPackedContext, +}; + +/// Helper function to create data for creating a single PDA. +pub fn create_cpi_inputs_for_new_account( + proof: CompressedProof, + new_address_params: NewAddressParamsPacked, + compressed_pda: OutputCompressedAccountWithPackedContext, + cpi_context: Option, +) -> InstructionDataInvokeCpi { + InstructionDataInvokeCpi { + proof: Some(proof), + new_address_params: vec![new_address_params], + relay_fee: None, + input_compressed_accounts_with_merkle_context: vec![], + output_compressed_accounts: vec![compressed_pda], + compress_or_decompress_lamports: None, + is_compress: false, + cpi_context, + } +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 2a9544eb18..e939fc4dfd 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -3,10 +3,17 @@ pub use light_sdk_macros::*; pub mod address; pub mod compressed_account; +pub mod constants; +pub use constants::*; pub mod context; pub mod error; +pub mod event; +pub mod legacy; pub mod merkle_context; pub mod program_merkle_context; +pub mod proof; +pub mod state; +pub mod token; pub mod traits; pub mod utils; pub mod verify; diff --git a/sdk/src/merkle_context.rs b/sdk/src/merkle_context.rs index 3eac25602a..3aba4ec17e 100644 --- a/sdk/src/merkle_context.rs +++ b/sdk/src/merkle_context.rs @@ -2,9 +2,6 @@ use std::collections::HashMap; use anchor_lang::prelude::{AccountMeta, AnchorDeserialize, AnchorSerialize, Pubkey}; -// TODO(vadorovsky): Consider moving these structs here. -pub use light_system_program::sdk::compressed_account::{MerkleContext, PackedMerkleContext}; - /// Collection of remaining accounts which are sent to the program. #[derive(Default)] pub struct RemainingAccounts { @@ -56,6 +53,34 @@ impl RemainingAccounts { } } +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Default)] +pub struct QueueIndex { + /// Id of queue in queue account. + pub queue_id: u8, + /// Index of compressed account hash in queue. + pub index: u16, +} + +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Default)] +pub struct MerkleContext { + pub merkle_tree_pubkey: Pubkey, + pub nullifier_queue_pubkey: Pubkey, + pub leaf_index: u32, + /// Index of leaf in queue. Placeholder of batched Merkle tree updates + /// currently unimplemented. + pub queue_index: Option, +} + +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Default)] +pub struct PackedMerkleContext { + pub merkle_tree_pubkey_index: u8, + pub nullifier_queue_pubkey_index: u8, + pub leaf_index: u32, + /// Index of leaf in queue. Placeholder of batched Merkle tree updates + /// currently unimplemented. + pub queue_index: Option, +} + pub fn pack_merkle_contexts( merkle_contexts: &[MerkleContext], remaining_accounts: &mut RemainingAccounts, diff --git a/sdk/src/proof.rs b/sdk/src/proof.rs new file mode 100644 index 0000000000..07ebcc33a5 --- /dev/null +++ b/sdk/src/proof.rs @@ -0,0 +1,49 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_indexed_merkle_tree::array::IndexedElement; +use num_bigint::BigUint; +use solana_program::pubkey::Pubkey; + +#[derive(Debug, Clone)] +pub struct MerkleProof { + pub hash: [u8; 32], + pub leaf_index: u64, + pub merkle_tree: Pubkey, + pub proof: Vec<[u8; 32]>, + pub root_seq: u64, +} + +// For consistency with the Photon API. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct NewAddressProofWithContext { + pub merkle_tree: Pubkey, + pub root: [u8; 32], + pub root_seq: u64, + pub low_address_index: u64, + pub low_address_value: [u8; 32], + pub low_address_next_index: u64, + pub low_address_next_value: [u8; 32], + pub low_address_proof: [[u8; 32]; 16], + pub new_low_element: Option>, + pub new_element: Option>, + pub new_element_next_value: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +pub struct CompressedProof { + pub a: [u8; 32], + pub b: [u8; 64], + pub c: [u8; 32], +} + +#[cfg(feature = "idl-build")] +impl anchor_lang::IdlBuild for CompressedProof {} + +#[derive(Debug)] +pub struct ProofRpcResult { + pub proof: CompressedProof, + pub root_indices: Vec, + pub address_root_indices: Vec, +} + +#[cfg(feature = "idl-build")] +impl anchor_lang::IdlBuild for ProofRpcResult {} diff --git a/sdk/src/state.rs b/sdk/src/state.rs new file mode 100644 index 0000000000..70691d2a07 --- /dev/null +++ b/sdk/src/state.rs @@ -0,0 +1,49 @@ +use borsh::BorshDeserialize; +use solana_program::pubkey::Pubkey; + +#[derive(BorshDeserialize, Debug, PartialEq, Default)] +pub struct MerkleTreeMetadata { + pub access_metadata: AccessMetadata, + pub rollover_metadata: RolloverMetadata, + // Queue associated with this Merkle tree. + pub associated_queue: Pubkey, + // Next Merkle tree to be used after rollover. + pub next_merkle_tree: Pubkey, +} + +#[derive(BorshDeserialize, Debug, PartialEq, Default)] +pub struct AccessMetadata { + /// Owner of the Merkle tree. + pub owner: Pubkey, + /// Program owner of the Merkle tree. This will be used for program owned Merkle trees. + pub program_owner: Pubkey, + /// Optional privileged forester pubkey, can be set for custom Merkle trees + /// without a network fee. Merkle trees without network fees are not + /// forested by light foresters. The variable is not used in the account + /// compression program but the registry program. The registry program + /// implements access control to prevent contention during forester. The + /// forester pubkey specified in this struct can bypass contention checks. + pub forester: Pubkey, +} + +#[derive(BorshDeserialize, Debug, PartialEq, Default)] +pub struct RolloverMetadata { + /// Unique index. + pub index: u64, + /// This fee is used for rent for the next account. + /// It accumulates in the account so that once the corresponding Merkle tree account is full it can be rolled over + pub rollover_fee: u64, + /// The threshold in percentage points when the account should be rolled over (95 corresponds to 95% filled). + pub rollover_threshold: u64, + /// Tip for maintaining the account. + pub network_fee: u64, + /// The slot when the account was rolled over, a rolled over account should not be written to. + pub rolledover_slot: u64, + /// If current slot is greater than rolledover_slot + close_threshold and + /// the account is empty it can be closed. No 'close' functionality has been + /// implemented yet. + pub close_threshold: u64, + /// Placeholder for bytes of additional accounts which are tied to the + /// Merkle trees operation and need to be rolled over as well. + pub additional_bytes: u64, +} diff --git a/sdk/src/token.rs b/sdk/src/token.rs new file mode 100644 index 0000000000..74c0b2354f --- /dev/null +++ b/sdk/src/token.rs @@ -0,0 +1,34 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +use crate::compressed_account::CompressedAccountWithMerkleContext; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[repr(u8)] +pub enum AccountState { + Initialized, + Frozen, +} + +#[derive(Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Clone)] +pub struct TokenData { + /// The mint associated with this account + pub mint: Pubkey, + /// The owner of this account. + pub owner: Pubkey, + /// The amount of tokens this account holds. + pub amount: u64, + /// If `delegate` is `Some` then `delegated_amount` represents + /// the amount authorized by the delegate + pub delegate: Option, + /// The account's state + pub state: AccountState, + /// Placeholder for TokenExtension tlv data (unimplemented) + pub tlv: Option>, +} + +#[derive(Debug, Clone)] +pub struct TokenDataWithMerkleContext { + pub token_data: TokenData, + pub compressed_account: CompressedAccountWithMerkleContext, +} diff --git a/sdk/src/traits.rs b/sdk/src/traits.rs index f59012d3ff..0949bed0b6 100644 --- a/sdk/src/traits.rs +++ b/sdk/src/traits.rs @@ -1,22 +1,18 @@ // Ported from light-system-program, adjusted for caller programs. -use account_compression::program::AccountCompression; use anchor_lang::prelude::*; -use light_system_program::{invoke_cpi::account::CpiContextAccount, program::LightSystemProgram}; pub trait InvokeAccounts<'info> { - fn get_registered_program_pda( - &self, - ) -> &Account<'info, account_compression::instructions::register_program::RegisteredProgram>; + fn get_registered_program_pda(&self) -> &AccountInfo<'info>; fn get_noop_program(&self) -> &AccountInfo<'info>; fn get_account_compression_authority(&self) -> &AccountInfo<'info>; - fn get_account_compression_program(&self) -> &Program<'info, AccountCompression>; + fn get_account_compression_program(&self) -> &AccountInfo<'info>; fn get_system_program(&self) -> &Program<'info, System>; fn get_compressed_sol_pda(&self) -> Option<&AccountInfo<'info>>; fn get_compression_recipient(&self) -> Option<&AccountInfo<'info>>; } pub trait LightSystemAccount<'info> { - fn get_light_system_program(&self) -> &Program<'info, LightSystemProgram>; + fn get_light_system_program(&self) -> &AccountInfo<'info>; } pub trait SignerAccounts<'info> { @@ -26,11 +22,11 @@ pub trait SignerAccounts<'info> { // Only used within the systemprogram pub trait InvokeCpiContextAccountMut<'info> { - fn get_cpi_context_account_mut(&mut self) -> &mut Option>; + fn get_cpi_context_account_mut(&mut self) -> &mut Option>; } pub trait InvokeCpiContextAccount<'info> { - fn get_cpi_context_account(&self) -> Option<&Account<'info, CpiContextAccount>>; + fn get_cpi_context_account(&self) -> Option<&AccountInfo<'info>>; } pub trait InvokeCpiAccounts<'info> { diff --git a/sdk/src/utils.rs b/sdk/src/utils.rs index 3cb6dcdd4e..7c3bbeed8e 100644 --- a/sdk/src/utils.rs +++ b/sdk/src/utils.rs @@ -1,14 +1,19 @@ use anchor_lang::solana_program::pubkey::Pubkey; -pub use light_system_program::{invoke::processor::CompressedProof, InstructionDataInvokeCpi}; -use light_system_program::{ - sdk::{compressed_account::PackedCompressedAccountWithMerkleContext, CompressedCpiContext}, - NewAddressParamsPacked, OutputCompressedAccountWithPackedContext, + +use crate::{ + address::NewAddressParamsPacked, + compressed_account::{ + OutputCompressedAccountWithPackedContext, PackedCompressedAccountWithMerkleContext, + }, + proof::CompressedProof, + verify::{CompressedCpiContext, InstructionDataInvokeCpi}, + PROGRAM_ID_ACCOUNT_COMPRESSION, }; pub fn get_registered_program_pda(program_id: &Pubkey) -> Pubkey { Pubkey::find_program_address( &[program_id.to_bytes().as_slice()], - &account_compression::ID, + &PROGRAM_ID_ACCOUNT_COMPRESSION, ) .0 } diff --git a/sdk/src/verify.rs b/sdk/src/verify.rs index 76a52aed3d..87724d637b 100644 --- a/sdk/src/verify.rs +++ b/sdk/src/verify.rs @@ -1,36 +1,47 @@ -use anchor_lang::{error::Error, prelude::*, Bumps}; +use anchor_lang::{prelude::*, Bumps}; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{instruction::Instruction, program::invoke_signed}; -use crate::traits::{ - InvokeAccounts, InvokeCpiAccounts, InvokeCpiContextAccount, LightSystemAccount, SignerAccounts, -}; -use light_system_program::{ - cpi::accounts::InvokeCpiInstruction, errors::SystemProgramError::CpiContextAccountUndefined, - sdk::CompressedCpiContext, InstructionDataInvokeCpi, +use crate::{ + address::NewAddressParamsPacked, + compressed_account::{ + OutputCompressedAccountWithPackedContext, PackedCompressedAccountWithMerkleContext, + }, + proof::CompressedProof, + traits::{ + InvokeAccounts, InvokeCpiAccounts, InvokeCpiContextAccount, LightSystemAccount, + SignerAccounts, + }, + CPI_AUTHORITY_PDA_SEED, PROGRAM_ID_LIGHT_SYSTEM, }; -// TODO: properly document compressed-cpi-context -// TODO: turn into a simple check! -// TOOD: CHECK needed bc can be different from own, if called from another program. -pub fn get_compressed_cpi_context_account<'info>( - ctx: &Context< - '_, - '_, - '_, - 'info, - impl InvokeAccounts<'info> - + LightSystemAccount<'info> - + InvokeCpiAccounts<'info> - + SignerAccounts<'info> - + Bumps, - >, - compressed_cpi_context: &CompressedCpiContext, -) -> Result> { - let cpi_context_account = ctx - .remaining_accounts - .get(compressed_cpi_context.cpi_context_account_index as usize) - .map(|account| account.to_account_info()) - .ok_or_else(|| Error::from(CpiContextAccountUndefined))?; - Ok(cpi_context_account) +pub fn find_cpi_signer(program_id: &Pubkey) -> Pubkey { + Pubkey::find_program_address([CPI_AUTHORITY_PDA_SEED].as_slice(), program_id).0 +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct CompressedCpiContext { + /// Is set by the program that is invoking the CPI to signal that is should + /// set the cpi context. + pub set_context: bool, + /// Is set to wipe the cpi context since someone could have set it before + /// with unrelated data. + pub first_set_context: bool, + /// Index of cpi context account in remaining accounts. + pub cpi_context_account_index: u8, +} + +#[derive(Debug, PartialEq, Default, Clone, BorshDeserialize, BorshSerialize)] +pub struct InstructionDataInvokeCpi { + pub proof: Option, + pub new_address_params: Vec, + pub input_compressed_accounts_with_merkle_context: + Vec, + pub output_compressed_accounts: Vec, + pub relay_fee: Option, + pub compress_or_decompress_lamports: Option, + pub is_compress: bool, + pub cpi_context: Option, } #[inline(always)] @@ -47,64 +58,165 @@ pub fn setup_cpi_accounts<'info>( + InvokeCpiContextAccount<'info> + Bumps, >, -) -> InvokeCpiInstruction<'info> { - InvokeCpiInstruction { - fee_payer: ctx.accounts.get_fee_payer().to_account_info(), - authority: ctx.accounts.get_authority().to_account_info(), - registered_program_pda: ctx.accounts.get_registered_program_pda().to_account_info(), - noop_program: ctx.accounts.get_noop_program().to_account_info(), - account_compression_authority: ctx - .accounts +) -> (Vec>, Vec) { + // The trick for having `None` accounts is to pass program ID, see + // https://github.com/coral-xyz/anchor/pull/2101 + let none_account_info = ctx.accounts.get_light_system_program().to_account_info(); + + let (cpi_context_account_info, cpi_context_account_meta) = + match ctx.accounts.get_cpi_context_account() { + Some(acc) => ( + acc.to_account_info(), + AccountMeta { + pubkey: acc.key(), + is_signer: false, + is_writable: true, + }, + ), + None => ( + none_account_info.clone(), + AccountMeta { + pubkey: ctx.accounts.get_light_system_program().key(), + is_signer: false, + is_writable: false, + }, + ), + }; + + let mut account_infos = vec![ + // fee_payer + ctx.accounts.get_fee_payer().to_account_info(), + // authority + ctx.accounts.get_authority().to_account_info(), + // registered_program_pda + ctx.accounts.get_registered_program_pda().to_account_info(), + // noop_program + ctx.accounts.get_noop_program().to_account_info(), + // account_compression_authority + ctx.accounts .get_account_compression_authority() .to_account_info(), - account_compression_program: ctx - .accounts + // account_compression_program + ctx.accounts .get_account_compression_program() .to_account_info(), - invoking_program: ctx.accounts.get_invoking_program().to_account_info(), - sol_pool_pda: None, - decompression_recipient: None, - system_program: ctx.accounts.get_system_program().to_account_info(), - cpi_context_account: ctx - .accounts - .get_cpi_context_account() - .map(|acc| acc.to_account_info()), + // invoking_program + ctx.accounts.get_invoking_program().to_account_info(), + // sol_pool_pda + none_account_info.clone(), + // decompression_recipient + none_account_info, + // system_program + ctx.accounts.get_system_program().to_account_info(), + // cpi_context_account + cpi_context_account_info, + ]; + for remaining_account in ctx.remaining_accounts { + account_infos.push(remaining_account.to_owned()); + } + + let mut account_metas = vec![ + // fee_payer + AccountMeta { + pubkey: account_infos[0].key(), + is_signer: true, + is_writable: true, + }, + // authority + AccountMeta { + pubkey: account_infos[1].key(), + is_signer: true, + is_writable: false, + }, + // registered_program_pda + AccountMeta { + pubkey: account_infos[2].key(), + is_signer: false, + is_writable: false, + }, + // noop_program + AccountMeta { + pubkey: account_infos[3].key(), + is_signer: false, + is_writable: false, + }, + // account_compression_authority + AccountMeta { + pubkey: account_infos[4].key(), + is_signer: false, + is_writable: false, + }, + // account_compression_program + AccountMeta { + pubkey: account_infos[5].key(), + is_signer: false, + is_writable: false, + }, + // invoking_program + AccountMeta { + pubkey: account_infos[6].key(), + is_signer: false, + is_writable: false, + }, + // sol_pool_pda + AccountMeta { + pubkey: account_infos[7].key(), + is_signer: false, + is_writable: false, + }, + // decompression_recipient + AccountMeta { + pubkey: account_infos[8].key(), + is_signer: false, + is_writable: false, + }, + // system_program + AccountMeta { + pubkey: account_infos[9].key(), + is_signer: false, + is_writable: false, + }, + cpi_context_account_meta, + ]; + for remaining_account in ctx.remaining_accounts { + account_metas.extend(remaining_account.to_account_metas(None)); } + + (account_infos, account_metas) +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct InvokeCpi { + pub inputs: Vec, } #[inline(always)] -pub fn invoke_cpi<'info, 'a, 'b, 'c>( - ctx: &Context< - '_, - '_, - '_, - 'info, - impl InvokeAccounts<'info> - + LightSystemAccount<'info> - + InvokeCpiAccounts<'info> - + SignerAccounts<'info> - + InvokeCpiContextAccount<'info> - + Bumps, - >, - cpi_accounts: light_system_program::cpi::accounts::InvokeCpiInstruction<'info>, +pub fn invoke_cpi( + account_infos: &[AccountInfo], + accounts_metas: Vec, inputs: Vec, - signer_seeds: &'a [&'b [&'c [u8]]], + signer_seeds: &[&[&[u8]]], ) -> Result<()> { - light_system_program::cpi::invoke_cpi( - CpiContext::new_with_signer( - ctx.accounts.get_light_system_program().to_account_info(), - cpi_accounts, - signer_seeds, - ) - .with_remaining_accounts(ctx.remaining_accounts.to_vec()), - inputs, - ) + let instruction_data = InvokeCpi { inputs }; + + // `InvokeCpi`'s discriminator + let mut data = [49, 212, 191, 129, 39, 194, 43, 196].to_vec(); + data.extend(instruction_data.try_to_vec()?); + + let instruction = Instruction { + program_id: PROGRAM_ID_LIGHT_SYSTEM, + accounts: accounts_metas, + data, + }; + invoke_signed(&instruction, account_infos, signer_seeds)?; + + Ok(()) } /// 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<'info, 'a, 'b, 'c>( +pub fn verify<'info, 'a, 'b, 'c, T>( ctx: &Context< '_, '_, @@ -117,12 +229,15 @@ pub fn verify<'info, 'a, 'b, 'c>( + InvokeCpiContextAccount<'info> + Bumps, >, - inputs_struct: &InstructionDataInvokeCpi, + inputs: &T, signer_seeds: &'a [&'b [&'c [u8]]], -) -> Result<()> { - let mut inputs: Vec = Vec::new(); - InstructionDataInvokeCpi::serialize(inputs_struct, &mut inputs).unwrap(); +) -> Result<()> +where + T: BorshSerialize, +{ + let inputs = inputs.try_to_vec()?; - let cpi_accounts = setup_cpi_accounts(ctx); - invoke_cpi(ctx, cpi_accounts, inputs, signer_seeds) + let (account_infos, account_metas) = setup_cpi_accounts(ctx); + invoke_cpi(&account_infos, account_metas, inputs, signer_seeds)?; + Ok(()) } diff --git a/test-utils/src/test_env.rs b/test-utils/src/test_env.rs index 5c027a8bbe..3e396418f9 100644 --- a/test-utils/src/test_env.rs +++ b/test-utils/src/test_env.rs @@ -287,6 +287,7 @@ pub const FORESTER_TEST_KEYPAIR: [u8; 64] = [ /// - registers a forester /// - advances to the active phase slot 2 /// - active phase doesn't end +// TODO(vadorovsky): Remove this function... pub async fn setup_test_programs_with_accounts( additional_programs: Option>, ) -> (ProgramTestRpcConnection, EnvAccounts) { @@ -305,6 +306,45 @@ pub async fn setup_test_programs_with_accounts( .await } +/// Setup test programs with accounts +/// deploys: +/// 1. light program +/// 2. account_compression program +/// 3. light_compressed_token program +/// 4. light_system_program program +/// +/// Sets up the following accounts: +/// 5. creates and initializes governance authority +/// 6. creates and initializes group authority +/// 7. registers the light_system_program program with the group authority +/// 8. initializes Merkle tree owned by +/// Note: +/// - registers a forester +/// - advances to the active phase slot 2 +/// - active phase doesn't end +// TODO(vadorovsky): ...in favor of this one. +pub async fn setup_test_programs_with_accounts_v2( + additional_programs: Option>, +) -> ( + light_client::rpc::test_rpc::ProgramTestRpcConnection, + EnvAccounts, +) { + setup_test_programs_with_accounts_with_protocol_config_v2( + additional_programs, + ProtocolConfig { + // Init with an active epoch which doesn't end + active_phase_length: 1_000_000_000, + slot_length: 1_000_000_000 - 1, + genesis_slot: 0, + registration_phase_length: 2, + ..Default::default() + }, + true, + ) + .await +} + +// TODO(vadorovsky): Remote this function... pub async fn setup_test_programs_with_accounts_with_protocol_config( additional_programs: Option>, protocol_config: ProtocolConfig, @@ -336,6 +376,41 @@ pub async fn setup_test_programs_with_accounts_with_protocol_config( (context, env_accounts) } +// TODO(vadorovsky): ...in favor of this one. +pub async fn setup_test_programs_with_accounts_with_protocol_config_v2( + additional_programs: Option>, + protocol_config: ProtocolConfig, + register_forester_and_advance_to_active_phase: bool, +) -> ( + light_client::rpc::test_rpc::ProgramTestRpcConnection, + EnvAccounts, +) { + let context = setup_test_programs(additional_programs).await; + let mut context = light_client::rpc::test_rpc::ProgramTestRpcConnection { context }; + let keypairs = EnvAccountKeypairs::program_test_default(); + // let payer = Keypair::from_bytes(&PAYER_KEYPAIR).unwrap(); + airdrop_lamports( + &mut context, + &keypairs.governance_authority.pubkey(), + 100_000_000_000, + ) + .await + .unwrap(); + // let forester = Keypair::from_bytes(&FORESTER_TEST_KEYPAIR).unwrap(); + airdrop_lamports(&mut context, &keypairs.forester.pubkey(), 10_000_000_000) + .await + .unwrap(); + let env_accounts = initialize_accounts( + &mut context, + keypairs, + protocol_config, + register_forester_and_advance_to_active_phase, + true, + ) + .await; + (context, env_accounts) +} + pub async fn setup_accounts(keypairs: EnvAccountKeypairs, url: SolanaRpcUrl) -> EnvAccounts { let mut rpc = SolanaRpcConnection::new(url, None);