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 7 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
23 changes: 23 additions & 0 deletions lang/attribute/event/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ pub fn emit(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
})
}

#[proc_macro]
pub fn emit_cpi(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
let tuple = parse_macro_input!(input as syn::ExprTuple);
ngundotra marked this conversation as resolved.
Show resolved Hide resolved

let elems = tuple.elems;
if elems.len() != 2 {
panic!("Expected a tuple with exactly two elements.");
}

let first = &elems[0];
ngundotra marked this conversation as resolved.
Show resolved Hide resolved
let second = &elems[1];

proc_macro::TokenStream::from(quote! {
ngundotra marked this conversation as resolved.
Show resolved Hide resolved
let program_info: &anchor_lang::solana_program::account_info::AccountInfo = &#first;
ngundotra marked this conversation as resolved.
Show resolved Hide resolved

let __disc: Vec<u8> = crate::event::EVENT_IX_TAG_LE.to_vec();
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
let __inner_data: &Vec<u8> = &anchor_lang::Event::data(&#second);
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
let __ix_data: Vec<u8> = __disc.iter().chain(__inner_data.iter()).cloned().collect();

anchor_lang::__private::_emit_cpi_invoke(__ix_data, program_info)?;
})
}

// 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
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();
20 changes: 17 additions & 3 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 @@ -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, emit_cpi, event};
pub use anchor_attribute_program::program;
pub use anchor_derive_accounts::Accounts;
pub use anchor_derive_space::InitSpace;
Expand Down Expand Up @@ -292,8 +293,8 @@ pub mod prelude {
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,
Expand Down Expand Up @@ -333,6 +334,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.
Expand All @@ -358,6 +361,17 @@ pub mod __private {
input.to_bytes()
}
}

#[doc(hidden)]
pub fn _emit_cpi_invoke(ix_data: Vec<u8>, program: &AccountInfo) -> anchor_lang::Result<()> {
callensm marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
12 changes: 12 additions & 0 deletions lang/syn/src/codegen/program/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved
}
}
_ => {
#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
17 changes: 17 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,17 @@ pub mod events {
});
Ok(())
}

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

#[derive(Accounts)]
Expand All @@ -31,6 +42,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,
Expand Down
29 changes: 29 additions & 0 deletions tests/events/tests/events.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const anchor = require("@coral-xyz/anchor");
const { bs58, base64 } = require("@coral-xyz/anchor/dist/cjs/utils/bytes");
ngundotra marked this conversation as resolved.
Show resolved Hide resolved
const { assert } = require("chai");

describe("events", () => {
Expand Down Expand Up @@ -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);
acheroncrypto marked this conversation as resolved.
Show resolved Hide resolved

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) {
Expand Down