Skip to content

Commit 23b90bf

Browse files
Feature: CPI Events API (#2438)
Co-authored-by: acheron <[email protected]>
1 parent c3625c8 commit 23b90bf

18 files changed

Lines changed: 494 additions & 85 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The minor version will be incremented upon a breaking change and the patch versi
1717
- 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))
1818
- bench: Add benchmarking for compute units usage ([#2466](https://github.com/coral-xyz/anchor/pull/2466))
1919
- 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)).
20+
- 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)).
2021

2122
### Fixes
2223

cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ bincode = "1.3.3"
2424
syn = { version = "1.0.60", features = ["full", "extra-traits"] }
2525
anchor-lang = { path = "../lang", version = "0.27.0" }
2626
anchor-client = { path = "../client", version = "0.27.0" }
27-
anchor-syn = { path = "../lang/syn", features = ["idl", "init-if-needed"], version = "0.27.0" }
27+
anchor-syn = { path = "../lang/syn", features = ["event-cpi", "idl", "init-if-needed"], version = "0.27.0" }
2828
serde_json = "1.0"
2929
shellexpand = "2.1.0"
3030
toml = "0.5.8"

lang/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ allow-missing-optionals = ["anchor-derive-accounts/allow-missing-optionals"]
1313
init-if-needed = ["anchor-derive-accounts/init-if-needed"]
1414
derive = []
1515
default = []
16+
event-cpi = ["anchor-attribute-event/event-cpi"]
1617
anchor-debug = [
1718
"anchor-attribute-access-control/anchor-debug",
1819
"anchor-attribute-account/anchor-debug",

lang/attribute/event/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ proc-macro = true
1313

1414
[features]
1515
anchor-debug = ["anchor-syn/anchor-debug"]
16+
event-cpi = ["anchor-syn/event-cpi"]
1617

1718
[dependencies]
1819
proc-macro2 = "1.0"

lang/attribute/event/src/lib.rs

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
extern crate proc_macro;
22

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

@@ -45,6 +47,14 @@ pub fn event(
4547
})
4648
}
4749

50+
// EventIndex is a marker macro. It functionally does nothing other than
51+
// allow one to mark fields with the `#[index]` inert attribute, which is
52+
// used to add metadata to IDLs.
53+
#[proc_macro_derive(EventIndex, attributes(index))]
54+
pub fn derive_event(_item: proc_macro::TokenStream) -> proc_macro::TokenStream {
55+
proc_macro::TokenStream::from(quote! {})
56+
}
57+
4858
/// Logs an event that can be subscribed to by clients.
4959
/// Uses the [`sol_log_data`](https://docs.rs/solana-program/latest/solana_program/log/fn.sol_log_data.html)
5060
/// syscall which results in the following log:
@@ -81,10 +91,127 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
8191
})
8292
}
8393

