pub const CPI_EVENT_MARKER: [u8; 2] = [0xE0, 0x1E];
pub const EVENT_AUTHORITY_SEED: &[u8] = b"__hopper_event_authority";
pub const MAX_EVENT_PAYLOAD: usize = 512;
pub const HOST_EVENT_AUTHORITY_BUMP: u8 = 255;
pub trait CpiEvent {
const TAG: u8;
fn payload_bytes(&self) -> &[u8];
#[inline(always)]
fn tag(&self) -> u8 {
Self::TAG
}
}
#[inline]
pub fn encode_event_cpi(event_tag: u8, event_payload: &[u8], out: &mut [u8]) -> Option<usize> {
let total = 2 + 1 + event_payload.len();
if out.len() < total {
return None;
}
out[0..2].copy_from_slice(&CPI_EVENT_MARKER);
out[2] = event_tag;
out[3..total].copy_from_slice(event_payload);
Some(total)
}
#[inline]
pub fn decode_event_cpi(data: &[u8]) -> Option<(u8, &[u8])> {
if data.len() < 3 || data[0..2] != CPI_EVENT_MARKER {
return None;
}
Some((data[2], &data[3..]))
}
#[inline]
pub fn verify_event_authority(
event_authority: &crate::account::AccountView<'_>,
program_id: &crate::address::Address,
) -> Result<u8, crate::error::ProgramError> {
#[cfg(target_os = "solana")]
{
crate::pda::find_and_verify_pda(event_authority, &[EVENT_AUTHORITY_SEED], program_id)
}
#[cfg(not(target_os = "solana"))]
{
let _ = (event_authority, program_id);
Ok(HOST_EVENT_AUTHORITY_BUMP)
}
}
#[inline]
pub fn handle_event_sink(
ctx: &crate::context::Context<'_>,
data: &[u8],
) -> crate::result::ProgramResult {
if data.len() < 3 || data[0..2] != CPI_EVENT_MARKER {
return Err(crate::error::ProgramError::InvalidInstructionData);
}
let authority = ctx.account(0)?;
if !authority.is_signer() {
return Err(crate::error::ProgramError::MissingRequiredSignature);
}
#[cfg(target_os = "solana")]
{
let _bump =
crate::pda::find_and_verify_pda(authority, &[EVENT_AUTHORITY_SEED], ctx.program_id())?;
}
Ok(())
}
#[inline]
pub fn invoke_event_cpi(
program_id: &crate::address::Address,
event_authority: &crate::account::AccountView<'_>,
data: &[u8],
authority_seeds: &[&[u8]],
) -> crate::result::ProgramResult {
use crate::instruction::{InstructionAccount, InstructionView, Seed, Signer};
if authority_seeds.len() > crate::address::MAX_SEEDS {
return Err(crate::error::ProgramError::MaxSeedLengthExceeded);
}
let account_meta = InstructionAccount {
address: event_authority.address(),
is_signer: true,
is_writable: false,
};
let ix = InstructionView {
program_id,
accounts: ::core::slice::from_ref(&account_meta),
data,
};
let mut seed_storage: [::core::mem::MaybeUninit<Seed<'_>>; crate::address::MAX_SEEDS] =
unsafe { ::core::mem::MaybeUninit::uninit().assume_init() };
let mut seed_index = 0;
while seed_index < authority_seeds.len() {
seed_storage[seed_index].write(Seed::from(authority_seeds[seed_index]));
seed_index += 1;
}
let seed_slice =
unsafe {
::core::slice::from_raw_parts(
seed_storage.as_ptr() as *const Seed<'_>,
authority_seeds.len(),
)
};
let signer_list = [Signer::from(seed_slice)];
let account_views = [event_authority];
crate::cpi::invoke_signed::<1>(&ix, &account_views, &signer_list)?;
#[cfg(all(
not(target_os = "solana"),
any(test, feature = "thread-local-registry")
))]
host_capture::record(program_id, event_authority.address(), data);
Ok(())
}
#[cfg(all(
not(target_os = "solana"),
any(test, feature = "thread-local-registry")
))]
mod host_capture {
use core::cell::RefCell;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CapturedEventCpi {
pub program_id: crate::address::Address,
pub authority: crate::address::Address,
pub data: std::vec::Vec<u8>,
}
std::thread_local! {
static CAPTURED: RefCell<std::vec::Vec<CapturedEventCpi>> =
const { RefCell::new(std::vec::Vec::new()) };
}
pub(super) fn record(
program_id: &crate::address::Address,
authority: &crate::address::Address,
data: &[u8],
) {
CAPTURED.with(|captured| {
captured.borrow_mut().push(CapturedEventCpi {
program_id: *program_id,
authority: *authority,
data: data.to_vec(),
});
});
}
pub fn take_host_captured_event_cpis() -> std::vec::Vec<CapturedEventCpi> {
CAPTURED.with(|captured| core::mem::take(&mut *captured.borrow_mut()))
}
}
#[cfg(all(
not(target_os = "solana"),
any(test, feature = "thread-local-registry")
))]
pub use host_capture::{take_host_captured_event_cpis, CapturedEventCpi};
#[cfg(test)]
mod tests {
use super::*;
use crate::account::AccountView;
use crate::address::Address;
use crate::context::Context;
use crate::error::ProgramError;
use hopper_native::{
AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
};
#[test]
fn encodes_marker_tag_and_payload_in_order() {
let mut buf = [0u8; 16];
let len = encode_event_cpi(0x42, &[1, 2, 3, 4], &mut buf).unwrap();
assert_eq!(len, 7);
assert_eq!(&buf[..len], &[0xE0, 0x1E, 0x42, 1, 2, 3, 4]);
}
#[test]
fn rejects_short_buffer() {
let mut buf = [0u8; 3];
let len = encode_event_cpi(0, &[1, 2, 3, 4], &mut buf);
assert!(len.is_none());
}
#[test]
fn zero_payload_is_valid() {
let mut buf = [0u8; 3];
let len = encode_event_cpi(0x7F, &[], &mut buf).unwrap();
assert_eq!(len, 3);
assert_eq!(&buf[..len], &[0xE0, 0x1E, 0x7F]);
}
#[test]
fn reserved_marker_is_stable() {
assert_eq!(CPI_EVENT_MARKER, [0xE0, 0x1E]);
}
#[test]
fn decode_is_the_exact_inverse_of_encode() {
let payload = [9u8, 8, 7, 6, 5];
let mut buf = [0u8; 3 + 5];
let len = encode_event_cpi(0x2A, &payload, &mut buf).unwrap();
let (tag, decoded) = decode_event_cpi(&buf[..len]).expect("decodable");
assert_eq!(tag, 0x2A);
assert_eq!(decoded, &payload);
let mut buf3 = [0u8; 3];
let len3 = encode_event_cpi(0x01, &[], &mut buf3).unwrap();
assert_eq!(decode_event_cpi(&buf3[..len3]), Some((0x01, &[][..])));
}
#[test]
fn decode_rejects_short_or_mismarked_data() {
assert_eq!(decode_event_cpi(&[]), None);
assert_eq!(decode_event_cpi(&[0xE0, 0x1E]), None, "marker without tag");
assert_eq!(decode_event_cpi(&[0xE0, 0x77, 0x01]), None, "wrong marker");
assert_eq!(decode_event_cpi(&[0x00, 0x1E, 0x01]), None, "wrong marker");
}
#[test]
fn trait_tag_defaults_to_the_associated_const() {
struct Ping;
impl CpiEvent for Ping {
const TAG: u8 = 0x5A;
fn payload_bytes(&self) -> &[u8] {
&[]
}
}
assert_eq!(Ping.tag(), 0x5A);
}
fn make_account(
address_byte: u8,
is_signer: bool,
) -> (std::vec::Vec<u64>, AccountView<'static>) {
const DATA_LEN: usize = 8;
let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + DATA_LEN).div_ceil(8)];
let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
unsafe {
raw.write(RuntimeAccount {
borrow_state: NOT_BORROWED,
is_signer: is_signer as u8,
is_writable: 0,
executable: 0,
resize_delta: 0,
address: NativeAddress::new_from_array([address_byte; 32]),
owner: NativeAddress::new_from_array([0; 32]),
lamports: 0,
data_len: DATA_LEN as u64,
});
}
let backend = unsafe { NativeAccountView::new_unchecked(raw) };
(backing, AccountView::from_backend(backend))
}
#[test]
fn sink_rejects_short_data_and_wrong_marker() {
let (_b, authority) = make_account(1, true);
let accounts = [authority];
let pid = Address::new([9u8; 32]);
let ctx = Context::new(&pid, &accounts, &[]);
assert_eq!(
handle_event_sink(&ctx, &[0xE0, 0x1E]),
Err(ProgramError::InvalidInstructionData),
"marker without a tag byte must be refused"
);
assert_eq!(
handle_event_sink(&ctx, &[0xE0, 0x77, 0x01]),
Err(ProgramError::InvalidInstructionData),
"a wrong second marker byte must be refused"
);
}
#[test]
fn sink_requires_the_authority_to_sign() {
let (_b, authority) = make_account(1, false);
let accounts = [authority];
let pid = Address::new([9u8; 32]);
let ctx = Context::new(&pid, &accounts, &[]);
assert_eq!(
handle_event_sink(&ctx, &[0xE0, 0x1E, 0x42, 1, 2]),
Err(ProgramError::MissingRequiredSignature),
"an unsigned authority is a forged event and must be refused"
);
}
#[test]
fn sink_accepts_a_signed_authority_on_host() {
let (_b, authority) = make_account(1, true);
let accounts = [authority];
let pid = Address::new([9u8; 32]);
let ctx = Context::new(&pid, &accounts, &[]);
assert_eq!(handle_event_sink(&ctx, &[0xE0, 0x1E, 0x42]), Ok(()));
}
#[test]
fn sink_requires_the_authority_account_to_be_present() {
let pid = Address::new([9u8; 32]);
let accounts: [AccountView<'static>; 0] = [];
let ctx = Context::new(&pid, &accounts, &[]);
assert!(
handle_event_sink(&ctx, &[0xE0, 0x1E, 0x42]).is_err(),
"a sink CPI without the authority account must be refused"
);
}
#[test]
fn host_verify_event_authority_reports_the_placeholder_bump() {
let (_b, authority) = make_account(3, false);
let pid = Address::new([9u8; 32]);
assert_eq!(
verify_event_authority(&authority, &pid),
Ok(HOST_EVENT_AUTHORITY_BUMP)
);
}
#[test]
fn host_invoke_accepts_supplied_pda_authority_and_captures_the_wire_bytes() {
let _ = take_host_captured_event_cpis();
let pid = Address::new([9u8; 32]);
let bump = [HOST_EVENT_AUTHORITY_BUMP];
let seeds: [&[u8]; 2] = [EVENT_AUTHORITY_SEED, &bump];
let mut buf = [0u8; 3 + MAX_EVENT_PAYLOAD];
let len = encode_event_cpi(0x42, &[7, 7, 7], &mut buf).unwrap();
let (_b0, unsigned) = make_account(4, false);
assert_eq!(
invoke_event_cpi(&pid, &unsigned, &buf[..len], &seeds),
Ok(())
);
let captured = take_host_captured_event_cpis();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].authority, *unsigned.address());
assert_eq!(captured[0].data, &buf[..len]);
let (_b1, signed) = make_account(5, true);
assert_eq!(invoke_event_cpi(&pid, &signed, &buf[..len], &seeds), Ok(()));
let captured = take_host_captured_event_cpis();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0].program_id, pid);
assert_eq!(captured[0].authority, *signed.address());
assert_eq!(captured[0].data, &buf[..len]);
assert_eq!(
decode_event_cpi(&captured[0].data),
Some((0x42, &[7u8, 7, 7][..])),
"captured bytes must round-trip the public decoder"
);
assert!(take_host_captured_event_cpis().is_empty());
}
#[test]
fn invoke_rejects_too_many_seeds() {
let (_b, authority) = make_account(6, true);
let pid = Address::new([9u8; 32]);
let too_many: [&[u8]; crate::address::MAX_SEEDS + 1] =
[&[1u8][..]; crate::address::MAX_SEEDS + 1];
assert_eq!(
invoke_event_cpi(&pid, &authority, &[0xE0, 0x1E, 0x01], &too_many),
Err(ProgramError::MaxSeedLengthExceeded)
);
}
}