diff --git a/README.md b/README.md index d671742..1294f63 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,7 @@ Lighthouse is provided "as is", with no warranties regarding its efficacy in com - Save on tranasction space by only needing to call write once. - Auto-increment validation - Check to make sure transactions are ran in sequence or they fail +- Decide on using CPI events vs program logs events + - Extra account overhead for cpi events pda + - program logs concat + - Do we even need logs :P diff --git a/macros/Cargo.lock b/macros/Cargo.lock new file mode 100644 index 0000000..22aba58 --- /dev/null +++ b/macros/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..f00ee2e --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" \ No newline at end of file diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..7b04f1e --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,119 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +// use proc_macro::TokenStream; +use quote::quote; +use syn::{ + parse_macro_input, punctuated::Punctuated, token::Comma, Data, DataStruct, DeriveInput, Field, + Fields, +}; +// use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(Optionize)] +pub fn optionize(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let optional_name = syn::Ident::new(&format!("Optional{}", name), name.span()); + let attrs = &input.attrs; + + let fields = match &input.data { + Data::Struct(data_struct) => &data_struct.fields, + _ => panic!("Optionize macro only works with structs"), + }; + + let optional_fields = fields.iter().map(|f| { + let name = &f.ident; + let ty = &f.ty; + quote! { pub #name: Option<#ty>, } + }); + + let derive_attrs: Vec<_> = attrs + .iter() + .filter(|attr| attr.path.is_ident("derive")) + .collect(); + + let expanded = quote! { + // Original struct with its attributes + // #( #attrs )* + // pub struct #name { + // #( #fields, )* + // } + + // Optional variant of the struct with the same derive attributes + #( #derive_attrs )* + pub struct #optional_name { + #( #optional_fields )* + } + }; + + TokenStream::from(expanded) +} + +#[proc_macro_derive(FieldEnum)] +pub fn field_enum(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + // Extract the struct name and data + let name = input.ident; + let data = input.data; + + match data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => { + let field_names = fields.named.iter().map(|f| &f.ident); + + // Generate enum variants from field names + let enum_name = quote::format_ident!("{}Fields", name); + let enum_tokens = quote! { + pub enum #enum_name { + #( #field_names ),* + } + }; + + // Convert generated enum into a TokenStream and return it + TokenStream::from(enum_tokens) + } + _ => panic!("FieldEnum macro only works with structs with named fields"), + } +} + +#[proc_macro_derive(FieldOffset)] +pub fn field_offset(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let struct_name = input.ident; + let field_names: Punctuated; + + let fields = if let syn::Data::Struct(data_struct) = input.data { + match data_struct.fields { + Fields::Named(fields) => { + field_names = fields.named.clone(); + field_names.iter().map(|f| { + let field_name = &f.ident; + let ty = &f.ty; + return quote! { + if field == stringify!(#field_name) { + return Some(std::mem::size_of::<#ty>()); + } + }; + }) + } + _ => unimplemented!("FieldOffset only supports named fields"), + } + } else { + unimplemented!("FieldOffset only supports structs"); + }; + + let expanded = quote! { + impl #struct_name { + pub fn get_field_offset(field: &str) -> Option { + #(#fields)* + None + } + } + }; + + TokenStream::from(expanded) +} diff --git a/programs/lighthouse/Cargo.lock b/programs/lighthouse/Cargo.lock index 07ea12f..16d1060 100644 --- a/programs/lighthouse/Cargo.lock +++ b/programs/lighthouse/Cargo.lock @@ -2086,8 +2086,10 @@ dependencies = [ "anchor-spl", "async-trait", "bytemuck", + "macros", "mpl-token-metadata", "num-traits", + "regex", "solana-banks-interface", "solana-program", "solana-program-test", @@ -2150,6 +2152,15 @@ dependencies = [ "libc", ] +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "memchr" version = "2.5.0" diff --git a/programs/lighthouse/program/Cargo.toml b/programs/lighthouse/program/Cargo.toml index 6d5356d..adb7b07 100644 --- a/programs/lighthouse/program/Cargo.toml +++ b/programs/lighthouse/program/Cargo.toml @@ -26,17 +26,16 @@ bytemuck = {version = "1.4.0", features = ["derive", "min_const_generics"]} mpl-token-metadata = { version = "2.0.0-beta.1", features = ["no-entrypoint"] } num-traits = "0.2.15" solana-program = "~1.16.5" -# spl-account-compression = { version="0.2.0", features = ["cpi"] } spl-associated-token-account = { version = ">= 1.1.3, < 3.0", features = ["no-entrypoint"] } spl-token = { version = ">= 3.5.0, < 5.0", features = ["no-entrypoint"] } +macros = { path = "../../../macros" } [dev-dependencies] async-trait = "0.1.71" -# mpl-token-auth-rules = { version = "1.4.3", features = ["no-entrypoint"] } solana-program-test = "~1.16.5" solana-sdk = "~1.16.5" spl-concurrent-merkle-tree = "0.2.0" spl-merkle-tree-reference = "0.1.0" spl-noop = { version = "0.1.3", features = ["no-entrypoint"] } solana-banks-interface = "1.14.10" -# solana-banks-interface = { version = "^1.14.18", features = ["no-entrypoint"] } \ No newline at end of file +regex = "1.5.4" \ No newline at end of file diff --git a/programs/lighthouse/program/src/error.rs b/programs/lighthouse/program/src/error.rs index c8978ae..b02f21f 100644 --- a/programs/lighthouse/program/src/error.rs +++ b/programs/lighthouse/program/src/error.rs @@ -1,6 +1,4 @@ use anchor_lang::prelude::*; -use mpl_token_metadata::error::MetadataError; -use num_traits::FromPrimitive; #[error_code] pub enum ProgramError { @@ -10,8 +8,29 @@ pub enum ProgramError { AssertionFailed, #[msg("NotEnoughAccounts")] NotEnoughAccounts, - #[msg("BorshValueMismatch")] - BorshValueMismatch, + #[msg("DataValueMismatch")] + DataValueMismatch, #[msg("UnsupportedOperator")] UnsupportedOperator, + #[msg("OutOfRange")] + OutOfRange, + #[msg("AccountBorrowFailed")] + AccountBorrowFailed, + #[msg("InvalidAccount")] + InvalidAccount, + + #[msg("InvalidDataLength")] + InvalidDataLength, + + #[msg("AccountOutOfRange")] + AccountOutOfRange, + + #[msg("AccountOwnerValidationFailed")] + AccountOwnerValidationFailed, + + #[msg("AccountFundedValidationFailed")] + AccountFundedValidationFailed, + + #[msg("AccountDiscriminatorValidationFailed")] + AccountDiscriminatorValidationFailed, } diff --git a/programs/lighthouse/program/src/lib.rs b/programs/lighthouse/program/src/lib.rs index 2b63fb9..4522524 100644 --- a/programs/lighthouse/program/src/lib.rs +++ b/programs/lighthouse/program/src/lib.rs @@ -2,7 +2,7 @@ #![allow(clippy::too_many_arguments)] use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; pub mod error; pub mod processor; @@ -35,7 +35,7 @@ pub mod lighthouse { pub fn write_v1<'info>( ctx: Context<'_, '_, '_, 'info, WriteV1<'info>>, cache_index: u8, - write_type: WriteType, + write_type: WriteTypeParameter, ) -> Result<()> { processor::v1::write(ctx, cache_index, write_type) } @@ -44,7 +44,7 @@ pub mod lighthouse { ctx: Context<'_, '_, '_, 'info, AssertV1<'info>>, assertions: Vec, logical_expression: Option>, - options: Option, + options: Option, ) -> Result<()> { processor::assert(ctx, assertions, logical_expression, options) } diff --git a/programs/lighthouse/program/src/processor/v1/assert.rs b/programs/lighthouse/program/src/processor/v1/assert.rs index cae3c6b..f232af9 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -1,19 +1,19 @@ -use std::collections::BTreeSet; - use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::program_pack::Pack; use crate::error::ProgramError; -use crate::structs::{Assertion, BorshField, BorshValue, Expression, Operator}; -use crate::utils::process_value; +use crate::structs::{AccountInfoDataField, Assertion, AssertionState, Expression}; +use crate::utils::print_assertion_result; #[derive(Accounts)] pub struct AssertV1<'info> { - pub system_program: Program<'info, System>, + // TODO: + pub cache: Option>, } #[derive(BorshDeserialize, BorshSerialize, Debug)] -pub struct Config { +pub struct AssertionConfig { pub verbose: bool, } @@ -21,386 +21,170 @@ pub fn assert<'info>( ctx: Context<'_, '_, '_, 'info, AssertV1<'info>>, assertions: Vec, logical_expression: Option>, - options: Option, + config: Option, ) -> Result<()> { - let remaining_accounts = &ctx.remaining_accounts.to_vec(); - - let verbose = options.map(|options| options.verbose).unwrap_or(false); - let mut assertion_results: Vec = vec![]; - let mut logically_dependent_assertions: Option> = None; - - if let Some(logical_expression) = &logical_expression { - if verbose { - msg!("Logical expression: {:?}", logical_expression); - } - - logically_dependent_assertions = Some(BTreeSet::new()); - let tree = logically_dependent_assertions.as_mut().unwrap(); + let remaining_accounts = &mut ctx.remaining_accounts.iter(); + let mut assertion_state = AssertionState::new(assertions.clone(), logical_expression)?; + + for (i, assertion) in assertions.into_iter().enumerate() { + let assertion_result: Result = match &assertion { + Assertion::AccountOwnedBy(pubkey, operator) => { + let account = remaining_accounts.next().unwrap(); + let result = account.owner.key().eq(pubkey); + + let value_str = account.owner.key().to_string(); + let expected_value_str = pubkey.to_string(); + + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); - for (_, logical_expression) in logical_expression.iter().enumerate() { - match logical_expression { - Expression::And(assertion_indexes) => { - for assertion_index in assertion_indexes { - tree.insert(*assertion_index); - } - } - Expression::Or(assertion_indexes) => { - for assertion_index in assertion_indexes { - tree.insert(*assertion_index); - } - } + Ok(result) } - } - } - - for (i, assertion_type) in assertions.into_iter().enumerate() { - if (i + 1) > remaining_accounts.len() { - msg!("The next assertion requires more accounts than were provided"); - return Err(ProgramError::NotEnoughAccounts.into()); - } - - let mut assertion_result = true; - if verbose { - msg!("Testing assertion {:?}", assertion_type); - } - - match assertion_type { - Assertion::AccountExists => { - let account = &remaining_accounts[i]; + Assertion::Memory(cache_offset, operator, memory_value) => { + let cache = ctx.accounts.cache.as_ref().unwrap(); // TODO: Graceful error handling + let cache_data = cache.try_borrow_data()?; // TODO: Graceful error handling + + let (value_str, expected_value_str, result) = memory_value + .deserialize_and_compare(cache_data, (cache_offset + 8) as usize, operator)?; + + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); - if account.data_is_empty() && account.lamports() == 0 { - assertion_result = false; - } + Ok(result) } - Assertion::AccountOwnedBy(pubkey) => { - let account = &remaining_accounts[i]; + Assertion::AccountData(account_offset, operator, memory_value) => { + let account = remaining_accounts.next().unwrap(); + let account_data = account.try_borrow_data()?; + + let (value_str, expected_value_str, result) = memory_value + .deserialize_and_compare(account_data, (*account_offset) as usize, operator)?; + + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); - if !account.owner.key().eq(&pubkey) { - assertion_result = false; - } + Ok(result) } - Assertion::RawAccountData(offset, operator, expected_slice) => { - let account = &remaining_accounts[i]; - let data = account.try_borrow_data()?; - - let slice = &data[offset as usize..(offset + expected_slice.len() as u64) as usize]; - - match operator { - Operator::Equal => { - if !slice.eq(&expected_slice) { - assertion_result = false; - } - } - Operator::NotEqual => { - if slice.eq(&expected_slice) { - assertion_result = false; - } - } - _ => return Err(ProgramError::UnsupportedOperator.into()), - } + Assertion::AccountBalance(balance_value, operator) => { + let account = remaining_accounts.next().unwrap(); + let result = operator.evaluate(&**account.try_borrow_lamports()?, balance_value); + + let value_str = account.get_lamports().to_string(); + let expected_value_str = balance_value.to_string(); + + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); - if verbose { - msg!( - "{} Assertion::RawAccountData ({}) -> {:?} {} {:?}", - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - account.key().to_string(), - slice, - operator.format(), - expected_slice, - ); - } + Ok(result) } - Assertion::BorshAccountData(offset, borsh_field, operator, expected_value) => { - let account = &remaining_accounts[i]; - let data = account.try_borrow_data()?; - - let value_str: String; - let expected_value_str: String; - - match borsh_field { - BorshField::U8 => { - (value_str, expected_value_str, assertion_result) = process_value::( - &data, - offset as u32, - 1, - &match expected_value { - BorshValue::U8(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }, - &borsh_field, - &operator, - )?; - } - BorshField::I8 => { - let slice = &data[offset as usize..(offset + 1) as usize]; - let value = i8::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::I8(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::U16 => { - let expected_value = match expected_value { - BorshValue::U16(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - let slice = &data[offset as usize..(offset + 2) as usize]; - let value = u16::try_from_slice(slice)?; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::I16 => { - let expected_value = match expected_value { - BorshValue::I16(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - let slice = &data[offset as usize..(offset + 2) as usize]; - let value = i16::try_from_slice(slice)?; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::U32 => { - let slice = &data[offset as usize..(offset + 4) as usize]; - let value = u32::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::U32(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::I32 => { - let slice = &data[offset as usize..(offset + 4) as usize]; - let value = i32::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::I32(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); + Assertion::TokenAccountBalance(balance_value, operator) => { + let account = remaining_accounts.next().unwrap(); - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::U64 => { - let slice: &[u8] = &data[offset as usize..(offset + 8) as usize]; - let value = u64::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::U64(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::I64 => { - let slice: &[u8] = &data[offset as usize..(offset + 8) as usize]; - let value = i64::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::I64(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::U128 => { - let slice: &[u8] = &data[offset as usize..(offset + 16) as usize]; - let value = u128::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::U128(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - BorshField::I128 => { - let slice: &[u8] = &data[offset as usize..(offset + 16) as usize]; - let value = i128::try_from_slice(slice)?; - - let expected_value = match expected_value { - BorshValue::I128(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - - // let value = i128::from_le_bytes(*array_ref![data, offset as usize, 16]); - } - BorshField::Bytes(bytes) => { - let slice: &[u8] = - &data[offset as usize..(offset + bytes.len() as u64) as usize]; - let value = u128::try_from_slice(slice)?; + if account.owner.eq(&spl_associated_token_account::id()) { + return Err(ProgramError::InvalidAccount.into()); + } - let expected_value = match expected_value { - BorshValue::U128(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; + let token_account = + spl_token::state::Account::unpack_from_slice(&account.try_borrow_data()?)?; - match operator { - Operator::Equal => {} - Operator::NotEqual => {} - _ => return Err(ProgramError::UnsupportedOperator.into()), - } + let result = operator.evaluate(&token_account.amount, balance_value); - assertion_result = operator.is_true(&value, &expected_value); + let value_str = token_account.amount.to_string(); + let expected_value_str = balance_value.to_string(); - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - } - - msg!( - "{} {} Assertion::BorshAccountData ({}) -> {} {} {}", - format!("[{:?}]", i), - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - account.key().to_string(), + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, value_str, - operator.format(), expected_value_str, ); - } - Assertion::AccountBalance(expected_balance, operator) => { - let account = &remaining_accounts[i]; - assertion_result = - operator.is_true(&**account.try_borrow_lamports()?, &expected_balance); - - if verbose { - msg!( - "{} Assertion::AccountBalance ({}) -> {} {} {}", - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - account.key().to_string(), - account.get_lamports(), - operator.format(), - expected_balance, - ); - } - } - Assertion::TokenAccountBalance(expected_balance, operator) => { - return Err(ProgramError::Unimplemented.into()); + Ok(result) } - } - - assertion_results.push(assertion_result); - - if (logical_expression.is_none() - || !logically_dependent_assertions - .as_ref() - .unwrap() - .contains(&(i as u8))) - && !assertion_result - { - return Err(ProgramError::AssertionFailed.into()); - } - } - - if let Some(logical_expressions) = &logical_expression { - for logical_expression in logical_expressions { - match logical_expression { - Expression::And(assertion_indexes) => { - let mut result = true; - - for assertion_index in assertion_indexes { - result = result && assertion_results[*assertion_index as usize]; - } - - if verbose { - msg!( - "{} Expression::And -> {:?} {}", - if result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - result, - assertion_indexes - .iter() - .map(|i| format!("[{}]", i)) - .collect::>() - .join(" AND ") - ); - } + Assertion::AccountInfo(account_info_fields, operator) => { + let account = remaining_accounts.next().unwrap(); + let operator_result = true; + + for account_info_field in account_info_fields { + let operator_result = match account_info_field { + AccountInfoDataField::Key(pubkey) => { + operator.evaluate(&account.key(), pubkey) + } + AccountInfoDataField::Owner(pubkey) => { + operator.evaluate(account.owner, pubkey) + } + AccountInfoDataField::Lamports(lamports) => { + operator.evaluate(&account.get_lamports(), lamports) + } + AccountInfoDataField::DataLength(data_length) => { + operator.evaluate(&(account.data_len() as u64), data_length) + } + AccountInfoDataField::Executable(executable) => { + operator.evaluate(&account.executable, executable) + } + AccountInfoDataField::IsSigner(is_signer) => { + operator.evaluate(&account.is_signer, is_signer) + } + AccountInfoDataField::IsWritable(is_writable) => { + operator.evaluate(&account.is_writable, is_writable) + } + AccountInfoDataField::RentEpoch(rent_epoch) => { + operator.evaluate(&account.rent_epoch as &u64, rent_epoch) + } + }; - if !result { - return Err(ProgramError::AssertionFailed.into()); + if !operator_result { + break; } } - Expression::Or(assertion_indexes) => { - let mut result = false; - - for assertion_index in assertion_indexes { - result = result || assertion_results[*assertion_index as usize]; - } - if verbose { - msg!( - "{} Expression::Or -> {:?} {}", - if result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - result, - assertion_indexes - .iter() - .map(|i| format!("[{}]", i)) - .collect::>() - .join(" OR ") - ); - } - - if !result { - return Err(ProgramError::AssertionFailed.into()); - } - } + Ok(operator_result) } - } + }; + + assertion_state.record_result(i, assertion_result?)?; } + msg!("assertion_state: {:?}", assertion_state); + assertion_state.evaluate()?; + Ok(()) } + +pub fn truncate_pubkey(pubkey: &Pubkey) -> String { + let mut pubkey_str = pubkey.to_string(); + pubkey_str.truncate(5); + pubkey_str.push_str("..."); + + pubkey_str +} diff --git a/programs/lighthouse/program/src/processor/v1/create_test_account.rs b/programs/lighthouse/program/src/processor/v1/create_test_account.rs index 37f19ee..dcb5c82 100644 --- a/programs/lighthouse/program/src/processor/v1/create_test_account.rs +++ b/programs/lighthouse/program/src/processor/v1/create_test_account.rs @@ -13,7 +13,8 @@ pub struct TestAccountV1 { pub u128: u128, pub i128: i128, pub bytes: [u8; 32], - pub string: String, + pub true_: bool, + pub false_: bool, } #[derive(Accounts)] @@ -50,7 +51,8 @@ pub fn create_test_account<'info>( test_account.u128 = (u64::MAX as u128) + 1; test_account.i128 = (i64::MIN as i128) - 1; test_account.bytes = [u8::MAX; 32]; - test_account.string = "Hello, World!".to_string(); + test_account.true_ = true; + test_account.false_ = false; Ok(()) } diff --git a/programs/lighthouse/program/src/processor/v1/write.rs b/programs/lighthouse/program/src/processor/v1/write.rs index 6f08f45..a536098 100644 --- a/programs/lighthouse/program/src/processor/v1/write.rs +++ b/programs/lighthouse/program/src/processor/v1/write.rs @@ -1,9 +1,8 @@ use anchor_lang::prelude::*; -use anchor_spl::token::TokenAccount; use borsh::BorshDeserialize; use crate::error::ProgramError; -use crate::structs::WriteType; +use crate::structs::{AccountInfoData, WriteType, WriteTypeParameter}; #[derive(Accounts)] #[instruction(cache_index: u8)] @@ -21,55 +20,51 @@ pub struct WriteV1<'info> { bump )] pub cache_account: UncheckedAccount<'info>, - pub rent: Sysvar<'info, Rent>, } pub fn write<'info>( ctx: Context<'_, '_, '_, 'info, WriteV1<'info>>, _: u8, - write_type: WriteType, + write_type: WriteTypeParameter, ) -> Result<()> { let cache_ref = &mut ctx.accounts.cache_account.try_borrow_mut_data()?; let cache_data_length = cache_ref.len(); - let mut cache_offset: usize; - let account_offset: usize; - let data_length: usize; - - (cache_offset, account_offset, data_length) = match write_type { - WriteType::AccountBalanceU8(_cache_offset) => (_cache_offset as usize, 0, 8), - WriteType::AccountBalanceU16(_cache_offset) => (_cache_offset as usize, 0, 16), - WriteType::AccountBalanceU32(_cache_offset) => (_cache_offset as usize, 0, 32), - WriteType::AccountDataU8(_cache_offset, account_offset, data_length) => ( - _cache_offset as usize, - account_offset as usize, - data_length as usize, - ), - WriteType::AccountDataU16(_cache_offset, account_offset, data_length) => ( - _cache_offset as usize, - account_offset as usize, - data_length as usize, - ), - WriteType::AccountDataU32(_cache_offset, account_offset, data_length) => ( - _cache_offset as usize, - account_offset as usize, - data_length as usize, - ), - // TODO: Implement these - WriteType::BorshFieldU8(_cache_offset, _) => (_cache_offset as usize, 0, 0), - WriteType::BorshFieldU16(_cache_offset, _) => (_cache_offset as usize, 0, 0), - WriteType::MintAccount => (0, 0, 0), - WriteType::TokenAccount(_cache_offset) => (_cache_offset as usize, 0, TokenAccount::LEN), - WriteType::TokenAccountOwner(_cache_offset) => (_cache_offset as usize, 0, 32), - WriteType::TokenAccountBalance(_cache_offset) => (_cache_offset as usize, 0, 8), + let (mut cache_offset, write_type) = match write_type { + WriteTypeParameter::WriteU8(cache_offset, write_type) => { + (cache_offset as usize, write_type) + } + WriteTypeParameter::WriteU16(cache_offset, write_type) => { + (cache_offset as usize, write_type) + } + WriteTypeParameter::WriteU32(cache_offset, write_type) => { + (cache_offset as usize, write_type) + } }; - cache_offset += 8; + cache_offset = cache_offset.checked_add(8).ok_or_else(|| { + msg!("Cache offset overflowed"); + ProgramError::OutOfRange + })?; + + let data_length = write_type + .size(ctx.remaining_accounts.first()) + .ok_or(ProgramError::InvalidDataLength)?; + if cache_data_length < (cache_offset + data_length) { + msg!("Cache offset overflowed"); + return Err(ProgramError::OutOfRange.into()); + } match write_type { - WriteType::AccountBalanceU8(_) - | WriteType::AccountBalanceU16(_) - | WriteType::AccountBalanceU32(_) => { + WriteType::Program => { + return Err(ProgramError::Unimplemented.into()); + } + WriteType::DataValue(borsh_value) => { + let data_slice = &(borsh_value.serialize())[0..data_length]; + cache_ref[cache_offset..(cache_offset + data_length)] + .copy_from_slice(data_slice.as_ref()); + } + WriteType::AccountBalance => { let source_account = ctx.remaining_accounts.first(); if let Some(target_account) = source_account { @@ -86,34 +81,78 @@ pub fn write<'info>( return Err(ProgramError::NotEnoughAccounts.into()); } } - WriteType::AccountDataU8(_, _, _) - | WriteType::AccountDataU16(_, _, _) - | WriteType::AccountDataU32(_, _, _) => { - msg!("write_type: AccountData"); - let source_account = ctx.remaining_accounts.first(); + WriteType::AccountData(account_offset, _, account_validation) => { + let target_account = ctx.remaining_accounts.first(); + let account_offset = account_offset as usize; + + // Additional validation on account that's been written to. + if let Some(target_account) = target_account { + if let Some(account_validation) = account_validation { + if let Some(owner) = account_validation.owner { + if owner != *target_account.owner { + return Err(ProgramError::AccountOwnerValidationFailed.into()); + } + } + + if let Some(assert_is_funded) = account_validation.is_funded { + let is_funded = target_account.lamports() == 0; + if assert_is_funded != is_funded { + return Err(ProgramError::AccountFundedValidationFailed.into()); + } + } + + if let Some(discriminator) = account_validation.discriminator { + let data = target_account.try_borrow_data().map_err(|err| { + msg!("Error: {:?}", err); + ProgramError::AccountBorrowFailed + })?; + + if discriminator.len() > data.len() { + msg!("Discriminator length is greater than account data length"); + return Err(ProgramError::AccountOutOfRange.into()); + } + + let data_slice = &data[0..discriminator.len()]; + + if !data_slice.eq(discriminator.as_slice()) { + return Err(ProgramError::AccountDiscriminatorValidationFailed.into()); + } + } + } - if let Some(target_account) = source_account { if (cache_offset + data_length) < cache_data_length { - let data = target_account.try_borrow_data()?; + let data = target_account.try_borrow_data().map_err(|err| { + msg!("Error: {:?}", err); + ProgramError::AccountBorrowFailed + })?; let data_slice = &data[account_offset..(account_offset + data_length)]; cache_ref[cache_offset..(cache_offset + data_length)] .copy_from_slice(data_slice.as_ref()); } else { - // TODO: MAKE A BETTER ERROR return Err(ProgramError::NotEnoughAccounts.into()); } } else { return Err(ProgramError::NotEnoughAccounts.into()); } } - WriteType::TokenAccount(_) => { - msg!("write_type: TokenAccount"); - let source_account = ctx.remaining_accounts.first(); + WriteType::AccountInfo => { + let target_account = ctx.remaining_accounts.first(); - if let Some(target_account) = source_account { + if let Some(target_account) = target_account { if (cache_offset + data_length) < cache_data_length { - let data = target_account.try_borrow_data()?; + let account_info = AccountInfoData { + key: *target_account.key, + is_signer: target_account.is_signer, + is_writable: target_account.is_writable, + executable: target_account.executable, + lamports: **target_account.try_borrow_lamports()?, // TODO: make this unwrap nicer + data_length: target_account.try_borrow_data()?.len() as u64, // TODO: make this unwrap nicer + owner: *target_account.owner, + rent_epoch: target_account.rent_epoch, + }; + + let data = account_info.try_to_vec()?; // TODO: map this unwrap error let data_slice = &data[0..data_length]; cache_ref[cache_offset..(cache_offset + data_length)] @@ -125,73 +164,7 @@ pub fn write<'info>( return Err(ProgramError::NotEnoughAccounts.into()); } } - WriteType::TokenAccountBalance(_) => { - msg!("write_type: TokenAccountBalance"); - let source_account = ctx.remaining_accounts.first(); - - if let Some(target_account) = source_account { - if (cache_offset + data_length) < cache_data_length { - let data = target_account.try_borrow_data()?; - let token_account = TokenAccount::try_deserialize(&mut data.as_ref())?; - let data_slice = token_account.amount.to_le_bytes(); - - cache_ref[cache_offset..(cache_offset + data_length)] - .copy_from_slice(data_slice.as_ref()); - } else { - return Err(ProgramError::NotEnoughAccounts.into()); - } - } else { - return Err(ProgramError::NotEnoughAccounts.into()); - } - } - WriteType::TokenAccountOwner(_) => { - msg!("write_type: TokenAccountOwner"); - let source_account = ctx.remaining_accounts.first(); - - if let Some(target_account) = source_account { - if (cache_offset + data_length) < cache_data_length { - let data = target_account.try_borrow_data()?; - let token_account = TokenAccount::try_deserialize(&mut data.as_ref())?; - let data_slice = token_account.owner.to_bytes(); - - cache_ref[cache_offset..(cache_offset + data_length)] - .copy_from_slice(data_slice.as_ref()); - } else { - return Err(ProgramError::NotEnoughAccounts.into()); - } - } else { - return Err(ProgramError::NotEnoughAccounts.into()); - } - } - _ => { - // TODO: MAKE A BETTER ERROR - return Err(ProgramError::NotEnoughAccounts.into()); - } - } + }; Ok(()) } - -// msg!( -// "cache_offset: {}, dest_start: {}, slice_length: {}", -// cache_offset, -// dest_start, -// slice_length -// ); - -// msg!( -// "cache_account_data.len(): {}, source_account_data.len(): {}", -// cache_account_data.len(), -// source_account_data.len() -// ); - -// if ((cache_offset + slice_length) as usize) < cache_account_data.len() { -// cache_account_data[cache_offset as usize..(cache_offset + slice_length) as usize] -// .copy_from_slice( -// &source_account_data[dest_start as usize..(dest_start + slice_length) as usize], -// ); -// } else { -// // Handle the error: destination slice is not large enough -// } - -// msg!("cache_account_data: {:?}", cache_account_data); diff --git a/programs/lighthouse/program/src/structs/assert/assertion.rs b/programs/lighthouse/program/src/structs/assert/assertion.rs new file mode 100644 index 0000000..2691429 --- /dev/null +++ b/programs/lighthouse/program/src/structs/assert/assertion.rs @@ -0,0 +1,63 @@ +use anchor_lang::prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, +}; +use solana_program::pubkey::Pubkey; + +use crate::structs::{operator::Operator, AccountInfoDataField, DataValue}; + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub enum Assertion { + // memory offset, assertion + Memory(u16, Operator, DataValue), + + AccountInfo(Vec, Operator), + + // account data offset, borsh type, operator + AccountData(u16, Operator, DataValue), + + // balance, operator + AccountBalance(u64, Operator), + + AccountOwnedBy(Pubkey, Operator), + + // token balance, operator + TokenAccountBalance(u64, Operator), + // TODO + // IsSigner, +} + +impl Assertion { + pub fn format(&self) -> String { + match self { + Assertion::Memory(offset, operator, value) => { + format!( + "Memory[{}] {} {}", + offset, + operator.format(), + value.format() + ) + } + Assertion::AccountData(offset, operator, value) => { + format!( + "AccountData[{}] {} {}", + offset, + operator.format(), + value.format() + ) + } + Assertion::AccountBalance(balance, operator) => { + format!("AccountBalance {} {}", balance, operator.format()) + } + Assertion::AccountOwnedBy(pubkey, operator) => { + format!("AccountOwnedBy {} {}", pubkey, operator.format()) + } + Assertion::TokenAccountBalance(balance, operator) => { + format!("TokenAccountBalance {} {}", balance, operator.format()) + } + Assertion::AccountInfo(fields, operator) => { + format!("AccountInfo {:?} {}", fields, operator.format()) + } + } + } +} diff --git a/programs/lighthouse/program/src/structs/assert/assertion_state.rs b/programs/lighthouse/program/src/structs/assert/assertion_state.rs new file mode 100644 index 0000000..6223909 --- /dev/null +++ b/programs/lighthouse/program/src/structs/assert/assertion_state.rs @@ -0,0 +1,104 @@ +use std::collections::BTreeSet; + +use crate::{ + error::ProgramError, + structs::{Assertion, Expression}, +}; +pub use anchor_lang::prelude::Result; +use solana_program::msg; + +#[derive(Debug)] +pub struct AssertionState { + pub assertion_results: Vec, + pub expressions: Vec, +} + +impl AssertionState { + pub fn new(assertions: Vec, expressions: Option>) -> Result { + let assertion_results: Vec = vec![true; assertions.len()]; + + if let Some(expressions) = expressions { + let expressions = &mut expressions.clone(); + + let btree = expressions + .iter() + .flat_map(|expression| match expression { + Expression::And(assertion_indexes) => assertion_indexes.clone(), + Expression::Or(assertion_indexes) => assertion_indexes.clone(), + }) + .collect::>(); + + // find set of indexes not in btree and create an AND expression + let mut missing_indexes: Vec = Vec::new(); + for i in 0..assertions.len() { + if !btree.contains(&(i as u8)) { + missing_indexes.push(i as u8); + } + } + if !missing_indexes.is_empty() { + expressions.push(Expression::And(missing_indexes)); + } + + // iterate through btree and make sure that all indexes are in the assertion_results + for index in btree { + if index as usize >= assertion_results.len() { + msg!("expression contained index out of bounds {:?}", index); + return Err(ProgramError::OutOfRange.into()); + } + } + + Ok(Self { + assertion_results, + expressions: expressions.clone(), + }) + } else { + let mut expressions: Vec = Vec::new(); + for (i, _) in assertion_results.iter().enumerate() { + expressions.push(Expression::And(vec![i as u8])); + } + + Ok(Self { + assertion_results, + expressions, + }) + } + } + + pub fn record_result(&mut self, index: usize, result: bool) -> Result<()> { + self.assertion_results[index] = result; + Ok(()) + } + + pub fn evaluate(&self) -> Result<()> { + for (_, expression) in self.expressions.iter().enumerate() { + match expression { + Expression::And(assertion_indexes) => { + let mut result = true; + + for assertion_index in assertion_indexes { + result = result && self.assertion_results[*assertion_index as usize]; + } + + if !result { + msg!("expression failed {:?}", expression); + return Err(ProgramError::AssertionFailed.into()); + } + } + Expression::Or(assertion_indexes) => { + let mut result = false; + + for assertion_index in assertion_indexes { + result = result || self.assertion_results[*assertion_index as usize]; + } + + if !result { + msg!("expression failed {:?}", expression); + return Err(ProgramError::AssertionFailed.into()); + } + } + } + } + + Ok(()) + } +} diff --git a/programs/lighthouse/program/src/structs/assert/mod.rs b/programs/lighthouse/program/src/structs/assert/mod.rs new file mode 100644 index 0000000..cb9b439 --- /dev/null +++ b/programs/lighthouse/program/src/structs/assert/mod.rs @@ -0,0 +1,5 @@ +pub mod assertion; +pub mod assertion_state; + +pub use assertion::*; +pub use assertion_state::*; diff --git a/programs/lighthouse/program/src/structs/assertion.rs b/programs/lighthouse/program/src/structs/assertion.rs deleted file mode 100644 index fb597a3..0000000 --- a/programs/lighthouse/program/src/structs/assertion.rs +++ /dev/null @@ -1,25 +0,0 @@ -use anchor_lang::prelude::{ - borsh, - borsh::{BorshDeserialize, BorshSerialize}, -}; -use solana_program::pubkey::Pubkey; - -use super::{BorshField, BorshValue, Operator}; - -#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] -pub enum Assertion { - // offset, borsh type, operator - BorshAccountData(u64, BorshField, Operator, BorshValue), - - RawAccountData(u64, Operator, Vec), - - // balance, operator - AccountBalance(u64, Operator), - - AccountExists, - - AccountOwnedBy(Pubkey), - - // token balance, operator - TokenAccountBalance(u64, Operator), -} diff --git a/programs/lighthouse/program/src/structs/borsh_field.rs b/programs/lighthouse/program/src/structs/borsh_field.rs index faf2cb0..6fad601 100644 --- a/programs/lighthouse/program/src/structs/borsh_field.rs +++ b/programs/lighthouse/program/src/structs/borsh_field.rs @@ -6,7 +6,7 @@ use anchor_lang::prelude::{ use super::Operator; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] -pub enum BorshField { +pub enum BorshType { U8, I8, U16, @@ -20,20 +20,20 @@ pub enum BorshField { Bytes(Vec), } -impl BorshField { +impl BorshType { pub fn is_supported_operator(&self, operator: &Operator) -> bool { match self { - BorshField::U8 => true, - BorshField::I8 => true, - BorshField::U16 => true, - BorshField::I16 => true, - BorshField::U32 => true, - BorshField::I32 => true, - BorshField::U64 => true, - BorshField::I64 => true, - BorshField::U128 => true, - BorshField::I128 => true, - BorshField::Bytes(_) => matches!(operator, Operator::Equal | Operator::NotEqual), + BorshType::U8 => true, + BorshType::I8 => true, + BorshType::U16 => true, + BorshType::I16 => true, + BorshType::U32 => true, + BorshType::I32 => true, + BorshType::U64 => true, + BorshType::I64 => true, + BorshType::U128 => true, + BorshType::I128 => true, + BorshType::Bytes(_) => matches!(operator, Operator::Equal | Operator::NotEqual), } } } diff --git a/programs/lighthouse/program/src/structs/borsh_value.rs b/programs/lighthouse/program/src/structs/borsh_value.rs deleted file mode 100644 index 11713fc..0000000 --- a/programs/lighthouse/program/src/structs/borsh_value.rs +++ /dev/null @@ -1,21 +0,0 @@ -use anchor_lang::prelude::{ - borsh, - borsh::{BorshDeserialize, BorshSerialize}, -}; - -use super::Operator; - -#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] -pub enum BorshValue { - U8(u8), - I8(i8), - U16(u16), - I16(i16), - U32(u32), - I32(i32), - U64(u64), - I64(i64), - U128(u128), - I128(i128), - Bytes(Vec), -} diff --git a/programs/lighthouse/program/src/structs/data_value.rs b/programs/lighthouse/program/src/structs/data_value.rs new file mode 100644 index 0000000..108ee11 --- /dev/null +++ b/programs/lighthouse/program/src/structs/data_value.rs @@ -0,0 +1,352 @@ +use std::cell::Ref; + +use anchor_lang::prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, +}; +use solana_program::pubkey::Pubkey; + +use crate::error::ProgramError; + +use super::operator::Operator; + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub enum DataType { + Bool, + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + U128, + I128, + Bytes, + Pubkey, +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub enum DataValue { + Bool(bool), + U8(u8), + I8(i8), + U16(u16), + I16(i16), + U32(u32), + I32(i32), + U64(u64), + I64(i64), + U128(u128), + I128(i128), + Bytes(Vec), + Pubkey(Pubkey), +} + +impl DataValue { + pub fn format(&self) -> String { + match self { + DataValue::Bool(value) => value.to_string(), + DataValue::U8(value) => value.to_string(), + DataValue::I8(value) => value.to_string(), + DataValue::U16(value) => value.to_string(), + DataValue::I16(value) => value.to_string(), + DataValue::U32(value) => value.to_string(), + DataValue::I32(value) => value.to_string(), + DataValue::U64(value) => value.to_string(), + DataValue::I64(value) => value.to_string(), + DataValue::U128(value) => value.to_string(), + DataValue::I128(value) => value.to_string(), + DataValue::Bytes(value) => { + let value_str = value + .iter() + .take(15) + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(""); + format!("0x{}", value_str) + } + DataValue::Pubkey(value) => value.to_string(), + } + } +} + +impl DataValue { + pub fn get_data_type(&self) -> DataType { + match self { + DataValue::Bool(_) => DataType::Bool, + DataValue::U8(_) => DataType::U8, + DataValue::I8(_) => DataType::I8, + DataValue::U16(_) => DataType::U16, + DataValue::I16(_) => DataType::I16, + DataValue::U32(_) => DataType::U32, + DataValue::I32(_) => DataType::I32, + DataValue::U64(_) => DataType::U64, + DataValue::I64(_) => DataType::I64, + DataValue::U128(_) => DataType::U128, + DataValue::I128(_) => DataType::I128, + DataValue::Bytes(_) => DataType::Bytes, + DataValue::Pubkey(_) => DataType::Pubkey, + } + } + + pub fn size(&self) -> usize { + match self { + DataValue::Bool(_) => 1, + DataValue::U8(_) => 1, + DataValue::I8(_) => 1, + DataValue::U16(_) => 2, + DataValue::I16(_) => 2, + DataValue::U32(_) => 4, + DataValue::I32(_) => 4, + DataValue::U64(_) => 8, + DataValue::I64(_) => 8, + DataValue::U128(_) => 16, + DataValue::I128(_) => 16, + DataValue::Bytes(value) => value.len(), + DataValue::Pubkey(_) => 32, + } + } + + pub fn serialize(self) -> Vec { + match self { + DataValue::Bool(value) => vec![value as u8], + DataValue::U8(value) => value.to_le_bytes().to_vec(), + DataValue::I8(value) => value.to_le_bytes().to_vec(), + DataValue::U16(value) => value.to_le_bytes().to_vec(), + DataValue::I16(value) => value.to_le_bytes().to_vec(), + DataValue::U32(value) => value.to_le_bytes().to_vec(), + DataValue::I32(value) => value.to_le_bytes().to_vec(), + DataValue::U64(value) => value.to_le_bytes().to_vec(), + DataValue::I64(value) => value.to_le_bytes().to_vec(), + DataValue::U128(value) => value.to_le_bytes().to_vec(), + DataValue::I128(value) => value.to_le_bytes().to_vec(), + DataValue::Bytes(value) => value, + DataValue::Pubkey(value) => value.to_bytes().to_vec(), + } + } + pub fn deserialize(data_type: DataType, bytes: &[u8]) -> Self { + match data_type { + DataType::Bool => { + let len = bytes.len(); + if len != 1 { + panic!("Invalid bool length: {}", len); + } else { + DataValue::Bool(bytes[0] != 0) + } + } + DataType::U8 => DataValue::U8(u8::from_le_bytes(bytes.try_into().unwrap())), + DataType::I8 => DataValue::I8(i8::from_le_bytes(bytes.try_into().unwrap())), + DataType::U16 => DataValue::U16(u16::from_le_bytes(bytes.try_into().unwrap())), + DataType::I16 => DataValue::I16(i16::from_le_bytes(bytes.try_into().unwrap())), + DataType::U32 => DataValue::U32(u32::from_le_bytes(bytes.try_into().unwrap())), + DataType::I32 => DataValue::I32(i32::from_le_bytes(bytes.try_into().unwrap())), + DataType::U64 => DataValue::U64(u64::from_le_bytes(bytes.try_into().unwrap())), + DataType::I64 => DataValue::I64(i64::from_le_bytes(bytes.try_into().unwrap())), + DataType::U128 => DataValue::U128(u128::from_le_bytes(bytes.try_into().unwrap())), + DataType::I128 => DataValue::I128(i128::from_le_bytes(bytes.try_into().unwrap())), + DataType::Bytes => DataValue::Bytes(bytes.to_vec()), + DataType::Pubkey => { + DataValue::Pubkey(Pubkey::new_from_array(bytes.try_into().unwrap())) + } + } + } + + pub fn compare(&self, other: &Self, operator: Operator) -> bool { + match (self, other) { + (DataValue::U8(a), DataValue::U8(b)) => operator.evaluate(a, b), + (DataValue::I8(a), DataValue::I8(b)) => operator.evaluate(a, b), + (DataValue::U16(a), DataValue::U16(b)) => operator.evaluate(a, b), + (DataValue::I16(a), DataValue::I16(b)) => operator.evaluate(a, b), + (DataValue::U32(a), DataValue::U32(b)) => operator.evaluate(a, b), + (DataValue::I32(a), DataValue::I32(b)) => operator.evaluate(a, b), + (DataValue::U64(a), DataValue::U64(b)) => operator.evaluate(a, b), + (DataValue::I64(a), DataValue::I64(b)) => operator.evaluate(a, b), + (DataValue::U128(a), DataValue::U128(b)) => operator.evaluate(a, b), + (DataValue::I128(a), DataValue::I128(b)) => operator.evaluate(a, b), + (DataValue::Bytes(a), DataValue::Bytes(b)) => operator.evaluate(a, b), + (DataValue::Pubkey(a), DataValue::Pubkey(b)) => operator.evaluate(a, b), + (_, _) => false, + } + } + + pub fn deserialize_and_compare( + &self, + data: Ref<'_, &mut [u8]>, + offset: usize, + operator: &Operator, + ) -> Result<(String, String, bool), ProgramError> { + let slice = &data[offset..(offset + self.size())]; + let value = DataValue::deserialize(self.get_data_type(), slice); + + match self { + DataValue::Bool(expected_value) => { + let value = match value { + DataValue::Bool(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::U8(expected_value) => { + let value = match value { + DataValue::U8(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::I8(expected_value) => { + let value = match value { + DataValue::I8(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::U16(expected_value) => { + let value = match value { + DataValue::U16(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::I16(expected_value) => { + let value = match value { + DataValue::I16(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::U32(expected_value) => { + let value = match value { + DataValue::U32(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::I32(expected_value) => { + let value = match value { + DataValue::I32(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::U64(expected_value) => { + let value = match value { + DataValue::U64(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::I64(expected_value) => { + let value = match value { + DataValue::I64(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::U128(expected_value) => { + let value = match value { + DataValue::U128(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::I128(expected_value) => { + let value = match value { + DataValue::I128(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::Bytes(expected_value) => { + match operator { + Operator::Equal => {} + Operator::NotEqual => {} + _ => return Err(ProgramError::DataValueMismatch), + } + + let value = match value { + DataValue::Bytes(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + // print array + let value_str = value + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(""); + let expected_value_str = expected_value + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(""); + let assertion_result = operator.evaluate(&value, expected_value); + + Ok((value_str, expected_value_str, assertion_result)) + } + DataValue::Pubkey(expected_value) => { + match operator { + Operator::Equal => {} + Operator::NotEqual => {} + _ => return Err(ProgramError::UnsupportedOperator), + } + + let value = match value { + DataValue::Pubkey(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + let value_str = value.to_string(); + let expected_value_str = expected_value.to_string(); + let assertion_result = operator.evaluate(&value, expected_value); + + Ok((value_str, expected_value_str, assertion_result)) + } + } + } +} diff --git a/programs/lighthouse/program/src/structs/expression.rs b/programs/lighthouse/program/src/structs/expression.rs index 9dae626..9c147f3 100644 --- a/programs/lighthouse/program/src/structs/expression.rs +++ b/programs/lighthouse/program/src/structs/expression.rs @@ -8,3 +8,12 @@ pub enum Expression { And(Vec), Or(Vec), } + +impl Expression { + pub fn contains_assertion_index(&self, index: &u8) -> bool { + match self { + Expression::And(assertion_indexes) => assertion_indexes.contains(index), + Expression::Or(assertion_indexes) => assertion_indexes.contains(index), + } + } +} diff --git a/programs/lighthouse/program/src/structs/mod.rs b/programs/lighthouse/program/src/structs/mod.rs index 5b4e48f..1cec384 100644 --- a/programs/lighthouse/program/src/structs/mod.rs +++ b/programs/lighthouse/program/src/structs/mod.rs @@ -1,13 +1,15 @@ -pub mod assertion; +pub mod assert; pub mod borsh_field; -pub mod borsh_value; +pub mod data_value; pub mod expression; pub mod operator; +pub mod write; pub mod write_type; -pub use assertion::*; +pub use assert::*; pub use borsh_field::*; -pub use borsh_value::*; +pub use data_value::*; pub use expression::*; pub use operator::*; +pub use write::*; pub use write_type::*; diff --git a/programs/lighthouse/program/src/structs/operator.rs b/programs/lighthouse/program/src/structs/operator.rs index c6d1671..f31de75 100644 --- a/programs/lighthouse/program/src/structs/operator.rs +++ b/programs/lighthouse/program/src/structs/operator.rs @@ -14,7 +14,7 @@ pub enum Operator { } impl Operator { - pub fn is_true( + pub fn evaluate( self, value: &T, expected_value: &T, diff --git a/programs/lighthouse/program/src/structs/write/account_info.rs b/programs/lighthouse/program/src/structs/write/account_info.rs new file mode 100644 index 0000000..823201b --- /dev/null +++ b/programs/lighthouse/program/src/structs/write/account_info.rs @@ -0,0 +1,36 @@ +use anchor_lang::prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, +}; +use solana_program::pubkey::Pubkey; + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub struct AccountInfoData { + pub key: Pubkey, + pub lamports: u64, + pub data_length: u64, + pub owner: Pubkey, + pub rent_epoch: u64, + pub is_signer: bool, + pub is_writable: bool, + pub executable: bool, +} + +impl AccountInfoData { + // length constant + pub fn size() -> u64 { + 32 + 8 + 8 + 32 + 8 + 1 + 1 + 1 + } +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub enum AccountInfoDataField { + Key(Pubkey), + Lamports(u64), + DataLength(u64), + Owner(Pubkey), + RentEpoch(u64), + IsSigner(bool), + IsWritable(bool), + Executable(bool), +} diff --git a/programs/lighthouse/program/src/structs/write/mod.rs b/programs/lighthouse/program/src/structs/write/mod.rs new file mode 100644 index 0000000..764fd8e --- /dev/null +++ b/programs/lighthouse/program/src/structs/write/mod.rs @@ -0,0 +1,5 @@ +pub mod account_info; +pub mod program; + +pub use account_info::*; +pub use program::*; diff --git a/programs/lighthouse/program/src/structs/write/program.rs b/programs/lighthouse/program/src/structs/write/program.rs new file mode 100644 index 0000000..b3c1c2a --- /dev/null +++ b/programs/lighthouse/program/src/structs/write/program.rs @@ -0,0 +1,30 @@ +use anchor_lang::prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, +}; +use solana_program::pubkey::Pubkey; + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub struct ProgramInfo { + pub id: Pubkey, + pub executable_data: Pubkey, +} + +// impl AccountInfoData { +// // length constant +// pub fn size() -> u64 { +// 32 + 8 + 8 + 32 + 8 + 1 + 1 + 1 +// } +// } + +// #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +// pub enum AccountInfoDataField { +// Key(Pubkey), +// Lamports(u64), +// DataLength(u64), +// Owner(Pubkey), +// RentEpoch(u64), +// IsSigner(bool), +// IsWritable(bool), +// Executable(bool), +// } diff --git a/programs/lighthouse/program/src/structs/write_type.rs b/programs/lighthouse/program/src/structs/write_type.rs index 23aca18..0f3e67a 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -1,29 +1,67 @@ use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; -use super::borsh_field::BorshField; +use super::DataValue; + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub struct AccountValidation { + pub owner: Option, + pub is_funded: Option, + pub discriminator: Option>, +} -// TODO: probably worth creating a macro that permeates all these size variants so -// sdk can optimize space. Need to make sure its smaller than 256 variants though #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum WriteType { - AccountBalanceU8(u8), - AccountBalanceU16(u16), - AccountBalanceU32(u32), + AccountBalance, - // CacheOffset, AccountOffset, Length - AccountDataU8(u8, u8, u8), - AccountDataU16(u16, u16, u16), - AccountDataU32(u32, u32, u32), + // Account Data Offset, Data Length, Validation + AccountData(u16, Option, Option), + AccountInfo, + DataValue(DataValue), + Program, +} - // CacheOffset, BorshField - BorshFieldU8(u8, BorshField), - BorshFieldU16(u16, BorshField), +impl WriteType { + pub fn size( + &self, + account_info: Option<&anchor_lang::prelude::AccountInfo<'_>>, + ) -> Option { + match self { + WriteType::AccountBalance => Some(8), + WriteType::AccountData(account_offset, data_length, _) => { + if let Some(data_length) = data_length { + Some(*data_length as usize) + } else { + match account_info { + Some(account_info) => Some( + account_info + .data_len() + .checked_sub(*account_offset as usize)?, + ), + None => None, + } + } + } - // - MintAccount, - TokenAccount(u16), - TokenAccountOwner(u16), - TokenAccountBalance(u16), - // Program Account Assertions + WriteType::AccountInfo => Some(8), + WriteType::DataValue(memory_value) => match memory_value { + DataValue::Bool(_) | DataValue::U8(_) | DataValue::I8(_) => Some(1), + DataValue::U16(_) | DataValue::I16(_) => Some(2), + DataValue::U32(_) | DataValue::I32(_) => Some(4), + DataValue::U64(_) | DataValue::I64(_) => Some(8), + DataValue::U128(_) | DataValue::I128(_) => Some(16), + DataValue::Bytes(bytes) => Some(bytes.len()), + DataValue::Pubkey(_) => Some(32), + }, + WriteType::Program => Some(64), + } + } +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub enum WriteTypeParameter { + // Memory offset, write type + WriteU8(u8, WriteType), + WriteU16(u16, WriteType), + WriteU32(u32, WriteType), } diff --git a/programs/lighthouse/program/src/utils.rs b/programs/lighthouse/program/src/utils.rs index d98fe7c..667b091 100644 --- a/programs/lighthouse/program/src/utils.rs +++ b/programs/lighthouse/program/src/utils.rs @@ -1,27 +1,34 @@ -use anchor_lang::prelude::{borsh::BorshDeserialize, *}; +use crate::{processor::assert::AssertionConfig, structs::Operator}; +use solana_program::msg; -use crate::{ - error, - structs::{BorshField, Operator}, -}; - -pub fn process_value( - data: &[u8], - offset: u32, - size: usize, - expected_value: &T, - borsh_field: &BorshField, +pub fn print_assertion_result( + config: &Option, + assertion_info: String, + assertion_result: bool, + assertion_index: usize, operator: &Operator, -) -> Result<(String, String, bool)> { - let slice = &data[offset as usize..(offset as usize + size)]; - let value = T::try_from_slice(slice).map_err(|_| error::ProgramError::BorshValueMismatch)?; - - borsh_field.is_supported_operator(operator); - let assertion_result = operator.is_true(&value, expected_value); + value_str: String, + expected_value_str: String, +) { + if let Some(config) = config { + if !config.verbose { + return; + } + } else { + return; + } - Ok(( - value.to_string(), - expected_value.to_string(), - assertion_result, - )) + msg!( + "{} {} {} -> {} {} {}", + format!("[{:?}]", assertion_index), + if assertion_result { + "[✅] SUCCESS" + } else { + "[❌] FAIL " + }, + assertion_info, + value_str, + operator.format(), + expected_value_str, + ); } diff --git a/programs/lighthouse/program/tests/simple.rs b/programs/lighthouse/program/tests/simple.rs deleted file mode 100644 index b5fcc84..0000000 --- a/programs/lighthouse/program/tests/simple.rs +++ /dev/null @@ -1,620 +0,0 @@ -pub mod utils; - -use std::io::Error; - -use anchor_lang::system_program::System; -use anchor_lang::InstructionData; -use lighthouse::structs::{Assertion, BorshField, BorshValue, Expression, Operator}; -use solana_program::instruction::Instruction; -use solana_program::pubkey::Pubkey; -use solana_program::rent::Rent; -use solana_program_test::tokio; -use solana_sdk::{signer::EncodableKeypair, transaction::Transaction}; - -use solana_banks_interface::BanksTransactionResultWithMetadata; -use utils::context::TestContext; -use utils::program::Program; - -pub fn find_test_account() -> (solana_program::pubkey::Pubkey, u8) { - solana_program::pubkey::Pubkey::find_program_address( - &["test_account".to_string().as_ref()], - &lighthouse::ID, - ) -} - -pub fn find_cache_account(user: Pubkey, cache_index: u8) -> (solana_program::pubkey::Pubkey, u8) { - solana_program::pubkey::Pubkey::find_program_address( - &["cache".to_string().as_ref(), user.as_ref(), &[cache_index]], - &lighthouse::ID, - ) -} - -#[tokio::test] -async fn test_basic() { - let context = &mut TestContext::new().await.unwrap(); - let mut program = Program::new(context.client()); - - let mut tx_builder = program.create_assertion( - &context.payer(), - vec![ - Assertion::AccountBalance(0, Operator::GreaterThan), - // Assertion::AccountBalance(0, Operator::LessThan), - ], - vec![ - context.payer().encodable_pubkey(), - context.payer().encodable_pubkey(), - ], - None, - ); - - process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; -} - -#[tokio::test] -async fn test_borsh_account_data() { - let context = &mut TestContext::new().await.unwrap(); - let mut program = Program::new(context.client()); - - let account = find_test_account().0; - - process_transaction_assert_success( - context, - program - .create_test_account(&context.payer()) - .to_transaction(vec![]) - .await, - ) - .await; - - process_transaction_assert_success( - context, - program - .create_assertion( - &context.payer(), - vec![ - Assertion::BorshAccountData( - 8, - BorshField::U8, - Operator::Equal, - BorshValue::U8(1), - ), - Assertion::BorshAccountData( - 9, - BorshField::I8, - Operator::Equal, - BorshValue::I8(-1), - ), - Assertion::BorshAccountData( - 10, - BorshField::U16, - Operator::Equal, - BorshValue::U16((u8::MAX as u16) + 1), - ), - Assertion::BorshAccountData( - 12, - BorshField::I16, - Operator::Equal, - BorshValue::I16((i8::MIN as i16) - 1), - ), - Assertion::BorshAccountData( - 14, - BorshField::U32, - Operator::Equal, - BorshValue::U32((u16::MAX as u32) + 1), - ), - Assertion::BorshAccountData( - 18, - BorshField::I32, - Operator::Equal, - BorshValue::I32((i16::MIN as i32) - 1), - ), - Assertion::BorshAccountData( - 22, - BorshField::U64, - Operator::Equal, - BorshValue::U64((u32::MAX as u64) + 1), - ), - Assertion::BorshAccountData( - 30, - BorshField::I64, - Operator::Equal, - BorshValue::I64((i32::MIN as i64) - 1), - ), - Assertion::BorshAccountData( - 38, - BorshField::U128, - Operator::Equal, - BorshValue::U128((u64::MAX as u128) + 1), - ), - Assertion::BorshAccountData( - 54, - BorshField::I128, - Operator::Equal, - BorshValue::I128((i64::MIN as i128) - 1), - ), - ], - vec![account; 10], - None, - ) - .to_transaction(vec![]) - .await, - ) - .await; - - // let tx = &tx_builder - // .to_transaction(vec![Instruction { - // program_id: lighthouse::ID, - // accounts: (lighthouse::accounts::CreateTestAccountV1 { - // signer: context.payer().encodable_pubkey(), - // test_account: find_cache().0, - // rent: Rent::id(), - // system_program: System::id(), - // }) - // .to_account_metas(None), - // data: (lighthouse::instruction::CreateTestAccountV1 {}).data(), - // }]) - // .await; - - // if let Err(err) = tx { - // println!("err: {:?}", err); - // panic!("Should have passed"); - // } else if let Ok(tx) = tx { - // println!("Tx size: {}", tx.message().serialize().len()); - - // let response = context - // .client() - // .process_transaction_with_metadata(tx.clone()) - // .await - // .unwrap(); - - // let logs = response.metadata.unwrap().log_messages; - - // for log in logs { - // println!("{:?}", log); - // } - - // println!( - // "account: {:?}", - // context - // .client() - // .get_account(find_cache().0) - // .await - // .unwrap() - // .unwrap() - // .data - // ); - // } else { - // panic!("Should have passed"); - // } -} - -#[tokio::test] -async fn test_logical_expression() { - let context = &mut TestContext::new().await.unwrap(); - - let mut program = Program::new(context.client()); - - let account = find_test_account().0; - // Create test account - process_transaction_assert_success( - context, - program - .create_test_account(&context.payer()) - .to_transaction(vec![]) - .await, - ) - .await; - - let mut tx_builder = program.create_assertion( - &context.payer(), - vec![ - Assertion::BorshAccountData(8, BorshField::U8, Operator::Equal, BorshValue::U8(1)), - Assertion::BorshAccountData(8, BorshField::U8, Operator::Equal, BorshValue::U8(5)), - Assertion::BorshAccountData( - 10, - BorshField::U16, - Operator::Equal, - BorshValue::U16((u8::MAX as u16) + 1), - ), - Assertion::BorshAccountData(10, BorshField::U16, Operator::Equal, BorshValue::U16(30)), - ], - vec![account, account, account, account], - Some(vec![ - Expression::Or(vec![0, 1]), - Expression::Or(vec![2, 3]), - Expression::And(vec![0, 2]), - ]), - ); - - let _ = - process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; - - // let value = &Expression::Or(vec![0, 1]) - - // let tx = &tx_builder - // .to_transaction(vec![Instruction { - // program_id: lighthouse::ID, - // accounts: lighthouse::accounts::CreateTestAccountV1 { - // signer: context.payer().encodable_pubkey(), - // test_account: find_cache().0, - // rent: Rent::id(), - // system_program: System::id(), - // } - // .to_account_metas(None), - // data: (lighthouse::instruction::CreateTestAccount {}).data(), - // }]) - // .await; - - // if let Err(err) = tx { - // println!("err: {:?}", err); - // panic!("Should have passed"); - // } else if let Ok(tx) = tx { - // println!("Tx size: {}", tx.message().serialize().len()); - - // let response = context - // .client() - // .process_transaction_with_metadata(tx.clone()) - // .await - // .unwrap(); - - // let logs = response.metadata.unwrap().log_messages; - - // for log in logs { - // println!("{:?}", log); - // } - - // println!( - // "account: {:?}", - // context - // .client() - // .get_account(find_cache().0) - // .await - // .unwrap() - // .unwrap() - // .data - // ); - // } else { - // panic!("Should have passed"); - // } -} - -#[tokio::test] -async fn test_raw_account_data() { - let context = &mut TestContext::new().await.unwrap(); - - let mut program = Program::new(context.client()); - - process_transaction_assert_success( - &context, - program - .create_assertion( - &context.payer(), - vec![ - Assertion::AccountBalance(0, Operator::GreaterThan), - Assertion::AccountBalance(999995999975001u64, Operator::LessThan), - ], - vec![ - context.payer().encodable_pubkey(), - context.payer().encodable_pubkey(), - ], - None, - ) - .to_transaction(vec![]) - .await, - ) - .await; - - let account = find_test_account().0; - - let mut tx_builder = program.create_assertion( - &context.payer(), - vec![Assertion::RawAccountData( - 0, - Operator::Equal, - vec![200, 208, 249, 117, 197, 42, 20, 255], - )], - vec![account], - None, - ); - - // let tx = tx_builder - // .to_transaction(vec![Instruction { - // program_id: lighthouse::ID, - // accounts: lighthouse::accounts::CreateTestAccount { - // signer: context.payer().encodable_pubkey(), - // test_account: find_cache().0, - // rent: Rent::id(), - // system_program: System::id(), - // } - // .to_account_metas(None), - // data: (lighthouse::instruction::CreateTestAccount {}).data(), - // }]) - // .await; - - // if let Err(err) = tx { - // println!("err: {:?}", err); - // panic!("Should have passed"); - // } else if let Ok(tx) = tx { - // println!("Tx size: {}", tx.message().serialize().len()); - - // let response = context - // .client() - // .process_transaction_with_metadata(tx) - // .await - // .unwrap(); - - // let logs = response.metadata.unwrap().log_messages; - - // for log in logs { - // println!("{:?}", log); - // } - - // println!( - // "account: {:?}", - // context - // .client() - // .get_account(find_cache().0) - // .await - // .unwrap() - // .unwrap() - // .data - // ); - // } else { - // panic!("Should have passed"); - // } -} - -#[tokio::test] -async fn test_write() { - let context = &mut TestContext::new().await.unwrap(); - let mut program = Program::new(context.client()); - - // Create cache - let mut create_cache_builder = program.create_cache_account(&context.payer(), 0, 256); - let tx = create_cache_builder.to_transaction(vec![]).await; - process_transaction_assert_success(context, tx).await; - - // Create test account - process_transaction_assert_success( - context, - program - .create_test_account(&context.payer()) - .to_transaction(vec![]) - .await, - ) - .await; - - let cache_account = find_cache_account(context.payer().encodable_pubkey(), 0).0; - - { - // Test writing account data to cache. - process_transaction_assert_success( - context, - program - .write_v1( - &context.payer(), - find_test_account().0, - 0, - lighthouse::structs::WriteType::AccountDataU16(0, 8, 128), - ) - .to_transaction(vec![]) - .await, - ) - .await; - - // Assert that data was properly written to cache. - let tx = program - .create_assertion( - &context.payer(), - vec![ - Assertion::BorshAccountData( - 8, - BorshField::U8, - Operator::Equal, - BorshValue::U8(1), - ), - Assertion::BorshAccountData( - 9, - BorshField::I8, - Operator::Equal, - BorshValue::I8(-1), - ), - Assertion::BorshAccountData( - 10, - BorshField::U16, - Operator::Equal, - BorshValue::U16((u8::MAX as u16) + 1), - ), - Assertion::BorshAccountData( - 12, - BorshField::I16, - Operator::Equal, - BorshValue::I16((i8::MIN as i16) - 1), - ), - Assertion::BorshAccountData( - 14, - BorshField::U32, - Operator::Equal, - BorshValue::U32((u16::MAX as u32) + 1), - ), - Assertion::BorshAccountData( - 18, - BorshField::I32, - Operator::Equal, - BorshValue::I32((i16::MIN as i32) - 1), - ), - Assertion::BorshAccountData( - 22, - BorshField::U64, - Operator::Equal, - BorshValue::U64((u32::MAX as u64) + 1), - ), - Assertion::BorshAccountData( - 30, - BorshField::I64, - Operator::Equal, - BorshValue::I64((i32::MIN as i64) - 1), - ), - Assertion::BorshAccountData( - 38, - BorshField::U128, - Operator::Equal, - BorshValue::U128((u64::MAX as u128) + 1), - ), - Assertion::BorshAccountData( - 54, - BorshField::I128, - Operator::Equal, - BorshValue::I128((i64::MIN as i128) - 1), - ), - ], - vec![cache_account; 10], - None, - ) - .to_transaction(vec![]) - .await; - - process_transaction_assert_success(context, tx).await; - } - { - // Test writing account balance to cache. - let mut load_cache_builder = program.write_v1( - &context.payer(), - find_test_account().0, - 0, - lighthouse::structs::WriteType::AccountBalanceU8(0), - ); - let tx = load_cache_builder.to_transaction(vec![]).await; - process_transaction_assert_success(context, tx).await; - - let tx = program - .create_assertion( - &context.payer(), - vec![Assertion::BorshAccountData( - 8, - BorshField::U64, - Operator::Equal, - BorshValue::U64(2672640), - )], - vec![cache_account], - None, - ) - .to_transaction(vec![]) - .await; - process_transaction_assert_success(context, tx).await; - } - { - let mut load_cache_builder = program.write_v1( - &context.payer(), - find_test_account().0, - 0, - lighthouse::structs::WriteType::AccountBalanceU8(33), - ); - let tx = load_cache_builder.to_transaction(vec![]).await; - process_transaction_assert_success(context, tx).await; - - let tx = program - .create_assertion( - &context.payer(), - vec![ - Assertion::BorshAccountData( - 8, - BorshField::U64, - Operator::Equal, - BorshValue::U64(2672640), - ), - Assertion::BorshAccountData( - 8 + 33, - BorshField::U64, - Operator::Equal, - BorshValue::U64(2672640), - ), - ], - vec![cache_account; 2], - None, - ) - .to_transaction(vec![]) - .await; - process_transaction_assert_success(context, tx).await; - } - { - let _ = &context - .fund_account(find_test_account().0, 1000) - .await - .unwrap(); - - println!("test 4"); - let load_cache_builder = program.write_v1( - &context.payer(), - find_test_account().0, - 0, - lighthouse::structs::WriteType::AccountBalanceU8(0), - ); - let tx = program - .create_assertion( - &context.payer(), - vec![Assertion::BorshAccountData( - 8, - BorshField::U64, - Operator::Equal, - BorshValue::U64(2672640 + 1000), - )], - vec![cache_account], - None, - ) - .to_transaction(load_cache_builder.ixs) - .await; - process_transaction_assert_success(context, tx).await; - } -} - -fn format_hex(data: &[u8]) -> String { - let mut result = String::new(); - for (i, chunk) in data.chunks(32).enumerate() { - // Write the offset - result.push_str(&format!("{:08x} ({:08}): ", i * 32, i * 32)); - - // Write each byte in the chunk - for byte in chunk { - result.push_str(&format!("{:02x} ", byte)); - } - - // Add a new line - result.push('\r'); - result.push('\n'); - } - result -} - -async fn process_transaction( - context: &TestContext, - tx: &Transaction, -) -> Result { - let result: solana_banks_interface::BanksTransactionResultWithMetadata = context - .client() - .process_transaction_with_metadata(tx.clone()) - .await - .unwrap(); - // .metadata - // .unwrap(); - - Ok(result) -} - -async fn process_transaction_assert_success( - context: &TestContext, - tx: Result>, -) { - let tx = tx.expect("Should have been processed"); - - let tx_metadata = process_transaction(context, &tx).await.unwrap(); - - if tx_metadata.result.is_err() { - let logs = tx_metadata.metadata.unwrap().log_messages; - for log in logs { - println!("{:?}", log); - } - - panic!("Transaction failed"); - } -} diff --git a/programs/lighthouse/program/tests/suites/assert/account_balance.rs b/programs/lighthouse/program/tests/suites/assert/account_balance.rs new file mode 100644 index 0000000..b6ed311 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/assert/account_balance.rs @@ -0,0 +1,29 @@ +use lighthouse::structs::{Assertion, Operator}; +use solana_program_test::tokio; +use solana_sdk::signer::EncodableKeypair; + +use crate::utils::process_transaction_assert_success; +use crate::utils::{ + context::TestContext, + program::{create_user, Program}, +}; + +#[tokio::test] +async fn test_basic() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + let mut tx_builder = program.create_assertion( + &user, + vec![ + Assertion::AccountBalance(0, Operator::GreaterThan), + // Assertion::AccountBalance(0, Operator::LessThan), + ], + vec![user.encodable_pubkey(), user.encodable_pubkey()], + None, + None, + ); + + process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; +} diff --git a/programs/lighthouse/program/tests/suites/assert/account_data.rs b/programs/lighthouse/program/tests/suites/assert/account_data.rs new file mode 100644 index 0000000..6435aa4 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/assert/account_data.rs @@ -0,0 +1,81 @@ +use crate::utils::context::TestContext; +use crate::utils::process_transaction_assert_success; +use crate::utils::program::{create_test_account, create_user, find_test_account, Program}; +use lighthouse::structs::{Assertion, DataValue, Operator}; +use solana_program_test::tokio; + +/// +/// Tests all data types using the `AccountData` assertion. +/// +#[tokio::test] +async fn test_borsh_account_data() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + create_test_account(context, &user).await.unwrap(); + process_transaction_assert_success( + context, + program + .create_assertion( + &user, + vec![ + Assertion::AccountData(8, Operator::Equal, DataValue::U8(1)), + Assertion::AccountData(9, Operator::Equal, DataValue::I8(-1)), + Assertion::AccountData( + 10, + Operator::Equal, + DataValue::U16((u8::MAX as u16) + 1), + ), + Assertion::AccountData( + 12, + Operator::Equal, + DataValue::I16((i8::MIN as i16) - 1), + ), + Assertion::AccountData( + 14, + Operator::Equal, + DataValue::U32((u16::MAX as u32) + 1), + ), + Assertion::AccountData( + 18, + Operator::Equal, + DataValue::I32((i16::MIN as i32) - 1), + ), + Assertion::AccountData( + 22, + Operator::Equal, + DataValue::U64((u32::MAX as u64) + 1), + ), + Assertion::AccountData( + 30, + Operator::Equal, + DataValue::I64((i32::MIN as i64) - 1), + ), + Assertion::AccountData( + 38, + Operator::Equal, + DataValue::U128((u64::MAX as u128) + 1), + ), + Assertion::AccountData( + 54, + Operator::Equal, + DataValue::I128((i64::MIN as i128) - 1), + ), + Assertion::AccountData( + 70, + Operator::Equal, + DataValue::Bytes(vec![u8::MAX; 32]), + ), + Assertion::AccountData(102, Operator::Equal, DataValue::Bool(true)), + Assertion::AccountData(103, Operator::Equal, DataValue::Bool(false)), + ], + vec![find_test_account().0; 13], + None, + None, + ) + .to_transaction(vec![]) + .await, + ) + .await; +} diff --git a/programs/lighthouse/program/tests/suites/assert/logical_expression.rs b/programs/lighthouse/program/tests/suites/assert/logical_expression.rs new file mode 100644 index 0000000..0e90078 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/assert/logical_expression.rs @@ -0,0 +1,85 @@ +use lighthouse::{ + error::ProgramError, + structs::{Assertion, DataValue, Expression, Operator}, +}; +use solana_program_test::tokio; + +use crate::utils::{ + context::TestContext, + process_transaction_assert_failure, process_transaction_assert_success, + program::{create_test_account, create_user, find_test_account, Program}, + utils::to_transaction_error, +}; +/// +/// Test various logical expressions (false OR true), (true OR false), (true AND true). +/// +#[tokio::test] +async fn test_logical_expression() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + create_test_account(context, &user).await.unwrap(); + + println!("Test that the assertion passes when the logical expression is true."); + let mut tx_builder = program.create_assertion( + &user, + vec![ + Assertion::AccountData(8, Operator::Equal, DataValue::U8(1)), + Assertion::AccountData(8, Operator::Equal, DataValue::U8(5)), + Assertion::AccountData(10, Operator::Equal, DataValue::U16((u8::MAX as u16) + 1)), + Assertion::AccountData(10, Operator::Equal, DataValue::U16(30)), + ], + vec![find_test_account().0; 4], + Some(vec![ + Expression::Or(vec![0, 1]), + Expression::Or(vec![3, 2]), + Expression::And(vec![0, 2]), + ]), + None, + ); + process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; + + // Test that the assertion fails when the logical expression is false. + println!("Test that the assertion fails when the logical expression is false."); + let mut tx_builder = program.create_assertion( + &user, + vec![ + Assertion::AccountData(8, Operator::Equal, DataValue::U8(1)), + Assertion::AccountData(8, Operator::Equal, DataValue::U8(5)), + Assertion::AccountData(10, Operator::Equal, DataValue::U16((u8::MAX as u16) + 1)), + Assertion::AccountData(10, Operator::Equal, DataValue::U16(30)), + ], + vec![find_test_account().0; 4], + Some(vec![Expression::Or(vec![1, 3])]), + None, + ); + process_transaction_assert_failure( + context, + tx_builder.to_transaction(vec![]).await, + to_transaction_error(0, ProgramError::AssertionFailed), + Some(&["1 == 5".to_string(), "256 == 30".to_string()]), + ) + .await; + + // Test that the assertion fails when the logical expression is false. + println!("Test that the assertion fails when the logical expression is false."); + let mut tx_builder = program.create_assertion( + &user, + vec![ + Assertion::AccountData(8, Operator::Equal, DataValue::U8(1)), + Assertion::AccountData(8, Operator::GreaterThan, DataValue::U8(0)), + Assertion::AccountData(10, Operator::LessThan, DataValue::U16(u8::MAX as u16)), + ], + vec![find_test_account().0; 4], + Some(vec![Expression::And(vec![0, 1]), Expression::Or(vec![2])]), + None, + ); + process_transaction_assert_failure( + context, + tx_builder.to_transaction(vec![]).await, + to_transaction_error(0, ProgramError::AssertionFailed), + Some(&["1 == 1".to_string(), "256 < 255".to_string()]), + ) + .await; +} diff --git a/programs/lighthouse/program/tests/suites/assert/mod.rs b/programs/lighthouse/program/tests/suites/assert/mod.rs new file mode 100644 index 0000000..4eec92d --- /dev/null +++ b/programs/lighthouse/program/tests/suites/assert/mod.rs @@ -0,0 +1,4 @@ +pub mod account_balance; +pub mod account_data; +pub mod logical_expression; +pub mod token_account_balance; diff --git a/programs/lighthouse/program/tests/suites/assert/token_account_balance.rs b/programs/lighthouse/program/tests/suites/assert/token_account_balance.rs new file mode 100644 index 0000000..c7d1474 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/assert/token_account_balance.rs @@ -0,0 +1,44 @@ +use anchor_spl::associated_token::get_associated_token_address; +use lighthouse::structs::{Assertion, Operator}; +use solana_program_test::tokio; +use solana_sdk::signer::Signer; + +use crate::utils::process_transaction_assert_success; +use crate::utils::program::{create_mint, mint_to}; +use crate::utils::{ + context::TestContext, + program::{create_user, Program}, +}; + +#[tokio::test] +async fn test_basic() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + let (tx, mint) = create_mint(context, &user).await.unwrap(); + process_transaction_assert_success(context, Ok(tx)).await; + + let tx = mint_to(context, &mint.pubkey(), &user, &user.pubkey(), 100) + .await + .unwrap(); + process_transaction_assert_success(context, Ok(tx)).await; + + let token_account = get_associated_token_address(&user.pubkey(), &mint.pubkey()); + let mut tx_builder = program.create_assertion( + &user, + vec![ + Assertion::TokenAccountBalance(0, Operator::GreaterThan), + Assertion::TokenAccountBalance(101, Operator::LessThan), + Assertion::TokenAccountBalance(100, Operator::LessThanOrEqual), + Assertion::TokenAccountBalance(100, Operator::GreaterThanOrEqual), + Assertion::TokenAccountBalance(100, Operator::Equal), + Assertion::TokenAccountBalance(99, Operator::NotEqual), + ], + vec![token_account; 6], + None, + None, + ); + + process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; +} diff --git a/programs/lighthouse/program/tests/suites/mod.rs b/programs/lighthouse/program/tests/suites/mod.rs new file mode 100644 index 0000000..8adc63a --- /dev/null +++ b/programs/lighthouse/program/tests/suites/mod.rs @@ -0,0 +1,3 @@ +pub mod assert; +pub mod simple; +pub mod write; diff --git a/programs/lighthouse/program/tests/suites/simple.rs b/programs/lighthouse/program/tests/suites/simple.rs new file mode 100644 index 0000000..fc39e2c --- /dev/null +++ b/programs/lighthouse/program/tests/suites/simple.rs @@ -0,0 +1,75 @@ +use crate::utils::context::TestContext; +use crate::utils::process_transaction_assert_success; +use crate::utils::program::{ + create_cache_account, create_test_account, create_user, find_cache_account, find_test_account, + Program, +}; +use lighthouse::structs::{Assertion, DataValue, Operator, WriteType}; +use solana_program_test::tokio; +use solana_sdk::signer::EncodableKeypair; + +#[tokio::test] +async fn test_write() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + // Create test account + let _ = create_test_account(context, &user).await; + let _ = create_cache_account(context, &user, 256).await; + + let cache_account = find_cache_account(user.encodable_pubkey(), 0).0; + + { + // Test writing account data to cache. + process_transaction_assert_success( + context, + program + .write_v1( + &user, + find_test_account().0, + 0, + lighthouse::structs::WriteTypeParameter::WriteU8( + 0, + WriteType::AccountData(8, Some(128), None), + ), + ) + .to_transaction(vec![]) + .await, + ) + .await; + + // Assert that data was properly written to cache. + let tx = program + .create_assertion( + &user, + vec![ + Assertion::Memory(0, Operator::Equal, DataValue::U8(1)), + Assertion::Memory(0, Operator::GreaterThan, DataValue::U8(0)), + Assertion::Memory(0, Operator::LessThan, DataValue::U8(2)), + Assertion::Memory(0, Operator::GreaterThanOrEqual, DataValue::U8(1)), + Assertion::Memory(0, Operator::LessThanOrEqual, DataValue::U8(1)), + Assertion::Memory(1, Operator::Equal, DataValue::I8(-1)), + Assertion::Memory(1, Operator::GreaterThan, DataValue::I8(-2)), + Assertion::Memory(1, Operator::LessThan, DataValue::I8(0)), + Assertion::Memory(1, Operator::GreaterThanOrEqual, DataValue::I8(-1)), + Assertion::Memory(1, Operator::LessThanOrEqual, DataValue::I8(-1)), + Assertion::Memory(2, Operator::Equal, DataValue::U16((u8::MAX as u16) + 1)), + Assertion::Memory(4, Operator::Equal, DataValue::I16((i8::MIN as i16) - 1)), + Assertion::Memory(6, Operator::Equal, DataValue::U32((u16::MAX as u32) + 1)), + Assertion::Memory(10, Operator::Equal, DataValue::I32((i16::MIN as i32) - 1)), + Assertion::Memory(14, Operator::Equal, DataValue::U64((u32::MAX as u64) + 1)), + Assertion::Memory(22, Operator::Equal, DataValue::I64((i32::MIN as i64) - 1)), + Assertion::Memory(30, Operator::Equal, DataValue::U128((u64::MAX as u128) + 1)), + Assertion::Memory(46, Operator::Equal, DataValue::I128((i64::MIN as i128) - 1)), + ], + vec![], + None, + Some(cache_account), + ) + .to_transaction(vec![]) + .await; + + process_transaction_assert_success(context, tx).await; + } +} diff --git a/programs/lighthouse/program/tests/suites/write/mod.rs b/programs/lighthouse/program/tests/suites/write/mod.rs new file mode 100644 index 0000000..5ea6287 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/write/mod.rs @@ -0,0 +1,2 @@ +// pub mod program; +pub mod simple; diff --git a/programs/lighthouse/program/tests/suites/write/program.rs b/programs/lighthouse/program/tests/suites/write/program.rs new file mode 100644 index 0000000..1bdf70a --- /dev/null +++ b/programs/lighthouse/program/tests/suites/write/program.rs @@ -0,0 +1,56 @@ +use lighthouse::structs::{Assertion, DataValue, Operator, WriteType}; +use solana_program_test::tokio; +use solana_sdk::signer::EncodableKeypair; + +use crate::utils::{ + context::TestContext, + process_transaction_assert_success, + program::{ + create_cache_account, create_test_account, create_user, find_cache_account, + find_test_account, Program, + }, +}; + +#[tokio::test] +async fn test_write_program() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + // Create test account + let _ = create_test_account(context, &user).await; + let _ = create_cache_account(context, &user, 256).await; + + let cache_account = find_cache_account(user.encodable_pubkey(), 0).0; + + { + // Test writing account data to cache. + process_transaction_assert_success( + context, + program + .write_v1( + &user, + lighthouse::ID, + 0, + lighthouse::structs::WriteTypeParameter::WriteU8(0, WriteType::Program), + ) + .to_transaction(vec![]) + .await, + ) + .await; + + // Assert that data was properly written to cache. + let tx = program + .create_assertion( + &user, + vec![Assertion::Memory(0, Operator::Equal, DataValue::U8(1))], + vec![], + None, + Some(cache_account), + ) + .to_transaction(vec![]) + .await; + + process_transaction_assert_success(context, tx).await; + } +} diff --git a/programs/lighthouse/program/tests/suites/write/simple.rs b/programs/lighthouse/program/tests/suites/write/simple.rs new file mode 100644 index 0000000..5b7843d --- /dev/null +++ b/programs/lighthouse/program/tests/suites/write/simple.rs @@ -0,0 +1,78 @@ +use lighthouse::structs::{Assertion, DataValue, Operator, WriteType}; +use solana_program_test::tokio; +use solana_sdk::signer::EncodableKeypair; + +use crate::utils::{ + context::TestContext, + process_transaction_assert_success, + program::{ + create_cache_account, create_test_account, create_user, find_cache_account, + find_test_account, Program, + }, +}; + +#[tokio::test] +async fn test_write() { + let context = &mut TestContext::new().await.unwrap(); + let mut program = Program::new(context.client()); + let user = create_user(context).await.unwrap(); + + // Create test account + let _ = create_test_account(context, &user).await; + let _ = create_cache_account(context, &user, 256).await; + + let cache_account = find_cache_account(user.encodable_pubkey(), 0).0; + + { + // Test writing account data to cache. + process_transaction_assert_success( + context, + program + .write_v1( + &user, + find_test_account().0, + 0, + lighthouse::structs::WriteTypeParameter::WriteU8( + 0, + WriteType::AccountData(8, Some(128), None), + ), + ) + .to_transaction(vec![]) + .await, + ) + .await; + + // Assert that data was properly written to cache. + let tx = program + .create_assertion( + &user, + vec![ + Assertion::Memory(0, Operator::Equal, DataValue::U8(1)), + Assertion::Memory(0, Operator::GreaterThan, DataValue::U8(0)), + Assertion::Memory(0, Operator::LessThan, DataValue::U8(2)), + Assertion::Memory(0, Operator::GreaterThanOrEqual, DataValue::U8(1)), + Assertion::Memory(0, Operator::LessThanOrEqual, DataValue::U8(1)), + Assertion::Memory(1, Operator::Equal, DataValue::I8(-1)), + Assertion::Memory(1, Operator::GreaterThan, DataValue::I8(-2)), + Assertion::Memory(1, Operator::LessThan, DataValue::I8(0)), + Assertion::Memory(1, Operator::GreaterThanOrEqual, DataValue::I8(-1)), + Assertion::Memory(1, Operator::LessThanOrEqual, DataValue::I8(-1)), + Assertion::Memory(2, Operator::Equal, DataValue::U16((u8::MAX as u16) + 1)), + Assertion::Memory(4, Operator::Equal, DataValue::I16((i8::MIN as i16) - 1)), + Assertion::Memory(6, Operator::Equal, DataValue::U32((u16::MAX as u32) + 1)), + Assertion::Memory(10, Operator::Equal, DataValue::I32((i16::MIN as i32) - 1)), + Assertion::Memory(14, Operator::Equal, DataValue::U64((u32::MAX as u64) + 1)), + Assertion::Memory(22, Operator::Equal, DataValue::I64((i32::MIN as i64) - 1)), + Assertion::Memory(30, Operator::Equal, DataValue::U128((u64::MAX as u128) + 1)), + Assertion::Memory(46, Operator::Equal, DataValue::I128((i64::MIN as i128) - 1)), + ], + vec![], + None, + Some(cache_account), + ) + .to_transaction(vec![]) + .await; + + process_transaction_assert_success(context, tx).await; + } +} diff --git a/programs/lighthouse/program/tests/tests.rs b/programs/lighthouse/program/tests/tests.rs new file mode 100644 index 0000000..18dd4e6 --- /dev/null +++ b/programs/lighthouse/program/tests/tests.rs @@ -0,0 +1,2 @@ +pub mod suites; +pub mod utils; diff --git a/programs/lighthouse/program/tests/utils/context.rs b/programs/lighthouse/program/tests/utils/context.rs index e2eea9d..604ce25 100644 --- a/programs/lighthouse/program/tests/utils/context.rs +++ b/programs/lighthouse/program/tests/utils/context.rs @@ -1,6 +1,4 @@ -use std::fmt::Display; - -use super::{clone_keypair, program_test, BanksResult, Error, Result}; +use super::{clone_keypair, program_test, Error, Result}; use solana_program::pubkey::Pubkey; use solana_program_test::{BanksClient, ProgramTestContext, ProgramTestError}; use solana_sdk::{ diff --git a/programs/lighthouse/program/tests/utils/mod.rs b/programs/lighthouse/program/tests/utils/mod.rs index 271891d..5ffad39 100644 --- a/programs/lighthouse/program/tests/utils/mod.rs +++ b/programs/lighthouse/program/tests/utils/mod.rs @@ -1,18 +1,15 @@ pub mod context; pub mod program; pub mod tx_builder; +pub mod utils; use anchor_lang::{self, InstructionData, ToAccountMetas}; -use async_trait::async_trait; use bytemuck::PodCastError; -use solana_program::{instruction::Instruction, pubkey::Pubkey, system_instruction}; -use solana_program_test::{BanksClientError, ProgramTest, ProgramTestContext}; -use solana_sdk::{ - signature::{Keypair, SignerError}, - signer::Signer, - transaction::Transaction, -}; +use solana_program::{instruction::Instruction, pubkey::Pubkey}; +use solana_program_test::{BanksClientError, ProgramTest}; +use solana_sdk::signature::{Keypair, SignerError}; use std::result; +pub use utils::{process_transaction_assert_failure, process_transaction_assert_success}; #[derive(Debug)] pub enum Error { diff --git a/programs/lighthouse/program/tests/utils/print.rs b/programs/lighthouse/program/tests/utils/print.rs new file mode 100644 index 0000000..e4b6c76 --- /dev/null +++ b/programs/lighthouse/program/tests/utils/print.rs @@ -0,0 +1,17 @@ +fn format_hex(data: &[u8]) -> String { + let mut result = String::new(); + for (i, chunk) in data.chunks(32).enumerate() { + // Write the offset + result.push_str(&format!("{:08x} ({:08}): ", i * 32, i * 32)); + + // Write each byte in the chunk + for byte in chunk { + result.push_str(&format!("{:02x} ", byte)); + } + + // Add a new line + result.push('\r'); + result.push('\n'); + } + result +} diff --git a/programs/lighthouse/program/tests/utils/program.rs b/programs/lighthouse/program/tests/utils/program.rs index 088763b..80b2f17 100644 --- a/programs/lighthouse/program/tests/utils/program.rs +++ b/programs/lighthouse/program/tests/utils/program.rs @@ -1,7 +1,7 @@ -use crate::{find_cache_account, find_test_account}; - use super::{ clone_keypair, + context::{TestContext, DEFAULT_LAMPORTS_FUND_AMOUNT}, + process_transaction_assert_success, tx_builder::{ AssertBuilder, CacheLoadAccountV1Builder, CreateCacheAccountBuilder, CreateTestAccountV1Builder, TxBuilder, @@ -9,15 +9,17 @@ use super::{ Error, Result, }; use anchor_lang::*; +use anchor_spl::associated_token; use lighthouse::{ - processor::Config, - structs::{Assertion, Expression, WriteType}, + processor::AssertionConfig, + structs::{Assertion, Expression, WriteTypeParameter}, }; use solana_program::{ instruction::{AccountMeta, Instruction}, + program_pack::Pack, pubkey::Pubkey, rent::Rent, - system_program, sysvar, + system_instruction, system_program, sysvar, }; use solana_program_test::BanksClient; use solana_sdk::{ @@ -25,23 +27,14 @@ use solana_sdk::{ signer::signers::Signers, transaction::Transaction, }; +use spl_token::state::Mint; -// A convenience object that records some of the parameters for compressed -// trees and generates TX builders with the default configuration for each -// operation. pub struct Program { client: BanksClient, } impl Program { - // This and `with_creator` use a bunch of defaults; things can be - // customized some more via the public access, or we can add extra - // methods to make things even easier. pub fn new(client: BanksClient) -> Self { - Self::with_creator(&Keypair::new(), client) - } - - pub fn with_creator(tree_creator: &Keypair, client: BanksClient) -> Self { Program { client } } @@ -106,10 +99,9 @@ impl Program { assertions: Vec, additional_accounts: Vec, logical_expression: Option>, + cache: Option, ) -> AssertBuilder { - let accounts = lighthouse::accounts::AssertV1 { - system_program: system_program::id(), - }; + let accounts = lighthouse::accounts::AssertV1 { cache }; let assertion_clone = (assertions).clone(); let logical_expression_clone = (logical_expression).clone(); @@ -118,7 +110,7 @@ impl Program { let data = lighthouse::instruction::AssertV1 { assertions, logical_expression, - options: Some(Config { verbose: true }), + options: Some(AssertionConfig { verbose: true }), }; self.tx_builder( @@ -127,14 +119,11 @@ impl Program { (), vec![Instruction { program_id: lighthouse::id(), - accounts: (lighthouse::accounts::AssertV1 { - system_program: system_program::id(), - }) - .to_account_metas(None), + accounts: (lighthouse::accounts::AssertV1 { cache }).to_account_metas(None), data: (lighthouse::instruction::AssertV1 { assertions: assertion_clone, logical_expression: logical_expression_clone, - options: Some(Config { verbose: true }), + options: Some(AssertionConfig { verbose: true }), }) .data(), }], @@ -160,7 +149,6 @@ impl Program { rent: sysvar::rent::id(), }; - // The conversions below should not fail. let data = lighthouse::instruction::CreateCacheAccountV1 { cache_index, cache_account_size, @@ -196,19 +184,18 @@ impl Program { payer: &Keypair, source_account: Pubkey, cache_index: u8, - write_type: WriteType, + write_type_parameter: WriteTypeParameter, ) -> CacheLoadAccountV1Builder { let accounts = lighthouse::accounts::WriteV1 { system_program: system_program::id(), signer: payer.pubkey(), cache_account: find_cache_account(payer.pubkey(), cache_index).0, - rent: sysvar::rent::id(), }; - let write_type_clone = write_type.clone(); + let write_type_clone = write_type_parameter.clone(); let data = lighthouse::instruction::WriteV1 { - write_type, + write_type: write_type_parameter, cache_index, }; @@ -216,7 +203,6 @@ impl Program { system_program: system_program::id(), signer: payer.pubkey(), cache_account: find_cache_account(payer.pubkey(), cache_index).0, - rent: sysvar::rent::id(), } .to_account_metas(None); ix_accounts.append(&mut vec![AccountMeta::new(source_account, false)]); @@ -248,9 +234,138 @@ impl Program { rent: sysvar::rent::id(), }; - // The conversions below should not fail. let data = lighthouse::instruction::CreateTestAccountV1 {}; self.tx_builder(accounts, data, (), vec![], payer.pubkey(), &[payer], vec![]) } } + +pub async fn create_test_account(context: &mut TestContext, payer: &Keypair) -> Result<()> { + let mut program = Program::new(context.client()); + let mut tx_builder = program.create_test_account(payer); + process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; + Ok(()) +} + +pub async fn create_cache_account( + context: &mut TestContext, + user: &Keypair, + size: u64, +) -> Result<()> { + let mut program = Program::new(context.client()); + let mut tx_builder = program.create_cache_account(user, 0, size); + process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; + Ok(()) +} + +pub fn find_test_account() -> (solana_program::pubkey::Pubkey, u8) { + solana_program::pubkey::Pubkey::find_program_address( + &["test_account".to_string().as_ref()], + &lighthouse::ID, + ) +} + +pub fn find_cache_account(user: Pubkey, cache_index: u8) -> (solana_program::pubkey::Pubkey, u8) { + solana_program::pubkey::Pubkey::find_program_address( + &["cache".to_string().as_ref(), user.as_ref(), &[cache_index]], + &lighthouse::ID, + ) +} + +pub async fn create_user(ctx: &mut TestContext) -> Result { + let user = Keypair::new(); + let _ = ctx + .fund_account(user.pubkey(), DEFAULT_LAMPORTS_FUND_AMOUNT) + .await; + + Ok(user) +} + +pub async fn create_mint(ctx: &mut TestContext, payer: &Keypair) -> Result<(Transaction, Keypair)> { + let mint = Keypair::new(); + + let mint_rent = Rent::default().minimum_balance(Mint::get_packed_len()); + let create_ix = system_instruction::create_account( + &payer.pubkey(), + &mint.pubkey(), + mint_rent, + Mint::get_packed_len() as u64, + &spl_token::id(), + ); + + let mint_ix = spl_token::instruction::initialize_mint2( + &spl_token::id(), + &mint.pubkey(), + &payer.pubkey(), + None, + 100, + ) + .unwrap(); + + let mut tx = Transaction::new_with_payer(&[create_ix, mint_ix], Some(&payer.pubkey())); + + let signers: &[Keypair; 2] = &[payer.insecure_clone(), mint.insecure_clone()]; + + // print all the accounts in tx and is_signer + for (i, account) in tx.message().account_keys.iter().enumerate() { + println!("account: {} {}", account, tx.message.is_signer(i)); + } + + // print the signers pubkey in array + for signer in signers.iter() { + let pos = tx.get_signing_keypair_positions(&[signer.pubkey()]); + println!( + "signer: {} {}", + signer.insecure_clone().pubkey(), + pos.unwrap()[0].unwrap_or(0) + ); + } + + tx.try_partial_sign( + &signers.iter().collect::>(), + ctx.client().get_latest_blockhash().await.unwrap(), + ) + .unwrap(); + + Ok((tx, mint)) +} + +pub async fn mint_to( + ctx: &mut TestContext, + mint: &Pubkey, + authority: &Keypair, + dest: &Pubkey, + amount: u64, +) -> Result { + let token_account = associated_token::get_associated_token_address(dest, mint); + let create_account_ix = + spl_associated_token_account::instruction::create_associated_token_account( + &authority.pubkey(), + dest, + mint, + &spl_token::id(), + ); + + let mint_to_ix = spl_token::instruction::mint_to( + &spl_token::id(), + mint, + &token_account, + &authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + let mut tx = + Transaction::new_with_payer(&[create_account_ix, mint_to_ix], Some(&authority.pubkey())); + + let signers: &[Keypair; 1] = &[authority.insecure_clone()]; + + tx.try_partial_sign( + &signers.iter().collect::>(), + ctx.client().get_latest_blockhash().await.unwrap(), + ) + .unwrap(); + + Ok(tx) +} diff --git a/programs/lighthouse/program/tests/utils/tx_builder.rs b/programs/lighthouse/program/tests/utils/tx_builder.rs index 4e192a5..c418518 100644 --- a/programs/lighthouse/program/tests/utils/tx_builder.rs +++ b/programs/lighthouse/program/tests/utils/tx_builder.rs @@ -121,7 +121,7 @@ where .chain(vec![ix]) .collect::>(); - let tx = &mut Transaction::new_with_payer(&ixs, Some(&self.payer)); + let tx = &mut Transaction::new_with_payer(ixs, Some(&self.payer)); // Using `try_partial_sign` to avoid panics (and get an error when something is // wrong instead) no matter what signers are configured. diff --git a/programs/lighthouse/program/tests/utils/utils.rs b/programs/lighthouse/program/tests/utils/utils.rs new file mode 100644 index 0000000..bdbe0ee --- /dev/null +++ b/programs/lighthouse/program/tests/utils/utils.rs @@ -0,0 +1,105 @@ +use super::context::TestContext; +use crate::utils; +use lighthouse::error::ProgramError; +use solana_banks_interface::BanksTransactionResultWithMetadata; +use solana_program::instruction::InstructionError; +use solana_sdk::transaction::{Transaction, TransactionError}; +use std::io::Error; + +pub async fn process_transaction( + context: &TestContext, + tx: &Transaction, +) -> Result { + let result: solana_banks_interface::BanksTransactionResultWithMetadata = context + .client() + .process_transaction_with_metadata(tx.clone()) + .await + .unwrap(); + + Ok(result) +} + +pub async fn process_transaction_assert_success( + context: &TestContext, + tx: Result>, +) { + let tx = tx.expect("Should have been processed"); + + let tx_metadata = process_transaction(context, &tx).await; + + if let Err(err) = tx_metadata { + panic!("Transaction failed to process: {:?}", err); + } + + let tx_metadata = tx_metadata.unwrap(); + + if tx_metadata.result.is_err() { + println!("Tx Result {:?}", tx_metadata.result.clone().err()); + } + + let logs = tx_metadata.metadata.unwrap().log_messages; + for log in logs { + println!("{:?}", log); + } + + if tx_metadata.result.is_err() { + panic!("Transaction failed"); + } +} + +pub async fn process_transaction_assert_failure( + context: &TestContext, + tx: Result>, + expected_error_code: TransactionError, + log_match_regex: Option<&[String]>, +) { + let tx = tx.expect("Should have been processed"); + + let tx_metadata = process_transaction(context, &tx).await.unwrap(); + + let logs = tx_metadata.metadata.clone().unwrap().log_messages; + for log in logs { + println!("{:?}", log); + } + + if tx_metadata.result.is_ok() { + panic!("Transaction should have failed"); + } + + let err = tx_metadata.result.unwrap_err(); + + if err != expected_error_code { + panic!("Transaction failed with unexpected error code"); + } + + if let Some(log_regex) = log_match_regex { + let regexes = log_regex + .iter() + .map(|s| regex::Regex::new(s).unwrap()) + .collect::>(); + + let logs = tx_metadata.metadata.unwrap().log_messages; + for log in &logs { + println!("{:?}", log); + } + + // find one log that matches each regex + for regex in regexes { + let mut found = false; + for log in &logs { + if regex.is_match(log) { + found = true; + break; + } + } + + if !found { + panic!("Log not found: {}", regex); + } + } + } +} + +pub fn to_transaction_error(ix_index: u8, program_error: ProgramError) -> TransactionError { + TransactionError::InstructionError(ix_index, InstructionError::Custom(program_error.into())) +}