From 1b9ff06fc7146d1c54a9d3c350350478c6732696 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 17 Mar 2023 16:53:58 -0400 Subject: [PATCH 01/40] add permissionless event cpi api --- lang/attribute/event/src/lib.rs | 10 ++++++++++ lang/src/event.rs | 3 +++ lang/src/lib.rs | 25 ++++++++++++++++++++++-- lang/syn/src/codegen/event.rs | 1 + lang/syn/src/codegen/program/dispatch.rs | 12 ++++++++++++ lang/syn/src/codegen/program/handlers.rs | 15 ++++++++++++++ 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 lang/src/event.rs create mode 100644 lang/syn/src/codegen/event.rs diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 33933bdfda..a0e7e2c404 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -81,6 +81,16 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } +#[proc_macro] +pub fn _emit_cpi_data(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let data: proc_macro2::TokenStream = input.into(); + proc_macro::TokenStream::from(quote! { + let __disc: Vec = crate::event::EVENT_IX_TAG_LE.to_vec(); + let __inner_data: &Vec = &anchor_lang::Event::data(&#data); + let __ix_data: Vec = __disc.iter().chain(__inner_data.iter()).cloned().collect(); + }) +} + // EventIndex is a marker macro. It functionally does nothing other than // allow one to mark fields with the `#[index]` inert attribute, which is // used to add metadata to IDLs. diff --git a/lang/src/event.rs b/lang/src/event.rs new file mode 100644 index 0000000000..99f2abde12 --- /dev/null +++ b/lang/src/event.rs @@ -0,0 +1,3 @@ +// Sha256(anchor:event)[..8] +pub const EVENT_IX_TAG: u64 = 0x1d9acb512ea545e4; +pub const EVENT_IX_TAG_LE: [u8; 8] = EVENT_IX_TAG.to_le_bytes(); diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 0317ea5cdc..9a21e1507c 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -25,7 +25,7 @@ extern crate self as anchor_lang; use bytemuck::{Pod, Zeroable}; use solana_program::account_info::AccountInfo; -use solana_program::instruction::AccountMeta; +use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::pubkey::Pubkey; use std::collections::{BTreeMap, BTreeSet}; use std::io::Write; @@ -37,6 +37,7 @@ mod bpf_writer; mod common; pub mod context; pub mod error; +pub mod event; #[doc(hidden)] pub mod idl; pub mod system_program; @@ -47,7 +48,7 @@ pub use anchor_attribute_access_control::access_control; pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; -pub use anchor_attribute_event::{emit, event}; +pub use anchor_attribute_event::{_emit_cpi_data, emit, event}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; pub use anchor_derive_space::InitSpace; @@ -193,6 +194,26 @@ pub trait Event: AnchorSerialize + AnchorDeserialize + Discriminator { fn data(&self) -> Vec; } +/// `emit!()` for an event that is emitted through a CPI +#[macro_export] +macro_rules! emit_cpi { + ($event:expr, $program_info:expr) => {{ + let program_info: &AccountInfo = $program_info; + _emit_cpi_data!($event); + crate::_emit_cpi_invoke(__ix_data, program_info)?; + }}; +} + +pub fn _emit_cpi_invoke(ix_data: Vec, program: &AccountInfo) -> Result<()> { + let ix: Instruction = Instruction::new_with_bytes( + program.key.clone(), + ix_data.as_ref(), + vec![AccountMeta::new_readonly(*program.key, false)], + ); + solana_program::program::invoke(&ix, &[program.clone()])?; + Ok(()) +} + // The serialized event data to be emitted via a Solana log. // TODO: remove this on the next major version upgrade. #[doc(hidden)] diff --git a/lang/syn/src/codegen/event.rs b/lang/syn/src/codegen/event.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/lang/syn/src/codegen/event.rs @@ -0,0 +1 @@ + diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index dac9015082..8929663c60 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -78,6 +78,18 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into()) } } + anchor_lang::event::EVENT_IX_TAG_LE => { + // If the method identifier is the event tag, then execute an event cpi + if cfg!(not(feature = "no-cpi-event")) { + __private::__events::__event_dispatch( + program_id, + accounts, + &ix_data, + ) + } else { + Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into()) + } + } _ => { #fallback_fn } diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index 48f0dc2495..f80ed8bfff 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -91,6 +91,16 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } }; + let non_inlined_event: proc_macro2::TokenStream = { + quote! { + #[inline(never)] + #[cfg(not(feature = "no-cpi-events"))] + pub fn __event_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], event_data: &[u8]) -> anchor_lang::Result<()> { + Ok(()) + } + } + }; + let non_inlined_handlers: Vec = program .ixs .iter() @@ -173,7 +183,12 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #idl_accounts_and_functions } + /// __idl mod defines handler for self-cpi based event logging + pub mod __events { + use super::*; + #non_inlined_event + } /// __global mod defines wrapped handlers for global instructions. pub mod __global { From eed5e6f9d8c12fcb38b2278316ffebe1ab85ac24 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 17 Mar 2023 16:54:21 -0400 Subject: [PATCH 02/40] add cpi event test --- tests/events/programs/events/src/lib.rs | 18 +++++++++++++++ tests/events/tests/events.js | 29 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index ef17b7b4e3..bcd3a8111a 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -2,6 +2,7 @@ //! subscribed to by a client. use anchor_lang::prelude::*; +use anchor_lang::{_emit_cpi_data, _emit_cpi_invoke, emit_cpi}; declare_id!("2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy"); @@ -23,6 +24,17 @@ pub mod events { }); Ok(()) } + + pub fn test_event_cpi(ctx: Context) -> Result<()> { + emit_cpi!( + MyOtherEvent { + data: 7, + label: "cpi".to_string(), + }, + &ctx.accounts.program.to_account_info() + ); + Ok(()) + } } #[derive(Accounts)] @@ -31,6 +43,12 @@ pub struct Initialize {} #[derive(Accounts)] pub struct TestEvent {} +#[derive(Accounts)] +pub struct TestEventCpi<'info> { + /// CHECK: this is the program itself + program: AccountInfo<'info>, +} + #[event] pub struct MyEvent { pub data: u64, diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index b938e0555c..082ec7ef84 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -1,4 +1,5 @@ const anchor = require("@coral-xyz/anchor"); +const { bs58, base64 } = require("@coral-xyz/anchor/dist/cjs/utils/bytes"); const { assert } = require("chai"); describe("events", () => { @@ -54,6 +55,34 @@ describe("events", () => { assert.strictEqual(eventTwo.data.toNumber(), 6); assert.strictEqual(eventTwo.label, "bye"); }); + + it("Self-CPI events work", async () => { + await sleep(200); + + let sendTx = await program.transaction.testEventCpi({ + accounts: { + program: program.programId, + }, + }); + + let provider = anchor.getProvider(); + let connection = provider.connection; + let txid = await provider.sendAndConfirm(sendTx, [], { + commitment: "confirmed", + }); + + let tx = await connection.getTransaction(txid, { commitment: "confirmed" }); + + let cpiEventData = tx.meta.innerInstructions[0].instructions[0].data; + let ixData = bs58.decode(cpiEventData); + let eventData = ixData.slice(8); + + let coder = new anchor.BorshEventCoder(program.idl); + let event = coder.decode(base64.encode(eventData)).data; + + assert.strictEqual(event.data.toNumber(), 7); + assert.strictEqual(event.label, "cpi"); + }); }); function sleep(ms) { From 6c9fced4d7b2566cdfdbe4cd1579e74fc9f5b650 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Mon, 20 Mar 2023 10:30:13 -0400 Subject: [PATCH 03/40] move __emit_cpi_invoke to __private in lib.rs --- lang/src/lib.rs | 27 ++++++++++++++----------- tests/events/programs/events/src/lib.rs | 2 +- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 9a21e1507c..85dc2179f2 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -25,7 +25,7 @@ extern crate self as anchor_lang; use bytemuck::{Pod, Zeroable}; use solana_program::account_info::AccountInfo; -use solana_program::instruction::{AccountMeta, Instruction}; +use solana_program::instruction::AccountMeta; use solana_program::pubkey::Pubkey; use std::collections::{BTreeMap, BTreeSet}; use std::io::Write; @@ -200,20 +200,10 @@ macro_rules! emit_cpi { ($event:expr, $program_info:expr) => {{ let program_info: &AccountInfo = $program_info; _emit_cpi_data!($event); - crate::_emit_cpi_invoke(__ix_data, program_info)?; + anchor_lang::__private::_emit_cpi_invoke(__ix_data, program_info)?; }}; } -pub fn _emit_cpi_invoke(ix_data: Vec, program: &AccountInfo) -> Result<()> { - let ix: Instruction = Instruction::new_with_bytes( - program.key.clone(), - ix_data.as_ref(), - vec![AccountMeta::new_readonly(*program.key, false)], - ); - solana_program::program::invoke(&ix, &[program.clone()])?; - Ok(()) -} - // The serialized event data to be emitted via a Solana log. // TODO: remove this on the next major version upgrade. #[doc(hidden)] @@ -354,6 +344,8 @@ pub mod __private { pub use bytemuck; + use solana_program::account_info::AccountInfo; + use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::pubkey::Pubkey; // Used to calculate the maximum between two expressions. @@ -379,6 +371,17 @@ pub mod __private { input.to_bytes() } } + + #[doc(hidden)] + pub fn _emit_cpi_invoke(ix_data: Vec, program: &AccountInfo) -> anchor_lang::Result<()> { + let ix: Instruction = Instruction::new_with_bytes( + program.key.clone(), + ix_data.as_ref(), + vec![AccountMeta::new_readonly(*program.key, false)], + ); + solana_program::program::invoke(&ix, &[program.clone()])?; + Ok(()) + } } /// Ensures a condition is true, otherwise returns with the given error. diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index bcd3a8111a..74a5ff57e8 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -2,7 +2,7 @@ //! subscribed to by a client. use anchor_lang::prelude::*; -use anchor_lang::{_emit_cpi_data, _emit_cpi_invoke, emit_cpi}; +use anchor_lang::{_emit_cpi_data, emit_cpi}; declare_id!("2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy"); From e1d234c26d665efa44e94cadc9e43eedb1816dd3 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Thu, 23 Mar 2023 22:55:04 -0400 Subject: [PATCH 04/40] export emit_cpi and _emit_cpi_data in prelude --- lang/src/lib.rs | 6 +++--- tests/events/programs/events/src/lib.rs | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 85dc2179f2..d412f01535 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -298,13 +298,13 @@ impl Key for Pubkey { /// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { pub use super::{ - access_control, account, accounts::account::Account, + _emit_cpi_data, access_control, account, accounts::account::Account, accounts::account_loader::AccountLoader, accounts::interface::Interface, accounts::interface_account::InterfaceAccount, accounts::program::Program, accounts::signer::Signer, accounts::system_account::SystemAccount, accounts::sysvar::Sysvar, accounts::unchecked_account::UncheckedAccount, constant, - context::Context, context::CpiContext, declare_id, emit, err, error, event, program, - require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, + context::Context, context::CpiContext, declare_id, emit, emit_cpi, err, error, event, + program, require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 74a5ff57e8..76388e5a5f 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -2,7 +2,6 @@ //! subscribed to by a client. use anchor_lang::prelude::*; -use anchor_lang::{_emit_cpi_data, emit_cpi}; declare_id!("2dhGsWUzy5YKUsjZdLHLmkNpUDAXkNa9MYWsPc4Ziqzy"); From f5c5ab9c0fc1d584e3753a323b88daca324cfbde Mon Sep 17 00:00:00 2001 From: ngundotra Date: Thu, 23 Mar 2023 23:00:15 -0400 Subject: [PATCH 05/40] remove empty file --- lang/syn/src/codegen/event.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 lang/syn/src/codegen/event.rs diff --git a/lang/syn/src/codegen/event.rs b/lang/syn/src/codegen/event.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/lang/syn/src/codegen/event.rs +++ /dev/null @@ -1 +0,0 @@ - From bca57d6f69b2a1c9ec8978e5c7596a33a852ef66 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 24 Mar 2023 10:34:20 -0400 Subject: [PATCH 06/40] rewrite emit_cpi as a proc_macro --- lang/attribute/event/src/lib.rs | 31 +++++++++++++++++++++---- lang/src/lib.rs | 14 ++--------- tests/events/programs/events/src/lib.rs | 6 ++--- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index a0e7e2c404..fe60b19573 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -82,15 +82,36 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } #[proc_macro] -pub fn _emit_cpi_data(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let data: proc_macro2::TokenStream = input.into(); +pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // let strct: proc_macro2::TokenStream = input.into(); + let tuple = parse_macro_input!(input as syn::ExprTuple); + + let elems = tuple.elems; + if elems.len() != 2 { + panic!("Expected a tuple with exactly two elements."); + } + + let first = &elems[0]; + let second = &elems[1]; + proc_macro::TokenStream::from(quote! { - let __disc: Vec = crate::event::EVENT_IX_TAG_LE.to_vec(); - let __inner_data: &Vec = &anchor_lang::Event::data(&#data); - let __ix_data: Vec = __disc.iter().chain(__inner_data.iter()).cloned().collect(); + let program_info: &anchor_lang::solana_program::account_info::AccountInfo = &#first; + + let __disc: Vec = crate::event::EVENT_IX_TAG_LE.to_vec(); + let __inner_data: &Vec = &anchor_lang::Event::data(&#second); + let __ix_data: Vec = __disc.iter().chain(__inner_data.iter()).cloned().collect(); + + anchor_lang::__private::_emit_cpi_invoke(__ix_data, program_info)?; }) } +// #[proc_macro] +// pub fn _emit_cpi_data(input: proc_macro::TokenStream) -> proc_macro::TokenStream { +// let data: proc_macro2::TokenStream = input.into(); +// proc_macro::TokenStream::from(quote! { +// }) +// } + // EventIndex is a marker macro. It functionally does nothing other than // allow one to mark fields with the `#[index]` inert attribute, which is // used to add metadata to IDLs. diff --git a/lang/src/lib.rs b/lang/src/lib.rs index d412f01535..16b3b35884 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -48,7 +48,7 @@ pub use anchor_attribute_access_control::access_control; pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; -pub use anchor_attribute_event::{_emit_cpi_data, emit, event}; +pub use anchor_attribute_event::{emit, emit_cpi, event}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; pub use anchor_derive_space::InitSpace; @@ -194,16 +194,6 @@ pub trait Event: AnchorSerialize + AnchorDeserialize + Discriminator { fn data(&self) -> Vec; } -/// `emit!()` for an event that is emitted through a CPI -#[macro_export] -macro_rules! emit_cpi { - ($event:expr, $program_info:expr) => {{ - let program_info: &AccountInfo = $program_info; - _emit_cpi_data!($event); - anchor_lang::__private::_emit_cpi_invoke(__ix_data, program_info)?; - }}; -} - // The serialized event data to be emitted via a Solana log. // TODO: remove this on the next major version upgrade. #[doc(hidden)] @@ -298,7 +288,7 @@ impl Key for Pubkey { /// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { pub use super::{ - _emit_cpi_data, access_control, account, accounts::account::Account, + access_control, account, accounts::account::Account, accounts::account_loader::AccountLoader, accounts::interface::Interface, accounts::interface_account::InterfaceAccount, accounts::program::Program, accounts::signer::Signer, accounts::system_account::SystemAccount, diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 76388e5a5f..083e4d6840 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -25,13 +25,13 @@ pub mod events { } pub fn test_event_cpi(ctx: Context) -> Result<()> { - emit_cpi!( + emit_cpi!(( + &ctx.accounts.program.to_account_info(), MyOtherEvent { data: 7, label: "cpi".to_string(), }, - &ctx.accounts.program.to_account_info() - ); + )); Ok(()) } } From ffd6fb14639ecf43b84faa3ba12da85a9bc88040 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Thu, 30 Mar 2023 13:28:39 -0400 Subject: [PATCH 07/40] remove unused code --- lang/attribute/event/src/lib.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index fe60b19573..a73eea9425 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -83,7 +83,6 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - // let strct: proc_macro2::TokenStream = input.into(); let tuple = parse_macro_input!(input as syn::ExprTuple); let elems = tuple.elems; @@ -105,13 +104,6 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } -// #[proc_macro] -// pub fn _emit_cpi_data(input: proc_macro::TokenStream) -> proc_macro::TokenStream { -// let data: proc_macro2::TokenStream = input.into(); -// proc_macro::TokenStream::from(quote! { -// }) -// } - // EventIndex is a marker macro. It functionally does nothing other than // allow one to mark fields with the `#[index]` inert attribute, which is // used to add metadata to IDLs. From aadba4eaecf2056775b094c7a6f8ff7e42cc4a6e Mon Sep 17 00:00:00 2001 From: ngundotra Date: Thu, 30 Mar 2023 14:06:41 -0400 Subject: [PATCH 08/40] inline _emit_cpi_invoke to proc_macro declaration --- lang/attribute/event/src/lib.rs | 10 ++++++++-- lang/src/lib.rs | 13 ------------- tests/events/package.json | 3 +++ tests/yarn.lock | 20 ++++++++++---------- 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index a73eea9425..49e8130157 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -98,9 +98,15 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let __disc: Vec = crate::event::EVENT_IX_TAG_LE.to_vec(); let __inner_data: &Vec = &anchor_lang::Event::data(&#second); - let __ix_data: Vec = __disc.iter().chain(__inner_data.iter()).cloned().collect(); + let ix_data: Vec = __disc.iter().chain(__inner_data.iter()).cloned().collect(); - anchor_lang::__private::_emit_cpi_invoke(__ix_data, program_info)?; + let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( + program_info.key.clone(), + ix_data.as_ref(), + vec![anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*program_info.key, false)], + ); + anchor_lang::solana_program::program::invoke(&ix, &[program_info.clone()]) + .map_err(|e: anchor_lang::solana_program::program_error::ProgramError| crate::Error::from(e))?; }) } diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 16b3b35884..310b4f975d 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -334,8 +334,6 @@ pub mod __private { pub use bytemuck; - use solana_program::account_info::AccountInfo; - use solana_program::instruction::{AccountMeta, Instruction}; use solana_program::pubkey::Pubkey; // Used to calculate the maximum between two expressions. @@ -361,17 +359,6 @@ pub mod __private { input.to_bytes() } } - - #[doc(hidden)] - pub fn _emit_cpi_invoke(ix_data: Vec, program: &AccountInfo) -> anchor_lang::Result<()> { - let ix: Instruction = Instruction::new_with_bytes( - program.key.clone(), - ix_data.as_ref(), - vec![AccountMeta::new_readonly(*program.key, false)], - ); - solana_program::program::invoke(&ix, &[program.clone()])?; - Ok(()) - } } /// Ensures a condition is true, otherwise returns with the given error. diff --git a/tests/events/package.json b/tests/events/package.json index e2a35186c0..0f0d6bbfb5 100644 --- a/tests/events/package.json +++ b/tests/events/package.json @@ -15,5 +15,8 @@ }, "scripts": { "test": "anchor test" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.27.0" } } diff --git a/tests/yarn.lock b/tests/yarn.lock index f0a8cc1209..3efd936dd6 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -16,12 +16,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@coral-xyz/anchor@=0.27.0": - version "0.26.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.26.0.tgz#c8e4f7177e93441afd030f22d777d54d0194d7d1" - integrity sha512-PxRl+wu5YyptWiR9F2MBHOLLibm87Z4IMUBPreX+DYBtPM+xggvcPi0KAN7+kIL4IrIhXI8ma5V0MCXxSN1pHg== +"@coral-xyz/anchor@=0.27.0", "@coral-xyz/anchor@file:../ts/packages/anchor": + version "0.27.0" dependencies: - "@coral-xyz/borsh" "^0.26.0" + "@coral-xyz/borsh" "^0.27.0" "@solana/web3.js" "^1.68.0" base64-js "^1.5.1" bn.js "^5.1.2" @@ -37,8 +35,10 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/anchor@file:../ts/packages/anchor": +"@coral-xyz/anchor@^0.27.0": version "0.27.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c" + integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g== dependencies: "@coral-xyz/borsh" "^0.27.0" "@solana/web3.js" "^1.68.0" @@ -56,10 +56,10 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/borsh@^0.26.0", "@coral-xyz/borsh@^0.27.0": - version "0.26.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.26.0.tgz#d054f64536d824634969e74138f9f7c52bbbc0d5" - integrity sha512-uCZ0xus0CszQPHYfWAqKS5swS1UxvePu83oOF+TWpUkedsNlg6p2p4azxZNSSqwXb9uXMFgxhuMBX9r3Xoi0vQ== +"@coral-xyz/borsh@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63" + integrity sha512-tJKzhLukghTWPLy+n8K8iJKgBq1yLT/AxaNd10yJrX8mI56ao5+OFAKAqW/h0i79KCvb4BK0VGO5ECmmolFz9A== dependencies: bn.js "^5.1.2" buffer-layout "^1.2.0" From bbd869be9ef8d6cbedcda4c0cb7025a27b87958f Mon Sep 17 00:00:00 2001 From: ngundotra Date: Tue, 2 May 2023 22:50:21 -0400 Subject: [PATCH 09/40] address acheron feedback --- lang/attribute/event/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 49e8130157..1cae4d1678 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -96,9 +96,9 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { proc_macro::TokenStream::from(quote! { let program_info: &anchor_lang::solana_program::account_info::AccountInfo = &#first; - let __disc: Vec = crate::event::EVENT_IX_TAG_LE.to_vec(); - let __inner_data: &Vec = &anchor_lang::Event::data(&#second); - let ix_data: Vec = __disc.iter().chain(__inner_data.iter()).cloned().collect(); + let __disc = crate::event::EVENT_IX_TAG_LE; + let __inner_data: Vec = anchor_lang::Event::data(&#second); + let ix_data: Vec = __disc.into_iter().chain(__inner_data.into_iter()).collect(); let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( program_info.key.clone(), From 7bc2c258a6c52276a93eb7461484683fc9a10e12 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Wed, 3 May 2023 12:02:57 -0400 Subject: [PATCH 10/40] optimize emit macro to reduce cloning --- lang/attribute/event/src/lib.rs | 14 +++++++------- tests/events/programs/events/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 1cae4d1678..68f7e8c46c 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -94,19 +94,19 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let second = &elems[1]; proc_macro::TokenStream::from(quote! { - let program_info: &anchor_lang::solana_program::account_info::AccountInfo = &#first; + let program_info: anchor_lang::solana_program::account_info::AccountInfo = #first; let __disc = crate::event::EVENT_IX_TAG_LE; let __inner_data: Vec = anchor_lang::Event::data(&#second); - let ix_data: Vec = __disc.into_iter().chain(__inner_data.into_iter()).collect(); + let __ix_data: Vec = __disc.into_iter().chain(__inner_data.into_iter()).collect(); - let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( - program_info.key.clone(), - ix_data.as_ref(), + let __ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( + *program_info.key, + __ix_data.as_ref(), vec![anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*program_info.key, false)], ); - anchor_lang::solana_program::program::invoke(&ix, &[program_info.clone()]) - .map_err(|e: anchor_lang::solana_program::program_error::ProgramError| crate::Error::from(e))?; + anchor_lang::solana_program::program::invoke(&__ix, &[program_info]) + .map_err(anchor_lang::error::Error::from)?; }) } diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 083e4d6840..1dac02b881 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -26,7 +26,7 @@ pub mod events { pub fn test_event_cpi(ctx: Context) -> Result<()> { emit_cpi!(( - &ctx.accounts.program.to_account_info(), + ctx.accounts.program.to_account_info(), MyOtherEvent { data: 7, label: "cpi".to_string(), From 3d04a717d5373feb4e35529b12a131665161031c Mon Sep 17 00:00:00 2001 From: ngundotra Date: Wed, 3 May 2023 12:17:53 -0400 Subject: [PATCH 11/40] explicitly only parse two args --- lang/attribute/event/src/lib.rs | 41 ++++++++++++++++++------- tests/events/programs/events/src/lib.rs | 6 ++-- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 68f7e8c46c..558a14a5d8 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -1,7 +1,10 @@ extern crate proc_macro; use quote::quote; -use syn::parse_macro_input; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, Expr, Token, +}; /// The event attribute allows a struct to be used with /// [emit!](./macro.emit.html) so that programs can log significant events in @@ -81,23 +84,39 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } -#[proc_macro] -pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let tuple = parse_macro_input!(input as syn::ExprTuple); +// Custom wrapper struct (thanks ChatGPT!) +struct TwoArgs { + arg1: Expr, + arg2: Expr, +} + +// Implement the `Parse` trait for the `TwoArgs` struct +impl Parse for TwoArgs { + fn parse(input: ParseStream) -> syn::Result { + let arg1: Expr = input.parse()?; + input.parse::()?; + let arg2: Expr = input.parse()?; + + if !input.is_empty() { + return Err(input.error("Expected exactly 2 arguments")); + } - let elems = tuple.elems; - if elems.len() != 2 { - panic!("Expected a tuple with exactly two elements."); + Ok(TwoArgs { arg1, arg2 }) } +} + +#[proc_macro] +pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let two_args = parse_macro_input!(input as TwoArgs); - let first = &elems[0]; - let second = &elems[1]; + let self_program_info = &two_args.arg1; + let event_struct = &two_args.arg2; proc_macro::TokenStream::from(quote! { - let program_info: anchor_lang::solana_program::account_info::AccountInfo = #first; + let program_info: anchor_lang::solana_program::account_info::AccountInfo = #self_program_info; let __disc = crate::event::EVENT_IX_TAG_LE; - let __inner_data: Vec = anchor_lang::Event::data(&#second); + let __inner_data: Vec = anchor_lang::Event::data(&#event_struct); let __ix_data: Vec = __disc.into_iter().chain(__inner_data.into_iter()).collect(); let __ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 1dac02b881..7d0fffc777 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -25,13 +25,13 @@ pub mod events { } pub fn test_event_cpi(ctx: Context) -> Result<()> { - emit_cpi!(( + emit_cpi!( ctx.accounts.program.to_account_info(), MyOtherEvent { data: 7, label: "cpi".to_string(), - }, - )); + } + ); Ok(()) } } From 014256f7c3879eb201d22a0351c009df9b758391 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Wed, 3 May 2023 12:23:36 -0400 Subject: [PATCH 12/40] update events package.json --- tests/events/package.json | 3 --- tests/yarn.lock | 21 --------------------- 2 files changed, 24 deletions(-) diff --git a/tests/events/package.json b/tests/events/package.json index 0f0d6bbfb5..e2a35186c0 100644 --- a/tests/events/package.json +++ b/tests/events/package.json @@ -15,8 +15,5 @@ }, "scripts": { "test": "anchor test" - }, - "dependencies": { - "@coral-xyz/anchor": "^0.27.0" } } diff --git a/tests/yarn.lock b/tests/yarn.lock index 3efd936dd6..437dd856ee 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -35,27 +35,6 @@ superstruct "^0.15.4" toml "^3.0.0" -"@coral-xyz/anchor@^0.27.0": - version "0.27.0" - resolved "https://registry.yarnpkg.com/@coral-xyz/anchor/-/anchor-0.27.0.tgz#621e5ef123d05811b97e49973b4ed7ede27c705c" - integrity sha512-+P/vPdORawvg3A9Wj02iquxb4T0C5m4P6aZBVYysKl4Amk+r6aMPZkUhilBkD6E4Nuxnoajv3CFykUfkGE0n5g== - dependencies: - "@coral-xyz/borsh" "^0.27.0" - "@solana/web3.js" "^1.68.0" - base64-js "^1.5.1" - bn.js "^5.1.2" - bs58 "^4.0.1" - buffer-layout "^1.2.2" - camelcase "^6.3.0" - cross-fetch "^3.1.5" - crypto-hash "^1.3.0" - eventemitter3 "^4.0.7" - js-sha256 "^0.9.0" - pako "^2.0.3" - snake-case "^3.0.4" - superstruct "^0.15.4" - toml "^3.0.0" - "@coral-xyz/borsh@^0.27.0": version "0.27.0" resolved "https://registry.yarnpkg.com/@coral-xyz/borsh/-/borsh-0.27.0.tgz#700c647ea5262b1488957ac7fb4e8acf72c72b63" From 76ee7be21f37e5baaec36f9b716446c23048b0e7 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Thu, 4 May 2023 09:20:39 -0400 Subject: [PATCH 13/40] add event instruction error code to anchor --- lang/src/error.rs | 5 +++++ lang/syn/src/codegen/program/dispatch.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lang/src/error.rs b/lang/src/error.rs index 33d382cf5c..274662b28e 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -44,6 +44,11 @@ pub enum ErrorCode { #[msg("IDL account must be empty in order to resize, try closing first")] IdlAccountNotEmpty, + // Event instructions + /// 1500 - The program was compiled without event instructions + #[msg("The program was compiled without event instructions")] + EventInstructionStub = 1500, + // Constraints /// 2000 - A mut constraint was violated #[msg("A mut constraint was violated")] diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index 8929663c60..ceee5dcb03 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -87,7 +87,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &ix_data, ) } else { - Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into()) + Err(anchor_lang::error::ErrorCode::EventInstructionStub.into()) } } _ => { From f4c225cfebba912c4f69476728cefd15cb0bb5f3 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Thu, 4 May 2023 17:18:49 -0400 Subject: [PATCH 14/40] add event authority --- lang/attribute/event/src/lib.rs | 30 ++++++++++++++++--------- tests/events/programs/events/src/lib.rs | 4 ++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 558a14a5d8..4b853bb1cc 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -85,46 +85,54 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } // Custom wrapper struct (thanks ChatGPT!) -struct TwoArgs { +struct ThreeArgs { arg1: Expr, arg2: Expr, + arg3: Expr, } // Implement the `Parse` trait for the `TwoArgs` struct -impl Parse for TwoArgs { +impl Parse for ThreeArgs { fn parse(input: ParseStream) -> syn::Result { let arg1: Expr = input.parse()?; input.parse::()?; let arg2: Expr = input.parse()?; + input.parse::()?; + let arg3: Expr = input.parse()?; if !input.is_empty() { - return Err(input.error("Expected exactly 2 arguments")); + return Err(input.error("Expected exactly 3 arguments")); } - Ok(TwoArgs { arg1, arg2 }) + Ok(ThreeArgs { arg1, arg2, arg3 }) } } #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let two_args = parse_macro_input!(input as TwoArgs); + let args = parse_macro_input!(input as ThreeArgs); - let self_program_info = &two_args.arg1; - let event_struct = &two_args.arg2; + let self_program_info = &args.arg1; + let event_authority_info = &args.arg2; + let event_struct = &args.arg3; proc_macro::TokenStream::from(quote! { - let program_info: anchor_lang::solana_program::account_info::AccountInfo = #self_program_info; + let __program_info: anchor_lang::solana_program::account_info::AccountInfo = #self_program_info; + let __event_authority_info: anchor_lang::solana_program::account_info::AccountInfo = #event_authority_info; let __disc = crate::event::EVENT_IX_TAG_LE; let __inner_data: Vec = anchor_lang::Event::data(&#event_struct); let __ix_data: Vec = __disc.into_iter().chain(__inner_data.into_iter()).collect(); let __ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( - *program_info.key, + *__program_info.key, __ix_data.as_ref(), - vec![anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*program_info.key, false)], + vec![ + anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*__program_info.key, false), + anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*__event_authority_info.key, true) + ] ); - anchor_lang::solana_program::program::invoke(&__ix, &[program_info]) + anchor_lang::solana_program::program::invoke_signed(&__ix, &[__program_info], &[&[b"__event_authority"]]) .map_err(anchor_lang::error::Error::from)?; }) } diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 7d0fffc777..9da36755f6 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -27,6 +27,7 @@ pub mod events { pub fn test_event_cpi(ctx: Context) -> Result<()> { emit_cpi!( ctx.accounts.program.to_account_info(), + ctx.accounts.event_authority.to_account_info(), MyOtherEvent { data: 7, label: "cpi".to_string(), @@ -46,6 +47,9 @@ pub struct TestEvent {} pub struct TestEventCpi<'info> { /// CHECK: this is the program itself program: AccountInfo<'info>, + /// CHECK: this is the global event authority + #[account(seeds=[b"__event_authority"], bump)] + event_authority: AccountInfo<'info>, } #[event] From 25bc04020e793e73bb7c4605a84bfb456cf54ca5 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 11:54:11 -0400 Subject: [PATCH 15/40] require event authority PDA to sign --- lang/attribute/event/src/lib.rs | 39 +++++++++++++++---------- tests/events/programs/events/src/lib.rs | 1 + tests/events/tests/events.js | 5 ++++ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 4b853bb1cc..afd97168ad 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -85,40 +85,50 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { } // Custom wrapper struct (thanks ChatGPT!) -struct ThreeArgs { - arg1: Expr, - arg2: Expr, - arg3: Expr, +struct EmitCpiArgs { + self_program_info: Expr, + event_authority_info: Expr, + event_authority_bump: Expr, + event_struct: Expr, } // Implement the `Parse` trait for the `TwoArgs` struct -impl Parse for ThreeArgs { +impl Parse for EmitCpiArgs { fn parse(input: ParseStream) -> syn::Result { - let arg1: Expr = input.parse()?; + let self_program_info: Expr = input.parse()?; input.parse::()?; - let arg2: Expr = input.parse()?; + let event_authority_info: Expr = input.parse()?; input.parse::()?; - let arg3: Expr = input.parse()?; + let event_authority_bump: Expr = input.parse()?; + input.parse::()?; + let event_struct: Expr = input.parse()?; if !input.is_empty() { return Err(input.error("Expected exactly 3 arguments")); } - Ok(ThreeArgs { arg1, arg2, arg3 }) + Ok(EmitCpiArgs { + self_program_info, + event_authority_info, + event_authority_bump, + event_struct, + }) } } #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let args = parse_macro_input!(input as ThreeArgs); + let args = parse_macro_input!(input as EmitCpiArgs); - let self_program_info = &args.arg1; - let event_authority_info = &args.arg2; - let event_struct = &args.arg3; + let self_program_info = &args.self_program_info; + let event_authority_info = &args.event_authority_info; + let event_authority_bump = &args.event_authority_bump; + let event_struct = &args.event_struct; proc_macro::TokenStream::from(quote! { let __program_info: anchor_lang::solana_program::account_info::AccountInfo = #self_program_info; let __event_authority_info: anchor_lang::solana_program::account_info::AccountInfo = #event_authority_info; + let __event_authority_bump: u8 = #event_authority_bump; let __disc = crate::event::EVENT_IX_TAG_LE; let __inner_data: Vec = anchor_lang::Event::data(&#event_struct); @@ -128,11 +138,10 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { *__program_info.key, __ix_data.as_ref(), vec![ - anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*__program_info.key, false), anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*__event_authority_info.key, true) ] ); - anchor_lang::solana_program::program::invoke_signed(&__ix, &[__program_info], &[&[b"__event_authority"]]) + anchor_lang::solana_program::program::invoke_signed(&__ix, &[__program_info, __event_authority_info], &[&[b"__event_authority", &[__event_authority_bump]]]) .map_err(anchor_lang::error::Error::from)?; }) } diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 9da36755f6..71a027be0d 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -28,6 +28,7 @@ pub mod events { emit_cpi!( ctx.accounts.program.to_account_info(), ctx.accounts.event_authority.to_account_info(), + *ctx.bumps.get("event_authority").unwrap(), MyOtherEvent { data: 7, label: "cpi".to_string(), diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index 082ec7ef84..7b58c46451 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -62,6 +62,10 @@ describe("events", () => { let sendTx = await program.transaction.testEventCpi({ accounts: { program: program.programId, + eventAuthority: anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + program.programId + )[0], }, }); @@ -69,6 +73,7 @@ describe("events", () => { let connection = provider.connection; let txid = await provider.sendAndConfirm(sendTx, [], { commitment: "confirmed", + skipPreflight: true, }); let tx = await connection.getTransaction(txid, { commitment: "confirmed" }); From 4786e855d9dc3103b3b573885215de718f9d84b0 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 12:05:18 -0400 Subject: [PATCH 16/40] turn on seeds to hide eventAuthority --- tests/events/Anchor.toml | 3 +++ tests/events/tests/events.js | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/events/Anchor.toml b/tests/events/Anchor.toml index 8a154d1452..848d54eba7 100644 --- a/tests/events/Anchor.toml +++ b/tests/events/Anchor.toml @@ -1,3 +1,6 @@ +[features] +seeds = true + [provider] cluster = "localnet" wallet = "~/.config/solana/id.json" diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index 7b58c46451..72589c8184 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -59,15 +59,12 @@ describe("events", () => { it("Self-CPI events work", async () => { await sleep(200); - let sendTx = await program.transaction.testEventCpi({ - accounts: { + let sendTx = await program.methods + .testEventCpi() + .accounts({ program: program.programId, - eventAuthority: anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("__event_authority")], - program.programId - )[0], - }, - }); + }) + .transaction(); let provider = anchor.getProvider(); let connection = provider.connection; From b072f5984577b3ebc2783c2e941a49ca213b3a4e Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 14:31:07 -0400 Subject: [PATCH 17/40] change feature to cpi-events --- lang/syn/Cargo.toml | 1 + lang/syn/src/codegen/program/dispatch.rs | 2 +- lang/syn/src/codegen/program/handlers.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lang/syn/Cargo.toml b/lang/syn/Cargo.toml index f1e7b85f0e..e222a4e41a 100644 --- a/lang/syn/Cargo.toml +++ b/lang/syn/Cargo.toml @@ -16,6 +16,7 @@ hash = [] default = [] anchor-debug = [] seeds = [] +cpi-events = [] [dependencies] proc-macro2 = { version = "1.0", features=["span-locations"]} diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index ceee5dcb03..1b0cdd69ab 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -80,7 +80,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } anchor_lang::event::EVENT_IX_TAG_LE => { // If the method identifier is the event tag, then execute an event cpi - if cfg!(not(feature = "no-cpi-event")) { + if cfg!(feature = "cpi-events") { __private::__events::__event_dispatch( program_id, accounts, diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index f80ed8bfff..d97a10caef 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -94,7 +94,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let non_inlined_event: proc_macro2::TokenStream = { quote! { #[inline(never)] - #[cfg(not(feature = "no-cpi-events"))] + #[cfg((feature = "cpi-events"))] pub fn __event_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], event_data: &[u8]) -> anchor_lang::Result<()> { Ok(()) } From 0b678cf6c12abcb8c5cf2a100b91165bc9d11911 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 16:50:48 -0400 Subject: [PATCH 18/40] fix no-idl, no-cpi-events, and cpi-events features --- lang/Cargo.toml | 1 + lang/attribute/event/Cargo.toml | 1 + lang/src/lib.rs | 10 +++++++--- lang/syn/src/codegen/program/dispatch.rs | 19 ++++++++++++++----- lang/syn/src/codegen/program/handlers.rs | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lang/Cargo.toml b/lang/Cargo.toml index af8b0529ce..89e2ad6bce 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -13,6 +13,7 @@ allow-missing-optionals = ["anchor-derive-accounts/allow-missing-optionals"] init-if-needed = ["anchor-derive-accounts/init-if-needed"] derive = [] default = [] +cpi-events = ["anchor-attribute-event/cpi-events"] anchor-debug = [ "anchor-attribute-access-control/anchor-debug", "anchor-attribute-account/anchor-debug", diff --git a/lang/attribute/event/Cargo.toml b/lang/attribute/event/Cargo.toml index 59857fe929..f82caeaa92 100644 --- a/lang/attribute/event/Cargo.toml +++ b/lang/attribute/event/Cargo.toml @@ -13,6 +13,7 @@ proc-macro = true [features] anchor-debug = ["anchor-syn/anchor-debug"] +cpi-events = ["anchor-syn/cpi-events"] [dependencies] proc-macro2 = "1.0" diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 310b4f975d..584bbce936 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -48,7 +48,9 @@ pub use anchor_attribute_access_control::access_control; pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; -pub use anchor_attribute_event::{emit, emit_cpi, event}; +#[cfg(not(feature = "no-cpi-events"))] +pub use anchor_attribute_event::emit_cpi; +pub use anchor_attribute_event::{emit, event}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; pub use anchor_derive_space::InitSpace; @@ -287,14 +289,16 @@ impl Key for Pubkey { /// The prelude contains all commonly used components of the crate. /// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { + #[cfg(not(feature = "no-cpi-events"))] + pub use super::emit_cpi; pub use super::{ access_control, account, accounts::account::Account, accounts::account_loader::AccountLoader, accounts::interface::Interface, accounts::interface_account::InterfaceAccount, accounts::program::Program, accounts::signer::Signer, accounts::system_account::SystemAccount, accounts::sysvar::Sysvar, accounts::unchecked_account::UncheckedAccount, constant, - context::Context, context::CpiContext, declare_id, emit, emit_cpi, err, error, event, - program, require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, + context::Context, context::CpiContext, declare_id, emit, err, error, event, program, + require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index 1b0cdd69ab..e2de5d215f 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -67,26 +67,35 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #(#global_dispatch_arms)* anchor_lang::idl::IDL_IX_TAG_LE => { // If the method identifier is the IDL tag, then execute an IDL - // instruction, injected into all Anchor programs. - if cfg!(not(feature = "no-idl")) { + // instruction, injected into all Anchor programs unless they have + // no-idl enabled + #[cfg!(not(feature = "no-idl"))] + { __private::__idl::__idl_dispatch( program_id, accounts, &ix_data, ) - } else { + } + #[cfg!(feature = "no-idl")] + { Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into()) } } anchor_lang::event::EVENT_IX_TAG_LE => { // If the method identifier is the event tag, then execute an event cpi - if cfg!(feature = "cpi-events") { + // against the noop instruction injected into all Anchor programs unless they have + // no-cpi-events enabled. + #[cfg(not(feature = "no-cpi-events"))] + { __private::__events::__event_dispatch( program_id, accounts, &ix_data, ) - } else { + } + #[cfg(feature = "no-cpi-events")] + { Err(anchor_lang::error::ErrorCode::EventInstructionStub.into()) } } diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index d97a10caef..f80ed8bfff 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -94,7 +94,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { let non_inlined_event: proc_macro2::TokenStream = { quote! { #[inline(never)] - #[cfg((feature = "cpi-events"))] + #[cfg(not(feature = "no-cpi-events"))] pub fn __event_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], event_data: &[u8]) -> anchor_lang::Result<()> { Ok(()) } From b7ccc2af7d0baf689defc3e242254dd23ccf1440 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 16:51:00 -0400 Subject: [PATCH 19/40] update tests --- tests/events/Anchor.toml | 1 + tests/events/programs/events/Cargo.toml | 3 ++- tests/events/programs/events/src/lib.rs | 18 +++++++++--------- tests/events/test-validator.log | 1 + 4 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 tests/events/test-validator.log diff --git a/tests/events/Anchor.toml b/tests/events/Anchor.toml index 848d54eba7..f1ee8be1d3 100644 --- a/tests/events/Anchor.toml +++ b/tests/events/Anchor.toml @@ -1,5 +1,6 @@ [features] seeds = true +# cpi-events = true [provider] cluster = "localnet" diff --git a/tests/events/programs/events/Cargo.toml b/tests/events/programs/events/Cargo.toml index ed8601cc57..7c8d8a0bbf 100644 --- a/tests/events/programs/events/Cargo.toml +++ b/tests/events/programs/events/Cargo.toml @@ -13,7 +13,8 @@ name = "events" no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] -default = [] +default = ["no-cpi-events"] +no-cpi-events = [] [dependencies] anchor-lang = { path = "../../../../lang" } diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 71a027be0d..2bd698647a 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -25,15 +25,15 @@ pub mod events { } pub fn test_event_cpi(ctx: Context) -> Result<()> { - emit_cpi!( - ctx.accounts.program.to_account_info(), - ctx.accounts.event_authority.to_account_info(), - *ctx.bumps.get("event_authority").unwrap(), - MyOtherEvent { - data: 7, - label: "cpi".to_string(), - } - ); + // emit_cpi!( + // ctx.accounts.program.to_account_info(), + // ctx.accounts.event_authority.to_account_info(), + // *ctx.bumps.get("event_authority").unwrap(), + // MyOtherEvent { + // data: 7, + // label: "cpi".to_string(), + // } + // ); Ok(()) } } diff --git a/tests/events/test-validator.log b/tests/events/test-validator.log new file mode 100644 index 0000000000..9eafc208c3 --- /dev/null +++ b/tests/events/test-validator.log @@ -0,0 +1 @@ +Error: program file does not exist: ../../tests/composite/target/deploy/composite.so From 8c2fbdc17194a0c7f4c0c63d0a9e318bed20096d Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 16:57:14 -0400 Subject: [PATCH 20/40] fix no-idl cfg dispatch --- lang/syn/src/codegen/program/dispatch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index e2de5d215f..d0ac1fe381 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -69,7 +69,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { // If the method identifier is the IDL tag, then execute an IDL // instruction, injected into all Anchor programs unless they have // no-idl enabled - #[cfg!(not(feature = "no-idl"))] + #[cfg(not(feature = "no-idl"))] { __private::__idl::__idl_dispatch( program_id, @@ -77,7 +77,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { &ix_data, ) } - #[cfg!(feature = "no-idl")] + #[cfg(feature = "no-idl")] { Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into()) } From e68be791ca9c4a2d0b6abb1cd4b9b4760a45b783 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 16:57:30 -0400 Subject: [PATCH 21/40] fix tests/events --- tests/events/programs/events/Cargo.toml | 2 +- tests/events/programs/events/src/lib.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/events/programs/events/Cargo.toml b/tests/events/programs/events/Cargo.toml index 7c8d8a0bbf..d90d4db1c6 100644 --- a/tests/events/programs/events/Cargo.toml +++ b/tests/events/programs/events/Cargo.toml @@ -13,7 +13,7 @@ name = "events" no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] -default = ["no-cpi-events"] +default = [] no-cpi-events = [] [dependencies] diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 2bd698647a..71a027be0d 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -25,15 +25,15 @@ pub mod events { } pub fn test_event_cpi(ctx: Context) -> Result<()> { - // emit_cpi!( - // ctx.accounts.program.to_account_info(), - // ctx.accounts.event_authority.to_account_info(), - // *ctx.bumps.get("event_authority").unwrap(), - // MyOtherEvent { - // data: 7, - // label: "cpi".to_string(), - // } - // ); + emit_cpi!( + ctx.accounts.program.to_account_info(), + ctx.accounts.event_authority.to_account_info(), + *ctx.bumps.get("event_authority").unwrap(), + MyOtherEvent { + data: 7, + label: "cpi".to_string(), + } + ); Ok(()) } } From d9cd325fb1a7e9d33ab5cd0a0f63514953e05f9b Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 16:58:23 -0400 Subject: [PATCH 22/40] remove cpi-events from Anchor.toml --- tests/events/Anchor.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/events/Anchor.toml b/tests/events/Anchor.toml index f1ee8be1d3..848d54eba7 100644 --- a/tests/events/Anchor.toml +++ b/tests/events/Anchor.toml @@ -1,6 +1,5 @@ [features] seeds = true -# cpi-events = true [provider] cluster = "localnet" From f9bfeff3f5d5e09bf5bd55904b31289c8f580551 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 17:18:49 -0400 Subject: [PATCH 23/40] add documentation --- lang/attribute/event/src/lib.rs | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index afd97168ad..eb38a6f72d 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -116,6 +116,51 @@ impl Parse for EmitCpiArgs { } } +/// Logs an event that can be subscribed to by clients. More stable than `emit!` because +/// RPCs are less likely to truncate CPI information than program logs. Generated code for this feature +/// can be disabled by adding `no-cpi-events` to the `defaults = []` section of your program's Cargo.toml. +/// +/// Uses a [`invoke_signed`](https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html) +/// syscall to store data in the ledger, which results in data being stored in the transaction metadata. +/// +/// This also requires the usage of an additional PDA, derived from &[b"__event_authority"], to guarantee that +/// the self-CPI is truly being invoked by the same program. Requiring this PDA to be a signer during `invoke_signed` syscall +/// ensures that the program is the one doing the logging. +/// +/// # Example +/// +/// ```rust,ignore +/// use anchor_lang::prelude::*; +/// +/// // handler function inside #[program] +/// pub fn do_something(ctx: Context) -> Result<()> { +/// emit_cpi!( +/// ctx.accounts.program.clone(), +/// ctx.accounts.event_authority.clone(), +/// *ctx.bumps.get("event_authority").unwrap(), +/// MyEvent { +/// data: 5, +/// label: [1,2,3,4,5], +/// } +/// ); +/// Ok(()) +/// } +/// +/// #[derive(Accounts)] +/// pub struct DoSomething<'info> { +/// /// CHECK: this account is needed to guarantee that your program is the one doing the logging +/// #[account(seeds=[b"__event_authority"], bump)] +/// pub event_authority: AccountInfo<'info>, +/// /// CHECK: this is your the program being invoked +/// pub program: AccountInfo<'info>, +/// } +/// +/// #[event] +/// pub struct MyEvent { +/// pub data: u64, +/// pub label: [u8; 5], +/// } +/// ``` #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let args = parse_macro_input!(input as EmitCpiArgs); From 5634cef4487daab301787ab402cf911a1c4c61e9 Mon Sep 17 00:00:00 2001 From: ngundotra Date: Fri, 5 May 2023 17:42:36 -0400 Subject: [PATCH 24/40] slightly better interface for self-program in ctx --- lang/attribute/event/src/lib.rs | 5 ++--- tests/events/Anchor.toml | 2 +- tests/events/programs/events/src/lib.rs | 3 +-- tests/events/test-validator.log | 1 - tests/events/tests/events.js | 6 +++++- 5 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 tests/events/test-validator.log diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index eb38a6f72d..3221cca13f 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -135,7 +135,7 @@ impl Parse for EmitCpiArgs { /// // handler function inside #[program] /// pub fn do_something(ctx: Context) -> Result<()> { /// emit_cpi!( -/// ctx.accounts.program.clone(), +/// ctx.accounts.program.to_account_info(), /// ctx.accounts.event_authority.clone(), /// *ctx.bumps.get("event_authority").unwrap(), /// MyEvent { @@ -151,8 +151,7 @@ impl Parse for EmitCpiArgs { /// /// CHECK: this account is needed to guarantee that your program is the one doing the logging /// #[account(seeds=[b"__event_authority"], bump)] /// pub event_authority: AccountInfo<'info>, -/// /// CHECK: this is your the program being invoked -/// pub program: AccountInfo<'info>, +/// pub program: Program<'info, crate::program::MyProgramName>, /// } /// /// #[event] diff --git a/tests/events/Anchor.toml b/tests/events/Anchor.toml index 848d54eba7..efef2a3f18 100644 --- a/tests/events/Anchor.toml +++ b/tests/events/Anchor.toml @@ -1,5 +1,5 @@ [features] -seeds = true +seeds = false [provider] cluster = "localnet" diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 71a027be0d..12fd3f04cb 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -46,8 +46,7 @@ pub struct TestEvent {} #[derive(Accounts)] pub struct TestEventCpi<'info> { - /// CHECK: this is the program itself - program: AccountInfo<'info>, + program: Program<'info, crate::program::Events>, /// CHECK: this is the global event authority #[account(seeds=[b"__event_authority"], bump)] event_authority: AccountInfo<'info>, diff --git a/tests/events/test-validator.log b/tests/events/test-validator.log deleted file mode 100644 index 9eafc208c3..0000000000 --- a/tests/events/test-validator.log +++ /dev/null @@ -1 +0,0 @@ -Error: program file does not exist: ../../tests/composite/target/deploy/composite.so diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index 72589c8184..cf7d0f9879 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -1,5 +1,5 @@ const anchor = require("@coral-xyz/anchor"); -const { bs58, base64 } = require("@coral-xyz/anchor/dist/cjs/utils/bytes"); +const { bs58, base64 } = require("@coral-xyz/anchor/utils/bytes"); const { assert } = require("chai"); describe("events", () => { @@ -63,6 +63,10 @@ describe("events", () => { .testEventCpi() .accounts({ program: program.programId, + eventAuthority: anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + program.programId + )[0], }) .transaction(); From 9cd071f918d0877ab54158c338be9237b2fe5af2 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 11 May 2023 11:37:44 +0200 Subject: [PATCH 25/40] Remove accounts and bump argument `emit_cpi` now only takes the event as an argument. Caveat: Context must be in scope and must exactly be `ctx`. --- lang/attribute/event/src/lib.rs | 65 ++++++++------------------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 3221cca13f..fdbaa17e77 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -1,10 +1,7 @@ extern crate proc_macro; use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - parse_macro_input, Expr, Token, -}; +use syn::parse_macro_input; /// The event attribute allows a struct to be used with /// [emit!](./macro.emit.html) so that programs can log significant events in @@ -84,38 +81,6 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } -// Custom wrapper struct (thanks ChatGPT!) -struct EmitCpiArgs { - self_program_info: Expr, - event_authority_info: Expr, - event_authority_bump: Expr, - event_struct: Expr, -} - -// Implement the `Parse` trait for the `TwoArgs` struct -impl Parse for EmitCpiArgs { - fn parse(input: ParseStream) -> syn::Result { - let self_program_info: Expr = input.parse()?; - input.parse::()?; - let event_authority_info: Expr = input.parse()?; - input.parse::()?; - let event_authority_bump: Expr = input.parse()?; - input.parse::()?; - let event_struct: Expr = input.parse()?; - - if !input.is_empty() { - return Err(input.error("Expected exactly 3 arguments")); - } - - Ok(EmitCpiArgs { - self_program_info, - event_authority_info, - event_authority_bump, - event_struct, - }) - } -} - /// Logs an event that can be subscribed to by clients. More stable than `emit!` because /// RPCs are less likely to truncate CPI information than program logs. Generated code for this feature /// can be disabled by adding `no-cpi-events` to the `defaults = []` section of your program's Cargo.toml. @@ -162,17 +127,12 @@ impl Parse for EmitCpiArgs { /// ``` #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let args = parse_macro_input!(input as EmitCpiArgs); - - let self_program_info = &args.self_program_info; - let event_authority_info = &args.event_authority_info; - let event_authority_bump = &args.event_authority_bump; - let event_struct = &args.event_struct; + let event_struct = parse_macro_input!(input as syn::Expr); proc_macro::TokenStream::from(quote! { - let __program_info: anchor_lang::solana_program::account_info::AccountInfo = #self_program_info; - let __event_authority_info: anchor_lang::solana_program::account_info::AccountInfo = #event_authority_info; - let __event_authority_bump: u8 = #event_authority_bump; + let __program_info = ctx.accounts.program.to_account_info(); + let __event_authority_info = ctx.accounts.event_authority.to_account_info(); + let __event_authority_bump = *ctx.bumps.get("event_authority").unwrap(); let __disc = crate::event::EVENT_IX_TAG_LE; let __inner_data: Vec = anchor_lang::Event::data(&#event_struct); @@ -182,11 +142,18 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { *__program_info.key, __ix_data.as_ref(), vec![ - anchor_lang::solana_program::instruction::AccountMeta::new_readonly(*__event_authority_info.key, true) - ] + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *__event_authority_info.key, + true, + ), + ], ); - anchor_lang::solana_program::program::invoke_signed(&__ix, &[__program_info, __event_authority_info], &[&[b"__event_authority", &[__event_authority_bump]]]) - .map_err(anchor_lang::error::Error::from)?; + anchor_lang::solana_program::program::invoke_signed( + &__ix, + &[__program_info, __event_authority_info], + &[&[b"__event_authority", &[__event_authority_bump]]], + ) + .map_err(anchor_lang::error::Error::from)?; }) } From 8b7f515a5687fd6951ff33d4e740097fe854edbd Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 11 May 2023 13:02:11 +0200 Subject: [PATCH 26/40] Add `event_cpi` attribute macro --- lang/attribute/event/src/lib.rs | 50 +++++++++++++++++++++++++++++++-- lang/src/lib.rs | 8 +++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index fdbaa17e77..56ccd9aba3 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -130,9 +130,9 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let event_struct = parse_macro_input!(input as syn::Expr); proc_macro::TokenStream::from(quote! { - let __program_info = ctx.accounts.program.to_account_info(); let __event_authority_info = ctx.accounts.event_authority.to_account_info(); let __event_authority_bump = *ctx.bumps.get("event_authority").unwrap(); + let __program_info = ctx.accounts.program.to_account_info(); let __disc = crate::event::EVENT_IX_TAG_LE; let __inner_data: Vec = anchor_lang::Event::data(&#event_struct); @@ -140,7 +140,7 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let __ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( *__program_info.key, - __ix_data.as_ref(), + &__ix_data, vec![ anchor_lang::solana_program::instruction::AccountMeta::new_readonly( *__event_authority_info.key, @@ -157,6 +157,52 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } +#[proc_macro_attribute] +pub fn event_cpi( + _attr: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let syn::ItemStruct { + attrs, + vis, + struct_token, + ident, + generics, + fields, + .. + } = parse_macro_input!(input as syn::ItemStruct); + let fields = fields.into_iter().collect::>(); + let fields = if fields.is_empty() { + quote! {} + } else { + quote! { #(#fields)*, } + }; + + let info_lifetime = generics + .lifetimes() + .next() + .map(|lifetime| quote! {#lifetime}) + .unwrap_or(quote! {'info}); + let generics = generics + .lt_token + .map(|_| quote! {#generics}) + .unwrap_or(quote! {<'info>}); + + proc_macro::TokenStream::from(quote! { + #(#attrs)* + #vis #struct_token #ident #generics { + #fields + + /// CHECK: Only the event authority can call the CPI event + #[account(seeds = [b"__event_authority"], bump)] + pub event_authority: AccountInfo<#info_lifetime>, + /// CHECK: The program itself + #[account(address = crate::ID)] + pub program: AccountInfo<#info_lifetime>, + } + }) +} + // EventIndex is a marker macro. It functionally does nothing other than // allow one to mark fields with the `#[index]` inert attribute, which is // used to add metadata to IDLs. diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 584bbce936..4664155123 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -48,9 +48,9 @@ pub use anchor_attribute_access_control::access_control; pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; -#[cfg(not(feature = "no-cpi-events"))] -pub use anchor_attribute_event::emit_cpi; pub use anchor_attribute_event::{emit, event}; +#[cfg(not(feature = "no-cpi-events"))] +pub use anchor_attribute_event::{emit_cpi, event_cpi}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; pub use anchor_derive_space::InitSpace; @@ -289,8 +289,6 @@ impl Key for Pubkey { /// The prelude contains all commonly used components of the crate. /// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { - #[cfg(not(feature = "no-cpi-events"))] - pub use super::emit_cpi; pub use super::{ access_control, account, accounts::account::Account, accounts::account_loader::AccountLoader, accounts::interface::Interface, @@ -304,6 +302,8 @@ pub mod prelude { AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; + #[cfg(not(feature = "no-cpi-events"))] + pub use super::{emit_cpi, event_cpi}; pub use anchor_attribute_error::*; pub use borsh; pub use error::*; From 1f456ea6f404bdcb177e83b7de031fea76dc5977 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 11 May 2023 13:40:44 +0200 Subject: [PATCH 27/40] Generate IDL accounts with `event_cpi` macro --- lang/attribute/event/src/lib.rs | 43 ++-------------- lang/syn/src/parser/accounts/mod.rs | 78 +++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 56ccd9aba3..58562c9027 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -1,5 +1,6 @@ extern crate proc_macro; +use anchor_syn::parser::accounts::add_event_cpi_accounts; use quote::quote; use syn::parse_macro_input; @@ -162,45 +163,9 @@ pub fn event_cpi( _attr: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { - let syn::ItemStruct { - attrs, - vis, - struct_token, - ident, - generics, - fields, - .. - } = parse_macro_input!(input as syn::ItemStruct); - let fields = fields.into_iter().collect::>(); - let fields = if fields.is_empty() { - quote! {} - } else { - quote! { #(#fields)*, } - }; - - let info_lifetime = generics - .lifetimes() - .next() - .map(|lifetime| quote! {#lifetime}) - .unwrap_or(quote! {'info}); - let generics = generics - .lt_token - .map(|_| quote! {#generics}) - .unwrap_or(quote! {<'info>}); - - proc_macro::TokenStream::from(quote! { - #(#attrs)* - #vis #struct_token #ident #generics { - #fields - - /// CHECK: Only the event authority can call the CPI event - #[account(seeds = [b"__event_authority"], bump)] - pub event_authority: AccountInfo<#info_lifetime>, - /// CHECK: The program itself - #[account(address = crate::ID)] - pub program: AccountInfo<#info_lifetime>, - } - }) + let accounts_struct = parse_macro_input!(input as syn::ItemStruct); + let accounts_struct = add_event_cpi_accounts(&accounts_struct).unwrap(); + proc_macro::TokenStream::from(quote! {#accounts_struct}) } // EventIndex is a marker macro. It functionally does nothing other than diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index d509b86499..9d5f1af34d 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -9,8 +9,8 @@ use syn::Path; pub mod constraints; -pub fn parse(strct: &syn::ItemStruct) -> ParseResult { - let instruction_api: Option> = strct +pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { + let instruction_api: Option> = accounts_struct .attrs .iter() .find(|a| { @@ -20,7 +20,20 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult { }) .map(|ix_attr| ix_attr.parse_args_with(Punctuated::::parse_terminated)) .transpose()?; - let fields = match &strct.fields { + + let is_event_cpi = accounts_struct + .attrs + .iter() + .filter_map(|attr| attr.path.get_ident()) + .find(|ident| *ident == "event_cpi") + .is_some(); + let accounts_struct = if is_event_cpi { + add_event_cpi_accounts(accounts_struct)? + } else { + accounts_struct.clone() + }; + + let fields = match &accounts_struct.fields { syn::Fields::Named(fields) => fields .named .iter() @@ -28,7 +41,7 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult { .collect::>>()?, _ => { return Err(ParseError::new_spanned( - &strct.fields, + &accounts_struct.fields, "fields must be named", )) } @@ -36,7 +49,62 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult { constraints_cross_checks(&fields)?; - Ok(AccountsStruct::new(strct.clone(), fields, instruction_api)) + Ok(AccountsStruct::new( + accounts_struct, + fields, + instruction_api, + )) +} + +/// Add necessary event CPI accounts to the given accounts struct. +pub fn add_event_cpi_accounts(accounts_struct: &syn::ItemStruct) -> ParseResult { + let syn::ItemStruct { + attrs, + vis, + struct_token, + ident, + generics, + fields, + .. + } = accounts_struct; + + let attrs = if attrs.is_empty() { + quote! {} + } else { + quote! { #(#attrs)* } + }; + + let fields = fields.into_iter().collect::>(); + let fields = if fields.is_empty() { + quote! {} + } else { + quote! { #(#fields)*, } + }; + + let info_lifetime = generics + .lifetimes() + .next() + .map(|lifetime| quote! {#lifetime}) + .unwrap_or(quote! {'info}); + let generics = generics + .lt_token + .map(|_| quote! {#generics}) + .unwrap_or(quote! {<'info>}); + + let accounts_struct = quote! { + #attrs + #vis #struct_token #ident #generics { + #fields + + /// CHECK: Only the event authority can call the CPI event + #[account(seeds = [b"__event_authority"], bump)] + pub event_authority: AccountInfo<#info_lifetime>, + /// CHECK: The program itself + #[account(address = crate::ID)] + pub program: AccountInfo<#info_lifetime>, + } + }; + syn::parse2(accounts_struct) } fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { From fe49ef62beacadf1fac236bcf70dccf6ba891cc0 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 11 May 2023 14:03:32 +0200 Subject: [PATCH 28/40] Resolve event CPI accounts in client --- .../anchor/src/program/accounts-resolver.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/ts/packages/anchor/src/program/accounts-resolver.ts b/ts/packages/anchor/src/program/accounts-resolver.ts index 3d144f7f1f..42b483e7d2 100644 --- a/ts/packages/anchor/src/program/accounts-resolver.ts +++ b/ts/packages/anchor/src/program/accounts-resolver.ts @@ -88,6 +88,7 @@ export class AccountsResolver { // addresses. That is, one PDA can be used as a seed in another. public async resolve() { await this.resolveConst(this._idlIx.accounts); + this._resolveEventCpi(this._idlIx.accounts); // Auto populate pdas and relations until we stop finding new accounts while ( @@ -225,6 +226,56 @@ export class AccountsResolver { } } + /** + * Resolve event CPI accounts `eventAuthority` and `program`. + * + * Accounts will only be resolved if they are declared next to each other to + * reduce the chance of name collision. + */ + private _resolveEventCpi( + accounts: IdlAccountItem[], + path: string[] = [] + ): void { + for (const i in accounts) { + const accountDescOrAccounts = accounts[i]; + const subAccounts = (accountDescOrAccounts as IdlAccounts).accounts; + if (subAccounts) { + this._resolveEventCpi(subAccounts, [ + ...path, + camelCase(accountDescOrAccounts.name), + ]); + } + + // Validate next index exists + const nextIndex = +i + 1; + if (nextIndex === accounts.length) return; + + const currentName = camelCase(accounts[i].name); + const nextName = camelCase(accounts[nextIndex].name); + + // Populate event CPI accounts if they exist + if (currentName === "eventAuthority" && nextName === "program") { + const currentPath = [...path, currentName]; + const nextPath = [...path, nextName]; + + if (!this.get(currentPath)) { + this.set( + currentPath, + PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + this._programId + )[0] + ); + } + if (!this.get(nextPath)) { + this.set(nextPath, this._programId); + } + + return; + } + } + } + private async resolvePdas( accounts: IdlAccountItem[], path: string[] = [] From 397108be54e3c8560b9b4da44e00cb5faa209dc7 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 11 May 2023 14:58:52 +0200 Subject: [PATCH 29/40] Update tests --- tests/events/programs/events/src/lib.rs | 21 ++++---------- tests/events/tests/events.js | 38 ++++++++++++------------- 2 files changed, 25 insertions(+), 34 deletions(-) diff --git a/tests/events/programs/events/src/lib.rs b/tests/events/programs/events/src/lib.rs index 12fd3f04cb..ff3f57c0e3 100644 --- a/tests/events/programs/events/src/lib.rs +++ b/tests/events/programs/events/src/lib.rs @@ -25,15 +25,10 @@ pub mod events { } pub fn test_event_cpi(ctx: Context) -> Result<()> { - emit_cpi!( - ctx.accounts.program.to_account_info(), - ctx.accounts.event_authority.to_account_info(), - *ctx.bumps.get("event_authority").unwrap(), - MyOtherEvent { - data: 7, - label: "cpi".to_string(), - } - ); + emit_cpi!(MyOtherEvent { + data: 7, + label: "cpi".to_string(), + }); Ok(()) } } @@ -44,13 +39,9 @@ pub struct Initialize {} #[derive(Accounts)] pub struct TestEvent {} +#[event_cpi] #[derive(Accounts)] -pub struct TestEventCpi<'info> { - program: Program<'info, crate::program::Events>, - /// CHECK: this is the global event authority - #[account(seeds=[b"__event_authority"], bump)] - event_authority: AccountInfo<'info>, -} +pub struct TestEventCpi {} #[event] pub struct MyEvent { diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index cf7d0f9879..dd529b8267 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -1,5 +1,5 @@ const anchor = require("@coral-xyz/anchor"); -const { bs58, base64 } = require("@coral-xyz/anchor/utils/bytes"); +const { bs58, base64 } = anchor.utils.bytes; const { assert } = require("chai"); describe("events", () => { @@ -59,7 +59,7 @@ describe("events", () => { it("Self-CPI events work", async () => { await sleep(200); - let sendTx = await program.methods + const tx = await program.methods .testEventCpi() .accounts({ program: program.programId, @@ -70,24 +70,24 @@ describe("events", () => { }) .transaction(); - let provider = anchor.getProvider(); - let connection = provider.connection; - let txid = await provider.sendAndConfirm(sendTx, [], { + const config = { commitment: "confirmed", - skipPreflight: true, - }); - - let tx = await connection.getTransaction(txid, { commitment: "confirmed" }); - - let cpiEventData = tx.meta.innerInstructions[0].instructions[0].data; - let ixData = bs58.decode(cpiEventData); - let eventData = ixData.slice(8); - - let coder = new anchor.BorshEventCoder(program.idl); - let event = coder.decode(base64.encode(eventData)).data; - - assert.strictEqual(event.data.toNumber(), 7); - assert.strictEqual(event.label, "cpi"); + }; + const txHash = await program.provider.sendAndConfirm(tx, [], config); + const txResult = await program.provider.connection.getTransaction( + txHash, + config + ); + + const ixData = anchor.utils.bytes.bs58.decode( + txResult.meta.innerInstructions[0].instructions[0].data + ); + const eventData = anchor.utils.bytes.base64.encode(ixData.slice(8)); + const event = program.coder.events.decode(eventData); + + assert.strictEqual(event.name, "MyOtherEvent"); + assert.strictEqual(event.data.label, "cpi"); + assert.strictEqual(event.data.data.toNumber(), 7); }); }); From cdd9776ada0f60e474929d005182e41173c2fc0f Mon Sep 17 00:00:00 2001 From: acheron Date: Tue, 23 May 2023 07:57:17 +0200 Subject: [PATCH 30/40] Fix clippy --- lang/syn/src/parser/accounts/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index 9d5f1af34d..99894f5d36 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -25,8 +25,7 @@ pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { .attrs .iter() .filter_map(|attr| attr.path.get_ident()) - .find(|ident| *ident == "event_cpi") - .is_some(); + .any(|ident| *ident == "event_cpi"); let accounts_struct = if is_event_cpi { add_event_cpi_accounts(accounts_struct)? } else { From 8cc264229ecd13df209cdfae33bd1d277343de4a Mon Sep 17 00:00:00 2001 From: acheron Date: Tue, 23 May 2023 08:18:40 +0200 Subject: [PATCH 31/40] Remove accounts from test --- tests/events/tests/events.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index dd529b8267..cb2244b217 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -1,5 +1,4 @@ const anchor = require("@coral-xyz/anchor"); -const { bs58, base64 } = anchor.utils.bytes; const { assert } = require("chai"); describe("events", () => { @@ -59,17 +58,7 @@ describe("events", () => { it("Self-CPI events work", async () => { await sleep(200); - const tx = await program.methods - .testEventCpi() - .accounts({ - program: program.programId, - eventAuthority: anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("__event_authority")], - program.programId - )[0], - }) - .transaction(); - + const tx = await program.methods.testEventCpi().transaction(); const config = { commitment: "confirmed", }; From 690e8c7c39779ce6a96b1fc365b31dad882057c2 Mon Sep 17 00:00:00 2001 From: acheron Date: Tue, 23 May 2023 08:22:40 +0200 Subject: [PATCH 32/40] Remove Anchor.toml features in tests --- tests/events/Anchor.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/events/Anchor.toml b/tests/events/Anchor.toml index efef2a3f18..8a154d1452 100644 --- a/tests/events/Anchor.toml +++ b/tests/events/Anchor.toml @@ -1,6 +1,3 @@ -[features] -seeds = false - [provider] cluster = "localnet" wallet = "~/.config/solana/id.json" From 66d72b1631c2b1bf6a2ae19f50c987ef4fb93abb Mon Sep 17 00:00:00 2001 From: acheron Date: Tue, 23 May 2023 10:30:05 +0200 Subject: [PATCH 33/40] Add malicious invocation test --- tests/events/tests/events.js | 157 +++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 63 deletions(-) diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index cb2244b217..d70d22588d 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -1,85 +1,116 @@ const anchor = require("@coral-xyz/anchor"); const { assert } = require("chai"); -describe("events", () => { +describe("Events", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Events; - it("Is initialized!", async () => { - let listener = null; + xdescribe("Normal events", () => { + it("Single event works", async () => { + let listener = null; - let [event, slot] = await new Promise((resolve, _reject) => { - listener = program.addEventListener("MyEvent", (event, slot) => { - resolve([event, slot]); + let [event, slot] = await new Promise((resolve, _reject) => { + listener = program.addEventListener("MyEvent", (event, slot) => { + resolve([event, slot]); + }); + program.rpc.initialize(); }); - program.rpc.initialize(); - }); - await program.removeEventListener(listener); - - assert.isAbove(slot, 0); - assert.strictEqual(event.data.toNumber(), 5); - assert.strictEqual(event.label, "hello"); - }); + await program.removeEventListener(listener); - it("Multiple events", async () => { - // Sleep so we don't get this transaction has already been processed. - await sleep(2000); + assert.isAbove(slot, 0); + assert.strictEqual(event.data.toNumber(), 5); + assert.strictEqual(event.label, "hello"); + }); - let listenerOne = null; - let listenerTwo = null; + it("Multiple events work", async () => { + let listenerOne = null; + let listenerTwo = null; - let [eventOne, slotOne] = await new Promise((resolve, _reject) => { - listenerOne = program.addEventListener("MyEvent", (event, slot) => { - resolve([event, slot]); + let [eventOne, slotOne] = await new Promise((resolve, _reject) => { + listenerOne = program.addEventListener("MyEvent", (event, slot) => { + resolve([event, slot]); + }); + program.rpc.initialize(); }); - program.rpc.initialize(); - }); - let [eventTwo, slotTwo] = await new Promise((resolve, _reject) => { - listenerTwo = program.addEventListener("MyOtherEvent", (event, slot) => { - resolve([event, slot]); + let [eventTwo, slotTwo] = await new Promise((resolve, _reject) => { + listenerTwo = program.addEventListener( + "MyOtherEvent", + (event, slot) => { + resolve([event, slot]); + } + ); + program.rpc.testEvent(); }); - program.rpc.testEvent(); - }); - await program.removeEventListener(listenerOne); - await program.removeEventListener(listenerTwo); + await program.removeEventListener(listenerOne); + await program.removeEventListener(listenerTwo); - assert.isAbove(slotOne, 0); - assert.strictEqual(eventOne.data.toNumber(), 5); - assert.strictEqual(eventOne.label, "hello"); + assert.isAbove(slotOne, 0); + assert.strictEqual(eventOne.data.toNumber(), 5); + assert.strictEqual(eventOne.label, "hello"); - assert.isAbove(slotTwo, 0); - assert.strictEqual(eventTwo.data.toNumber(), 6); - assert.strictEqual(eventTwo.label, "bye"); + assert.isAbove(slotTwo, 0); + assert.strictEqual(eventTwo.data.toNumber(), 6); + assert.strictEqual(eventTwo.label, "bye"); + }); }); - it("Self-CPI events work", async () => { - await sleep(200); - - const tx = await program.methods.testEventCpi().transaction(); - const config = { - commitment: "confirmed", - }; - const txHash = await program.provider.sendAndConfirm(tx, [], config); - const txResult = await program.provider.connection.getTransaction( - txHash, - config - ); - - const ixData = anchor.utils.bytes.bs58.decode( - txResult.meta.innerInstructions[0].instructions[0].data - ); - const eventData = anchor.utils.bytes.base64.encode(ixData.slice(8)); - const event = program.coder.events.decode(eventData); - - assert.strictEqual(event.name, "MyOtherEvent"); - assert.strictEqual(event.data.label, "cpi"); - assert.strictEqual(event.data.data.toNumber(), 7); + describe("CPI events", () => { + it("Works without accounts being specified", async () => { + const tx = await program.methods.testEventCpi().transaction(); + const config = { + commitment: "confirmed", + }; + const txHash = await program.provider.sendAndConfirm(tx, [], config); + const txResult = await program.provider.connection.getTransaction( + txHash, + config + ); + + const ixData = anchor.utils.bytes.bs58.decode( + txResult.meta.innerInstructions[0].instructions[0].data + ); + const eventData = anchor.utils.bytes.base64.encode(ixData.slice(8)); + const event = program.coder.events.decode(eventData); + + assert.strictEqual(event.name, "MyOtherEvent"); + assert.strictEqual(event.data.label, "cpi"); + assert.strictEqual(event.data.data.toNumber(), 7); + }); + + it("Malicious invocation throws", async () => { + const tx = new anchor.web3.Transaction(); + tx.add( + new anchor.web3.TransactionInstruction({ + programId: program.programId, + keys: [ + { + pubkey: anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("__event_authority")], + program.programId + )[0], + isSigner: false, + isWritable: false, + }, + { + pubkey: program.programId, + isSigner: false, + isWritable: false, + }, + ], + data: Buffer.from([0xe4, 0x45, 0xa5, 0x2e, 0x51, 0xcb, 0x9a, 0x1d]), + }) + ); + + try { + await program.provider.sendAndConfirm(tx, []); + } catch { + return; + } + + throw new Error("Was able to invoke the self-CPI instruction"); + }); }); }); - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} From 87652d2f90ae00788482fbd540aff3d8be56a386 Mon Sep 17 00:00:00 2001 From: acheron Date: Tue, 23 May 2023 17:35:12 +0200 Subject: [PATCH 34/40] Validate authority in the self-cpi handler to block malicious invocations --- lang/attribute/event/src/lib.rs | 52 +++++++++++++----------- lang/syn/src/codegen/program/handlers.rs | 30 +++++++++++++- lang/syn/src/parser/accounts/mod.rs | 37 ++++++++++++++--- tests/events/tests/events.js | 8 ++-- 4 files changed, 93 insertions(+), 34 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 58562c9027..a0a9917992 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -1,6 +1,6 @@ extern crate proc_macro; -use anchor_syn::parser::accounts::add_event_cpi_accounts; +use anchor_syn::parser::accounts::{add_event_cpi_accounts, EventAuthority}; use quote::quote; use syn::parse_macro_input; @@ -130,31 +130,37 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let event_struct = parse_macro_input!(input as syn::Expr); + let authority = EventAuthority::get(); + let authority_name = authority.name_token_stream(); + let authority_name_str = authority.name; + let authority_seeds = authority.seeds; + proc_macro::TokenStream::from(quote! { - let __event_authority_info = ctx.accounts.event_authority.to_account_info(); - let __event_authority_bump = *ctx.bumps.get("event_authority").unwrap(); - let __program_info = ctx.accounts.program.to_account_info(); + { + let authority_info = ctx.accounts.#authority_name.to_account_info(); + let authority_bump = *ctx.bumps.get(#authority_name_str).unwrap(); - let __disc = crate::event::EVENT_IX_TAG_LE; - let __inner_data: Vec = anchor_lang::Event::data(&#event_struct); - let __ix_data: Vec = __disc.into_iter().chain(__inner_data.into_iter()).collect(); + let disc = anchor_lang::event::EVENT_IX_TAG_LE; + let inner_data = anchor_lang::Event::data(&#event_struct); + let ix_data: Vec = disc.into_iter().chain(inner_data.into_iter()).collect(); - let __ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( - *__program_info.key, - &__ix_data, - vec![ - anchor_lang::solana_program::instruction::AccountMeta::new_readonly( - *__event_authority_info.key, - true, - ), - ], - ); - anchor_lang::solana_program::program::invoke_signed( - &__ix, - &[__program_info, __event_authority_info], - &[&[b"__event_authority", &[__event_authority_bump]]], - ) - .map_err(anchor_lang::error::Error::from)?; + let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes( + crate::ID, + &ix_data, + vec![ + anchor_lang::solana_program::instruction::AccountMeta::new_readonly( + *authority_info.key, + true, + ), + ], + ); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &[authority_info], + &[&[#authority_seeds, &[authority_bump]]], + ) + .map_err(anchor_lang::error::Error::from)?; + } }) } diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index f80ed8bfff..ef1ca69749 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -1,6 +1,6 @@ -use crate::codegen::program::common::*; use crate::program_codegen::idl::idl_accounts_and_functions; use crate::Program; +use crate::{codegen::program::common::*, parser::accounts::EventAuthority}; use heck::CamelCase; use quote::{quote, ToTokens}; @@ -92,10 +92,36 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { }; let non_inlined_event: proc_macro2::TokenStream = { + let authority = EventAuthority::get(); + let authority_name = authority.name; + let authority_seeds = authority.seeds; + quote! { #[inline(never)] #[cfg(not(feature = "no-cpi-events"))] - pub fn __event_dispatch(program_id: &Pubkey, accounts: &[AccountInfo], event_data: &[u8]) -> anchor_lang::Result<()> { + pub fn __event_dispatch( + program_id: &Pubkey, + accounts: &[AccountInfo], + event_data: &[u8], + ) -> anchor_lang::Result<()> { + let given_event_authority = next_account_info(&mut accounts.iter())?; + if !given_event_authority.is_signer { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSigner, + ) + .with_account_name(#authority_name)); + } + + let (expected_event_authority, _) = + Pubkey::find_program_address(&[#authority_seeds], &program_id); + if given_event_authority.key() != expected_event_authority { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name(#authority_name) + .with_pubkeys((given_event_authority.key(), expected_event_authority))); + } + Ok(()) } } diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index 99894f5d36..0f11cb0198 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -55,6 +55,30 @@ pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { )) } +/// This struct is used to keep the authority account information in sync. +pub struct EventAuthority { + /// Account name of the event authority + pub name: &'static str, + /// Seeds expression of the event authority + pub seeds: proc_macro2::TokenStream, +} + +impl EventAuthority { + /// Returns the account name and the seeds(in order) of the log event authority. + pub fn get() -> Self { + Self { + name: "event_authority", + seeds: quote! {b"__event_authority"}, + } + } + + /// Returns the name without surrounding quotes. + pub fn name_token_stream(&self) -> proc_macro2::TokenStream { + let name_token_stream = syn::parse_str::(self.name).unwrap(); + quote! {#name_token_stream} + } +} + /// Add necessary event CPI accounts to the given accounts struct. pub fn add_event_cpi_accounts(accounts_struct: &syn::ItemStruct) -> ParseResult { let syn::ItemStruct { @@ -90,16 +114,19 @@ pub fn add_event_cpi_accounts(accounts_struct: &syn::ItemStruct) -> ParseResult< .map(|_| quote! {#generics}) .unwrap_or(quote! {<'info>}); + let authority = EventAuthority::get(); + let authority_name = authority.name_token_stream(); + let authority_seeds = authority.seeds; + let accounts_struct = quote! { #attrs #vis #struct_token #ident #generics { #fields - /// CHECK: Only the event authority can call the CPI event - #[account(seeds = [b"__event_authority"], bump)] - pub event_authority: AccountInfo<#info_lifetime>, - /// CHECK: The program itself - #[account(address = crate::ID)] + /// CHECK: Only the event authority can call self-CPI + #[account(seeds = [#authority_seeds], bump)] + pub #authority_name: AccountInfo<#info_lifetime>, + /// CHECK: Self-CPI will fail if the program is not the current program pub program: AccountInfo<#info_lifetime>, } }; diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index d70d22588d..ea8c6bd8f9 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -6,7 +6,7 @@ describe("Events", () => { anchor.setProvider(anchor.AnchorProvider.env()); const program = anchor.workspace.Events; - xdescribe("Normal events", () => { + describe("Normal event", () => { it("Single event works", async () => { let listener = null; @@ -57,7 +57,7 @@ describe("Events", () => { }); }); - describe("CPI events", () => { + describe("Self-CPI event", () => { it("Works without accounts being specified", async () => { const tx = await program.methods.testEventCpi().transaction(); const config = { @@ -106,8 +106,8 @@ describe("Events", () => { try { await program.provider.sendAndConfirm(tx, []); - } catch { - return; + } catch (e) { + if (e.logs.some((log) => log.includes("ConstraintSigner"))) return; } throw new Error("Was able to invoke the self-CPI instruction"); From eb051efdb3ea24a376a4faa62e2f19efbc812ecc Mon Sep 17 00:00:00 2001 From: acheron Date: Wed, 24 May 2023 17:42:25 +0200 Subject: [PATCH 35/40] Make `event-cpi` feature opt-in instead of opt-out --- cli/Cargo.toml | 2 +- lang/Cargo.toml | 2 +- lang/attribute/event/Cargo.toml | 2 +- lang/attribute/event/src/lib.rs | 23 +++-- lang/src/error.rs | 4 +- lang/src/lib.rs | 4 +- lang/syn/Cargo.toml | 2 +- lang/syn/src/codegen/program/dispatch.rs | 34 ++++--- lang/syn/src/codegen/program/handlers.rs | 93 ++++++++++--------- lang/syn/src/parser/accounts/event_cpi.rs | 81 ++++++++++++++++ lang/syn/src/parser/accounts/mod.rs | 107 ++++------------------ tests/events/programs/events/Cargo.toml | 3 +- tests/events/tests/events.js | 1 + 13 files changed, 192 insertions(+), 166 deletions(-) create mode 100644 lang/syn/src/parser/accounts/event_cpi.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ff0942fd01..c1b2fa026d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -22,7 +22,7 @@ anyhow = "1.0.32" syn = { version = "1.0.60", features = ["full", "extra-traits"] } anchor-lang = { path = "../lang", version = "0.27.0" } anchor-client = { path = "../client", version = "0.27.0" } -anchor-syn = { path = "../lang/syn", features = ["idl", "init-if-needed"], version = "0.27.0" } +anchor-syn = { path = "../lang/syn", features = ["event-cpi", "idl", "init-if-needed"], version = "0.27.0" } serde_json = "1.0" shellexpand = "2.1.0" toml = "0.5.8" diff --git a/lang/Cargo.toml b/lang/Cargo.toml index 89e2ad6bce..c250115141 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -13,7 +13,7 @@ allow-missing-optionals = ["anchor-derive-accounts/allow-missing-optionals"] init-if-needed = ["anchor-derive-accounts/init-if-needed"] derive = [] default = [] -cpi-events = ["anchor-attribute-event/cpi-events"] +event-cpi = ["anchor-attribute-event/event-cpi"] anchor-debug = [ "anchor-attribute-access-control/anchor-debug", "anchor-attribute-account/anchor-debug", diff --git a/lang/attribute/event/Cargo.toml b/lang/attribute/event/Cargo.toml index f82caeaa92..7c364d373d 100644 --- a/lang/attribute/event/Cargo.toml +++ b/lang/attribute/event/Cargo.toml @@ -13,7 +13,7 @@ proc-macro = true [features] anchor-debug = ["anchor-syn/anchor-debug"] -cpi-events = ["anchor-syn/cpi-events"] +event-cpi = ["anchor-syn/event-cpi"] [dependencies] proc-macro2 = "1.0" diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index a0a9917992..10c72f79fb 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -1,6 +1,7 @@ extern crate proc_macro; -use anchor_syn::parser::accounts::{add_event_cpi_accounts, EventAuthority}; +#[cfg(feature = "event-cpi")] +use anchor_syn::parser::accounts::event_cpi::{add_event_cpi_accounts, EventAuthority}; use quote::quote; use syn::parse_macro_input; @@ -46,6 +47,14 @@ pub fn event( }) } +// EventIndex is a marker macro. It functionally does nothing other than +// allow one to mark fields with the `#[index]` inert attribute, which is +// used to add metadata to IDLs. +#[proc_macro_derive(EventIndex, attributes(index))] +pub fn derive_event(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { + proc_macro::TokenStream::from(quote! {}) +} + /// Logs an event that can be subscribed to by clients. /// Uses the [`sol_log_data`](https://docs.rs/solana-program/latest/solana_program/log/fn.sol_log_data.html) /// syscall which results in the following log: @@ -82,6 +91,7 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } +// TODO: Update docs /// Logs an event that can be subscribed to by clients. More stable than `emit!` because /// RPCs are less likely to truncate CPI information than program logs. Generated code for this feature /// can be disabled by adding `no-cpi-events` to the `defaults = []` section of your program's Cargo.toml. @@ -126,6 +136,7 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// pub label: [u8; 5], /// } /// ``` +#[cfg(feature = "event-cpi")] #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let event_struct = parse_macro_input!(input as syn::Expr); @@ -164,6 +175,8 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } +/// TODO: Add docs +#[cfg(feature = "event-cpi")] #[proc_macro_attribute] pub fn event_cpi( _attr: proc_macro::TokenStream, @@ -173,11 +186,3 @@ pub fn event_cpi( let accounts_struct = add_event_cpi_accounts(&accounts_struct).unwrap(); proc_macro::TokenStream::from(quote! {#accounts_struct}) } - -// EventIndex is a marker macro. It functionally does nothing other than -// allow one to mark fields with the `#[index]` inert attribute, which is -// used to add metadata to IDLs. -#[proc_macro_derive(EventIndex, attributes(index))] -pub fn derive_event(_item: proc_macro::TokenStream) -> proc_macro::TokenStream { - proc_macro::TokenStream::from(quote! {}) -} diff --git a/lang/src/error.rs b/lang/src/error.rs index 274662b28e..aba9587baa 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -45,8 +45,8 @@ pub enum ErrorCode { IdlAccountNotEmpty, // Event instructions - /// 1500 - The program was compiled without event instructions - #[msg("The program was compiled without event instructions")] + /// 1500 - The program was compiled without `event-cpi` feature + #[msg("The program was compiled without `event-cpi` feature")] EventInstructionStub = 1500, // Constraints diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 4664155123..8ce31f56a8 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -49,7 +49,7 @@ pub use anchor_attribute_account::{account, declare_id, zero_copy}; pub use anchor_attribute_constant::constant; pub use anchor_attribute_error::*; pub use anchor_attribute_event::{emit, event}; -#[cfg(not(feature = "no-cpi-events"))] +#[cfg(feature = "event-cpi")] pub use anchor_attribute_event::{emit_cpi, event_cpi}; pub use anchor_attribute_program::program; pub use anchor_derive_accounts::Accounts; @@ -302,7 +302,7 @@ pub mod prelude { AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; - #[cfg(not(feature = "no-cpi-events"))] + #[cfg(feature = "event-cpi")] pub use super::{emit_cpi, event_cpi}; pub use anchor_attribute_error::*; pub use borsh; diff --git a/lang/syn/Cargo.toml b/lang/syn/Cargo.toml index e222a4e41a..7908cd0574 100644 --- a/lang/syn/Cargo.toml +++ b/lang/syn/Cargo.toml @@ -16,7 +16,7 @@ hash = [] default = [] anchor-debug = [] seeds = [] -cpi-events = [] +event-cpi = [] [dependencies] proc-macro2 = { version = "1.0", features=["span-locations"]} diff --git a/lang/syn/src/codegen/program/dispatch.rs b/lang/syn/src/codegen/program/dispatch.rs index d0ac1fe381..2c6090b887 100644 --- a/lang/syn/src/codegen/program/dispatch.rs +++ b/lang/syn/src/codegen/program/dispatch.rs @@ -27,9 +27,13 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } }) .collect(); + let fallback_fn = gen_fallback(program).unwrap_or(quote! { Err(anchor_lang::error::ErrorCode::InstructionFallbackNotFound.into()) }); + + let event_cpi_handler = generate_event_cpi_handler(); + quote! { /// Performs method dispatch. /// @@ -83,21 +87,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } } anchor_lang::event::EVENT_IX_TAG_LE => { - // If the method identifier is the event tag, then execute an event cpi - // against the noop instruction injected into all Anchor programs unless they have - // no-cpi-events enabled. - #[cfg(not(feature = "no-cpi-events"))] - { - __private::__events::__event_dispatch( - program_id, - accounts, - &ix_data, - ) - } - #[cfg(feature = "no-cpi-events")] - { - Err(anchor_lang::error::ErrorCode::EventInstructionStub.into()) - } + #event_cpi_handler } _ => { #fallback_fn @@ -117,3 +107,17 @@ pub fn gen_fallback(program: &Program) -> Option { } }) } + +/// Generate the event-cpi instruction handler based on whether the `event-cpi` feature is enabled. +pub fn generate_event_cpi_handler() -> proc_macro2::TokenStream { + #[cfg(feature = "event-cpi")] + quote! { + // `event-cpi` feature is enabled, dispatch self-cpi instruction + __private::__events::__event_dispatch(program_id, accounts, &ix_data) + } + #[cfg(not(feature = "event-cpi"))] + quote! { + // `event-cpi` feature is not enabled + Err(anchor_lang::error::ErrorCode::EventInstructionStub.into()) + } +} diff --git a/lang/syn/src/codegen/program/handlers.rs b/lang/syn/src/codegen/program/handlers.rs index ef1ca69749..144ed79d3a 100644 --- a/lang/syn/src/codegen/program/handlers.rs +++ b/lang/syn/src/codegen/program/handlers.rs @@ -1,6 +1,6 @@ +use crate::codegen::program::common::*; use crate::program_codegen::idl::idl_accounts_and_functions; use crate::Program; -use crate::{codegen::program::common::*, parser::accounts::EventAuthority}; use heck::CamelCase; use quote::{quote, ToTokens}; @@ -91,41 +91,7 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { } }; - let non_inlined_event: proc_macro2::TokenStream = { - let authority = EventAuthority::get(); - let authority_name = authority.name; - let authority_seeds = authority.seeds; - - quote! { - #[inline(never)] - #[cfg(not(feature = "no-cpi-events"))] - pub fn __event_dispatch( - program_id: &Pubkey, - accounts: &[AccountInfo], - event_data: &[u8], - ) -> anchor_lang::Result<()> { - let given_event_authority = next_account_info(&mut accounts.iter())?; - if !given_event_authority.is_signer { - return Err(anchor_lang::error::Error::from( - anchor_lang::error::ErrorCode::ConstraintSigner, - ) - .with_account_name(#authority_name)); - } - - let (expected_event_authority, _) = - Pubkey::find_program_address(&[#authority_seeds], &program_id); - if given_event_authority.key() != expected_event_authority { - return Err(anchor_lang::error::Error::from( - anchor_lang::error::ErrorCode::ConstraintSeeds, - ) - .with_account_name(#authority_name) - .with_pubkeys((given_event_authority.key(), expected_event_authority))); - } - - Ok(()) - } - } - }; + let event_cpi_mod = generate_event_cpi_mod(); let non_inlined_handlers: Vec = program .ixs @@ -209,19 +175,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream { #idl_accounts_and_functions } - /// __idl mod defines handler for self-cpi based event logging - pub mod __events { - use super::*; - - #non_inlined_event - } - /// __global mod defines wrapped handlers for global instructions. pub mod __global { use super::*; #(#non_inlined_handlers)* } + + #event_cpi_mod } } } @@ -230,3 +191,49 @@ fn generate_ix_variant_name(name: String) -> proc_macro2::TokenStream { let n = name.to_camel_case(); n.parse().unwrap() } + +/// Generate the event module based on whether the `event-cpi` feature is enabled. +fn generate_event_cpi_mod() -> proc_macro2::TokenStream { + #[cfg(feature = "event-cpi")] + { + let authority = crate::parser::accounts::event_cpi::EventAuthority::get(); + let authority_name = authority.name; + let authority_seeds = authority.seeds; + + quote! { + /// __events mod defines handler for self-cpi based event logging + pub mod __events { + use super::*; + + #[inline(never)] + pub fn __event_dispatch( + program_id: &Pubkey, + accounts: &[AccountInfo], + event_data: &[u8], + ) -> anchor_lang::Result<()> { + let given_event_authority = next_account_info(&mut accounts.iter())?; + if !given_event_authority.is_signer { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSigner, + ) + .with_account_name(#authority_name)); + } + + let (expected_event_authority, _) = + Pubkey::find_program_address(&[#authority_seeds], &program_id); + if given_event_authority.key() != expected_event_authority { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintSeeds, + ) + .with_account_name(#authority_name) + .with_pubkeys((given_event_authority.key(), expected_event_authority))); + } + + Ok(()) + } + } + } + } + #[cfg(not(feature = "event-cpi"))] + quote! {} +} diff --git a/lang/syn/src/parser/accounts/event_cpi.rs b/lang/syn/src/parser/accounts/event_cpi.rs new file mode 100644 index 0000000000..134abb34ee --- /dev/null +++ b/lang/syn/src/parser/accounts/event_cpi.rs @@ -0,0 +1,81 @@ +use quote::quote; + +/// This struct is used to keep the authority account information in sync. +pub struct EventAuthority { + /// Account name of the event authority + pub name: &'static str, + /// Seeds expression of the event authority + pub seeds: proc_macro2::TokenStream, +} + +impl EventAuthority { + /// Returns the account name and the seeds expression of the event authority. + pub fn get() -> Self { + Self { + name: "event_authority", + seeds: quote! {b"__event_authority"}, + } + } + + /// Returns the name without surrounding quotes. + pub fn name_token_stream(&self) -> proc_macro2::TokenStream { + let name_token_stream = syn::parse_str::(self.name).unwrap(); + quote! {#name_token_stream} + } +} + +/// Add necessary event CPI accounts to the given accounts struct. +pub fn add_event_cpi_accounts( + accounts_struct: &syn::ItemStruct, +) -> syn::parse::Result { + let syn::ItemStruct { + attrs, + vis, + struct_token, + ident, + generics, + fields, + .. + } = accounts_struct; + + let attrs = if attrs.is_empty() { + quote! {} + } else { + quote! { #(#attrs)* } + }; + + let fields = fields.into_iter().collect::>(); + let fields = if fields.is_empty() { + quote! {} + } else { + quote! { #(#fields)*, } + }; + + let info_lifetime = generics + .lifetimes() + .next() + .map(|lifetime| quote! {#lifetime}) + .unwrap_or(quote! {'info}); + let generics = generics + .lt_token + .map(|_| quote! {#generics}) + .unwrap_or(quote! {<'info>}); + + let authority = EventAuthority::get(); + let authority_name = authority.name_token_stream(); + let authority_seeds = authority.seeds; + + let accounts_struct = quote! { + #attrs + #vis #struct_token #ident #generics { + #fields + + /// CHECK: Only the event authority can call self-CPI + #[account(seeds = [#authority_seeds], bump)] + pub #authority_name: AccountInfo<#info_lifetime>, + /// CHECK: Self-CPI will fail if the program is not the current program + pub program: AccountInfo<#info_lifetime>, + } + }; + syn::parse2(accounts_struct) +} diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index 0f11cb0198..dae82140a8 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -1,3 +1,7 @@ +pub mod constraints; +#[cfg(feature = "event-cpi")] +pub mod event_cpi; + use crate::parser::docs; use crate::*; use syn::parse::{Error as ParseError, Result as ParseResult}; @@ -7,8 +11,6 @@ use syn::token::Comma; use syn::Expr; use syn::Path; -pub mod constraints; - pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { let instruction_api: Option> = accounts_struct .attrs @@ -21,16 +23,21 @@ pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { .map(|ix_attr| ix_attr.parse_args_with(Punctuated::::parse_terminated)) .transpose()?; - let is_event_cpi = accounts_struct - .attrs - .iter() - .filter_map(|attr| attr.path.get_ident()) - .any(|ident| *ident == "event_cpi"); - let accounts_struct = if is_event_cpi { - add_event_cpi_accounts(accounts_struct)? - } else { - accounts_struct.clone() + #[cfg(feature = "event-cpi")] + let accounts_struct = { + let is_event_cpi = accounts_struct + .attrs + .iter() + .filter_map(|attr| attr.path.get_ident()) + .any(|ident| *ident == "event_cpi"); + if is_event_cpi { + event_cpi::add_event_cpi_accounts(accounts_struct)? + } else { + accounts_struct.clone() + } }; + #[cfg(not(feature = "event-cpi"))] + let accounts_struct = accounts_struct.clone(); let fields = match &accounts_struct.fields { syn::Fields::Named(fields) => fields @@ -55,84 +62,6 @@ pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult { )) } -/// This struct is used to keep the authority account information in sync. -pub struct EventAuthority { - /// Account name of the event authority - pub name: &'static str, - /// Seeds expression of the event authority - pub seeds: proc_macro2::TokenStream, -} - -impl EventAuthority { - /// Returns the account name and the seeds(in order) of the log event authority. - pub fn get() -> Self { - Self { - name: "event_authority", - seeds: quote! {b"__event_authority"}, - } - } - - /// Returns the name without surrounding quotes. - pub fn name_token_stream(&self) -> proc_macro2::TokenStream { - let name_token_stream = syn::parse_str::(self.name).unwrap(); - quote! {#name_token_stream} - } -} - -/// Add necessary event CPI accounts to the given accounts struct. -pub fn add_event_cpi_accounts(accounts_struct: &syn::ItemStruct) -> ParseResult { - let syn::ItemStruct { - attrs, - vis, - struct_token, - ident, - generics, - fields, - .. - } = accounts_struct; - - let attrs = if attrs.is_empty() { - quote! {} - } else { - quote! { #(#attrs)* } - }; - - let fields = fields.into_iter().collect::>(); - let fields = if fields.is_empty() { - quote! {} - } else { - quote! { #(#fields)*, } - }; - - let info_lifetime = generics - .lifetimes() - .next() - .map(|lifetime| quote! {#lifetime}) - .unwrap_or(quote! {'info}); - let generics = generics - .lt_token - .map(|_| quote! {#generics}) - .unwrap_or(quote! {<'info>}); - - let authority = EventAuthority::get(); - let authority_name = authority.name_token_stream(); - let authority_seeds = authority.seeds; - - let accounts_struct = quote! { - #attrs - #vis #struct_token #ident #generics { - #fields - - /// CHECK: Only the event authority can call self-CPI - #[account(seeds = [#authority_seeds], bump)] - pub #authority_name: AccountInfo<#info_lifetime>, - /// CHECK: Self-CPI will fail if the program is not the current program - pub program: AccountInfo<#info_lifetime>, - } - }; - syn::parse2(accounts_struct) -} - fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> { // COMMON ERROR MESSAGE let message = |constraint: &str, field: &str, required: bool| { diff --git a/tests/events/programs/events/Cargo.toml b/tests/events/programs/events/Cargo.toml index d90d4db1c6..7b00b1a898 100644 --- a/tests/events/programs/events/Cargo.toml +++ b/tests/events/programs/events/Cargo.toml @@ -14,7 +14,6 @@ no-entrypoint = [] no-idl = [] cpi = ["no-entrypoint"] default = [] -no-cpi-events = [] [dependencies] -anchor-lang = { path = "../../../../lang" } +anchor-lang = { path = "../../../../lang", features = ["event-cpi"] } diff --git a/tests/events/tests/events.js b/tests/events/tests/events.js index ea8c6bd8f9..28a3599f90 100644 --- a/tests/events/tests/events.js +++ b/tests/events/tests/events.js @@ -108,6 +108,7 @@ describe("Events", () => { await program.provider.sendAndConfirm(tx, []); } catch (e) { if (e.logs.some((log) => log.includes("ConstraintSigner"))) return; + console.log(e); } throw new Error("Was able to invoke the self-CPI instruction"); From fd182fe43682ada7d48a0647e46aa4b406fc4ff6 Mon Sep 17 00:00:00 2001 From: acheron Date: Wed, 24 May 2023 18:23:54 +0200 Subject: [PATCH 36/40] Fix parsing multiple fields --- lang/syn/src/parser/accounts/event_cpi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/syn/src/parser/accounts/event_cpi.rs b/lang/syn/src/parser/accounts/event_cpi.rs index 134abb34ee..173333f639 100644 --- a/lang/syn/src/parser/accounts/event_cpi.rs +++ b/lang/syn/src/parser/accounts/event_cpi.rs @@ -48,7 +48,7 @@ pub fn add_event_cpi_accounts( let fields = if fields.is_empty() { quote! {} } else { - quote! { #(#fields)*, } + quote! { #(#fields,)* } }; let info_lifetime = generics From 852b8bd680aca8c9a14c98f15873bd4f6bb70af4 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 25 May 2023 18:38:23 +0200 Subject: [PATCH 37/40] Generate attributes and fields inside the main `TokenStream` --- lang/syn/src/parser/accounts/event_cpi.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lang/syn/src/parser/accounts/event_cpi.rs b/lang/syn/src/parser/accounts/event_cpi.rs index 173333f639..fc7f0751bb 100644 --- a/lang/syn/src/parser/accounts/event_cpi.rs +++ b/lang/syn/src/parser/accounts/event_cpi.rs @@ -38,18 +38,7 @@ pub fn add_event_cpi_accounts( .. } = accounts_struct; - let attrs = if attrs.is_empty() { - quote! {} - } else { - quote! { #(#attrs)* } - }; - let fields = fields.into_iter().collect::>(); - let fields = if fields.is_empty() { - quote! {} - } else { - quote! { #(#fields,)* } - }; let info_lifetime = generics .lifetimes() @@ -66,9 +55,9 @@ pub fn add_event_cpi_accounts( let authority_seeds = authority.seeds; let accounts_struct = quote! { - #attrs + #(#attrs)* #vis #struct_token #ident #generics { - #fields + #(#fields,)* /// CHECK: Only the event authority can call self-CPI #[account(seeds = [#authority_seeds], bump)] From aa11826b17a6d411bb2c2cb2bb06b7d41fe370e2 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 25 May 2023 21:59:54 +0200 Subject: [PATCH 38/40] Add documentation --- lang/attribute/event/src/lib.rs | 85 +++++++++++++++-------- lang/src/lib.rs | 1 + lang/syn/src/parser/accounts/event_cpi.rs | 2 +- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 10c72f79fb..6aed02f2f8 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -91,51 +91,48 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } -// TODO: Update docs -/// Logs an event that can be subscribed to by clients. More stable than `emit!` because -/// RPCs are less likely to truncate CPI information than program logs. Generated code for this feature -/// can be disabled by adding `no-cpi-events` to the `defaults = []` section of your program's Cargo.toml. +/// Log an event by making a self-CPI that can be subscribed to by clients. +/// +/// This way of logging events is more reliable than [`emit!`](emit!) because RPCs are less likely +/// to truncate CPI information than program logs. /// /// Uses a [`invoke_signed`](https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html) -/// syscall to store data in the ledger, which results in data being stored in the transaction metadata. +/// syscall to store the event data in the ledger, which results in the data being stored in the +/// transaction metadata. +/// +/// This method requires the usage of an additional PDA to guarantee that the self-CPI is truly +/// being invoked by the same program. Requiring this PDA to be a signer during `invoke_signed` +/// syscall ensures that the program is the one doing the logging. /// -/// This also requires the usage of an additional PDA, derived from &[b"__event_authority"], to guarantee that -/// the self-CPI is truly being invoked by the same program. Requiring this PDA to be a signer during `invoke_signed` syscall -/// ensures that the program is the one doing the logging. +/// The necessary accounts are added to the accounts struct via [`#[event_cpi]`](event_cpi) +/// attribute macro. /// /// # Example /// -/// ```rust,ignore +/// ```ignore /// use anchor_lang::prelude::*; /// -/// // handler function inside #[program] -/// pub fn do_something(ctx: Context) -> Result<()> { -/// emit_cpi!( -/// ctx.accounts.program.to_account_info(), -/// ctx.accounts.event_authority.clone(), -/// *ctx.bumps.get("event_authority").unwrap(), -/// MyEvent { -/// data: 5, -/// label: [1,2,3,4,5], -/// } -/// ); -/// Ok(()) +/// #[program] +/// pub mod my_program { +/// use super::*; +/// +/// pub fn my_instruction(ctx: Context) -> Result<()> { +/// emit_cpi!(MyEvent { data: 42 }); +/// Ok(()) +/// } /// } /// +/// #[event_cpi] /// #[derive(Accounts)] -/// pub struct DoSomething<'info> { -/// /// CHECK: this account is needed to guarantee that your program is the one doing the logging -/// #[account(seeds=[b"__event_authority"], bump)] -/// pub event_authority: AccountInfo<'info>, -/// pub program: Program<'info, crate::program::MyProgramName>, -/// } +/// pub struct MyInstruction {} /// /// #[event] /// pub struct MyEvent { /// pub data: u64, -/// pub label: [u8; 5], /// } /// ``` +/// +/// *Only available with `event-cpi` feature enabled.* #[cfg(feature = "event-cpi")] #[proc_macro] pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -175,7 +172,37 @@ pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream { }) } -/// TODO: Add docs +/// An attribute macro to add necessary event CPI accounts to the given accounts struct. +/// +/// Two accounts named `event_authority` and `program` will be appended to the list of accounts. +/// +/// # Example +/// +/// ```ignore +/// #[event_cpi] +/// #[derive(Accounts)] +/// pub struct MyInstruction<'info> { +/// pub signer: Signer<'info>, +/// } +/// ``` +/// +/// The code above will be expanded to: +/// +/// ```ignore +/// #[derive(Accounts)] +/// pub struct MyInstruction<'info> { +/// pub signer: Signer<'info>, +/// /// CHECK: Only the event authority can invoke self-CPI +/// #[account(seeds = [b"__event_authority"], bump)] +/// pub event_authority: AccountInfo<'info>, +/// /// CHECK: Self-CPI will fail if the program is not the current program +/// pub program: AccountInfo<'info>, +/// } +/// ``` +/// +/// See [`emit_cpi!`](emit_cpi!) for a full example. +/// +/// *Only available with `event-cpi` feature enabled.* #[cfg(feature = "event-cpi")] #[proc_macro_attribute] pub fn event_cpi( diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 8ce31f56a8..1fb1a80918 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -37,6 +37,7 @@ mod bpf_writer; mod common; pub mod context; pub mod error; +#[doc(hidden)] pub mod event; #[doc(hidden)] pub mod idl; diff --git a/lang/syn/src/parser/accounts/event_cpi.rs b/lang/syn/src/parser/accounts/event_cpi.rs index fc7f0751bb..fccbd2b3f4 100644 --- a/lang/syn/src/parser/accounts/event_cpi.rs +++ b/lang/syn/src/parser/accounts/event_cpi.rs @@ -59,7 +59,7 @@ pub fn add_event_cpi_accounts( #vis #struct_token #ident #generics { #(#fields,)* - /// CHECK: Only the event authority can call self-CPI + /// CHECK: Only the event authority can invoke self-CPI #[account(seeds = [#authority_seeds], bump)] pub #authority_name: AccountInfo<#info_lifetime>, /// CHECK: Self-CPI will fail if the program is not the current program From ac45fc7f9da3dbe8dd4f76b89d2857e4b4b6a9d5 Mon Sep 17 00:00:00 2001 From: acheron Date: Thu, 25 May 2023 22:11:35 +0200 Subject: [PATCH 39/40] Add a note about `ctx` being in scope --- lang/attribute/event/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lang/attribute/event/src/lib.rs b/lang/attribute/event/src/lib.rs index 6aed02f2f8..7301162d3e 100644 --- a/lang/attribute/event/src/lib.rs +++ b/lang/attribute/event/src/lib.rs @@ -132,6 +132,8 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// } /// ``` /// +/// **NOTE:** This macro requires `ctx` to be in scope. +/// /// *Only available with `event-cpi` feature enabled.* #[cfg(feature = "event-cpi")] #[proc_macro] From 22b902a06b5af80606439d6fe3b79ea90ddf7073 Mon Sep 17 00:00:00 2001 From: acheron Date: Fri, 26 May 2023 17:54:21 +0200 Subject: [PATCH 40/40] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784473b24d..431168dfae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Add support for Solidity programs. `anchor init` and `anchor new` take an option `--solidity` which creates solidity code rather than rust. `anchor build` and `anchor test` work accordingly ([#2421](https://github.com/coral-xyz/anchor/pull/2421)) - bench: Add benchmarking for compute units usage ([#2466](https://github.com/coral-xyz/anchor/pull/2466)) - cli: `idl set-buffer`, `idl set-authority` and `idl close` take an option `--print-only`. which prints transaction in a base64 Borsh compatible format but not sent to the cluster. It's helpful when managing authority under a multisig, e.g., a user can create a proposal for a `Custom Instruction` in SPL Governance ([#2486](https://github.com/coral-xyz/anchor/pull/2486)). +- lang: Add `emit_cpi!` and `#[event_cpi]` macros(behind `event-cpi` feature flag) to store event logs in transaction metadata ([#2438](https://github.com/coral-xyz/anchor/pull/2438)). ### Fixes