Skip to content

Commit

Permalink
Feature: CPI Events API (coral-xyz#2438)
Browse files Browse the repository at this point in the history
Co-authored-by: acheron <acheroncrypto@gmail.com>
  • Loading branch information
ngundotra and acheroncrypto authored May 26, 2023
1 parent c3625c8 commit 23b90bf
Show file tree
Hide file tree
Showing 18 changed files with 494 additions and 85 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ bincode = "1.3.3"
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"
Expand Down
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 = []
event-cpi = ["anchor-attribute-event/event-cpi"]
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"]
event-cpi = ["anchor-syn/event-cpi"]

[dependencies]
proc-macro2 = "1.0"
Expand Down
139 changes: 133 additions & 6 deletions lang/attribute/event/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
extern crate proc_macro;

#[cfg(feature = "event-cpi")]
use anchor_syn::parser::accounts::event_cpi::{add_event_cpi_accounts, EventAuthority};
use quote::quote;
use syn::parse_macro_input;

Expand Down Expand Up @@ -45,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:
Expand Down Expand Up @@ -81,10 +91,127 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
})
}

// 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! {})
/// 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 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.
///
/// The necessary accounts are added to the accounts struct via [`#[event_cpi]`](event_cpi)
/// attribute macro.
///
/// # Example
///
/// ```ignore
/// use anchor_lang::prelude::*;
///
/// #[program]
/// pub mod my_program {
/// use super::*;
///
/// pub fn my_instruction(ctx: Context<MyInstruction>) -> Result<()> {
/// emit_cpi!(MyEvent { data: 42 });
/// Ok(())
/// }
/// }
///
/// #[event_cpi]
/// #[derive(Accounts)]
/// pub struct MyInstruction {}
///
/// #[event]
/// pub struct MyEvent {
/// pub data: u64,
/// }
/// ```
///
/// **NOTE:** This macro requires `ctx` to be in scope.
///
/// *Only available with `event-cpi` feature enabled.*
#[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);

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 authority_info = ctx.accounts.#authority_name.to_account_info();
let authority_bump = *ctx.bumps.get(#authority_name_str).unwrap();

let disc = anchor_lang::event::EVENT_IX_TAG_LE;
let inner_data = 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(
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)?;
}
})
}

/// 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(
_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})
}
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-cpi` feature
#[msg("The program was compiled without `event-cpi` feature")]
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();
6 changes: 6 additions & 0 deletions lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ mod common;
pub mod context;
pub mod error;
#[doc(hidden)]
pub mod event;
#[doc(hidden)]
pub mod idl;
pub mod system_program;

Expand All @@ -48,6 +50,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(feature = "event-cpi")]
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 +303,8 @@ pub mod prelude {
AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner,
ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas,
};
#[cfg(feature = "event-cpi")]
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 = []
event-cpi = []

[dependencies]
proc-macro2 = { version = "1.0", features=["span-locations"]}
Expand Down
31 changes: 28 additions & 3 deletions lang/syn/src/codegen/program/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -67,17 +71,24 @@ 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 => {
#event_cpi_handler
}
_ => {
#fallback_fn
}
Expand All @@ -96,3 +107,17 @@ pub fn gen_fallback(program: &Program) -> Option<proc_macro2::TokenStream> {
}
})
}

/// 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())
}
}
52 changes: 50 additions & 2 deletions lang/syn/src/codegen/program/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
}
};

let event_cpi_mod = generate_event_cpi_mod();

let non_inlined_handlers: Vec<proc_macro2::TokenStream> = program
.ixs
.iter()
Expand Down Expand Up @@ -173,14 +175,14 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
#idl_accounts_and_functions
}



/// __global mod defines wrapped handlers for global instructions.
pub mod __global {
use super::*;

#(#non_inlined_handlers)*
}

#event_cpi_mod
}
}
}
Expand All @@ -189,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! {}
}
Loading

0 comments on commit 23b90bf

Please sign in to comment.