From 509c9c8d0398dc8d87acc012aa043506668db0cf Mon Sep 17 00:00:00 2001 From: Jac0xb Date: Tue, 5 Dec 2023 03:25:06 -0500 Subject: [PATCH 1/6] Add support for account info, begin writing tests --- README.md | 4 + optionize_macro/Cargo.lock | 47 +++++ optionize_macro/Cargo.toml | 12 ++ optionize_macro/src/lib.rs | 45 +++++ programs/lighthouse/Cargo.lock | 10 + programs/lighthouse/program/Cargo.toml | 6 +- programs/lighthouse/program/src/error.rs | 4 + .../program/src/processor/v1/assert.rs | 1 + .../program/src/processor/v1/write.rs | 63 +++++- .../program/src/structs/account_info.rs | 39 ++++ .../program/src/structs/assertion.rs | 4 +- .../lighthouse/program/src/structs/mod.rs | 2 + .../program/src/structs/write_type.rs | 9 + programs/lighthouse/program/tests/simple.rs | 183 +++++++++++++++++- 14 files changed, 408 insertions(+), 21 deletions(-) create mode 100644 optionize_macro/Cargo.lock create mode 100644 optionize_macro/Cargo.toml create mode 100644 optionize_macro/src/lib.rs create mode 100644 programs/lighthouse/program/src/structs/account_info.rs 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/optionize_macro/Cargo.lock b/optionize_macro/Cargo.lock new file mode 100644 index 0000000..1d5efa5 --- /dev/null +++ b/optionize_macro/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "optionize_macro" +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/optionize_macro/Cargo.toml b/optionize_macro/Cargo.toml new file mode 100644 index 0000000..c4294df --- /dev/null +++ b/optionize_macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "optionize_macro" +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/optionize_macro/src/lib.rs b/optionize_macro/src/lib.rs new file mode 100644 index 0000000..f847c36 --- /dev/null +++ b/optionize_macro/src/lib.rs @@ -0,0 +1,45 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Attribute, Data, 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) +} diff --git a/programs/lighthouse/Cargo.lock b/programs/lighthouse/Cargo.lock index 07ea12f..f995928 100644 --- a/programs/lighthouse/Cargo.lock +++ b/programs/lighthouse/Cargo.lock @@ -2088,6 +2088,7 @@ dependencies = [ "bytemuck", "mpl-token-metadata", "num-traits", + "optionize_macro", "solana-banks-interface", "solana-program", "solana-program-test", @@ -2597,6 +2598,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "optionize_macro" +version = "0.1.0" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.32", + "syn 1.0.109", +] + [[package]] name = "os_str_bytes" version = "6.5.1" diff --git a/programs/lighthouse/program/Cargo.toml b/programs/lighthouse/program/Cargo.toml index 6d5356d..f785c24 100644 --- a/programs/lighthouse/program/Cargo.toml +++ b/programs/lighthouse/program/Cargo.toml @@ -26,17 +26,15 @@ 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"] } +optionize_macro = { path = "../../../optionize_macro" } [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 +solana-banks-interface = "1.14.10" \ No newline at end of file diff --git a/programs/lighthouse/program/src/error.rs b/programs/lighthouse/program/src/error.rs index c8978ae..15dcc2c 100644 --- a/programs/lighthouse/program/src/error.rs +++ b/programs/lighthouse/program/src/error.rs @@ -14,4 +14,8 @@ pub enum ProgramError { BorshValueMismatch, #[msg("UnsupportedOperator")] UnsupportedOperator, + #[msg("CacheOutOfRange")] + CacheOutOfRange, + #[msg("AccountBorrowFailed")] + AccountBorrowFailed, } diff --git a/programs/lighthouse/program/src/processor/v1/assert.rs b/programs/lighthouse/program/src/processor/v1/assert.rs index cae3c6b..32f5e9c 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -324,6 +324,7 @@ pub fn assert<'info>( Assertion::TokenAccountBalance(expected_balance, operator) => { return Err(ProgramError::Unimplemented.into()); } + (_) => {} // REMOVE } assertion_results.push(assertion_result); diff --git a/programs/lighthouse/program/src/processor/v1/write.rs b/programs/lighthouse/program/src/processor/v1/write.rs index 6f08f45..2d760d8 100644 --- a/programs/lighthouse/program/src/processor/v1/write.rs +++ b/programs/lighthouse/program/src/processor/v1/write.rs @@ -3,7 +3,7 @@ use anchor_spl::token::TokenAccount; use borsh::BorshDeserialize; use crate::error::ProgramError; -use crate::structs::WriteType; +use crate::structs::{AccountInfoData, WriteType}; #[derive(Accounts)] #[instruction(cache_index: u8)] @@ -36,6 +36,7 @@ pub fn write<'info>( let account_offset: usize; let data_length: usize; + // TODO: make less messy but the main point is to allow more compact instruction data. (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), @@ -55,16 +56,27 @@ pub fn write<'info>( 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), + WriteType::AccountInfoU8(_cache_offset) => { + (_cache_offset as usize, 0, AccountInfoData::size() as usize) + } + WriteType::AccountInfoU16(_cache_offset) => { + (_cache_offset as usize, 0, AccountInfoData::size() as usize) + } + WriteType::AccountInfoU32(_cache_offset) => { + (_cache_offset as usize, 0, AccountInfoData::size() as usize) + } }; - cache_offset += 8; + // Cache offset can never write to the first 8 bytes of the cache account + cache_offset = cache_offset + .checked_add(8) + .ok_or(ProgramError::CacheOutOfRange)?; match write_type { WriteType::AccountBalanceU8(_) @@ -94,13 +106,15 @@ pub fn write<'info>( 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 { @@ -108,12 +122,16 @@ pub fn write<'info>( } } WriteType::TokenAccount(_) => { + // TODO: Not sure we really need this, could be extracted by user + msg!("write_type: TokenAccount"); let source_account = ctx.remaining_accounts.first(); - if let Some(target_account) = source_account { + if let Some(source_account) = source_account { + // TODO: add validation to token account + if (cache_offset + data_length) < cache_data_length { - let data = target_account.try_borrow_data()?; + let data = source_account.try_borrow_data()?; let data_slice = &data[0..data_length]; cache_ref[cache_offset..(cache_offset + data_length)] @@ -163,6 +181,37 @@ pub fn write<'info>( return Err(ProgramError::NotEnoughAccounts.into()); } } + WriteType::AccountInfoU8(_) + | WriteType::AccountInfoU16(_) + | WriteType::AccountInfoU32(_) => { + msg!("write_type: AccountInfoU8"); + let source_account = ctx.remaining_accounts.first(); + + if let Some(target_account) = source_account { + if (cache_offset + data_length) < cache_data_length { + 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)] + .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()); diff --git a/programs/lighthouse/program/src/structs/account_info.rs b/programs/lighthouse/program/src/structs/account_info.rs new file mode 100644 index 0000000..52d0e87 --- /dev/null +++ b/programs/lighthouse/program/src/structs/account_info.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, +}; +use optionize_macro::Optionize; +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 + } +} + +// TODO: check data borsh size of this struct +// Created the optionze macro but intellisense sucks +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub struct OptionalAccountInfoData { + pub key: Option, + pub lamports: Option, + pub data_length: Option, + pub owner: Option, + pub rent_epoch: Option, + pub is_signer: Option, + pub is_writable: Option, + pub executable: Option, +} diff --git a/programs/lighthouse/program/src/structs/assertion.rs b/programs/lighthouse/program/src/structs/assertion.rs index fb597a3..d113d65 100644 --- a/programs/lighthouse/program/src/structs/assertion.rs +++ b/programs/lighthouse/program/src/structs/assertion.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::{ }; use solana_program::pubkey::Pubkey; -use super::{BorshField, BorshValue, Operator}; +use super::{AccountInfoData, BorshField, BorshValue, Operator, OptionalAccountInfoData}; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum Assertion { @@ -22,4 +22,6 @@ pub enum Assertion { // token balance, operator TokenAccountBalance(u64, Operator), + + AccountInfo(OptionalAccountInfoData), } diff --git a/programs/lighthouse/program/src/structs/mod.rs b/programs/lighthouse/program/src/structs/mod.rs index 5b4e48f..faba9d2 100644 --- a/programs/lighthouse/program/src/structs/mod.rs +++ b/programs/lighthouse/program/src/structs/mod.rs @@ -1,3 +1,4 @@ +pub mod account_info; pub mod assertion; pub mod borsh_field; pub mod borsh_value; @@ -5,6 +6,7 @@ pub mod expression; pub mod operator; pub mod write_type; +pub use account_info::*; pub use assertion::*; pub use borsh_field::*; pub use borsh_value::*; diff --git a/programs/lighthouse/program/src/structs/write_type.rs b/programs/lighthouse/program/src/structs/write_type.rs index 23aca18..22e05f2 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -16,6 +16,13 @@ pub enum WriteType { AccountDataU16(u16, u16, u16), AccountDataU32(u32, u32, u32), + // CacheOffset + AccountInfoU8(u8), + AccountInfoU16(u16), + AccountInfoU32(u32), + + // TODO: + // CacheOffset, BorshField BorshFieldU8(u8, BorshField), BorshFieldU16(u16, BorshField), @@ -26,4 +33,6 @@ pub enum WriteType { TokenAccountOwner(u16), TokenAccountBalance(u16), // Program Account Assertions + + // Always add variants to the end of this enum to avoid messing with indexers. } diff --git a/programs/lighthouse/program/tests/simple.rs b/programs/lighthouse/program/tests/simple.rs index b5fcc84..50f718e 100644 --- a/programs/lighthouse/program/tests/simple.rs +++ b/programs/lighthouse/program/tests/simple.rs @@ -2,9 +2,12 @@ pub mod utils; use std::io::Error; +use anchor_lang::accounts::account::Account; use anchor_lang::system_program::System; -use anchor_lang::InstructionData; -use lighthouse::structs::{Assertion, BorshField, BorshValue, Expression, Operator}; +use anchor_lang::{AnchorDeserialize, InstructionData}; +use lighthouse::structs::{ + AccountInfoData, Assertion, BorshField, BorshValue, Expression, Operator, +}; use solana_program::instruction::Instruction; use solana_program::pubkey::Pubkey; use solana_program::rent::Rent; @@ -265,13 +268,13 @@ async fn test_logical_expression() { // println!( // "account: {:?}", - // context - // .client() - // .get_account(find_cache().0) - // .await - // .unwrap() - // .unwrap() - // .data + // context + // .client() + // .get_account(find_cache().0) + // .await + // .unwrap() + // .unwrap() + // .data // ); // } else { // panic!("Should have passed"); @@ -586,6 +589,168 @@ fn format_hex(data: &[u8]) -> String { result } +#[tokio::test] +async fn test_assert_account_info() { + 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::AccountInfoU8(0), + ) + .to_transaction(vec![]) + .await, + ) + .await; + + let data = context + .client() + .get_account(cache_account) + .await + .unwrap() + .unwrap() + .data; + + println!("cache account: {}", format_hex(&data)); + println!( + "deserialized account info: {:?}", + AccountInfoData::try_from_slice(&data[8..8 + AccountInfoData::size() as usize]) + ); + + // 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), + )], + 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; + // } +} + async fn process_transaction( context: &TestContext, tx: &Transaction, From 414698e94491bff7e693e61dd3dd24a5f24403f6 Mon Sep 17 00:00:00 2001 From: Jac0xb Date: Sat, 9 Dec 2023 08:37:42 -0800 Subject: [PATCH 2/6] Rewrite v1 --- programs/lighthouse/program/src/error.rs | 6 +- programs/lighthouse/program/src/lib.rs | 4 +- .../program/src/processor/v1/assert.rs | 308 ++--- .../program/src/processor/v1/write.rs | 182 +-- .../program/src/structs/account_info.rs | 1 - .../program/src/structs/assertion.rs | 11 +- .../program/src/structs/borsh_field.rs | 26 +- .../program/src/structs/borsh_value.rs | 21 - .../program/src/structs/data_value.rs | 289 ++++ .../lighthouse/program/src/structs/mod.rs | 4 +- .../program/src/structs/operator.rs | 2 + .../program/src/structs/write_type.rs | 91 +- programs/lighthouse/program/src/utils.rs | 41 +- programs/lighthouse/program/tests/simple.rs | 1188 ++++++++--------- .../lighthouse/program/tests/utils/program.rs | 14 +- 15 files changed, 1120 insertions(+), 1068 deletions(-) delete mode 100644 programs/lighthouse/program/src/structs/borsh_value.rs create mode 100644 programs/lighthouse/program/src/structs/data_value.rs diff --git a/programs/lighthouse/program/src/error.rs b/programs/lighthouse/program/src/error.rs index 15dcc2c..5433c78 100644 --- a/programs/lighthouse/program/src/error.rs +++ b/programs/lighthouse/program/src/error.rs @@ -10,12 +10,14 @@ pub enum ProgramError { AssertionFailed, #[msg("NotEnoughAccounts")] NotEnoughAccounts, - #[msg("BorshValueMismatch")] - BorshValueMismatch, + #[msg("DataValueMismatch")] + DataValueMismatch, #[msg("UnsupportedOperator")] UnsupportedOperator, #[msg("CacheOutOfRange")] CacheOutOfRange, #[msg("AccountBorrowFailed")] AccountBorrowFailed, + #[msg("InvalidAccount")] + InvalidAccount, } diff --git a/programs/lighthouse/program/src/lib.rs b/programs/lighthouse/program/src/lib.rs index 2b63fb9..27df9bd 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) } diff --git a/programs/lighthouse/program/src/processor/v1/assert.rs b/programs/lighthouse/program/src/processor/v1/assert.rs index 32f5e9c..d27fd0c 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -4,12 +4,12 @@ use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use crate::error::ProgramError; -use crate::structs::{Assertion, BorshField, BorshValue, Expression, Operator}; -use crate::utils::process_value; +use crate::structs::{Assertion, Expression}; #[derive(Accounts)] pub struct AssertV1<'info> { - pub system_program: Program<'info, System>, + // TODO: + pub cache: Option>, } #[derive(BorshDeserialize, BorshSerialize, Debug)] @@ -53,241 +53,51 @@ pub fn assert<'info>( } } - for (i, assertion_type) in assertions.into_iter().enumerate() { + for (i, assertion) 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]; + let mut assertion_result = false; - if account.data_is_empty() && account.lamports() == 0 { - assertion_result = false; - } - } + match assertion { Assertion::AccountOwnedBy(pubkey) => { let account = &remaining_accounts[i]; - - if !account.owner.key().eq(&pubkey) { - assertion_result = false; - } + assertion_result = account.owner.key().eq(&pubkey); } - 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]; + 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 - 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()), - } + let (value_str, expected_value_str, assertion_result) = memory_value + .deserialize_and_compare(*cache_data, (cache_offset + 8) as usize, &operator)?; - if verbose { - msg!( - "{} Assertion::RawAccountData ({}) -> {:?} {} {:?}", - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - account.key().to_string(), - slice, - operator.format(), - expected_slice, - ); - } + msg!( + "{} {} AssertionParameter::Memory ({}) -> {} {} {}", + format!("[{:?}]", i), + if assertion_result { + "[✅] SUCCESS" + } else { + "[❌] FAIL " + }, + cache.key().to_string(), + value_str, + operator.format(), + expected_value_str, + ); } - Assertion::BorshAccountData(offset, borsh_field, operator, expected_value) => { + Assertion::AccountData(account_offset, operator, memory_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()), - }; + let account_data = account.try_borrow_data()?; - assertion_result = operator.is_true(&value, &expected_value); + let (value_str, expected_value_str, result) = memory_value + .deserialize_and_compare(*account_data, account_offset as usize, &operator)?; - 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); - - 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)?; - - let expected_value = match expected_value { - BorshValue::U128(value) => value, - _ => return Err(ProgramError::BorshValueMismatch.into()), - }; - - match operator { - Operator::Equal => {} - Operator::NotEqual => {} - _ => return Err(ProgramError::UnsupportedOperator.into()), - } - - assertion_result = operator.is_true(&value, &expected_value); - - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - } - } + assertion_result = result; msg!( - "{} {} Assertion::BorshAccountData ({}) -> {} {} {}", + "{} {} Assertion::AccountData ({}) -> {} {} {}", format!("[{:?}]", i), if assertion_result { "[✅] SUCCESS" @@ -324,6 +134,62 @@ pub fn assert<'info>( Assertion::TokenAccountBalance(expected_balance, operator) => { return Err(ProgramError::Unimplemented.into()); } + Assertion::AccountInfo(optional_account_info_data) => { + let account = &remaining_accounts[i]; + + let account_info_data = optional_account_info_data; + // { + // OptionalAccountInfoData::None => return Err(ProgramError::Unimplemented.into()), + // OptionalAccountInfoData::Some(account_info_data) => account_info_data, + // }; + + let mut assertion_result = true; + + if let Some(owner) = &account_info_data.owner { + if !account.owner.key().eq(owner) { + assertion_result = false; + } + } + + if let Some(lamports) = &account_info_data.lamports { + if !account.get_lamports().eq(lamports) { + assertion_result = false; + } + } + + // if let Some(data_length) = &account_info_data.data_length { + // if !account.data_len().eq(&(data_length as usize)) { + // assertion_result = false; + // } + // } + + // if let Some(data) = &account_info_data.data { + // let account_data = account.try_borrow_data()?; + + // if !account_data.eq(data) { + // assertion_result = false; + // } + // } + + if let Some(rent_epoch) = &account_info_data.rent_epoch { + if !account.rent_epoch.eq(rent_epoch) { + assertion_result = false; + } + } + + if verbose { + msg!( + "{} Assertion::AccountInfo ({}) -> {:?}", + if assertion_result { + "[✅] SUCCESS" + } else { + "[❌] FAIL " + }, + account.key().to_string(), + account_info_data, + ); + } + } (_) => {} // REMOVE } diff --git a/programs/lighthouse/program/src/processor/v1/write.rs b/programs/lighthouse/program/src/processor/v1/write.rs index 2d760d8..e1796ae 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::{AccountInfoData, WriteType}; +use crate::structs::{AccountInfoData, WriteType, WriteTypeParameter}; #[derive(Accounts)] #[instruction(cache_index: u8)] @@ -27,61 +26,44 @@ pub struct WriteV1<'info> { 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; - - // TODO: make less messy but the main point is to allow more compact instruction data. - (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, - ), - 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), - WriteType::AccountInfoU8(_cache_offset) => { - (_cache_offset as usize, 0, AccountInfoData::size() as usize) + let (mut cache_offset, write_type) = match write_type { + WriteTypeParameter::WriteU8(cache_offset, write_type) => { + (cache_offset as usize, write_type) } - WriteType::AccountInfoU16(_cache_offset) => { - (_cache_offset as usize, 0, AccountInfoData::size() as usize) + WriteTypeParameter::WriteU16(cache_offset, write_type) => { + (cache_offset as usize, write_type) } - WriteType::AccountInfoU32(_cache_offset) => { - (_cache_offset as usize, 0, AccountInfoData::size() as usize) + WriteTypeParameter::WriteU32(cache_offset, write_type) => { + (cache_offset as usize, write_type) } }; - - // Cache offset can never write to the first 8 bytes of the cache account cache_offset = cache_offset .checked_add(8) .ok_or(ProgramError::CacheOutOfRange)?; + let data_length = write_type.size(); + match write_type { - WriteType::AccountBalanceU8(_) - | WriteType::AccountBalanceU16(_) - | WriteType::AccountBalanceU32(_) => { + WriteType::MintAccount => {} + WriteType::TokenAccount2022 => {} + WriteType::TokenAccountLegacy => {} + WriteType::Program => {} + WriteType::DataValue(borsh_value) => { + if (cache_offset + data_length) < cache_data_length { + let data_slice = &(borsh_value.serialize())[0..data_length]; + + cache_ref[cache_offset..(cache_offset + data_length)] + .copy_from_slice(data_slice.as_ref()); + } else { + return Err(ProgramError::NotEnoughAccounts.into()); + } + } + WriteType::AccountBalance => { let source_account = ctx.remaining_accounts.first(); if let Some(target_account) = source_account { @@ -98,13 +80,16 @@ 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, data_length) => { + let target_account = ctx.remaining_accounts.first(); + let data_length = data_length as usize; + let account_offset = account_offset as usize; + + if let Some(target_account) = target_account { + if write_type.account_validation(target_account) { + return Err(ProgramError::InvalidAccount.into()); + } - if let Some(target_account) = source_account { if (cache_offset + data_length) < cache_data_length { let data = target_account.try_borrow_data().map_err(|err| { msg!("Error: {:?}", err); @@ -121,73 +106,10 @@ pub fn write<'info>( return Err(ProgramError::NotEnoughAccounts.into()); } } - WriteType::TokenAccount(_) => { - // TODO: Not sure we really need this, could be extracted by user - - msg!("write_type: TokenAccount"); - let source_account = ctx.remaining_accounts.first(); - - if let Some(source_account) = source_account { - // TODO: add validation to token account + WriteType::AccountInfo => { + let target_account = ctx.remaining_accounts.first(); - if (cache_offset + data_length) < cache_data_length { - let data = source_account.try_borrow_data()?; - let data_slice = &data[0..data_length]; - - 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::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()); - } - } - WriteType::AccountInfoU8(_) - | WriteType::AccountInfoU16(_) - | WriteType::AccountInfoU32(_) => { - msg!("write_type: AccountInfoU8"); - let source_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 account_info = AccountInfoData { key: *target_account.key, @@ -212,35 +134,7 @@ pub fn write<'info>( 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/account_info.rs b/programs/lighthouse/program/src/structs/account_info.rs index 52d0e87..493558e 100644 --- a/programs/lighthouse/program/src/structs/account_info.rs +++ b/programs/lighthouse/program/src/structs/account_info.rs @@ -2,7 +2,6 @@ use anchor_lang::prelude::{ borsh, borsh::{BorshDeserialize, BorshSerialize}, }; -use optionize_macro::Optionize; use solana_program::pubkey::Pubkey; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] diff --git a/programs/lighthouse/program/src/structs/assertion.rs b/programs/lighthouse/program/src/structs/assertion.rs index d113d65..9a0aa2d 100644 --- a/programs/lighthouse/program/src/structs/assertion.rs +++ b/programs/lighthouse/program/src/structs/assertion.rs @@ -4,20 +4,19 @@ use anchor_lang::prelude::{ }; use solana_program::pubkey::Pubkey; -use super::{AccountInfoData, BorshField, BorshValue, Operator, OptionalAccountInfoData}; +use super::{DataValue, Operator, OptionalAccountInfoData}; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum Assertion { - // offset, borsh type, operator - BorshAccountData(u64, BorshField, Operator, BorshValue), + // memory offset, assertion + Memory(u16, Operator, DataValue), - RawAccountData(u64, Operator, Vec), + // account data offset, borsh type, operator + AccountData(u16, Operator, DataValue), // balance, operator AccountBalance(u64, Operator), - AccountExists, - AccountOwnedBy(Pubkey), // token balance, 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..be4e35d --- /dev/null +++ b/programs/lighthouse/program/src/structs/data_value.rs @@ -0,0 +1,289 @@ +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 { + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + U128, + I128, + Bytes, + Pubkey, +} + +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub enum DataValue { + 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 serialize(self) -> Vec { + match self { + 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::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.is_true(a, b), + (DataValue::I8(a), DataValue::I8(b)) => operator.is_true(a, b), + (DataValue::U16(a), DataValue::U16(b)) => operator.is_true(a, b), + (DataValue::I16(a), DataValue::I16(b)) => operator.is_true(a, b), + (DataValue::U32(a), DataValue::U32(b)) => operator.is_true(a, b), + (DataValue::I32(a), DataValue::I32(b)) => operator.is_true(a, b), + (DataValue::U64(a), DataValue::U64(b)) => operator.is_true(a, b), + (DataValue::I64(a), DataValue::I64(b)) => operator.is_true(a, b), + (DataValue::U128(a), DataValue::U128(b)) => operator.is_true(a, b), + (DataValue::I128(a), DataValue::I128(b)) => operator.is_true(a, b), + (DataValue::Bytes(a), DataValue::Bytes(b)) => operator.is_true(a, b), + (DataValue::Pubkey(a), DataValue::Pubkey(b)) => operator.is_true(a, b), + (_, _) => false, + } + } + + pub fn deserialize_and_compare( + self, + data: &[u8], + offset: usize, + operator: &Operator, + ) -> Result<(String, String, bool), ProgramError> { + let mut value_str = String::new(); + let mut expected_value_str = String::new(); + let mut assertion_result = false; + + match self { + DataValue::U8(expected_value) => { + let slice = &data[offset as usize..(offset + 1) as usize]; + let value = DataValue::deserialize(DataType::U8, slice); + + let value = match value { + DataValue::U8(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = expected_value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::I8(expected_value) => { + let slice = &data[offset as usize..(offset + 1) as usize]; + let value = DataValue::deserialize(DataType::I8, slice); + + let value = match value { + DataValue::I8(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::U16(expected_value) => { + let slice = &data[offset as usize..(offset + 2) as usize]; + let value = DataValue::deserialize(DataType::U16, slice); + + let value = match value { + DataValue::U16(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::I16(expected_value) => { + let slice = &data[offset as usize..(offset + 2) as usize]; + let value = DataValue::deserialize(DataType::I16, slice); + + let value = match value { + DataValue::I16(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = expected_value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::U32(expected_value) => { + let slice = &data[offset as usize..(offset + 4) as usize]; + let value = DataValue::deserialize(DataType::U32, slice); + + let value = match value { + DataValue::U32(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::I32(expected_value) => { + let slice = &data[offset as usize..(offset + 4) as usize]; + let value = DataValue::deserialize(DataType::I32, slice); + + let value = match value { + DataValue::I32(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::U64(expected_value) => { + let slice = &data[offset as usize..(offset + 8) as usize]; + let value = DataValue::deserialize(DataType::U64, slice); + + let value = match value { + DataValue::U64(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::I64(expected_value) => { + let slice = &data[offset as usize..(offset + 8) as usize]; + let value = DataValue::deserialize(DataType::I64, slice); + + let value = match value { + DataValue::I64(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::U128(expected_value) => { + let slice = &data[offset as usize..(offset + 16) as usize]; + let value = DataValue::deserialize(DataType::U128, slice); + + let value = match value { + DataValue::U128(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = expected_value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::I128(expected_value) => { + let slice = &data[offset as usize..(offset + 16) as usize]; + let value = DataValue::deserialize(DataType::I128, slice); + + let value = match value { + DataValue::I128(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + value_str = value.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::Bytes(expected_value) => { + let slice: &[u8] = &data[offset as usize..(offset + expected_value.len() as usize)]; + let value = DataValue::deserialize(DataType::Bytes, slice); + + match operator { + Operator::Equal => {} + Operator::NotEqual => {} + _ => return Err(ProgramError::UnsupportedOperator.into()), + } + + let value = match value { + DataValue::Bytes(value) => value, + _ => return Err(ProgramError::DataValueMismatch.into()), + }; + + // print array + value_str = value + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(""); + expected_value_str = expected_value + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(""); + assertion_result = operator.is_true(&value, &expected_value); + } + DataValue::Pubkey(expected_value) => { + let slice = &data[offset as usize..(offset + 32) as usize]; + let value = DataValue::deserialize(DataType::Pubkey, slice); + + match operator { + Operator::Equal => {} + Operator::NotEqual => {} + _ => return Err(ProgramError::UnsupportedOperator), + } + + let value = match value { + DataValue::Pubkey(value) => value, + _ => return Err(ProgramError::DataValueMismatch), + }; + + value_str = value_str.to_string(); + expected_value_str = expected_value.to_string(); + assertion_result = operator.is_true(&value, &expected_value); + } + } + + Ok((value_str, expected_value_str, assertion_result)) + } +} diff --git a/programs/lighthouse/program/src/structs/mod.rs b/programs/lighthouse/program/src/structs/mod.rs index faba9d2..471dd0d 100644 --- a/programs/lighthouse/program/src/structs/mod.rs +++ b/programs/lighthouse/program/src/structs/mod.rs @@ -1,7 +1,7 @@ pub mod account_info; pub mod assertion; pub mod borsh_field; -pub mod borsh_value; +pub mod data_value; pub mod expression; pub mod operator; pub mod write_type; @@ -9,7 +9,7 @@ pub mod write_type; pub use account_info::*; pub use assertion::*; pub use borsh_field::*; -pub use borsh_value::*; +pub use data_value::*; pub use expression::*; pub use operator::*; pub use write_type::*; diff --git a/programs/lighthouse/program/src/structs/operator.rs b/programs/lighthouse/program/src/structs/operator.rs index c6d1671..dc8771e 100644 --- a/programs/lighthouse/program/src/structs/operator.rs +++ b/programs/lighthouse/program/src/structs/operator.rs @@ -11,6 +11,8 @@ pub enum Operator { LessThan, GreaterThanOrEqual, LessThanOrEqual, + // Todo + // WithinThreshold } impl Operator { diff --git a/programs/lighthouse/program/src/structs/write_type.rs b/programs/lighthouse/program/src/structs/write_type.rs index 22e05f2..a89c22b 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -1,38 +1,75 @@ use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token, + token::{Mint, TokenAccount}, +}; use borsh::{BorshDeserialize, BorshSerialize}; -use super::borsh_field::BorshField; +use super::DataValue; -// 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), - - // CacheOffset, AccountOffset, Length - AccountDataU8(u8, u8, u8), - AccountDataU16(u16, u16, u16), - AccountDataU32(u32, u32, u32), + AccountBalance, - // CacheOffset - AccountInfoU8(u8), - AccountInfoU16(u16), - AccountInfoU32(u32), - - // TODO: + // Account Data Offset, Data Length + AccountData(u16, u16), + AccountInfo, + DataValue(DataValue), + MintAccount, + TokenAccountLegacy, + TokenAccount2022, + Program, +} - // CacheOffset, BorshField - BorshFieldU8(u8, BorshField), - BorshFieldU16(u16, BorshField), +impl WriteType { + pub fn size(&self) -> usize { + match self { + WriteType::AccountBalance => 8, + WriteType::AccountData(_, len) => *len as usize, + WriteType::AccountInfo => 8, + WriteType::DataValue(memory_value) => match memory_value { + DataValue::U8(_) | DataValue::I8(_) => 1, + DataValue::U16(_) | DataValue::I16(_) => 2, + DataValue::U32(_) | DataValue::I32(_) => 4, + DataValue::U64(_) | DataValue::I64(_) => 8, + DataValue::U128(_) | DataValue::I128(_) => 16, + DataValue::Bytes(bytes) => bytes.len(), + DataValue::Pubkey(_) => 32, + }, + // TODO: It might just be better/make more sense to create variants for mints, token accounts and let them choose what to write in the accounts + WriteType::MintAccount => Mint::LEN, // TODO: Test + WriteType::TokenAccountLegacy => TokenAccount::LEN, // TODO: Test + WriteType::TokenAccount2022 => usize::MAX, // TODO: Get actual size + WriteType::Program => 8, // TODO: Get actual size + } + } - // - MintAccount, - TokenAccount(u16), - TokenAccountOwner(u16), - TokenAccountBalance(u16), - // Program Account Assertions + pub fn account_validation(&self, account: &AccountInfo<'_>) -> bool { + match self { + WriteType::AccountBalance => true, + WriteType::AccountData(_, _) => true, + WriteType::AccountInfo => true, + WriteType::DataValue(_) => false, + WriteType::MintAccount => { + account.owner == &spl_token::id() && account.data_len() == Mint::LEN + } + WriteType::TokenAccountLegacy => { + account.owner == &associated_token::ID && account.data_len() == TokenAccount::LEN + } + WriteType::TokenAccount2022 => { + false // TODO: Support + } + _ => true, + } + } +} - // Always add variants to the end of this enum to avoid messing with indexers. +// 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 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..f5ac9fe 100644 --- a/programs/lighthouse/program/src/utils.rs +++ b/programs/lighthouse/program/src/utils.rs @@ -1,27 +1,24 @@ use anchor_lang::prelude::{borsh::BorshDeserialize, *}; -use crate::{ - error, - structs::{BorshField, Operator}, -}; +use crate::{error, structs::Operator}; -pub fn process_value( - data: &[u8], - offset: u32, - size: usize, - expected_value: &T, - borsh_field: &BorshField, - 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)?; +// pub fn process_value( +// data: &[u8], +// offset: u32, +// size: usize, +// expected_value: &T, +// borsh_field: &BorshField, +// 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); +// borsh_field.is_supported_operator(operator); +// let assertion_result = operator.is_true(&value, expected_value); - Ok(( - value.to_string(), - expected_value.to_string(), - assertion_result, - )) -} +// Ok(( +// value.to_string(), +// expected_value.to_string(), +// assertion_result, +// )) +// } diff --git a/programs/lighthouse/program/tests/simple.rs b/programs/lighthouse/program/tests/simple.rs index 50f718e..930bb49 100644 --- a/programs/lighthouse/program/tests/simple.rs +++ b/programs/lighthouse/program/tests/simple.rs @@ -6,7 +6,7 @@ use anchor_lang::accounts::account::Account; use anchor_lang::system_program::System; use anchor_lang::{AnchorDeserialize, InstructionData}; use lighthouse::structs::{ - AccountInfoData, Assertion, BorshField, BorshValue, Expression, Operator, + AccountInfoData, Assertion, DataValue, Expression, Operator, OptionalAccountInfoData, }; use solana_program::instruction::Instruction; use solana_program::pubkey::Pubkey; @@ -75,65 +75,47 @@ async fn test_borsh_account_data() { .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( + Assertion::AccountData(8, Operator::Equal, DataValue::U8(1)), + Assertion::AccountData(9, Operator::Equal, DataValue::I8(-1)), + Assertion::AccountData( 10, - BorshField::U16, Operator::Equal, - BorshValue::U16((u8::MAX as u16) + 1), + DataValue::U16((u8::MAX as u16) + 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 12, - BorshField::I16, Operator::Equal, - BorshValue::I16((i8::MIN as i16) - 1), + DataValue::I16((i8::MIN as i16) - 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 14, - BorshField::U32, Operator::Equal, - BorshValue::U32((u16::MAX as u32) + 1), + DataValue::U32((u16::MAX as u32) + 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 18, - BorshField::I32, Operator::Equal, - BorshValue::I32((i16::MIN as i32) - 1), + DataValue::I32((i16::MIN as i32) - 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 22, - BorshField::U64, Operator::Equal, - BorshValue::U64((u32::MAX as u64) + 1), + DataValue::U64((u32::MAX as u64) + 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 30, - BorshField::I64, Operator::Equal, - BorshValue::I64((i32::MIN as i64) - 1), + DataValue::I64((i32::MIN as i64) - 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 38, - BorshField::U128, Operator::Equal, - BorshValue::U128((u64::MAX as u128) + 1), + DataValue::U128((u64::MAX as u128) + 1), ), - Assertion::BorshAccountData( + Assertion::AccountData( 54, - BorshField::I128, Operator::Equal, - BorshValue::I128((i64::MIN as i128) - 1), + DataValue::I128((i64::MIN as i128) - 1), ), ], vec![account; 10], @@ -191,565 +173,569 @@ async fn test_borsh_account_data() { // } } -#[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 -} - -#[tokio::test] -async fn test_assert_account_info() { - 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::AccountInfoU8(0), - ) - .to_transaction(vec![]) - .await, - ) - .await; - - let data = context - .client() - .get_account(cache_account) - .await - .unwrap() - .unwrap() - .data; - - println!("cache account: {}", format_hex(&data)); - println!( - "deserialized account info: {:?}", - AccountInfoData::try_from_slice(&data[8..8 + AccountInfoData::size() as usize]) - ); - - // 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), - )], - 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; - // } -} +// #[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::AccountData(8, BorshField::U8, Operator::Equal, DataValue::U8(1)), +// Assertion::AccountData(8, BorshField::U8, Operator::Equal, DataValue::U8(5)), +// Assertion::AccountData( +// 10, +// BorshField::U16, +// Operator::Equal, +// DataValue::U16((u8::MAX as u16) + 1), +// ), +// Assertion::AccountData(10, BorshField::U16, Operator::Equal, DataValue::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 +// } + +// #[tokio::test] +// async fn test_assert_account_info() { +// 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::AccountInfoU8(0), +// ) +// .to_transaction(vec![]) +// .await, +// ) +// .await; + +// let data = context +// .client() +// .get_account(find_test_account().0) +// .await +// .unwrap() +// .unwrap(); +// // .data; + +// // println!("cache account: {}", format_hex(&data)); +// // println!( +// // "deserialized account info: {:?}", +// // AccountInfoData::try_from_slice(&data[8..8 + AccountInfoData::size() as usize]) +// // ); + +// // Assert that data was properly written to cache. +// let tx = program +// .create_assertion( +// &context.payer(), +// vec![Assertion::AccountInfo(OptionalAccountInfoData { +// owner: Some(lighthouse::ID), +// key: Some(find_test_account().0), +// lamports: None, +// data_length: None, +// rent_epoch: None, +// is_signer: None, +// is_writable: None, +// executable: None, +// })], +// vec![find_test_account().0; 1], +// 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; +// // } +// } async fn process_transaction( context: &TestContext, @@ -774,12 +760,12 @@ async fn process_transaction_assert_success( 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); - } + let logs = tx_metadata.metadata.unwrap().log_messages; + for log in logs { + println!("{:?}", log); + } + if tx_metadata.result.is_err() { panic!("Transaction failed"); } } diff --git a/programs/lighthouse/program/tests/utils/program.rs b/programs/lighthouse/program/tests/utils/program.rs index 088763b..3366c94 100644 --- a/programs/lighthouse/program/tests/utils/program.rs +++ b/programs/lighthouse/program/tests/utils/program.rs @@ -11,7 +11,7 @@ use super::{ use anchor_lang::*; use lighthouse::{ processor::Config, - structs::{Assertion, Expression, WriteType}, + structs::{Assertion, Expression, WriteType, WriteTypeParameter}, }; use solana_program::{ instruction::{AccountMeta, Instruction}, @@ -108,7 +108,8 @@ impl Program { logical_expression: Option>, ) -> AssertBuilder { let accounts = lighthouse::accounts::AssertV1 { - system_program: system_program::id(), + // system_program: system_program::id(), + cache: None, }; let assertion_clone = (assertions).clone(); @@ -128,7 +129,8 @@ impl Program { vec![Instruction { program_id: lighthouse::id(), accounts: (lighthouse::accounts::AssertV1 { - system_program: system_program::id(), + // system_program: system_program::id(), + cache: None, }) .to_account_metas(None), data: (lighthouse::instruction::AssertV1 { @@ -196,7 +198,7 @@ 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(), @@ -205,10 +207,10 @@ impl Program { 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, }; From ebd63ecd14ee615d56199bea9c07d423766edca4 Mon Sep 17 00:00:00 2001 From: Jac0xb Date: Sun, 10 Dec 2023 13:41:42 -0800 Subject: [PATCH 3/6] Add macros, improve assertion logic --- {optionize_macro => macros}/Cargo.lock | 2 +- {optionize_macro => macros}/Cargo.toml | 2 +- macros/src/lib.rs | 119 +++ optionize_macro/src/lib.rs | 45 - programs/lighthouse/Cargo.lock | 21 +- programs/lighthouse/program/Cargo.toml | 5 +- .../program/src/processor/v1/assert.rs | 134 ++- .../src/processor/v1/create_test_account.rs | 6 +- .../program/src/processor/v1/write.rs | 3 +- .../program/src/structs/account_info.rs | 20 +- .../program/src/structs/assertion.rs | 4 +- .../program/src/structs/data_value.rs | 235 +++-- .../program/src/structs/operator.rs | 2 +- .../program/src/structs/write_type.rs | 4 +- programs/lighthouse/program/tests/simple.rs | 903 ++++++------------ .../lighthouse/program/tests/utils/program.rs | 8 +- 16 files changed, 624 insertions(+), 889 deletions(-) rename {optionize_macro => macros}/Cargo.lock (97%) rename {optionize_macro => macros}/Cargo.toml (86%) create mode 100644 macros/src/lib.rs delete mode 100644 optionize_macro/src/lib.rs diff --git a/optionize_macro/Cargo.lock b/macros/Cargo.lock similarity index 97% rename from optionize_macro/Cargo.lock rename to macros/Cargo.lock index 1d5efa5..22aba58 100644 --- a/optionize_macro/Cargo.lock +++ b/macros/Cargo.lock @@ -3,7 +3,7 @@ version = 3 [[package]] -name = "optionize_macro" +name = "macros" version = "0.1.0" dependencies = [ "proc-macro2", diff --git a/optionize_macro/Cargo.toml b/macros/Cargo.toml similarity index 86% rename from optionize_macro/Cargo.toml rename to macros/Cargo.toml index c4294df..f00ee2e 100644 --- a/optionize_macro/Cargo.toml +++ b/macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "optionize_macro" +name = "macros" version = "0.1.0" edition = "2021" diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..58dca09 --- /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, Attribute, 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/optionize_macro/src/lib.rs b/optionize_macro/src/lib.rs deleted file mode 100644 index f847c36..0000000 --- a/optionize_macro/src/lib.rs +++ /dev/null @@ -1,45 +0,0 @@ -extern crate proc_macro; - -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, Attribute, Data, 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) -} diff --git a/programs/lighthouse/Cargo.lock b/programs/lighthouse/Cargo.lock index f995928..16d1060 100644 --- a/programs/lighthouse/Cargo.lock +++ b/programs/lighthouse/Cargo.lock @@ -2086,9 +2086,10 @@ dependencies = [ "anchor-spl", "async-trait", "bytemuck", + "macros", "mpl-token-metadata", "num-traits", - "optionize_macro", + "regex", "solana-banks-interface", "solana-program", "solana-program-test", @@ -2151,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" @@ -2598,15 +2608,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "optionize_macro" -version = "0.1.0" -dependencies = [ - "proc-macro2 1.0.66", - "quote 1.0.32", - "syn 1.0.109", -] - [[package]] name = "os_str_bytes" version = "6.5.1" diff --git a/programs/lighthouse/program/Cargo.toml b/programs/lighthouse/program/Cargo.toml index f785c24..adb7b07 100644 --- a/programs/lighthouse/program/Cargo.toml +++ b/programs/lighthouse/program/Cargo.toml @@ -28,7 +28,7 @@ num-traits = "0.2.15" solana-program = "~1.16.5" spl-associated-token-account = { version = ">= 1.1.3, < 3.0", features = ["no-entrypoint"] } spl-token = { version = ">= 3.5.0, < 5.0", features = ["no-entrypoint"] } -optionize_macro = { path = "../../../optionize_macro" } +macros = { path = "../../../macros" } [dev-dependencies] async-trait = "0.1.71" @@ -37,4 +37,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" \ No newline at end of file +solana-banks-interface = "1.14.10" +regex = "1.5.4" \ No newline at end of file diff --git a/programs/lighthouse/program/src/processor/v1/assert.rs b/programs/lighthouse/program/src/processor/v1/assert.rs index d27fd0c..31ea512 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use crate::error::ProgramError; -use crate::structs::{Assertion, Expression}; +use crate::structs::{AccountInfoDataField, Assertion, Expression}; #[derive(Accounts)] pub struct AssertV1<'info> { @@ -23,7 +23,7 @@ pub fn assert<'info>( logical_expression: Option>, options: Option, ) -> Result<()> { - let remaining_accounts = &ctx.remaining_accounts.to_vec(); + let remaining_accounts = &mut ctx.remaining_accounts.iter(); let verbose = options.map(|options| options.verbose).unwrap_or(false); let mut assertion_results: Vec = vec![]; @@ -54,25 +54,22 @@ pub fn assert<'info>( } for (i, assertion) 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 = false; match assertion { Assertion::AccountOwnedBy(pubkey) => { - let account = &remaining_accounts[i]; + let account = remaining_accounts.next().unwrap(); assertion_result = account.owner.key().eq(&pubkey); } 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, assertion_result) = memory_value + let (value_str, expected_value_str, result) = memory_value .deserialize_and_compare(*cache_data, (cache_offset + 8) as usize, &operator)?; + assertion_result = result; + msg!( "{} {} AssertionParameter::Memory ({}) -> {} {} {}", format!("[{:?}]", i), @@ -81,14 +78,14 @@ pub fn assert<'info>( } else { "[❌] FAIL " }, - cache.key().to_string(), + "Cache...".to_string(), value_str, operator.format(), expected_value_str, ); } Assertion::AccountData(account_offset, operator, memory_value) => { - let account = &remaining_accounts[i]; + let account = remaining_accounts.next().unwrap(); let account_data = account.try_borrow_data()?; let (value_str, expected_value_str, result) = memory_value @@ -98,23 +95,23 @@ pub fn assert<'info>( msg!( "{} {} Assertion::AccountData ({}) -> {} {} {}", - format!("[{:?}]", i), + format!("[{:02}]", i), if assertion_result { "[✅] SUCCESS" } else { "[❌] FAIL " }, - account.key().to_string(), + truncate_pubkey(&account.key()), value_str, operator.format(), expected_value_str, ); } Assertion::AccountBalance(expected_balance, operator) => { - let account = &remaining_accounts[i]; + let account = remaining_accounts.next().unwrap(); assertion_result = - operator.is_true(&**account.try_borrow_lamports()?, &expected_balance); + operator.evaluate(&**account.try_borrow_lamports()?, &expected_balance); if verbose { msg!( @@ -124,7 +121,7 @@ pub fn assert<'info>( } else { "[❌] FAIL " }, - account.key().to_string(), + truncate_pubkey(&account.key()), account.get_lamports(), operator.format(), expected_balance, @@ -134,63 +131,56 @@ pub fn assert<'info>( Assertion::TokenAccountBalance(expected_balance, operator) => { return Err(ProgramError::Unimplemented.into()); } - Assertion::AccountInfo(optional_account_info_data) => { - let account = &remaining_accounts[i]; - - let account_info_data = optional_account_info_data; - // { - // OptionalAccountInfoData::None => return Err(ProgramError::Unimplemented.into()), - // OptionalAccountInfoData::Some(account_info_data) => account_info_data, - // }; - - let mut assertion_result = true; - - if let Some(owner) = &account_info_data.owner { - if !account.owner.key().eq(owner) { - assertion_result = false; - } - } - - if let Some(lamports) = &account_info_data.lamports { - if !account.get_lamports().eq(lamports) { - assertion_result = false; + Assertion::AccountInfo(account_info_fields, operator) => { + let account = remaining_accounts.next().unwrap(); + + for account_info_field in account_info_fields { + match account_info_field { + AccountInfoDataField::Key(pubkey) => { + assertion_result = operator.evaluate(&account.key(), &pubkey); + } + AccountInfoDataField::Owner(pubkey) => { + assertion_result = operator.evaluate(account.owner, &pubkey); + } + AccountInfoDataField::Lamports(lamports) => { + assertion_result = + operator.evaluate(&account.get_lamports(), &lamports); + } + AccountInfoDataField::DataLength(data_length) => { + assertion_result = + operator.evaluate(&(account.data_len() as u64), &data_length); + } + AccountInfoDataField::Executable(executable) => { + assertion_result = operator.evaluate(&account.executable, &executable); + } + AccountInfoDataField::IsSigner(is_signer) => { + assertion_result = operator.evaluate(&account.is_signer, &is_signer); + } + AccountInfoDataField::IsWritable(is_writable) => { + assertion_result = + operator.evaluate(&account.is_writable, &is_writable); + } + AccountInfoDataField::RentEpoch(rent_epoch) => { + assertion_result = + operator.evaluate(&account.rent_epoch as &u64, &rent_epoch); + } } } - // if let Some(data_length) = &account_info_data.data_length { - // if !account.data_len().eq(&(data_length as usize)) { - // assertion_result = false; - // } + // if verbose { + // msg!( + // "{} Assertion::AccountInfo ({}) -> {:?}", + // if assertion_result { + // "[✅] SUCCESS" + // } else { + // "[❌] FAIL " + // }, + // account.key().to_string(), + // account_info_data, + // ); // } - - // if let Some(data) = &account_info_data.data { - // let account_data = account.try_borrow_data()?; - - // if !account_data.eq(data) { - // assertion_result = false; - // } - // } - - if let Some(rent_epoch) = &account_info_data.rent_epoch { - if !account.rent_epoch.eq(rent_epoch) { - assertion_result = false; - } - } - - if verbose { - msg!( - "{} Assertion::AccountInfo ({}) -> {:?}", - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - account.key().to_string(), - account_info_data, - ); - } } - (_) => {} // REMOVE + _ => {} // REMOVE } assertion_results.push(assertion_result); @@ -271,3 +261,11 @@ pub fn assert<'info>( 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 e1796ae..f6cd9a3 100644 --- a/programs/lighthouse/program/src/processor/v1/write.rs +++ b/programs/lighthouse/program/src/processor/v1/write.rs @@ -86,7 +86,8 @@ pub fn write<'info>( let account_offset = account_offset as usize; if let Some(target_account) = target_account { - if write_type.account_validation(target_account) { + if !write_type.account_validation(target_account) { + msg!("Could not validation account"); return Err(ProgramError::InvalidAccount.into()); } diff --git a/programs/lighthouse/program/src/structs/account_info.rs b/programs/lighthouse/program/src/structs/account_info.rs index 493558e..823201b 100644 --- a/programs/lighthouse/program/src/structs/account_info.rs +++ b/programs/lighthouse/program/src/structs/account_info.rs @@ -23,16 +23,14 @@ impl AccountInfoData { } } -// TODO: check data borsh size of this struct -// Created the optionze macro but intellisense sucks #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] -pub struct OptionalAccountInfoData { - pub key: Option, - pub lamports: Option, - pub data_length: Option, - pub owner: Option, - pub rent_epoch: Option, - pub is_signer: Option, - pub is_writable: Option, - pub executable: Option, +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/assertion.rs b/programs/lighthouse/program/src/structs/assertion.rs index 9a0aa2d..60b9492 100644 --- a/programs/lighthouse/program/src/structs/assertion.rs +++ b/programs/lighthouse/program/src/structs/assertion.rs @@ -4,7 +4,7 @@ use anchor_lang::prelude::{ }; use solana_program::pubkey::Pubkey; -use super::{DataValue, Operator, OptionalAccountInfoData}; +use super::{AccountInfoDataField, DataValue, Operator}; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum Assertion { @@ -22,5 +22,5 @@ pub enum Assertion { // token balance, operator TokenAccountBalance(u64, Operator), - AccountInfo(OptionalAccountInfoData), + AccountInfo(Vec, Operator), } diff --git a/programs/lighthouse/program/src/structs/data_value.rs b/programs/lighthouse/program/src/structs/data_value.rs index be4e35d..a34a49c 100644 --- a/programs/lighthouse/program/src/structs/data_value.rs +++ b/programs/lighthouse/program/src/structs/data_value.rs @@ -10,6 +10,7 @@ use super::operator::Operator; #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum DataType { + Bool, U8, I8, U16, @@ -26,6 +27,7 @@ pub enum DataType { #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum DataValue { + Bool(bool), U8(u8), I8(i8), U16(u16), @@ -41,8 +43,45 @@ pub enum DataValue { } 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(), @@ -59,6 +98,14 @@ impl DataValue { } 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())), @@ -78,18 +125,18 @@ impl DataValue { pub fn compare(&self, other: &Self, operator: Operator) -> bool { match (self, other) { - (DataValue::U8(a), DataValue::U8(b)) => operator.is_true(a, b), - (DataValue::I8(a), DataValue::I8(b)) => operator.is_true(a, b), - (DataValue::U16(a), DataValue::U16(b)) => operator.is_true(a, b), - (DataValue::I16(a), DataValue::I16(b)) => operator.is_true(a, b), - (DataValue::U32(a), DataValue::U32(b)) => operator.is_true(a, b), - (DataValue::I32(a), DataValue::I32(b)) => operator.is_true(a, b), - (DataValue::U64(a), DataValue::U64(b)) => operator.is_true(a, b), - (DataValue::I64(a), DataValue::I64(b)) => operator.is_true(a, b), - (DataValue::U128(a), DataValue::U128(b)) => operator.is_true(a, b), - (DataValue::I128(a), DataValue::I128(b)) => operator.is_true(a, b), - (DataValue::Bytes(a), DataValue::Bytes(b)) => operator.is_true(a, b), - (DataValue::Pubkey(a), DataValue::Pubkey(b)) => operator.is_true(a, b), + (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, } } @@ -100,173 +147,159 @@ impl DataValue { offset: usize, operator: &Operator, ) -> Result<(String, String, bool), ProgramError> { - let mut value_str = String::new(); - let mut expected_value_str = String::new(); - let mut assertion_result = false; + let slice = &data[offset..(offset + self.size())]; + let value = DataValue::deserialize(self.get_data_type(), slice); match self { - DataValue::U8(expected_value) => { - let slice = &data[offset as usize..(offset + 1) as usize]; - let value = DataValue::deserialize(DataType::U8, slice); + 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.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = expected_value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 1) as usize]; - let value = DataValue::deserialize(DataType::I8, slice); - let value = match value { DataValue::I8(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 2) as usize]; - let value = DataValue::deserialize(DataType::U16, slice); - let value = match value { DataValue::U16(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 2) as usize]; - let value = DataValue::deserialize(DataType::I16, slice); - let value = match value { DataValue::I16(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = expected_value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 4) as usize]; - let value = DataValue::deserialize(DataType::U32, slice); - let value = match value { DataValue::U32(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 4) as usize]; - let value = DataValue::deserialize(DataType::I32, slice); - let value = match value { DataValue::I32(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 8) as usize]; - let value = DataValue::deserialize(DataType::U64, slice); - let value = match value { DataValue::U64(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 8) as usize]; - let value = DataValue::deserialize(DataType::I64, slice); - let value = match value { DataValue::I64(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 16) as usize]; - let value = DataValue::deserialize(DataType::U128, slice); - let value = match value { DataValue::U128(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = expected_value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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 slice = &data[offset as usize..(offset + 16) as usize]; - let value = DataValue::deserialize(DataType::I128, slice); - let value = match value { DataValue::I128(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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) => { - let slice: &[u8] = &data[offset as usize..(offset + expected_value.len() as usize)]; - let value = DataValue::deserialize(DataType::Bytes, slice); - match operator { Operator::Equal => {} Operator::NotEqual => {} - _ => return Err(ProgramError::UnsupportedOperator.into()), + _ => return Err(ProgramError::DataValueMismatch), } let value = match value { DataValue::Bytes(value) => value, - _ => return Err(ProgramError::DataValueMismatch.into()), + _ => return Err(ProgramError::DataValueMismatch), }; // print array - value_str = value + let value_str = value .iter() .map(|byte| format!("{:02x}", byte)) .collect::>() .join(""); - expected_value_str = expected_value + let expected_value_str = expected_value .iter() .map(|byte| format!("{:02x}", byte)) .collect::>() .join(""); - assertion_result = operator.is_true(&value, &expected_value); + let assertion_result = operator.evaluate(&value, &expected_value); + + Ok((value_str, expected_value_str, assertion_result)) } DataValue::Pubkey(expected_value) => { - let slice = &data[offset as usize..(offset + 32) as usize]; - let value = DataValue::deserialize(DataType::Pubkey, slice); - match operator { Operator::Equal => {} Operator::NotEqual => {} @@ -278,12 +311,12 @@ impl DataValue { _ => return Err(ProgramError::DataValueMismatch), }; - value_str = value_str.to_string(); - expected_value_str = expected_value.to_string(); - assertion_result = operator.is_true(&value, &expected_value); + 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)) } } - - Ok((value_str, expected_value_str, assertion_result)) } } diff --git a/programs/lighthouse/program/src/structs/operator.rs b/programs/lighthouse/program/src/structs/operator.rs index dc8771e..9f39806 100644 --- a/programs/lighthouse/program/src/structs/operator.rs +++ b/programs/lighthouse/program/src/structs/operator.rs @@ -16,7 +16,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_type.rs b/programs/lighthouse/program/src/structs/write_type.rs index a89c22b..ecfeafb 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -28,7 +28,7 @@ impl WriteType { WriteType::AccountData(_, len) => *len as usize, WriteType::AccountInfo => 8, WriteType::DataValue(memory_value) => match memory_value { - DataValue::U8(_) | DataValue::I8(_) => 1, + DataValue::Bool(_) | DataValue::U8(_) | DataValue::I8(_) => 1, DataValue::U16(_) | DataValue::I16(_) => 2, DataValue::U32(_) | DataValue::I32(_) => 4, DataValue::U64(_) | DataValue::I64(_) => 8, @@ -49,7 +49,7 @@ impl WriteType { WriteType::AccountBalance => true, WriteType::AccountData(_, _) => true, WriteType::AccountInfo => true, - WriteType::DataValue(_) => false, + WriteType::DataValue(_) => true, WriteType::MintAccount => { account.owner == &spl_token::id() && account.data_len() == Mint::LEN } diff --git a/programs/lighthouse/program/tests/simple.rs b/programs/lighthouse/program/tests/simple.rs index 930bb49..f57dfb3 100644 --- a/programs/lighthouse/program/tests/simple.rs +++ b/programs/lighthouse/program/tests/simple.rs @@ -2,20 +2,17 @@ pub mod utils; use std::io::Error; -use anchor_lang::accounts::account::Account; -use anchor_lang::system_program::System; -use anchor_lang::{AnchorDeserialize, InstructionData}; -use lighthouse::structs::{ - AccountInfoData, Assertion, DataValue, Expression, Operator, OptionalAccountInfoData, -}; -use solana_program::instruction::Instruction; +use lighthouse::error::ProgramError; +use lighthouse::structs::{Assertion, DataValue, Expression, Operator, WriteType}; +use solana_program::instruction::InstructionError; use solana_program::pubkey::Pubkey; -use solana_program::rent::Rent; use solana_program_test::tokio; +use solana_sdk::signature::Keypair; +use solana_sdk::transaction::TransactionError; use solana_sdk::{signer::EncodableKeypair, transaction::Transaction}; use solana_banks_interface::BanksTransactionResultWithMetadata; -use utils::context::TestContext; +use utils::context::{TestContext, DEFAULT_LAMPORTS_FUND_AMOUNT}; use utils::program::Program; pub fn find_test_account() -> (solana_program::pubkey::Pubkey, u8) { @@ -36,44 +33,37 @@ pub fn find_cache_account(user: Pubkey, cache_index: u8) -> (solana_program::pub 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( - &context.payer(), + &user, vec![ Assertion::AccountBalance(0, Operator::GreaterThan), // Assertion::AccountBalance(0, Operator::LessThan), ], - vec![ - context.payer().encodable_pubkey(), - context.payer().encodable_pubkey(), - ], + vec![user.encodable_pubkey(), user.encodable_pubkey()], + None, None, ); process_transaction_assert_success(context, tx_builder.to_transaction(vec![]).await).await; } +/// +/// 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(); - let account = find_test_account().0; - - process_transaction_assert_success( - context, - program - .create_test_account(&context.payer()) - .to_transaction(vec![]) - .await, - ) - .await; - + create_test_account(context, &user).await.unwrap(); process_transaction_assert_success( context, program .create_assertion( - &context.payer(), + &user, vec![ Assertion::AccountData(8, Operator::Equal, DataValue::U8(1)), Assertion::AccountData(9, Operator::Equal, DataValue::I8(-1)), @@ -117,625 +107,185 @@ async fn test_borsh_account_data() { 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![account; 10], + vec![find_test_account().0; 13], + None, None, ) .to_transaction(vec![]) .await, ) .await; +} + +/// +/// 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(); - // 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"); - // } + 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 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. + 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, 2])]), + 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; } -// #[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::AccountData(8, BorshField::U8, Operator::Equal, DataValue::U8(1)), -// Assertion::AccountData(8, BorshField::U8, Operator::Equal, DataValue::U8(5)), -// Assertion::AccountData( -// 10, -// BorshField::U16, -// Operator::Equal, -// DataValue::U16((u8::MAX as u16) + 1), -// ), -// Assertion::AccountData(10, BorshField::U16, Operator::Equal, DataValue::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 -// } - -// #[tokio::test] -// async fn test_assert_account_info() { -// 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::AccountInfoU8(0), -// ) -// .to_transaction(vec![]) -// .await, -// ) -// .await; - -// let data = context -// .client() -// .get_account(find_test_account().0) -// .await -// .unwrap() -// .unwrap(); -// // .data; - -// // println!("cache account: {}", format_hex(&data)); -// // println!( -// // "deserialized account info: {:?}", -// // AccountInfoData::try_from_slice(&data[8..8 + AccountInfoData::size() as usize]) -// // ); - -// // Assert that data was properly written to cache. -// let tx = program -// .create_assertion( -// &context.payer(), -// vec![Assertion::AccountInfo(OptionalAccountInfoData { -// owner: Some(lighthouse::ID), -// key: Some(find_test_account().0), -// lamports: None, -// data_length: None, -// rent_epoch: None, -// is_signer: None, -// is_writable: None, -// executable: None, -// })], -// vec![find_test_account().0; 1], -// 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; -// // } -// } +#[tokio::test] +async fn test_account_balance() { + let context = &mut TestContext::new().await.unwrap(); + + let mut program = Program::new(context.client()); + // create_test_account(context, user).await.unwrap(); +} + +#[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, 128), + ), + ) + .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; + } +} + +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, @@ -746,8 +296,6 @@ async fn process_transaction( .process_transaction_with_metadata(tx.clone()) .await .unwrap(); - // .metadata - // .unwrap(); Ok(result) } @@ -769,3 +317,84 @@ async fn process_transaction_assert_success( panic!("Transaction failed"); } } + +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(); + + 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); + } + } + } +} + +async fn create_test_account(context: &mut TestContext, payer: &Keypair) -> Result<(), Error> { + 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(()) +} + +async fn create_cache_account( + context: &mut TestContext, + user: &Keypair, + size: u64, +) -> Result<(), Error> { + 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(()) +} + +fn to_transaction_error(ix_index: u8, program_error: ProgramError) -> TransactionError { + TransactionError::InstructionError(ix_index, InstructionError::Custom(program_error.into())) +} + +async fn create_user(ctx: &mut TestContext) -> Result { + let user = Keypair::new(); + let _ = ctx + .fund_account(user.encodable_pubkey(), DEFAULT_LAMPORTS_FUND_AMOUNT) + .await; + + Ok(user) +} + +// Tests to write diff --git a/programs/lighthouse/program/tests/utils/program.rs b/programs/lighthouse/program/tests/utils/program.rs index 3366c94..cc7181f 100644 --- a/programs/lighthouse/program/tests/utils/program.rs +++ b/programs/lighthouse/program/tests/utils/program.rs @@ -106,11 +106,9 @@ impl Program { assertions: Vec, additional_accounts: Vec, logical_expression: Option>, + cache: Option, ) -> AssertBuilder { - let accounts = lighthouse::accounts::AssertV1 { - // system_program: system_program::id(), - cache: None, - }; + let accounts = lighthouse::accounts::AssertV1 { cache }; let assertion_clone = (assertions).clone(); let logical_expression_clone = (logical_expression).clone(); @@ -130,7 +128,7 @@ impl Program { program_id: lighthouse::id(), accounts: (lighthouse::accounts::AssertV1 { // system_program: system_program::id(), - cache: None, + cache, }) .to_account_metas(None), data: (lighthouse::instruction::AssertV1 { From 091a70dbc048ba8cd20f76a106c27a10af50033c Mon Sep 17 00:00:00 2001 From: Jac0xb Date: Sun, 10 Dec 2023 22:12:14 -0800 Subject: [PATCH 4/6] Implement more tests --- programs/lighthouse/program/src/error.rs | 21 ++- .../program/src/processor/v1/assert.rs | 22 +-- .../program/src/processor/v1/write.rs | 66 +++++-- .../program/src/structs/data_value.rs | 4 +- .../lighthouse/program/src/structs/mod.rs | 4 +- .../src/structs/{ => write}/account_info.rs | 0 .../program/src/structs/write/mod.rs | 5 + .../program/src/structs/write/program.rs | 33 ++++ .../program/src/structs/write_type.rs | 65 ++----- .../lighthouse/program/tests/suites/mod.rs | 2 + .../program/tests/{ => suites}/simple.rs | 171 ++---------------- .../program/tests/suites/write/mod.rs | 2 + .../program/tests/suites/write/program.rs | 56 ++++++ .../program/tests/suites/write/simple.rs | 78 ++++++++ programs/lighthouse/program/tests/tests.rs | 2 + .../lighthouse/program/tests/utils/context.rs | 4 +- .../lighthouse/program/tests/utils/mod.rs | 13 +- .../lighthouse/program/tests/utils/print.rs | 17 ++ .../lighthouse/program/tests/utils/program.rs | 67 ++++--- .../program/tests/utils/tx_builder.rs | 2 +- .../lighthouse/program/tests/utils/utils.rs | 90 +++++++++ 21 files changed, 447 insertions(+), 277 deletions(-) rename programs/lighthouse/program/src/structs/{ => write}/account_info.rs (100%) create mode 100644 programs/lighthouse/program/src/structs/write/mod.rs create mode 100644 programs/lighthouse/program/src/structs/write/program.rs create mode 100644 programs/lighthouse/program/tests/suites/mod.rs rename programs/lighthouse/program/tests/{ => suites}/simple.rs (66%) create mode 100644 programs/lighthouse/program/tests/suites/write/mod.rs create mode 100644 programs/lighthouse/program/tests/suites/write/program.rs create mode 100644 programs/lighthouse/program/tests/suites/write/simple.rs create mode 100644 programs/lighthouse/program/tests/tests.rs create mode 100644 programs/lighthouse/program/tests/utils/print.rs create mode 100644 programs/lighthouse/program/tests/utils/utils.rs diff --git a/programs/lighthouse/program/src/error.rs b/programs/lighthouse/program/src/error.rs index 5433c78..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 { @@ -14,10 +12,25 @@ pub enum ProgramError { DataValueMismatch, #[msg("UnsupportedOperator")] UnsupportedOperator, - #[msg("CacheOutOfRange")] - CacheOutOfRange, + #[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/processor/v1/assert.rs b/programs/lighthouse/program/src/processor/v1/assert.rs index 31ea512..8d45770 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -66,12 +66,12 @@ pub fn assert<'info>( 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)?; + .deserialize_and_compare(cache_data, (cache_offset + 8) as usize, &operator)?; assertion_result = result; msg!( - "{} {} AssertionParameter::Memory ({}) -> {} {} {}", + "{} {} Assertion::Memory ({}) -> {} {} {}", format!("[{:?}]", i), if assertion_result { "[✅] SUCCESS" @@ -89,7 +89,7 @@ pub fn assert<'info>( 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)?; + .deserialize_and_compare(account_data, account_offset as usize, &operator)?; assertion_result = result; @@ -128,7 +128,7 @@ pub fn assert<'info>( ); } } - Assertion::TokenAccountBalance(expected_balance, operator) => { + Assertion::TokenAccountBalance(_, _) => { return Err(ProgramError::Unimplemented.into()); } Assertion::AccountInfo(account_info_fields, operator) => { @@ -166,21 +166,7 @@ pub fn assert<'info>( } } } - - // if verbose { - // msg!( - // "{} Assertion::AccountInfo ({}) -> {:?}", - // if assertion_result { - // "[✅] SUCCESS" - // } else { - // "[❌] FAIL " - // }, - // account.key().to_string(), - // account_info_data, - // ); - // } } - _ => {} // REMOVE } assertion_results.push(assertion_result); diff --git a/programs/lighthouse/program/src/processor/v1/write.rs b/programs/lighthouse/program/src/processor/v1/write.rs index f6cd9a3..1f16571 100644 --- a/programs/lighthouse/program/src/processor/v1/write.rs +++ b/programs/lighthouse/program/src/processor/v1/write.rs @@ -20,7 +20,6 @@ pub struct WriteV1<'info> { bump )] pub cache_account: UncheckedAccount<'info>, - pub rent: Sysvar<'info, Rent>, } pub fn write<'info>( @@ -42,18 +41,21 @@ pub fn write<'info>( (cache_offset as usize, write_type) } }; - cache_offset = cache_offset - .checked_add(8) - .ok_or(ProgramError::CacheOutOfRange)?; + + cache_offset = cache_offset.checked_add(8).ok_or_else(|| { + msg!("Cache offset overflowed"); + ProgramError::OutOfRange + })?; let data_length = write_type.size(); match write_type { - WriteType::MintAccount => {} - WriteType::TokenAccount2022 => {} - WriteType::TokenAccountLegacy => {} - WriteType::Program => {} + WriteType::Program => { + return Err(ProgramError::Unimplemented.into()); + } WriteType::DataValue(borsh_value) => { + let data_length = data_length.ok_or(ProgramError::InvalidDataLength)?; + if (cache_offset + data_length) < cache_data_length { let data_slice = &(borsh_value.serialize())[0..data_length]; @@ -67,6 +69,8 @@ pub fn write<'info>( let source_account = ctx.remaining_accounts.first(); if let Some(target_account) = source_account { + let data_length = data_length.ok_or(ProgramError::InvalidDataLength)?; + if (cache_offset + data_length) < cache_data_length { let data = target_account.lamports(); let data_slice = &data.to_le_bytes(); @@ -80,17 +84,51 @@ pub fn write<'info>( return Err(ProgramError::NotEnoughAccounts.into()); } } - WriteType::AccountData(account_offset, data_length) => { + WriteType::AccountData(account_offset, _, account_validation) => { let target_account = ctx.remaining_accounts.first(); - let data_length = data_length as usize; let account_offset = account_offset as usize; if let Some(target_account) = target_account { - if !write_type.account_validation(target_account) { - msg!("Could not validation account"); - return Err(ProgramError::InvalidAccount.into()); + 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()); + } + } } + let data_length = data_length.unwrap_or( + target_account + .data_len() + .checked_sub(account_offset) + .ok_or(ProgramError::AccountOutOfRange)?, + ); + if (cache_offset + data_length) < cache_data_length { let data = target_account.try_borrow_data().map_err(|err| { msg!("Error: {:?}", err); @@ -111,6 +149,8 @@ pub fn write<'info>( let target_account = ctx.remaining_accounts.first(); if let Some(target_account) = target_account { + let data_length = data_length.ok_or(ProgramError::InvalidDataLength)?; + if (cache_offset + data_length) < cache_data_length { let account_info = AccountInfoData { key: *target_account.key, diff --git a/programs/lighthouse/program/src/structs/data_value.rs b/programs/lighthouse/program/src/structs/data_value.rs index a34a49c..ab0a61b 100644 --- a/programs/lighthouse/program/src/structs/data_value.rs +++ b/programs/lighthouse/program/src/structs/data_value.rs @@ -1,3 +1,5 @@ +use std::cell::Ref; + use anchor_lang::prelude::{ borsh, borsh::{BorshDeserialize, BorshSerialize}, @@ -143,7 +145,7 @@ impl DataValue { pub fn deserialize_and_compare( self, - data: &[u8], + data: Ref<'_, &mut [u8]>, offset: usize, operator: &Operator, ) -> Result<(String, String, bool), ProgramError> { diff --git a/programs/lighthouse/program/src/structs/mod.rs b/programs/lighthouse/program/src/structs/mod.rs index 471dd0d..5b8dd66 100644 --- a/programs/lighthouse/program/src/structs/mod.rs +++ b/programs/lighthouse/program/src/structs/mod.rs @@ -1,15 +1,15 @@ -pub mod account_info; pub mod assertion; pub mod borsh_field; pub mod data_value; pub mod expression; pub mod operator; +pub mod write; pub mod write_type; -pub use account_info::*; pub use assertion::*; pub use borsh_field::*; 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/account_info.rs b/programs/lighthouse/program/src/structs/write/account_info.rs similarity index 100% rename from programs/lighthouse/program/src/structs/account_info.rs rename to programs/lighthouse/program/src/structs/write/account_info.rs 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..c4a9361 --- /dev/null +++ b/programs/lighthouse/program/src/structs/write/program.rs @@ -0,0 +1,33 @@ +use anchor_lang::{ + prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, + }, + Id, +}; +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 ecfeafb..360712a 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -1,71 +1,46 @@ use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token, - token::{Mint, TokenAccount}, -}; use borsh::{BorshDeserialize, BorshSerialize}; use super::DataValue; +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] +pub struct AccountValidation { + pub owner: Option, + pub is_funded: Option, + pub discriminator: Option>, +} + #[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] pub enum WriteType { AccountBalance, // Account Data Offset, Data Length - AccountData(u16, u16), + AccountData(u16, Option, Option), AccountInfo, DataValue(DataValue), - MintAccount, - TokenAccountLegacy, - TokenAccount2022, Program, } impl WriteType { - pub fn size(&self) -> usize { + pub fn size(&self) -> Option { match self { - WriteType::AccountBalance => 8, - WriteType::AccountData(_, len) => *len as usize, - WriteType::AccountInfo => 8, + WriteType::AccountBalance => Some(8), + WriteType::AccountData(_, len, _) => len.as_ref().map(|len| *len as usize), + WriteType::AccountInfo => Some(8), WriteType::DataValue(memory_value) => match memory_value { - DataValue::Bool(_) | DataValue::U8(_) | DataValue::I8(_) => 1, - DataValue::U16(_) | DataValue::I16(_) => 2, - DataValue::U32(_) | DataValue::I32(_) => 4, - DataValue::U64(_) | DataValue::I64(_) => 8, - DataValue::U128(_) | DataValue::I128(_) => 16, - DataValue::Bytes(bytes) => bytes.len(), - DataValue::Pubkey(_) => 32, + 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), }, - // TODO: It might just be better/make more sense to create variants for mints, token accounts and let them choose what to write in the accounts - WriteType::MintAccount => Mint::LEN, // TODO: Test - WriteType::TokenAccountLegacy => TokenAccount::LEN, // TODO: Test - WriteType::TokenAccount2022 => usize::MAX, // TODO: Get actual size - WriteType::Program => 8, // TODO: Get actual size - } - } - - pub fn account_validation(&self, account: &AccountInfo<'_>) -> bool { - match self { - WriteType::AccountBalance => true, - WriteType::AccountData(_, _) => true, - WriteType::AccountInfo => true, - WriteType::DataValue(_) => true, - WriteType::MintAccount => { - account.owner == &spl_token::id() && account.data_len() == Mint::LEN - } - WriteType::TokenAccountLegacy => { - account.owner == &associated_token::ID && account.data_len() == TokenAccount::LEN - } - WriteType::TokenAccount2022 => { - false // TODO: Support - } - _ => true, + WriteType::Program => Some(64), } } } -// 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 WriteTypeParameter { // Memory offset, write type diff --git a/programs/lighthouse/program/tests/suites/mod.rs b/programs/lighthouse/program/tests/suites/mod.rs new file mode 100644 index 0000000..56d6469 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/mod.rs @@ -0,0 +1,2 @@ +// pub mod simple; +pub mod write; diff --git a/programs/lighthouse/program/tests/simple.rs b/programs/lighthouse/program/tests/suites/simple.rs similarity index 66% rename from programs/lighthouse/program/tests/simple.rs rename to programs/lighthouse/program/tests/suites/simple.rs index f57dfb3..e41ba8a 100644 --- a/programs/lighthouse/program/tests/simple.rs +++ b/programs/lighthouse/program/tests/suites/simple.rs @@ -1,33 +1,14 @@ -pub mod utils; - -use std::io::Error; - +use crate::utils::context::TestContext; +use crate::utils::program::{ + create_cache_account, create_test_account, create_user, find_cache_account, find_test_account, + Program, +}; +use crate::utils::utils::to_transaction_error; +use crate::utils::{process_transaction_assert_failure, process_transaction_assert_success}; use lighthouse::error::ProgramError; use lighthouse::structs::{Assertion, DataValue, Expression, Operator, WriteType}; -use solana_program::instruction::InstructionError; -use solana_program::pubkey::Pubkey; use solana_program_test::tokio; -use solana_sdk::signature::Keypair; -use solana_sdk::transaction::TransactionError; -use solana_sdk::{signer::EncodableKeypair, transaction::Transaction}; - -use solana_banks_interface::BanksTransactionResultWithMetadata; -use utils::context::{TestContext, DEFAULT_LAMPORTS_FUND_AMOUNT}; -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, - ) -} +use solana_sdk::signer::EncodableKeypair; #[tokio::test] async fn test_basic() { @@ -197,9 +178,9 @@ async fn test_logical_expression() { #[tokio::test] async fn test_account_balance() { - let context = &mut TestContext::new().await.unwrap(); + // let context = &mut TestContext::new().await.unwrap(); - let mut program = Program::new(context.client()); + // let mut program = Program::new(context.client()); // create_test_account(context, user).await.unwrap(); } @@ -226,7 +207,7 @@ async fn test_write() { 0, lighthouse::structs::WriteTypeParameter::WriteU8( 0, - WriteType::AccountData(8, 128), + WriteType::AccountData(8, Some(128), None), ), ) .to_transaction(vec![]) @@ -268,133 +249,3 @@ async fn test_write() { 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(); - - 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(); - - let logs = tx_metadata.metadata.unwrap().log_messages; - for log in logs { - println!("{:?}", log); - } - - if tx_metadata.result.is_err() { - panic!("Transaction failed"); - } -} - -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(); - - 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); - } - } - } -} - -async fn create_test_account(context: &mut TestContext, payer: &Keypair) -> Result<(), Error> { - 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(()) -} - -async fn create_cache_account( - context: &mut TestContext, - user: &Keypair, - size: u64, -) -> Result<(), Error> { - 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(()) -} - -fn to_transaction_error(ix_index: u8, program_error: ProgramError) -> TransactionError { - TransactionError::InstructionError(ix_index, InstructionError::Custom(program_error.into())) -} - -async fn create_user(ctx: &mut TestContext) -> Result { - let user = Keypair::new(); - let _ = ctx - .fund_account(user.encodable_pubkey(), DEFAULT_LAMPORTS_FUND_AMOUNT) - .await; - - Ok(user) -} - -// Tests to write 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..2433459 --- /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 cc7181f..7566bc2 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, @@ -11,7 +11,7 @@ use super::{ use anchor_lang::*; use lighthouse::{ processor::Config, - structs::{Assertion, Expression, WriteType, WriteTypeParameter}, + structs::{Assertion, Expression, WriteTypeParameter}, }; use solana_program::{ instruction::{AccountMeta, Instruction}, @@ -26,22 +26,12 @@ use solana_sdk::{ transaction::Transaction, }; -// 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 } } @@ -126,11 +116,7 @@ impl Program { (), vec![Instruction { program_id: lighthouse::id(), - accounts: (lighthouse::accounts::AssertV1 { - // system_program: system_program::id(), - cache, - }) - .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, @@ -160,7 +146,6 @@ impl Program { rent: sysvar::rent::id(), }; - // The conversions below should not fail. let data = lighthouse::instruction::CreateCacheAccountV1 { cache_index, cache_account_size, @@ -202,7 +187,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(), }; let write_type_clone = write_type_parameter.clone(); @@ -216,7 +200,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 +231,49 @@ 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) +} 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..453823a --- /dev/null +++ b/programs/lighthouse/program/tests/utils/utils.rs @@ -0,0 +1,90 @@ +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.unwrap(); + + 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(); + + 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())) +} From fc623aecf159d673ad02321dcd7aecde5bfbf26b Mon Sep 17 00:00:00 2001 From: Jac0xb Date: Sat, 16 Dec 2023 20:39:24 -0500 Subject: [PATCH 5/6] Refactor tests --- .../program/src/processor/v1/assert.rs | 237 ++++++++---------- .../program/src/processor/v1/write.rs | 35 +-- .../src/structs/assert/assertion_state.rs | 104 ++++++++ .../program/src/structs/assert/mod.rs | 3 + .../program/src/structs/assertion.rs | 2 +- .../program/src/structs/expression.rs | 9 + .../lighthouse/program/src/structs/mod.rs | 2 + .../program/src/structs/write_type.rs | 25 +- programs/lighthouse/program/src/utils.rs | 46 ++-- .../tests/suites/assert/account_balance.rs | 29 +++ .../tests/suites/assert/account_data.rs | 81 ++++++ .../tests/suites/assert/logical_expression.rs | 85 +++++++ .../program/tests/suites/assert/mod.rs | 3 + .../lighthouse/program/tests/suites/mod.rs | 3 +- .../lighthouse/program/tests/suites/simple.rs | 180 +------------ .../program/tests/suites/write/mod.rs | 4 +- .../lighthouse/program/tests/utils/utils.rs | 5 + 17 files changed, 484 insertions(+), 369 deletions(-) create mode 100644 programs/lighthouse/program/src/structs/assert/assertion_state.rs create mode 100644 programs/lighthouse/program/src/structs/assert/mod.rs create mode 100644 programs/lighthouse/program/tests/suites/assert/account_balance.rs create mode 100644 programs/lighthouse/program/tests/suites/assert/account_data.rs create mode 100644 programs/lighthouse/program/tests/suites/assert/logical_expression.rs create mode 100644 programs/lighthouse/program/tests/suites/assert/mod.rs diff --git a/programs/lighthouse/program/src/processor/v1/assert.rs b/programs/lighthouse/program/src/processor/v1/assert.rs index 8d45770..dd03a6b 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -4,7 +4,8 @@ use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use crate::error::ProgramError; -use crate::structs::{AccountInfoDataField, Assertion, Expression}; +use crate::structs::{AccountInfoDataField, Assertion, AssertionState, Expression}; +use crate::utils::print_result; #[derive(Accounts)] pub struct AssertV1<'info> { @@ -26,40 +27,22 @@ pub fn assert<'info>( let remaining_accounts = &mut ctx.remaining_accounts.iter(); 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(); - - 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); - } - } - } - } - } + let mut assertion_state = AssertionState::new(assertions.clone(), logical_expression)?; for (i, assertion) in assertions.into_iter().enumerate() { let mut assertion_result = false; match assertion { - Assertion::AccountOwnedBy(pubkey) => { + Assertion::AccountOwnedBy(pubkey, operator) => { let account = remaining_accounts.next().unwrap(); assertion_result = account.owner.key().eq(&pubkey); + + let value_str = account.owner.key().to_string(); + let expected_value_str = pubkey.to_string(); + + if verbose { + print_result(assertion_result, i, operator, value_str, expected_value_str); + } } Assertion::Memory(cache_offset, operator, memory_value) => { let cache = ctx.accounts.cache.as_ref().unwrap(); // TODO: Graceful error handling @@ -70,19 +53,9 @@ pub fn assert<'info>( assertion_result = result; - msg!( - "{} {} Assertion::Memory ({}) -> {} {} {}", - format!("[{:?}]", i), - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - "Cache...".to_string(), - value_str, - operator.format(), - expected_value_str, - ); + if verbose { + print_result(assertion_result, i, operator, value_str, expected_value_str); + } } Assertion::AccountData(account_offset, operator, memory_value) => { let account = remaining_accounts.next().unwrap(); @@ -93,19 +66,9 @@ pub fn assert<'info>( assertion_result = result; - msg!( - "{} {} Assertion::AccountData ({}) -> {} {} {}", - format!("[{:02}]", i), - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - truncate_pubkey(&account.key()), - value_str, - operator.format(), - expected_value_str, - ); + if verbose { + print_result(assertion_result, i, operator, value_str, expected_value_str); + } } Assertion::AccountBalance(expected_balance, operator) => { let account = remaining_accounts.next().unwrap(); @@ -114,17 +77,12 @@ pub fn assert<'info>( operator.evaluate(&**account.try_borrow_lamports()?, &expected_balance); if verbose { - msg!( - "{} Assertion::AccountBalance ({}) -> {} {} {}", - if assertion_result { - "[✅] SUCCESS" - } else { - "[❌] FAIL " - }, - truncate_pubkey(&account.key()), - account.get_lamports(), - operator.format(), - expected_balance, + print_result( + assertion_result, + i, + operator, + account.get_lamports().to_string(), + expected_balance.to_string(), ); } } @@ -169,82 +127,87 @@ pub fn assert<'info>( } } - assertion_results.push(assertion_result); + assertion_state.record_result(i, 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 ") - ); - } - - if !result { - return Err(ProgramError::AssertionFailed.into()); - } - } - Expression::Or(assertion_indexes) => { - let mut result = false; + // assertion_results.push(assertion_result); - 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()); - } - } - } - } + // if (logical_expression.is_none() + // || !logically_dependent_assertions + // .as_ref() + // .unwrap() + // .contains(&(i as u8))) + // && !assertion_result + // { + // return Err(ProgramError::AssertionFailed.into()); + // } } + msg!("assertion_state: {:?}", assertion_state); + assertion_state.evaluate()?; + + // 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 ") + // ); + // } + + // if !result { + // return Err(ProgramError::AssertionFailed.into()); + // } + // } + // 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(()) } diff --git a/programs/lighthouse/program/src/processor/v1/write.rs b/programs/lighthouse/program/src/processor/v1/write.rs index 1f16571..a536098 100644 --- a/programs/lighthouse/program/src/processor/v1/write.rs +++ b/programs/lighthouse/program/src/processor/v1/write.rs @@ -47,30 +47,27 @@ pub fn write<'info>( ProgramError::OutOfRange })?; - let data_length = write_type.size(); + 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::Program => { return Err(ProgramError::Unimplemented.into()); } WriteType::DataValue(borsh_value) => { - let data_length = data_length.ok_or(ProgramError::InvalidDataLength)?; - - if (cache_offset + data_length) < cache_data_length { - let data_slice = &(borsh_value.serialize())[0..data_length]; - - cache_ref[cache_offset..(cache_offset + data_length)] - .copy_from_slice(data_slice.as_ref()); - } else { - return Err(ProgramError::NotEnoughAccounts.into()); - } + 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 { - let data_length = data_length.ok_or(ProgramError::InvalidDataLength)?; - if (cache_offset + data_length) < cache_data_length { let data = target_account.lamports(); let data_slice = &data.to_le_bytes(); @@ -88,6 +85,7 @@ pub fn write<'info>( 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 { @@ -122,13 +120,6 @@ pub fn write<'info>( } } - let data_length = data_length.unwrap_or( - target_account - .data_len() - .checked_sub(account_offset) - .ok_or(ProgramError::AccountOutOfRange)?, - ); - if (cache_offset + data_length) < cache_data_length { let data = target_account.try_borrow_data().map_err(|err| { msg!("Error: {:?}", err); @@ -149,8 +140,6 @@ pub fn write<'info>( let target_account = ctx.remaining_accounts.first(); if let Some(target_account) = target_account { - let data_length = data_length.ok_or(ProgramError::InvalidDataLength)?; - if (cache_offset + data_length) < cache_data_length { let account_info = AccountInfoData { key: *target_account.key, @@ -175,7 +164,7 @@ pub fn write<'info>( return Err(ProgramError::NotEnoughAccounts.into()); } } - } + }; Ok(()) } 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..e8849ed --- /dev/null +++ b/programs/lighthouse/program/src/structs/assert/mod.rs @@ -0,0 +1,3 @@ +pub mod assertion_state; + +pub use assertion_state::*; diff --git a/programs/lighthouse/program/src/structs/assertion.rs b/programs/lighthouse/program/src/structs/assertion.rs index 60b9492..bf10eba 100644 --- a/programs/lighthouse/program/src/structs/assertion.rs +++ b/programs/lighthouse/program/src/structs/assertion.rs @@ -17,7 +17,7 @@ pub enum Assertion { // balance, operator AccountBalance(u64, Operator), - AccountOwnedBy(Pubkey), + AccountOwnedBy(Pubkey, Operator), // token balance, operator TokenAccountBalance(u64, Operator), 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 5b8dd66..ba3e506 100644 --- a/programs/lighthouse/program/src/structs/mod.rs +++ b/programs/lighthouse/program/src/structs/mod.rs @@ -1,3 +1,4 @@ +pub mod assert; pub mod assertion; pub mod borsh_field; pub mod data_value; @@ -6,6 +7,7 @@ pub mod operator; pub mod write; pub mod write_type; +pub use assert::*; pub use assertion::*; pub use borsh_field::*; pub use data_value::*; diff --git a/programs/lighthouse/program/src/structs/write_type.rs b/programs/lighthouse/program/src/structs/write_type.rs index 360712a..15ff18f 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -1,4 +1,4 @@ -use anchor_lang::prelude::*; +use anchor_lang::{accounts::account, prelude::*}; use borsh::{BorshDeserialize, BorshSerialize}; use super::DataValue; @@ -14,7 +14,7 @@ pub struct AccountValidation { pub enum WriteType { AccountBalance, - // Account Data Offset, Data Length + // Account Data Offset, Data Length, Validation AccountData(u16, Option, Option), AccountInfo, DataValue(DataValue), @@ -22,10 +22,27 @@ pub enum WriteType { } impl WriteType { - pub fn size(&self) -> Option { + pub fn size( + &self, + account_info: Option<&anchor_lang::prelude::AccountInfo<'_>>, + ) -> Option { match self { WriteType::AccountBalance => Some(8), - WriteType::AccountData(_, len, _) => len.as_ref().map(|len| *len as usize), + 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, + } + } + } + WriteType::AccountInfo => Some(8), WriteType::DataValue(memory_value) => match memory_value { DataValue::Bool(_) | DataValue::U8(_) | DataValue::I8(_) => Some(1), diff --git a/programs/lighthouse/program/src/utils.rs b/programs/lighthouse/program/src/utils.rs index f5ac9fe..e2e645b 100644 --- a/programs/lighthouse/program/src/utils.rs +++ b/programs/lighthouse/program/src/utils.rs @@ -1,24 +1,24 @@ -use anchor_lang::prelude::{borsh::BorshDeserialize, *}; +use crate::structs::Operator; +use solana_program::msg; -use crate::{error, structs::Operator}; - -// pub fn process_value( -// data: &[u8], -// offset: u32, -// size: usize, -// expected_value: &T, -// borsh_field: &BorshField, -// 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); - -// Ok(( -// value.to_string(), -// expected_value.to_string(), -// assertion_result, -// )) -// } +pub fn print_result( + assertion_result: bool, + assertion_index: usize, + operator: Operator, + value_str: String, + expected_value_str: String, +) { + msg!( + "{} {} Assertion::Memory ({}) -> {} {} {}", + format!("[{:?}]", assertion_index), + if assertion_result { + "[✅] SUCCESS" + } else { + "[❌] FAIL " + }, + "Cache...".to_string(), + value_str, + operator.format(), + expected_value_str, + ); +} 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..c8b4296 --- /dev/null +++ b/programs/lighthouse/program/tests/suites/assert/mod.rs @@ -0,0 +1,3 @@ +pub mod account_balance; +pub mod account_data; +pub mod logical_expression; diff --git a/programs/lighthouse/program/tests/suites/mod.rs b/programs/lighthouse/program/tests/suites/mod.rs index 56d6469..8adc63a 100644 --- a/programs/lighthouse/program/tests/suites/mod.rs +++ b/programs/lighthouse/program/tests/suites/mod.rs @@ -1,2 +1,3 @@ -// pub mod simple; +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 index e41ba8a..fc39e2c 100644 --- a/programs/lighthouse/program/tests/suites/simple.rs +++ b/programs/lighthouse/program/tests/suites/simple.rs @@ -1,189 +1,13 @@ 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 crate::utils::utils::to_transaction_error; -use crate::utils::{process_transaction_assert_failure, process_transaction_assert_success}; -use lighthouse::error::ProgramError; -use lighthouse::structs::{Assertion, DataValue, Expression, Operator, WriteType}; +use lighthouse::structs::{Assertion, DataValue, Operator, WriteType}; use solana_program_test::tokio; use solana_sdk::signer::EncodableKeypair; -#[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; -} - -/// -/// 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; -} - -/// -/// 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(); - - 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 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. - 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, 2])]), - 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; -} - -#[tokio::test] -async fn test_account_balance() { - // let context = &mut TestContext::new().await.unwrap(); - - // let mut program = Program::new(context.client()); - // create_test_account(context, user).await.unwrap(); -} - #[tokio::test] async fn test_write() { let context = &mut TestContext::new().await.unwrap(); diff --git a/programs/lighthouse/program/tests/suites/write/mod.rs b/programs/lighthouse/program/tests/suites/write/mod.rs index 2433459..5ea6287 100644 --- a/programs/lighthouse/program/tests/suites/write/mod.rs +++ b/programs/lighthouse/program/tests/suites/write/mod.rs @@ -1,2 +1,2 @@ -pub mod program; -// pub mod simple; +// pub mod program; +pub mod simple; diff --git a/programs/lighthouse/program/tests/utils/utils.rs b/programs/lighthouse/program/tests/utils/utils.rs index 453823a..8872d5c 100644 --- a/programs/lighthouse/program/tests/utils/utils.rs +++ b/programs/lighthouse/program/tests/utils/utils.rs @@ -47,6 +47,11 @@ pub async fn process_transaction_assert_failure( 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"); } From fa77f78787dad9abca209e2b252c0728c23cc201 Mon Sep 17 00:00:00 2001 From: Jac0xb Date: Sun, 17 Dec 2023 21:50:00 -0500 Subject: [PATCH 6/6] Implement token account balance assertion --- macros/src/lib.rs | 4 +- programs/lighthouse/program/src/lib.rs | 2 +- .../program/src/processor/v1/assert.rs | 236 ++++++++---------- .../program/src/structs/assert/assertion.rs | 63 +++++ .../program/src/structs/assert/mod.rs | 2 + .../program/src/structs/assertion.rs | 26 -- .../program/src/structs/data_value.rs | 56 +++-- .../lighthouse/program/src/structs/mod.rs | 2 - .../program/src/structs/operator.rs | 2 - .../program/src/structs/write/program.rs | 9 +- .../program/src/structs/write_type.rs | 2 +- programs/lighthouse/program/src/utils.rs | 20 +- .../program/tests/suites/assert/mod.rs | 1 + .../suites/assert/token_account_balance.rs | 44 ++++ .../lighthouse/program/tests/utils/program.rs | 100 +++++++- .../lighthouse/program/tests/utils/utils.rs | 12 +- 16 files changed, 384 insertions(+), 197 deletions(-) create mode 100644 programs/lighthouse/program/src/structs/assert/assertion.rs delete mode 100644 programs/lighthouse/program/src/structs/assertion.rs create mode 100644 programs/lighthouse/program/tests/suites/assert/token_account_balance.rs diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 58dca09..7b04f1e 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -4,8 +4,8 @@ use proc_macro::TokenStream; // use proc_macro::TokenStream; use quote::quote; use syn::{ - parse_macro_input, punctuated::Punctuated, token::Comma, Attribute, Data, DataStruct, - DeriveInput, Field, Fields, + parse_macro_input, punctuated::Punctuated, token::Comma, Data, DataStruct, DeriveInput, Field, + Fields, }; // use syn::{parse_macro_input, DeriveInput}; diff --git a/programs/lighthouse/program/src/lib.rs b/programs/lighthouse/program/src/lib.rs index 27df9bd..4522524 100644 --- a/programs/lighthouse/program/src/lib.rs +++ b/programs/lighthouse/program/src/lib.rs @@ -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 dd03a6b..f232af9 100644 --- a/programs/lighthouse/program/src/processor/v1/assert.rs +++ b/programs/lighthouse/program/src/processor/v1/assert.rs @@ -1,11 +1,10 @@ -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::{AccountInfoDataField, Assertion, AssertionState, Expression}; -use crate::utils::print_result; +use crate::utils::print_assertion_result; #[derive(Accounts)] pub struct AssertV1<'info> { @@ -14,7 +13,7 @@ pub struct AssertV1<'info> { } #[derive(BorshDeserialize, BorshSerialize, Debug)] -pub struct Config { +pub struct AssertionConfig { pub verbose: bool, } @@ -22,192 +21,163 @@ pub fn assert<'info>( ctx: Context<'_, '_, '_, 'info, AssertV1<'info>>, assertions: Vec, logical_expression: Option>, - options: Option, + config: Option, ) -> Result<()> { let remaining_accounts = &mut ctx.remaining_accounts.iter(); - - let verbose = options.map(|options| options.verbose).unwrap_or(false); let mut assertion_state = AssertionState::new(assertions.clone(), logical_expression)?; for (i, assertion) in assertions.into_iter().enumerate() { - let mut assertion_result = false; - - match assertion { + let assertion_result: Result = match &assertion { Assertion::AccountOwnedBy(pubkey, operator) => { let account = remaining_accounts.next().unwrap(); - assertion_result = account.owner.key().eq(&pubkey); + let result = account.owner.key().eq(pubkey); let value_str = account.owner.key().to_string(); let expected_value_str = pubkey.to_string(); - if verbose { - print_result(assertion_result, i, operator, value_str, expected_value_str); - } + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); + + Ok(result) } 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)?; - - assertion_result = result; - - if verbose { - print_result(assertion_result, i, operator, value_str, expected_value_str); - } + .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, + ); + + Ok(result) } 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)?; - - assertion_result = result; - - if verbose { - print_result(assertion_result, i, operator, value_str, expected_value_str); - } + .deserialize_and_compare(account_data, (*account_offset) as usize, operator)?; + + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); + + Ok(result) + } + 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, + ); + + Ok(result) } - Assertion::AccountBalance(expected_balance, operator) => { + Assertion::TokenAccountBalance(balance_value, operator) => { let account = remaining_accounts.next().unwrap(); - assertion_result = - operator.evaluate(&**account.try_borrow_lamports()?, &expected_balance); - - if verbose { - print_result( - assertion_result, - i, - operator, - account.get_lamports().to_string(), - expected_balance.to_string(), - ); + if account.owner.eq(&spl_associated_token_account::id()) { + return Err(ProgramError::InvalidAccount.into()); } - } - Assertion::TokenAccountBalance(_, _) => { - return Err(ProgramError::Unimplemented.into()); + + let token_account = + spl_token::state::Account::unpack_from_slice(&account.try_borrow_data()?)?; + + let result = operator.evaluate(&token_account.amount, balance_value); + + let value_str = token_account.amount.to_string(); + let expected_value_str = balance_value.to_string(); + + print_assertion_result( + &config, + assertion.format(), + result, + i, + operator, + value_str, + expected_value_str, + ); + + Ok(result) } Assertion::AccountInfo(account_info_fields, operator) => { let account = remaining_accounts.next().unwrap(); + let operator_result = true; for account_info_field in account_info_fields { - match account_info_field { + let operator_result = match account_info_field { AccountInfoDataField::Key(pubkey) => { - assertion_result = operator.evaluate(&account.key(), &pubkey); + operator.evaluate(&account.key(), pubkey) } AccountInfoDataField::Owner(pubkey) => { - assertion_result = operator.evaluate(account.owner, &pubkey); + operator.evaluate(account.owner, pubkey) } AccountInfoDataField::Lamports(lamports) => { - assertion_result = - operator.evaluate(&account.get_lamports(), &lamports); + operator.evaluate(&account.get_lamports(), lamports) } AccountInfoDataField::DataLength(data_length) => { - assertion_result = - operator.evaluate(&(account.data_len() as u64), &data_length); + operator.evaluate(&(account.data_len() as u64), data_length) } AccountInfoDataField::Executable(executable) => { - assertion_result = operator.evaluate(&account.executable, &executable); + operator.evaluate(&account.executable, executable) } AccountInfoDataField::IsSigner(is_signer) => { - assertion_result = operator.evaluate(&account.is_signer, &is_signer); + operator.evaluate(&account.is_signer, is_signer) } AccountInfoDataField::IsWritable(is_writable) => { - assertion_result = - operator.evaluate(&account.is_writable, &is_writable); + operator.evaluate(&account.is_writable, is_writable) } AccountInfoDataField::RentEpoch(rent_epoch) => { - assertion_result = - operator.evaluate(&account.rent_epoch as &u64, &rent_epoch); + operator.evaluate(&account.rent_epoch as &u64, rent_epoch) } + }; + + if !operator_result { + break; } } - } - } - - assertion_state.record_result(i, assertion_result)?; - // assertion_results.push(assertion_result); + Ok(operator_result) + } + }; - // if (logical_expression.is_none() - // || !logically_dependent_assertions - // .as_ref() - // .unwrap() - // .contains(&(i as u8))) - // && !assertion_result - // { - // return Err(ProgramError::AssertionFailed.into()); - // } + assertion_state.record_result(i, assertion_result?)?; } msg!("assertion_state: {:?}", assertion_state); assertion_state.evaluate()?; - // 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 ") - // ); - // } - - // if !result { - // return Err(ProgramError::AssertionFailed.into()); - // } - // } - // 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(()) } 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/mod.rs b/programs/lighthouse/program/src/structs/assert/mod.rs index e8849ed..cb9b439 100644 --- a/programs/lighthouse/program/src/structs/assert/mod.rs +++ b/programs/lighthouse/program/src/structs/assert/mod.rs @@ -1,3 +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 bf10eba..0000000 --- a/programs/lighthouse/program/src/structs/assertion.rs +++ /dev/null @@ -1,26 +0,0 @@ -use anchor_lang::prelude::{ - borsh, - borsh::{BorshDeserialize, BorshSerialize}, -}; -use solana_program::pubkey::Pubkey; - -use super::{AccountInfoDataField, DataValue, Operator}; - -#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)] -pub enum Assertion { - // memory offset, assertion - Memory(u16, Operator, DataValue), - - // account data offset, borsh type, operator - AccountData(u16, Operator, DataValue), - - // balance, operator - AccountBalance(u64, Operator), - - AccountOwnedBy(Pubkey, Operator), - - // token balance, operator - TokenAccountBalance(u64, Operator), - - AccountInfo(Vec, Operator), -} diff --git a/programs/lighthouse/program/src/structs/data_value.rs b/programs/lighthouse/program/src/structs/data_value.rs index ab0a61b..108ee11 100644 --- a/programs/lighthouse/program/src/structs/data_value.rs +++ b/programs/lighthouse/program/src/structs/data_value.rs @@ -44,6 +44,34 @@ pub enum DataValue { 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 { @@ -144,7 +172,7 @@ impl DataValue { } pub fn deserialize_and_compare( - self, + &self, data: Ref<'_, &mut [u8]>, offset: usize, operator: &Operator, @@ -161,7 +189,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::U8(expected_value) => { @@ -172,7 +200,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::I8(expected_value) => { @@ -183,7 +211,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::U16(expected_value) => { @@ -194,7 +222,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::I16(expected_value) => { @@ -205,7 +233,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::U32(expected_value) => { @@ -216,7 +244,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::I32(expected_value) => { @@ -227,7 +255,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::U64(expected_value) => { @@ -238,7 +266,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::I64(expected_value) => { @@ -249,7 +277,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::U128(expected_value) => { @@ -260,7 +288,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::I128(expected_value) => { @@ -271,7 +299,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } DataValue::Bytes(expected_value) => { @@ -297,7 +325,7 @@ impl DataValue { .map(|byte| format!("{:02x}", byte)) .collect::>() .join(""); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } @@ -315,7 +343,7 @@ impl DataValue { let value_str = value.to_string(); let expected_value_str = expected_value.to_string(); - let assertion_result = operator.evaluate(&value, &expected_value); + let assertion_result = operator.evaluate(&value, expected_value); Ok((value_str, expected_value_str, assertion_result)) } diff --git a/programs/lighthouse/program/src/structs/mod.rs b/programs/lighthouse/program/src/structs/mod.rs index ba3e506..1cec384 100644 --- a/programs/lighthouse/program/src/structs/mod.rs +++ b/programs/lighthouse/program/src/structs/mod.rs @@ -1,5 +1,4 @@ pub mod assert; -pub mod assertion; pub mod borsh_field; pub mod data_value; pub mod expression; @@ -8,7 +7,6 @@ pub mod write; pub mod write_type; pub use assert::*; -pub use assertion::*; pub use borsh_field::*; pub use data_value::*; pub use expression::*; diff --git a/programs/lighthouse/program/src/structs/operator.rs b/programs/lighthouse/program/src/structs/operator.rs index 9f39806..f31de75 100644 --- a/programs/lighthouse/program/src/structs/operator.rs +++ b/programs/lighthouse/program/src/structs/operator.rs @@ -11,8 +11,6 @@ pub enum Operator { LessThan, GreaterThanOrEqual, LessThanOrEqual, - // Todo - // WithinThreshold } impl Operator { diff --git a/programs/lighthouse/program/src/structs/write/program.rs b/programs/lighthouse/program/src/structs/write/program.rs index c4a9361..b3c1c2a 100644 --- a/programs/lighthouse/program/src/structs/write/program.rs +++ b/programs/lighthouse/program/src/structs/write/program.rs @@ -1,9 +1,6 @@ -use anchor_lang::{ - prelude::{ - borsh, - borsh::{BorshDeserialize, BorshSerialize}, - }, - Id, +use anchor_lang::prelude::{ + borsh, + borsh::{BorshDeserialize, BorshSerialize}, }; use solana_program::pubkey::Pubkey; diff --git a/programs/lighthouse/program/src/structs/write_type.rs b/programs/lighthouse/program/src/structs/write_type.rs index 15ff18f..0f3e67a 100644 --- a/programs/lighthouse/program/src/structs/write_type.rs +++ b/programs/lighthouse/program/src/structs/write_type.rs @@ -1,4 +1,4 @@ -use anchor_lang::{accounts::account, prelude::*}; +use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use super::DataValue; diff --git a/programs/lighthouse/program/src/utils.rs b/programs/lighthouse/program/src/utils.rs index e2e645b..667b091 100644 --- a/programs/lighthouse/program/src/utils.rs +++ b/programs/lighthouse/program/src/utils.rs @@ -1,22 +1,32 @@ -use crate::structs::Operator; +use crate::{processor::assert::AssertionConfig, structs::Operator}; use solana_program::msg; -pub fn print_result( +pub fn print_assertion_result( + config: &Option, + assertion_info: String, assertion_result: bool, assertion_index: usize, - operator: Operator, + operator: &Operator, value_str: String, expected_value_str: String, ) { + if let Some(config) = config { + if !config.verbose { + return; + } + } else { + return; + } + msg!( - "{} {} Assertion::Memory ({}) -> {} {} {}", + "{} {} {} -> {} {} {}", format!("[{:?}]", assertion_index), if assertion_result { "[✅] SUCCESS" } else { "[❌] FAIL " }, - "Cache...".to_string(), + assertion_info, value_str, operator.format(), expected_value_str, diff --git a/programs/lighthouse/program/tests/suites/assert/mod.rs b/programs/lighthouse/program/tests/suites/assert/mod.rs index c8b4296..4eec92d 100644 --- a/programs/lighthouse/program/tests/suites/assert/mod.rs +++ b/programs/lighthouse/program/tests/suites/assert/mod.rs @@ -1,3 +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/utils/program.rs b/programs/lighthouse/program/tests/utils/program.rs index 7566bc2..80b2f17 100644 --- a/programs/lighthouse/program/tests/utils/program.rs +++ b/programs/lighthouse/program/tests/utils/program.rs @@ -9,15 +9,17 @@ use super::{ Error, Result, }; use anchor_lang::*; +use anchor_spl::associated_token; use lighthouse::{ - processor::Config, + 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,6 +27,7 @@ use solana_sdk::{ signer::signers::Signers, transaction::Transaction, }; +use spl_token::state::Mint; pub struct Program { client: BanksClient, @@ -107,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( @@ -120,7 +123,7 @@ impl Program { data: (lighthouse::instruction::AssertV1 { assertions: assertion_clone, logical_expression: logical_expression_clone, - options: Some(Config { verbose: true }), + options: Some(AssertionConfig { verbose: true }), }) .data(), }], @@ -277,3 +280,92 @@ pub async fn create_user(ctx: &mut TestContext) -> Result { 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/utils.rs b/programs/lighthouse/program/tests/utils/utils.rs index 8872d5c..bdbe0ee 100644 --- a/programs/lighthouse/program/tests/utils/utils.rs +++ b/programs/lighthouse/program/tests/utils/utils.rs @@ -25,7 +25,17 @@ pub async fn process_transaction_assert_success( ) { let tx = tx.expect("Should have been processed"); - let tx_metadata = process_transaction(context, &tx).await.unwrap(); + 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 {