84-
// EventIndex is a marker macro. It functionally does nothing other than
85-
// allow one to mark fields with the `#[index]` inert attribute, which is
86-
// used to add metadata to IDLs.
87-
#[proc_macro_derive(EventIndex, attributes(index))]
88-
pub fn derive_event(_item: proc_macro::TokenStream) -> proc_macro::TokenStream {
89-
proc_macro::TokenStream::from(quote! {})
94+
/// Log an event by making a self-CPI that can be subscribed to by clients.
95+
///
96+
/// This way of logging events is more reliable than [`emit!`](emit!) because RPCs are less likely
97+
/// to truncate CPI information than program logs.
98+
///
99+
/// Uses a [`invoke_signed`](https://docs.rs/solana-program/latest/solana_program/program/fn.invoke_signed.html)
100+
/// syscall to store the event data in the ledger, which results in the data being stored in the
101+
/// transaction metadata.
102+
///
103+
/// This method requires the usage of an additional PDA to guarantee that the self-CPI is truly
104+
/// being invoked by the same program. Requiring this PDA to be a signer during `invoke_signed`
105+
/// syscall ensures that the program is the one doing the logging.
106+
///
107+
/// The necessary accounts are added to the accounts struct via [`#[event_cpi]`](event_cpi)
108+
/// attribute macro.
109+
///
110+
/// # Example
111+
///
112+
/// ```ignore
113+
/// use anchor_lang::prelude::*;
114+
///
115+
/// #[program]
116+
/// pub mod my_program {
117+
/// use super::*;
118+
///
119+
/// pub fn my_instruction(ctx: Context<MyInstruction>) -> Result<()> {
120+
/// emit_cpi!(MyEvent { data: 42 });
121+
/// Ok(())
122+
/// }
123+
/// }
124+
///
125+
/// #[event_cpi]
126+
/// #[derive(Accounts)]
127+
/// pub struct MyInstruction {}
128+
///
129+
/// #[event]
130+
/// pub struct MyEvent {
131+
/// pub data: u64,
132+
/// }
133+
/// ```
134+
///
135+
/// **NOTE:** This macro requires `ctx` to be in scope.
136+
///
137+
/// *Only available with `event-cpi` feature enabled.*
138+
#[cfg(feature = "event-cpi")]
139+
#[proc_macro]
140+
pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
141+
let event_struct = parse_macro_input!(input as syn::Expr);
142+
143+
let authority = EventAuthority::get();
144+
let authority_name = authority.name_token_stream();
145+
let authority_name_str = authority.name;
146+
let authority_seeds = authority.seeds;
147+
148+
proc_macro::TokenStream::from(quote! {
149+
{
150+
let authority_info = ctx.accounts.#authority_name.to_account_info();
151+
let authority_bump = *ctx.bumps.get(#authority_name_str).unwrap();
152+
153+
let disc = anchor_lang::event::EVENT_IX_TAG_LE;
154+
let inner_data = anchor_lang::Event::data(&#event_struct);
155+
let ix_data: Vec<u8> = disc.into_iter().chain(inner_data.into_iter()).collect();
156+
157+
let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
158+
crate::ID,
159+
&ix_data,
160+
vec![
161+
anchor_lang::solana_program::instruction::AccountMeta::new_readonly(
162+
*authority_info.key,
163+
true,
164+
),
165+
],
166+
);
167+
anchor_lang::solana_program::program::invoke_signed(
168+
&ix,
169+
&[authority_info],
170+
&[&[#authority_seeds, &[authority_bump]]],
171+
)
172+
.map_err(anchor_lang::error::Error::from)?;
173+
}
174+
})
175+
}
176+
177+
/// An attribute macro to add necessary event CPI accounts to the given accounts struct.
178+
///
179+
/// Two accounts named `event_authority` and `program` will be appended to the list of accounts.
180+
///
181+
/// # Example
182+
///
183+
/// ```ignore
184+
/// #[event_cpi]
185+
/// #[derive(Accounts)]
186+
/// pub struct MyInstruction<'info> {
187+
/// pub signer: Signer<'info>,
188+
/// }
189+
/// ```
190+
///
191+
/// The code above will be expanded to:
192+
///
193+
/// ```ignore
194+
/// #[derive(Accounts)]
195+
/// pub struct MyInstruction<'info> {
196+
/// pub signer: Signer<'info>,
197+
/// /// CHECK: Only the event authority can invoke self-CPI
198+
/// #[account(seeds = [b"__event_authority"], bump)]
199+
/// pub event_authority: AccountInfo<'info>,
200+
/// /// CHECK: Self-CPI will fail if the program is not the current program
201+
/// pub program: AccountInfo<'info>,
202+
/// }
203+
/// ```
204+
///
205+
/// See [`emit_cpi!`](emit_cpi!) for a full example.
206+
///
207+
/// *Only available with `event-cpi` feature enabled.*
208+
#[cfg(feature = "event-cpi")]
209+
#[proc_macro_attribute]
210+
pub fn event_cpi(
211+
_attr: proc_macro::TokenStream,
212+
input: proc_macro::TokenStream,
213+
) -> proc_macro::TokenStream {
214+
let accounts_struct = parse_macro_input!(input as syn::ItemStruct);
215+
let accounts_struct = add_event_cpi_accounts(&accounts_struct).unwrap();
216+
proc_macro::TokenStream::from(quote! {#accounts_struct})
90217
}

lang/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ pub enum ErrorCode {
4444
#[msg("IDL account must be empty in order to resize, try closing first")]
4545
IdlAccountNotEmpty,
4646

47+
// Event instructions
48+
/// 1500 - The program was compiled without `event-cpi` feature
49+
#[msg("The program was compiled without `event-cpi` feature")]
50+
EventInstructionStub = 1500,
51+
4752
// Constraints
4853
/// 2000 - A mut constraint was violated
4954
#[msg("A mut constraint was violated")]

lang/src/event.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Sha256(anchor:event)[..8]
2+
pub const EVENT_IX_TAG: u64 = 0x1d9acb512ea545e4;
3+
pub const EVENT_IX_TAG_LE: [u8; 8] = EVENT_IX_TAG.to_le_bytes();

lang/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ mod common;
3838
pub mod context;
3939
pub mod error;
4040
#[doc(hidden)]
41+
pub mod event;
42+
#[doc(hidden)]
4143
pub mod idl;
4244
pub mod system_program;
4345

@@ -48,6 +50,8 @@ pub use anchor_attribute_account::{account, declare_id, zero_copy};
4850
pub use anchor_attribute_constant::constant;
4951
pub use anchor_attribute_error::*;
5052
pub use anchor_attribute_event::{emit, event};
53+
#[cfg(feature = "event-cpi")]
54+
pub use anchor_attribute_event::{emit_cpi, event_cpi};
5155
pub use anchor_attribute_program::program;
5256
pub use anchor_derive_accounts::Accounts;
5357
pub use anchor_derive_space::InitSpace;
@@ -299,6 +303,8 @@ pub mod prelude {
299303
AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner,
300304
ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas,
301305
};
306+
#[cfg(feature = "event-cpi")]
307+
pub use super::{emit_cpi, event_cpi};
302308
pub use anchor_attribute_error::*;
303309
pub use borsh;
304310
pub use error::*;

lang/syn/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ hash = []
1616
default = []
1717
anchor-debug = []
1818
seeds = []
19+
event-cpi = []
1920

2021
[dependencies]
2122
proc-macro2 = { version = "1.0", features=["span-locations"]}

lang/syn/src/codegen/program/dispatch.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
2727
}
2828
})
2929
.collect();
30+
3031
let fallback_fn = gen_fallback(program).unwrap_or(quote! {
3132
Err(anchor_lang::error::ErrorCode::InstructionFallbackNotFound.into())
3233
});
34+
35+
let event_cpi_handler = generate_event_cpi_handler();
36+
3337
quote! {
3438
/// Performs method dispatch.
3539
///
@@ -67,17 +71,24 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
6771
#(#global_dispatch_arms)*
6872
anchor_lang::idl::IDL_IX_TAG_LE => {
6973
// If the method identifier is the IDL tag, then execute an IDL
70-
// instruction, injected into all Anchor programs.
71-
if cfg!(not(feature = "no-idl")) {
74+
// instruction, injected into all Anchor programs unless they have
75+
// no-idl enabled
76+
#[cfg(not(feature = "no-idl"))]
77+
{
7278
__private::__idl::__idl_dispatch(
7379
program_id,
7480
accounts,
7581
&ix_data,
7682
)
77-
} else {
83+
}
84+
#[cfg(feature = "no-idl")]
85+
{
7886
Err(anchor_lang::error::ErrorCode::IdlInstructionStub.into())
7987
}
8088
}
89+
anchor_lang::event::EVENT_IX_TAG_LE => {
90+
#event_cpi_handler
91+
}
8192
_ => {
8293
#fallback_fn
8394
}
@@ -96,3 +107,17 @@ pub fn gen_fallback(program: &Program) -> Option<proc_macro2::TokenStream> {
96107
}
97108
})
98109
}
110+
111+
/// Generate the event-cpi instruction handler based on whether the `event-cpi` feature is enabled.
112+
pub fn generate_event_cpi_handler() -> proc_macro2::TokenStream {
113+
#[cfg(feature = "event-cpi")]
114+
quote! {
115+
// `event-cpi` feature is enabled, dispatch self-cpi instruction
116+
__private::__events::__event_dispatch(program_id, accounts, &ix_data)
117+
}
118+
#[cfg(not(feature = "event-cpi"))]
119+
quote! {
120+
// `event-cpi` feature is not enabled
121+
Err(anchor_lang::error::ErrorCode::EventInstructionStub.into())
122+
}
123+
}

0 commit comments

Comments
 (0)