Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: CPI Events API #2438

Merged
merged 41 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1b9ff06
add permissionless event cpi api
ngundotra Mar 17, 2023
eed5e6f
add cpi event test
ngundotra Mar 17, 2023
6c9fced
move __emit_cpi_invoke to __private in lib.rs
ngundotra Mar 20, 2023
e1d234c
export emit_cpi and _emit_cpi_data in prelude
ngundotra Mar 24, 2023
f5c5ab9
remove empty file
ngundotra Mar 24, 2023
bca57d6
rewrite emit_cpi as a proc_macro
ngundotra Mar 24, 2023
ffd6fb1
remove unused code
ngundotra Mar 30, 2023
aadba4e
inline _emit_cpi_invoke to proc_macro declaration
ngundotra Mar 30, 2023
bbd869b
address acheron feedback
ngundotra May 3, 2023
7bc2c25
optimize emit macro to reduce cloning
ngundotra May 3, 2023
3d04a71
explicitly only parse two args
ngundotra May 3, 2023
014256f
update events package.json
ngundotra May 3, 2023
76ee7be
add event instruction error code to anchor
ngundotra May 4, 2023
f4c225c
add event authority
ngundotra May 4, 2023
25bc040
require event authority PDA to sign
ngundotra May 5, 2023
4786e85
turn on seeds to hide eventAuthority
ngundotra May 5, 2023
b072f59
change feature to cpi-events
ngundotra May 5, 2023
0b678cf
fix no-idl, no-cpi-events, and cpi-events features
ngundotra May 5, 2023
b7ccc2a
update tests
ngundotra May 5, 2023
8c2fbdc
fix no-idl cfg dispatch
ngundotra May 5, 2023
e68be79
fix tests/events
ngundotra May 5, 2023
d9cd325
remove cpi-events from Anchor.toml
ngundotra May 5, 2023
f9bfeff
add documentation
ngundotra May 5, 2023
5634cef
slightly better interface for self-program in ctx
ngundotra May 5, 2023
9cd071f
Remove accounts and bump argument
acheroncrypto May 11, 2023
8b7f515
Add `event_cpi` attribute macro
acheroncrypto May 11, 2023
1f456ea
Generate IDL accounts with `event_cpi` macro
acheroncrypto May 11, 2023
fe49ef6
Resolve event CPI accounts in client
acheroncrypto May 11, 2023
397108b
Update tests
acheroncrypto May 11, 2023
cdd9776
Fix clippy
acheroncrypto May 23, 2023
8cc2642
Remove accounts from test
acheroncrypto May 23, 2023
690e8c7
Remove Anchor.toml features in tests
acheroncrypto May 23, 2023
66d72b1
Add malicious invocation test
acheroncrypto May 23, 2023
87652d2
Validate authority in the self-cpi handler to block malicious invocat…
acheroncrypto May 23, 2023
eb051ef
Make `event-cpi` feature opt-in instead of opt-out
acheroncrypto May 24, 2023
fd182fe
Fix parsing multiple fields
acheroncrypto May 24, 2023
852b8bd
Generate attributes and fields inside the main `TokenStream`
acheroncrypto May 25, 2023
aa11826
Add documentation
acheroncrypto May 25, 2023
ac45fc7
Add a note about `ctx` being in scope
acheroncrypto May 25, 2023
9b17a4c
Merge master
acheroncrypto May 26, 2023
22b902a
Update CHANGELOG
acheroncrypto May 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lang/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions lang/attribute/event/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
87 changes: 87 additions & 0 deletions lang/attribute/event/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -81,6 +82,92 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
})
}

/// 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<DoSomething>) -> 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(())
/// }
///
/// #[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>,
/// }
///
/// #[event]
/// pub struct MyEvent {
/// pub data: u64,
/// pub label: [u8; 5],
/// }
/// ```
#[proc_macro]
pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
let event_struct = parse_macro_input!(input as syn::Expr);

proc_macro::TokenStream::from(quote! {
ngundotra marked this conversation as resolved.
Show resolved Hide resolved
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<u8> = anchor_lang::Event::data(&#event_struct);
let __ix_data: Vec<u8> = __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)?;
})
}

#[proc_macro_attribute]
pub fn event_cpi(
_attr: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
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
// allow one to mark fields with the `#[index]` inert attribute, which is
// used to add metadata to IDLs.
Expand Down
5 changes: 5 additions & 0 deletions lang/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
3 changes: 3 additions & 0 deletions lang/src/event.rs
Original file line number Diff line number Diff line change
@@ -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();
5 changes: 5 additions & 0 deletions lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,6 +49,8 @@ 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"))]
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;
Expand Down Expand Up @@ -299,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::*;
Expand Down
1 change: 1 addition & 0 deletions lang/syn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ hash = []
default = []
anchor-debug = []
seeds = []
cpi-events = []

[dependencies]
proc-macro2 = { version = "1.0", features=["span-locations"]}
Expand Down
27 changes: 24 additions & 3 deletions lang/syn/src/codegen/program/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,38 @@ 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
// against the noop instruction injected into all Anchor programs unless they have
// no-cpi-events enabled.
#[cfg(not(feature = "no-cpi-events"))]
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
{
__private::__events::__event_dispatch(
program_id,
accounts,
&ix_data,
)
}
#[cfg(feature = "no-cpi-events")]
{
Err(anchor_lang::error::ErrorCode::EventInstructionStub.into())
}
}
_ => {
#fallback_fn
}
Expand Down
15 changes: 15 additions & 0 deletions lang/syn/src/codegen/program/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
Ok(())
Copy link
Member

@armaniferrante armaniferrante Mar 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using the instructions sysvar to check that this call was self-referential, i.e., that the previous instruction was to this program? Do we want to allow other programs or even top level instructions to call this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about two different macros, emit_cpi! and emit_cpi_signed! ?

I'll look into the introspection stuff.

I think devs should have options

Copy link
Contributor

@Arrowana Arrowana Mar 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would say log authority is better, it is also an account to be provided but much simpler to check.
What would be the advantage of the ix introspection?
I don't feel dev should have the option when there is no advantage to a solution, it only bloats anchor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why log_authority is even needed here?

Are people writing indexers that parse ixs from TransactionStatus Metadata separately from their transaction context? Is that the norm? I don't understand how log_authority helps indexers? Sure it prevents other programs from invoking that instruction, but it will still show up in getSignaturesForAddress.

Can you provide a concrete example of a program that benefits from having log_authority guarding the event ix?

Copy link
Contributor

@Arrowana Arrowana Apr 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to ask @jarry-xiao, I think it is mainly to avoid an extra burden for log parsers yes, to have to check that the caller is indeed the correct program. So it might not be 100% necessary

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not 100% necessary, but it prevents footguns (which I think is one of the key tenets of anchor).

We’ve chatted about this before, and I gave a concrete example of making a CPI from a rogue program into the log instruction of the target program. The parser can defend against this but the logic becomes more error prone.

I think it’s fine if you provide an indexing example of how one might process this along with a test case of how it blocks out erroneous instances of calling the log instruction (both through top level transaction calls and CPIs from rogue programs)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also log_authority means you don’t need introspection, which I think is a win. To my knowledge, you still need to explicitly pass in SysvarInstructions

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here’s a super concrete example. Suppose you have a target program T and a rogue program R. R first makes a normal call to T, which will emit a regular event. Then in the same instruction it makes a call to T’s log instruction. You can figure out this is erroneous if you have the tx stack depth, but I think it’s ambiguous in the current interface.

In a vacuum you also know nothing about T’s logging patterns. Maybe it calls emit_cpi! a variable number of times. The point is there’s no way to know (again, until stack depth is in the transaction object which could realistically take months)

However if R’s call to T’s log instruction fails 100% of the time, then there’s nothing to ever worry about

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another point for the log authority, if your program allows arbitrary cpi (or only arbitrary self cpi) for any reason (flash loan capability...), then someone can write random logs without the log authority.

}
}
};

let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
.ixs
.iter()
Expand Down Expand Up @@ -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 {
Expand Down
78 changes: 73 additions & 5 deletions lang/syn/src/parser/accounts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use syn::Path;

pub mod constraints;

pub fn parse(strct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
let instruction_api: Option<Punctuated<Expr, Comma>> = strct
pub fn parse(accounts_struct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
let instruction_api: Option<Punctuated<Expr, Comma>> = accounts_struct
.attrs
.iter()
.find(|a| {
Expand All @@ -20,23 +20,91 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
})
.map(|ix_attr| ix_attr.parse_args_with(Punctuated::<Expr, Comma>::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()
.map(parse_account_field)
.collect::<ParseResult<Vec<AccountField>>>()?,
_ => {
return Err(ParseError::new_spanned(
&strct.fields,
&accounts_struct.fields,
"fields must be named",
))
}
};

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<syn::ItemStruct> {
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::<Vec<_>>();
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)]
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
pub event_authority: AccountInfo<#info_lifetime>,
/// CHECK: The program itself
#[account(address = crate::ID)]
pub program: AccountInfo<#info_lifetime>,
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
}
};
syn::parse2(accounts_struct)
}

fn constraints_cross_checks(fields: &[AccountField]) -> ParseResult<()> {
Expand Down
3 changes: 3 additions & 0 deletions tests/events/Anchor.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[features]
seeds = false

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
Expand Down
1 change: 1 addition & 0 deletions tests/events/programs/events/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []
no-cpi-events = []

[dependencies]
anchor-lang = { path = "../../../../lang" }
12 changes: 12 additions & 0 deletions tests/events/programs/events/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ pub mod events {
});
Ok(())
}

pub fn test_event_cpi(ctx: Context<TestEventCpi>) -> Result<()> {
emit_cpi!(MyOtherEvent {
data: 7,
label: "cpi".to_string(),
});
Ok(())
}
}

#[derive(Accounts)]
Expand All @@ -31,6 +39,10 @@ pub struct Initialize {}
#[derive(Accounts)]
pub struct TestEvent {}

#[event_cpi]
#[derive(Accounts)]
pub struct TestEventCpi {}

#[event]
pub struct MyEvent {
pub data: u64,
Expand Down
Loading