[message-buffer 10/X] - Message Buffer Admin IXs & variable length (#779)

* ok

* ok

* it runs

* add this stuff

* working on tests

* feat(message-buffer): finish create_buffer ix, update put_all

* feat: rename bufferheader to messageBuffer, add delete_buffer impl

* feat(message-buffer): remove unused code, add additional checks, update unit tests

* style(message-buffer): fix pre-commit, run fmt & clippy

* fix(message-buffer): add verification checks, fix ts test

* refactor(message-buffer): rename update_whitelist_authority to admin

* fix(message-buffer): address PR comments

---------

Co-authored-by: Jayant Krishnamurthy <jayantkrishnamurthy@gmail.com>
This commit is contained in:
swimricky 2023-04-25 08:45:16 -07:00 committed by GitHub
parent 6a5b1c434a
commit 794bd84c6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1117 additions and 355 deletions

View File

@ -71,13 +71,13 @@ repos:
language: "rust"
entry: cargo +nightly fmt --manifest-path ./message_buffer/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false
files: accumulator_updater
files: message_buffer
- id: cargo-clippy-message-buffer
name: Cargo clippy for message buffer contract
language: "rust"
entry: cargo +nightly clippy --manifest-path ./message_buffer/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
pass_filenames: false
files: accumulator_updater
files: message_buffer
# Hooks for solana receiver contract
- id: cargo-fmt-solana-receiver
name: Cargo format for solana target chain contract

View File

@ -0,0 +1,152 @@
use {
crate::{
instructions::is_uninitialized_account,
state::*,
MessageBufferError,
MESSAGE,
},
anchor_lang::{
prelude::*,
solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE,
system_program::{
self,
Allocate,
Assign,
Transfer,
},
},
};
pub fn create_buffer<'info>(
ctx: Context<'_, '_, '_, 'info, CreateBuffer<'info>>,
allowed_program_auth: Pubkey,
base_account_key: Pubkey,
target_size: u32,
) -> Result<()> {
let buffer_account = ctx
.remaining_accounts
.first()
.ok_or(MessageBufferError::MessageBufferNotProvided)?;
ctx.accounts
.whitelist
.is_allowed_program_auth(&allowed_program_auth)?;
require_gte!(
target_size,
MessageBuffer::HEADER_LEN as u32,
MessageBufferError::MessageBufferTooSmall
);
require_gte!(
MAX_PERMITTED_DATA_INCREASE,
target_size as usize,
MessageBufferError::TargetSizeDeltaExceeded
);
if is_uninitialized_account(buffer_account) {
let (pda, bump) = Pubkey::find_program_address(
&[
allowed_program_auth.as_ref(),
MESSAGE.as_bytes(),
base_account_key.as_ref(),
],
&crate::ID,
);
require_keys_eq!(buffer_account.key(), pda);
let signer_seeds = [
allowed_program_auth.as_ref(),
MESSAGE.as_bytes(),
base_account_key.as_ref(),
&[bump],
];
CreateBuffer::create_account(
buffer_account,
target_size as usize,
&ctx.accounts.admin,
&[signer_seeds.as_slice()],
&ctx.accounts.system_program,
)?;
let loader =
AccountLoader::<MessageBuffer>::try_from_unchecked(&crate::ID, buffer_account)?;
{
let mut message_buffer = loader.load_init()?;
*message_buffer = MessageBuffer::new(bump);
}
loader.exit(&crate::ID)?;
}
Ok(())
}
#[derive(Accounts)]
pub struct CreateBuffer<'info> {
#[account(
seeds = [b"message".as_ref(), b"whitelist".as_ref()],
bump = whitelist.bump,
has_one = admin,
)]
pub whitelist: Account<'info, Whitelist>,
// Also pays for account creation
#[account(mut)]
pub admin: Signer<'info>,
pub system_program: Program<'info, System>,
// remaining_accounts: - [AccumulatorInput PDA]
}
impl<'info> CreateBuffer<'info> {
/// Manually invoke transfer, allocate & assign ixs to create an account
/// to handle situation where an account already has lamports
/// since system_program::create_account will fail in this case
fn create_account<'a>(
new_account_info: &AccountInfo<'a>,
space: usize,
payer: &Signer<'a>,
seeds: &[&[&[u8]]],
system_program: &AccountInfo<'a>,
) -> Result<()> {
let target_rent = Rent::get()?.minimum_balance(space);
if new_account_info.lamports() < target_rent {
system_program::transfer(
CpiContext::new_with_signer(
system_program.to_account_info(),
Transfer {
from: payer.to_account_info(),
to: new_account_info.to_account_info(),
},
seeds,
),
target_rent - new_account_info.lamports(),
)?;
};
system_program::allocate(
CpiContext::new_with_signer(
system_program.to_account_info(),
Allocate {
account_to_allocate: new_account_info.to_account_info(),
},
seeds,
),
space.try_into().unwrap(),
)?;
system_program::assign(
CpiContext::new_with_signer(
system_program.to_account_info(),
Assign {
account_to_assign: new_account_info.to_account_info(),
},
seeds,
),
&crate::ID,
)?;
Ok(())
}
}

View File

@ -0,0 +1,65 @@
use {
crate::{
instructions::verify_message_buffer,
state::*,
MessageBufferError,
MESSAGE,
},
anchor_lang::prelude::*,
};
pub fn delete_buffer<'info>(
ctx: Context<'_, '_, '_, 'info, DeleteBuffer<'info>>,
allowed_program_auth: Pubkey,
base_account_key: Pubkey,
bump: u8,
) -> Result<()> {
let message_buffer_account_info = ctx
.remaining_accounts
.first()
.ok_or(MessageBufferError::MessageBufferNotProvided)?;
ctx.accounts
.whitelist
.is_allowed_program_auth(&allowed_program_auth)?;
verify_message_buffer(message_buffer_account_info)?;
let expected_key = Pubkey::create_program_address(
&[
allowed_program_auth.as_ref(),
MESSAGE.as_bytes(),
base_account_key.as_ref(),
&[bump],
],
&crate::ID,
)
.map_err(|_| MessageBufferError::InvalidPDA)?;
require_keys_eq!(
message_buffer_account_info.key(),
expected_key,
MessageBufferError::InvalidPDA
);
let loader = AccountLoader::<MessageBuffer>::try_from_unchecked(
&crate::ID,
message_buffer_account_info,
)?;
loader.close(ctx.accounts.admin.to_account_info())?;
Ok(())
}
#[derive(Accounts)]
pub struct DeleteBuffer<'info> {
#[account(
seeds = [b"message".as_ref(), b"whitelist".as_ref()],
bump = whitelist.bump,
has_one = admin,
)]
pub whitelist: Account<'info, Whitelist>,
// Also the recipient of the lamports from closing the buffer account
#[account(mut)]
pub admin: Signer<'info>,
// remaining_account: - [AccumulatorInput PDA]
}

View File

@ -1,3 +1,56 @@
pub use put_all::*;
use {
crate::{
state::MessageBuffer,
MessageBufferError,
},
anchor_lang::{
prelude::*,
system_program,
Discriminator,
},
};
pub use {
create_buffer::*,
delete_buffer::*,
put_all::*,
resize_buffer::*,
};
mod create_buffer;
mod delete_buffer;
mod put_all;
mod resize_buffer;
// String constants for deriving PDAs.
// An authorized program's message buffer will have PDA seeds [authorized_program_pda, MESSAGE, base_account_key],
// where authorized_program_pda is the
pub const MESSAGE: &str = "message";
pub const FUND: &str = "fund";
pub fn is_uninitialized_account(ai: &AccountInfo) -> bool {
ai.data_is_empty() && ai.owner == &system_program::ID
}
/// Verify message buffer account is initialized and has the correct discriminator.
///
/// Note: manually checking because using anchor's `AccountLoader.load()`
/// will panic since the `AccountInfo.data_len()` will not match the
/// size of the `MessageBuffer` since the `MessageBuffer` struct does not
/// include the messages.
pub fn verify_message_buffer(message_buffer_account_info: &AccountInfo) -> Result<()> {
if is_uninitialized_account(message_buffer_account_info) {
return err!(MessageBufferError::MessageBufferUninitialized);
}
let data = message_buffer_account_info.try_borrow_data()?;
if data.len() < MessageBuffer::discriminator().len() {
return Err(ErrorCode::AccountDiscriminatorNotFound.into());
}
let disc_bytes = &data[0..8];
if disc_bytes != &MessageBuffer::discriminator() {
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
}
Ok(())
}

View File

@ -1,144 +1,55 @@
use {
crate::{
instructions::verify_message_buffer,
state::*,
AccumulatorUpdaterError,
},
anchor_lang::{
prelude::*,
system_program::{
self,
CreateAccount,
},
MessageBufferError,
},
anchor_lang::prelude::*,
std::mem,
};
pub const MESSAGE: &str = "message";
pub const FUND: &str = "fund";
pub fn put_all<'info>(
ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
base_account_key: Pubkey,
messages: Vec<Vec<u8>>,
) -> Result<()> {
let cpi_caller_auth = ctx.accounts.whitelist_verifier.is_allowed()?;
let accumulator_input_ai = ctx
let message_buffer_account_info = ctx
.remaining_accounts
.first()
.ok_or(AccumulatorUpdaterError::MessageBufferNotProvided)?;
.ok_or(MessageBufferError::MessageBufferNotProvided)?;
let loader;
verify_message_buffer(message_buffer_account_info)?;
{
let accumulator_input = &mut (if is_uninitialized_account(accumulator_input_ai) {
let (pda, bump) = Pubkey::find_program_address(
&[
cpi_caller_auth.as_ref(),
MESSAGE.as_bytes(),
base_account_key.as_ref(),
],
&crate::ID,
);
require_keys_eq!(accumulator_input_ai.key(), pda);
let signer_seeds = [
cpi_caller_auth.as_ref(),
MESSAGE.as_bytes(),
base_account_key.as_ref(),
&[bump],
];
let fund_pda_bump = *ctx
.bumps
.get(FUND)
.ok_or(AccumulatorUpdaterError::FundBumpNotFound)?;
let fund_signer_seeds = [FUND.as_bytes(), &[fund_pda_bump]];
PutAll::create_account(
accumulator_input_ai,
8 + MessageBuffer::INIT_SPACE,
&ctx.accounts.fund,
&[signer_seeds.as_slice(), fund_signer_seeds.as_slice()],
&ctx.accounts.system_program,
)?;
loader = AccountLoader::<MessageBuffer>::try_from_unchecked(
&crate::ID,
accumulator_input_ai,
)?;
let mut accumulator_input = loader.load_init()?;
accumulator_input.header = BufferHeader::new(bump);
accumulator_input
} else {
loader = AccountLoader::<MessageBuffer>::try_from(accumulator_input_ai)?;
let mut accumulator_input = loader.load_mut()?;
accumulator_input.header.set_version();
accumulator_input
});
// note: redundant for uninitialized code path but safer to check here.
// compute budget cost should be minimal
accumulator_input.validate(
accumulator_input_ai.key(),
cpi_caller_auth,
base_account_key,
)?;
let account_data = &mut message_buffer_account_info.try_borrow_mut_data()?;
let header_end_index = mem::size_of::<MessageBuffer>() + 8;
let (header_bytes, body_bytes) = account_data.split_at_mut(header_end_index);
let (num_msgs, num_bytes) = accumulator_input.put_all(&messages);
if num_msgs != messages.len() {
msg!("unable to fit all messages in accumulator input account. Wrote {}/{} messages and {} bytes", num_msgs, messages.len(), num_bytes);
}
let message_buffer: &mut MessageBuffer = bytemuck::from_bytes_mut(&mut header_bytes[8..]);
message_buffer.validate(
message_buffer_account_info.key(),
cpi_caller_auth,
base_account_key,
)?;
message_buffer.refresh_header();
let (num_msgs, num_bytes) = message_buffer.put_all_in_buffer(body_bytes, &messages);
if num_msgs != messages.len() {
// FIXME: make this into an emit! event
msg!("unable to fit all messages in accumulator input account. Wrote {}/{} messages and {} bytes", num_msgs, messages.len(), num_bytes);
}
loader.exit(&crate::ID)?;
Ok(())
}
pub fn is_uninitialized_account(ai: &AccountInfo) -> bool {
ai.data_is_empty() && ai.owner == &system_program::ID
}
#[derive(Accounts)]
#[instruction( base_account_key: Pubkey)]
pub struct PutAll<'info> {
/// `Fund` is a system account that holds
/// the lamports that will be used to fund
/// `AccumulatorInput` account initialization
#[account(
mut,
seeds = [b"fund".as_ref()],
owner = system_program::System::id(),
bump,
)]
pub fund: SystemAccount<'info>,
pub whitelist_verifier: WhitelistVerifier<'info>,
pub system_program: Program<'info, System>,
// remaining_accounts: - [AccumulatorInput PDA]
}
impl<'info> PutAll<'info> {
fn create_account<'a>(
account_info: &AccountInfo<'a>,
space: usize,
payer: &AccountInfo<'a>,
seeds: &[&[&[u8]]],
system_program: &AccountInfo<'a>,
) -> Result<()> {
let lamports = Rent::get()?.minimum_balance(space);
system_program::create_account(
CpiContext::new_with_signer(
system_program.to_account_info(),
CreateAccount {
from: payer.to_account_info(),
to: account_info.to_account_info(),
},
seeds,
),
lamports,
space.try_into().unwrap(),
&crate::ID,
)?;
Ok(())
}
}

View File

@ -0,0 +1,109 @@
use {
crate::{
instructions::verify_message_buffer,
state::*,
MessageBufferError,
MESSAGE,
},
anchor_lang::{
prelude::*,
solana_program::entrypoint::MAX_PERMITTED_DATA_INCREASE,
system_program::{
self,
Transfer,
},
},
};
pub fn resize_buffer<'info>(
ctx: Context<'_, '_, '_, 'info, ResizeBuffer<'info>>,
allowed_program_auth: Pubkey,
base_account_key: Pubkey,
buffer_bump: u8,
target_size: u32,
) -> Result<()> {
let message_buffer_account_info = ctx
.remaining_accounts
.first()
.ok_or(MessageBufferError::MessageBufferNotProvided)?;
ctx.accounts
.whitelist
.is_allowed_program_auth(&allowed_program_auth)?;
verify_message_buffer(message_buffer_account_info)?;
require_gte!(
target_size,
MessageBuffer::HEADER_LEN as u32,
MessageBufferError::MessageBufferTooSmall
);
let target_size = target_size as usize;
let target_size_delta = target_size.saturating_sub(message_buffer_account_info.data_len());
require_gte!(
MAX_PERMITTED_DATA_INCREASE,
target_size_delta,
MessageBufferError::TargetSizeDeltaExceeded
);
let expected_key = Pubkey::create_program_address(
&[
allowed_program_auth.as_ref(),
MESSAGE.as_bytes(),
base_account_key.as_ref(),
&[buffer_bump],
],
&crate::ID,
)
.map_err(|_| MessageBufferError::InvalidPDA)?;
require_keys_eq!(
message_buffer_account_info.key(),
expected_key,
MessageBufferError::InvalidPDA
);
if target_size_delta > 0 {
let target_rent = Rent::get()?.minimum_balance(target_size);
if message_buffer_account_info.lamports() < target_rent {
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.admin.to_account_info(),
to: message_buffer_account_info.to_account_info(),
},
),
target_rent - message_buffer_account_info.lamports(),
)?;
}
message_buffer_account_info
.realloc(target_size, false)
.map_err(|_| MessageBufferError::ReallocFailed)?;
} else {
// Not transferring excess lamports back to admin.
// Account will retain more lamports than necessary.
message_buffer_account_info.realloc(target_size, false)?;
}
Ok(())
}
#[derive(Accounts)]
#[instruction(
allowed_program_auth: Pubkey, base_account_key: Pubkey,
buffer_bump: u8, target_size: u32
)]
pub struct ResizeBuffer<'info> {
#[account(
seeds = [b"message".as_ref(), b"whitelist".as_ref()],
bump = whitelist.bump,
has_one = admin,
)]
pub whitelist: Account<'info, Whitelist>,
// Also pays for account creation
#[account(mut)]
pub admin: Signer<'info>,
pub system_program: Program<'info, System>,
// remaining_accounts: - [AccumulatorInput PDA]
}

View File

@ -16,13 +16,13 @@ pub mod message_buffer {
use super::*;
/// Initializes the whitelist and sets it's authority to the provided pubkey
/// Initializes the whitelist and sets it's admin to the provided pubkey
/// Once initialized, the authority must sign all further changes to the whitelist.
pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> Result<()> {
require_keys_neq!(authority, Pubkey::default());
pub fn initialize(ctx: Context<Initialize>, admin: Pubkey) -> Result<()> {
require_keys_neq!(admin, Pubkey::default());
let whitelist = &mut ctx.accounts.whitelist;
whitelist.bump = *ctx.bumps.get("whitelist").unwrap();
whitelist.authority = authority;
whitelist.admin = admin;
Ok(())
}
@ -40,23 +40,19 @@ pub mod message_buffer {
Ok(())
}
/// Sets the new authority for the whitelist
pub fn update_whitelist_authority(
ctx: Context<UpdateWhitelist>,
new_authority: Pubkey,
) -> Result<()> {
/// Sets the new admin for the whitelist
pub fn update_whitelist_admin(ctx: Context<UpdateWhitelist>, new_admin: Pubkey) -> Result<()> {
let whitelist = &mut ctx.accounts.whitelist;
whitelist.validate_new_authority(new_authority)?;
whitelist.authority = new_authority;
whitelist.validate_new_admin(new_admin)?;
whitelist.admin = new_admin;
Ok(())
}
/// Insert messages/inputs for the Accumulator. All inputs derived from the
/// `base_account_key` will go into the same PDA. The PDA is derived with
/// seeds = [cpi_caller_auth, b"accumulator", base_account_key]
///
///
/// Put messages into the Accumulator. All messages put for the same
/// `base_account_key` go into the same buffer PDA. The PDA's address is
/// `[allowed_program_auth, MESSAGE, base_account_key]`, where `allowed_program_auth`
/// is the whitelisted pubkey who authorized this call.
///
/// * `base_account_key` - Pubkey of the original account the
/// `MessageBuffer` is derived from
@ -74,7 +70,6 @@ pub mod message_buffer {
/// any existing contents.
///
/// TODO:
/// - try handling re-allocation of the accumulator_input space
/// - handle updates ("paging/batches of messages")
///
pub fn put_all<'info>(
@ -84,6 +79,74 @@ pub mod message_buffer {
) -> Result<()> {
instructions::put_all(ctx, base_account_key, messages)
}
/// Initializes the buffer account with the `target_size`
///
/// *`allowed_program_auth` - The whitelisted pubkey representing an
/// allowed program. Used as one of the seeds
/// for deriving the `MessageBuffer` PDA.
/// * `base_account_key` - Pubkey of the original account the
/// `MessageBuffer` is derived from
/// (e.g. pyth price account)
/// *`target_size` - Initial size to allocate for the
/// `MessageBuffer` PDA. `target_size`
/// must be >= HEADER_LEN && <= 10240
pub fn create_buffer<'info>(
ctx: Context<'_, '_, '_, 'info, CreateBuffer<'info>>,
allowed_program_auth: Pubkey,
base_account_key: Pubkey,
target_size: u32,
) -> Result<()> {
instructions::create_buffer(ctx, allowed_program_auth, base_account_key, target_size)
}
/// Resizes the buffer account to the `target_size`
///
/// *`allowed_program_auth` - The whitelisted pubkey representing an
/// allowed program. Used as one of the seeds
/// for deriving the `MessageBuffer` PDA.
/// * `base_account_key` - Pubkey of the original account the
/// `MessageBuffer` is derived from
/// (e.g. pyth price account)
/// *`target_size` - Size to re-allocate for the
/// `MessageBuffer` PDA. If increasing the size,
/// max delta of current_size & target_size is 10240
/// *`buffer_bump` - Bump seed for the `MessageBuffer` PDA
pub fn resize_buffer<'info>(
ctx: Context<'_, '_, '_, 'info, ResizeBuffer<'info>>,
allowed_program_auth: Pubkey,
base_account_key: Pubkey,
buffer_bump: u8,
target_size: u32,
) -> Result<()> {
instructions::resize_buffer(
ctx,
allowed_program_auth,
base_account_key,
buffer_bump,
target_size,
)
}
/// Closes the buffer account and transfers the remaining lamports to the
/// `admin` account
///
/// *`allowed_program_auth` - The whitelisted pubkey representing an
/// allowed program. Used as one of the seeds
/// for deriving the `MessageBuffer` PDA.
/// * `base_account_key` - Pubkey of the original account the
/// `MessageBuffer` is derived from
/// (e.g. pyth price account)
/// *`buffer_bump` - Bump seed for the `MessageBuffer` PDA
pub fn delete_buffer<'info>(
ctx: Context<'_, '_, '_, 'info, DeleteBuffer<'info>>,
allowed_program_auth: Pubkey,
base_account_key: Pubkey,
buffer_bump: u8,
) -> Result<()> {
instructions::delete_buffer(ctx, allowed_program_auth, base_account_key, buffer_bump)
}
}
#[derive(Accounts)]
@ -107,19 +170,19 @@ pub struct UpdateWhitelist<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub authority: Signer<'info>,
pub admin: Signer<'info>,
#[account(
mut,
seeds = [b"message".as_ref(), b"whitelist".as_ref()],
bump = whitelist.bump,
has_one = authority
has_one = admin
)]
pub whitelist: Account<'info, Whitelist>,
}
#[error_code]
pub enum AccumulatorUpdaterError {
pub enum MessageBufferError {
#[msg("CPI Caller not allowed")]
CallerNotAllowed,
#[msg("Whitelist already contains program")]
@ -140,6 +203,14 @@ pub enum AccumulatorUpdaterError {
CurrentDataLengthExceeded,
#[msg("Message Buffer not provided")]
MessageBufferNotProvided,
#[msg("Message Buffer is not sufficiently large")]
MessageBufferTooSmall,
#[msg("Fund Bump not found")]
FundBumpNotFound,
#[msg("Reallocation failed")]
ReallocFailed,
#[msg("Target size too large for reallocation/initialization. Max delta is 10240")]
TargetSizeDeltaExceeded,
#[msg("MessageBuffer Uninitialized")]
MessageBufferUninitialized,
}

View File

@ -5,7 +5,7 @@ macro_rules! accumulator_input_seeds {
$cpi_caller_pid.as_ref(),
b"message".as_ref(),
$base_account.as_ref(),
&[$accumulator_input.header.bump],
&[$accumulator_input.bump],
]
};
}

View File

@ -1,36 +1,31 @@
use {
crate::{
accumulator_input_seeds,
AccumulatorUpdaterError,
MessageBufferError,
},
anchor_lang::prelude::*,
};
/// `MessageBuffer` is an arbitrary set of bytes
/// that will be included in the AccumulatorSysvar
/// A MessageBuffer will have the following structure
/// ```ignore
/// struct MessageBuffer {
/// header: BufferHeader,
/// messages: [u8; accountInfo.data.len - header.header_len]
/// }
/// ```
///
/// where `MESSAGES_LEN` can be dynamic. There is actual
/// no messages field in the `MessageBuffer` struct definition due to messages
/// needing to be a dynamic length while supporting zero_copy
/// at the same time.
///
/// The actual contents of data are set/handled by
/// the CPI calling program (e.g. Pyth Oracle)
///
/// TODO: implement custom serialization & set alignment
/// A `MessageBuffer` AccountInfo.data will look like:
/// [ <discrimintator>, <buffer_header>, <messages> ]
/// (0..8) (8..header_len) (header_len...accountInfo.data.len)
#[account(zero_copy)]
#[derive(Debug, InitSpace)]
pub struct MessageBuffer {
pub header: BufferHeader,
// 10KB - 8 (discriminator) - 514 (header)
// TODO: do we want to initialize this to the max size?
// - will lead to more data being passed around for validators
pub messages: [u8; 9_718],
}
//TODO:
// - implement custom serialization & set alignment
// - what other fields are needed?
#[zero_copy]
#[derive(InitSpace, Debug)]
pub struct BufferHeader {
pub struct MessageBuffer {
/* header */
pub bump: u8, // 1
pub version: u8, // 1
// byte offset of accounts where data starts
@ -41,14 +36,19 @@ pub struct BufferHeader {
/// => msg1 = account_info.data[(header_len + 0)..(header_len + 10)]
/// => msg2 = account_info.data[(header_len + 10)..(header_len + 14)]
pub end_offsets: [u16; 255], // 510
/* messages */
// not defined in struct since needs to support variable length
// and work with zero_copy
// pub messages: [u8; accountInfo.data.len - header_len]
}
impl BufferHeader {
impl MessageBuffer {
// HEADER_LEN allows for append-only forward-compatibility for the header.
// this is the number of bytes from the beginning of the account_info.data
// to the start of the `AccumulatorInput` data.
pub const HEADER_LEN: u16 = 8 + BufferHeader::INIT_SPACE as u16;
pub const HEADER_LEN: u16 = 8 + MessageBuffer::INIT_SPACE as u16;
pub const CURRENT_VERSION: u8 = 1;
@ -61,34 +61,35 @@ impl BufferHeader {
}
}
pub fn set_version(&mut self) {
pub fn refresh_header(&mut self) {
self.header_len = Self::HEADER_LEN;
self.version = Self::CURRENT_VERSION;
}
}
impl MessageBuffer {
pub fn new(bump: u8) -> Self {
let header = BufferHeader::new(bump);
Self {
header,
messages: [0u8; 9_718],
}
self.end_offsets = [0u16; u8::MAX as usize];
}
/// `put_all` writes all the messages to the `AccumulatorInput` account
/// and updates the `end_offsets` array.
///
/// TODO: the first byte of destination is the first non-header byte of the
/// message buffer account
///
/// Returns tuple of the number of messages written and the end_offset
/// of the last message
///
// TODO: add a end_offsets index parameter for "continuation"
// TODO: test max size of parameters that can be passed into CPI call
pub fn put_all(&mut self, values: &Vec<Vec<u8>>) -> (usize, u16) {
pub fn put_all_in_buffer(
&mut self,
destination: &mut [u8],
values: &Vec<Vec<u8>>,
) -> (usize, u16) {
let mut offset = 0u16;
for (i, v) in values.iter().enumerate() {
let start = offset;
let len = u16::try_from(v.len());
if len.is_err() {
msg!("len err");
return (i, start);
}
let end = offset.checked_add(len.unwrap());
@ -96,23 +97,22 @@ impl MessageBuffer {
return (i, start);
}
let end = end.unwrap();
if end > self.messages.len() as u16 {
if end > destination.len() as u16 {
return (i, start);
}
self.header.end_offsets[i] = end;
self.messages[(start as usize)..(end as usize)].copy_from_slice(v);
self.end_offsets[i] = end;
destination[(start as usize)..(end as usize)].copy_from_slice(v);
offset = end
}
(values.len(), offset)
}
fn derive_pda(&self, cpi_caller: Pubkey, base_account: Pubkey) -> Result<Pubkey> {
let res = Pubkey::create_program_address(
accumulator_input_seeds!(self, cpi_caller, base_account),
&crate::ID,
)
.map_err(|_| AccumulatorUpdaterError::InvalidPDA)?;
.map_err(|_| MessageBufferError::InvalidPDA)?;
Ok(res)
}
@ -123,15 +123,18 @@ impl MessageBuffer {
}
}
#[cfg(test)]
mod test {
use {
super::*,
bytemuck::bytes_of,
std::mem::{
align_of,
size_of,
anchor_lang::solana_program::keccak::hashv,
bytemuck::bytes_of_mut,
std::{
io::Write,
mem::{
align_of,
size_of,
},
},
};
@ -143,18 +146,22 @@ mod test {
bytes
}
fn sighash(namespace: &str, name: &str) -> [u8; 8] {
let preimage = format!("{namespace}:{name}");
let mut sighash = [0u8; 8];
sighash.copy_from_slice(&hashv(&[preimage.as_bytes()]).to_bytes()[..8]);
sighash
}
#[test]
fn test_sizes_and_alignments() {
let (header_idx_size, header_idx_align) =
(size_of::<BufferHeader>(), align_of::<BufferHeader>());
let (message_buffer_size, message_buffer_align) =
(size_of::<MessageBuffer>(), align_of::<MessageBuffer>());
let (input_size, input_align) = (size_of::<MessageBuffer>(), align_of::<MessageBuffer>());
assert_eq!(header_idx_size, 514);
assert_eq!(header_idx_align, 2);
assert_eq!(input_size, 10_232);
assert_eq!(input_align, 2);
assert_eq!(message_buffer_size, 514);
assert_eq!(message_buffer_align, 2);
}
#[test]
@ -162,35 +169,50 @@ mod test {
let data = vec![vec![12, 34], vec![56, 78, 90]];
let data_bytes: Vec<Vec<u8>> = data.into_iter().map(data_bytes).collect();
let accumulator_input = &mut MessageBuffer::new(0);
let message_buffer = &mut MessageBuffer::new(0);
let header_len = message_buffer.header_len as usize;
let message_buffer_bytes = bytes_of_mut(message_buffer);
// assuming account_info.data.len() == 10KB
let messages = &mut vec![0u8; 10_240 - header_len];
let account_info_data = &mut vec![];
let discriminator = &mut sighash("accounts", "MessageBuffer");
account_info_data.write_all(discriminator).unwrap();
account_info_data.write_all(message_buffer_bytes).unwrap();
account_info_data
.write_all(messages.as_mut_slice())
.unwrap();
let _account_data_len = account_info_data.len();
let destination = &mut account_info_data[(message_buffer.header_len as usize)..];
let (num_msgs, num_bytes) = message_buffer.put_all_in_buffer(destination, &data_bytes);
let (num_msgs, num_bytes) = accumulator_input.put_all(&data_bytes);
assert_eq!(num_msgs, 2);
assert_eq!(num_bytes, 5);
assert_eq!(accumulator_input.header.end_offsets[0], 2);
assert_eq!(accumulator_input.header.end_offsets[1], 5);
assert_eq!(message_buffer.end_offsets[0], 2);
assert_eq!(message_buffer.end_offsets[1], 5);
let message_buffer_bytes = bytes_of(accumulator_input);
// The header_len field represents the size of all data prior to the message bytes.
// This includes the account discriminator, which is not part of the header struct.
// Subtract the size of the discriminator (8 bytes) to compensate
let header_len = accumulator_input.header.header_len as usize - 8;
// let account_data = bytes_of(accumulator_input);
let iter = accumulator_input
.header
.end_offsets
.iter()
.take_while(|x| **x != 0);
// // The header_len field represents the size of all data prior to the message bytes.
// // This includes the account discriminator, which is not part of the header struct.
// // Subtract the size of the discriminator (8 bytes) to compensate
// let header_len = message_buffer.header_len as usize - 8;
let header_len = message_buffer.header_len as usize;
let iter = message_buffer.end_offsets.iter().take_while(|x| **x != 0);
let mut start = header_len;
let mut data_iter = data_bytes.iter();
for offset in iter {
let end_offset = header_len + *offset as usize;
let message_buffer_data = &message_buffer_bytes[start..end_offset];
let message_buffer_data = &account_info_data[start..end_offset];
let expected_data = data_iter.next().unwrap();
assert_eq!(message_buffer_data, expected_data.as_slice());
start = end_offset;
@ -202,40 +224,48 @@ mod test {
let data = vec![vec![0u8; 9_718 - 2], vec![0u8], vec![0u8; 2]];
let data_bytes: Vec<Vec<u8>> = data.into_iter().map(data_bytes).collect();
let message_buffer = &mut MessageBuffer::new(0);
let (num_msgs, num_bytes) = message_buffer.put_all(&data_bytes);
let header_len = message_buffer.header_len as usize;
let message_buffer_bytes = bytes_of_mut(message_buffer);
// assuming account_info.data.len() == 10KB
let messages = &mut vec![0u8; 10_240 - header_len];
let account_info_data = &mut vec![];
let discriminator = &mut sighash("accounts", "MessageBuffer");
account_info_data.write_all(discriminator).unwrap();
account_info_data.write_all(message_buffer_bytes).unwrap();
account_info_data
.write_all(messages.as_mut_slice())
.unwrap();
let _account_data_len = account_info_data.len();
let destination = &mut account_info_data[(message_buffer.header_len as usize)..];
let (num_msgs, num_bytes) = message_buffer.put_all_in_buffer(destination, &data_bytes);
assert_eq!(num_msgs, 2);
assert_eq!(
num_bytes,
data_bytes[0..2].iter().map(|x| x.len()).sum::<usize>() as u16
);
let message_buffer_bytes = bytes_of(message_buffer);
// The header_len field represents the size of all data prior to the message bytes.
// This includes the account discriminator, which is not part of the header struct.
// Subtract the size of the discriminator (8 bytes) to compensate
let header_len = message_buffer.header.header_len as usize - 8;
let iter = message_buffer
.header
.end_offsets
.iter()
.take_while(|x| **x != 0);
let iter = message_buffer.end_offsets.iter().take_while(|x| **x != 0);
let mut start = header_len;
let mut data_iter = data_bytes.iter();
for offset in iter {
let end_offset = header_len + *offset as usize;
let message_buffer_data = &message_buffer_bytes[start..end_offset];
let message_buffer_data = &account_info_data[start..end_offset];
let expected_data = data_iter.next().unwrap();
assert_eq!(message_buffer_data, expected_data.as_slice());
start = end_offset;
}
assert_eq!(message_buffer.header.end_offsets[2], 0);
assert_eq!(message_buffer.end_offsets[2], 0);
}
//
#[test]
fn test_put_all_long_vec() {
let data = vec![
@ -247,40 +277,52 @@ mod test {
];
let data_bytes: Vec<Vec<u8>> = data.into_iter().map(data_bytes).collect();
// let message_buffer = &mut MessageBufferTemp::new(0);
// let (num_msgs, num_bytes) = message_buffer.put_all(&data_bytes);
let message_buffer = &mut MessageBuffer::new(0);
let (num_msgs, num_bytes) = message_buffer.put_all(&data_bytes);
let header_len = message_buffer.header_len as usize;
let message_buffer_bytes = bytes_of_mut(message_buffer);
// assuming account_info.data.len() == 10KB
let messages = &mut vec![0u8; 10_240 - header_len];
let account_info_data = &mut vec![];
let discriminator = &mut sighash("accounts", "MessageBuffer");
account_info_data.write_all(discriminator).unwrap();
account_info_data.write_all(message_buffer_bytes).unwrap();
account_info_data
.write_all(messages.as_mut_slice())
.unwrap();
let _account_data_len = account_info_data.len();
let destination = &mut account_info_data[(message_buffer.header_len as usize)..];
let (num_msgs, num_bytes) = message_buffer.put_all_in_buffer(destination, &data_bytes);
assert_eq!(num_msgs, 3);
assert_eq!(
num_bytes,
data_bytes[0..3].iter().map(|x| x.len()).sum::<usize>() as u16
);
let message_buffer_bytes = bytes_of(message_buffer);
// *note* minus 8 here since no account discriminator when using
// `bytes_of`directly on accumulator_input
let header_len = message_buffer.header.header_len as usize - 8;
let iter = message_buffer
.header
.end_offsets
.iter()
.take_while(|x| **x != 0);
let iter = message_buffer.end_offsets.iter().take_while(|x| **x != 0);
let mut start = header_len;
let mut data_iter = data_bytes.iter();
for offset in iter {
let end_offset = header_len + *offset as usize;
let message_buffer_data = &message_buffer_bytes[start..end_offset];
let message_buffer_data = &account_info_data[start..end_offset];
let expected_data = data_iter.next().unwrap();
assert_eq!(message_buffer_data, expected_data.as_slice());
start = end_offset;
}
assert_eq!(message_buffer.header.end_offsets[0], 9_715);
assert_eq!(message_buffer.header.end_offsets[1], 9_716);
assert_eq!(message_buffer.header.end_offsets[2], 9_717);
assert_eq!(message_buffer.header.end_offsets[3], 0);
assert_eq!(message_buffer.header.end_offsets[4], 0);
assert_eq!(message_buffer.end_offsets[0], 9_715);
assert_eq!(message_buffer.end_offsets[1], 9_716);
assert_eq!(message_buffer.end_offsets[2], 9_717);
assert_eq!(message_buffer.end_offsets[3], 0);
assert_eq!(message_buffer.end_offsets[4], 0);
}
}

View File

@ -1,5 +1,5 @@
use {
crate::AccumulatorUpdaterError,
crate::MessageBufferError,
anchor_lang::prelude::*,
};
@ -10,7 +10,7 @@ use {
#[derive(InitSpace)]
pub struct Whitelist {
pub bump: u8,
pub authority: Pubkey,
pub admin: Pubkey,
#[max_len(32)]
pub allowed_programs: Vec<Pubkey>,
}
@ -19,18 +19,26 @@ impl Whitelist {
pub fn validate_programs(&self, allowed_programs: &[Pubkey]) -> Result<()> {
require!(
!self.allowed_programs.contains(&Pubkey::default()),
AccumulatorUpdaterError::InvalidAllowedProgram
MessageBufferError::InvalidAllowedProgram
);
require_gte!(
32,
allowed_programs.len(),
AccumulatorUpdaterError::MaximumAllowedProgramsExceeded
MessageBufferError::MaximumAllowedProgramsExceeded
);
Ok(())
}
pub fn validate_new_authority(&self, new_authority: Pubkey) -> Result<()> {
require_keys_neq!(new_authority, Pubkey::default());
pub fn validate_new_admin(&self, new_admin: Pubkey) -> Result<()> {
require_keys_neq!(new_admin, Pubkey::default());
Ok(())
}
pub fn is_allowed_program_auth(&self, auth: &Pubkey) -> Result<()> {
require!(
self.allowed_programs.contains(auth),
MessageBufferError::CallerNotAllowed
);
Ok(())
}
}
@ -51,10 +59,7 @@ impl<'info> WhitelistVerifier<'info> {
pub fn is_allowed(&self) -> Result<Pubkey> {
let auth = self.cpi_caller_auth.key();
let whitelist = &self.whitelist;
require!(
whitelist.allowed_programs.contains(&auth),
AccumulatorUpdaterError::CallerNotAllowed
);
whitelist.is_allowed_program_auth(&auth)?;
Ok(auth)
}
}

View File

@ -62,10 +62,8 @@ impl<'info> AddPrice<'info> {
inputs: Vec<Vec<u8>>,
) -> anchor_lang::Result<()> {
let mut accounts = vec![
AccountMeta::new(ctx.accounts.fund.key(), false),
AccountMeta::new_readonly(ctx.accounts.accumulator_whitelist.key(), false),
AccountMeta::new_readonly(ctx.accounts.auth.key(), true),
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
];
accounts.extend_from_slice(
&ctx.remaining_accounts
@ -131,8 +129,6 @@ pub struct AddPrice<'info> {
pub pyth_price_account: AccountLoader<'info, PriceAccount>,
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
pub fund: SystemAccount<'info>,
/// also needed for accumulator_updater
pub system_program: Program<'info, System>,
/// CHECK: whitelist
@ -147,5 +143,5 @@ pub struct AddPrice<'info> {
pub auth: SystemAccount<'info>,
pub message_buffer_program: Program<'info, MessageBufferProgram>,
// Remaining Accounts
// should all be new uninitialized accounts
// MessageBuffer PDA
}

View File

@ -42,10 +42,6 @@ pub struct UpdatePrice<'info> {
bump,
)]
pub pyth_price_account: AccountLoader<'info, PriceAccount>,
#[account(mut)]
pub fund: SystemAccount<'info>,
/// Needed for accumulator_updater
pub system_program: Program<'info, System>,
/// CHECK: whitelist
pub accumulator_whitelist: UncheckedAccount<'info>,
#[account(
@ -90,10 +86,8 @@ impl<'info> UpdatePrice<'info> {
values: Vec<Vec<u8>>,
) -> anchor_lang::Result<()> {
let mut accounts = vec![
AccountMeta::new(ctx.accounts.fund.key(), false),
AccountMeta::new_readonly(ctx.accounts.accumulator_whitelist.key(), false),
AccountMeta::new_readonly(ctx.accounts.auth.key(), true),
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
];
accounts.extend_from_slice(
&ctx.remaining_accounts

View File

@ -1,5 +1,10 @@
import * as anchor from "@coral-xyz/anchor";
import { IdlTypes, Program, BorshAccountsCoder } from "@coral-xyz/anchor";
import {
IdlAccounts,
IdlTypes,
Program,
BorshAccountsCoder,
} from "@coral-xyz/anchor";
import { MessageBuffer } from "../target/types/message_buffer";
import { MockCpiCaller } from "../target/types/mock_cpi_caller";
import lumina from "@lumina-dev/test";
@ -9,20 +14,17 @@ import bs58 from "bs58";
// Enables tool that runs in local browser for easier debugging of
// transactions in this test - https://lumina.fyi/debug
lumina();
// lumina();
const messageBufferProgram = anchor.workspace
.MessageBuffer as Program<MessageBuffer>;
const mockCpiProg = anchor.workspace.MockCpiCaller as Program<MockCpiCaller>;
let whitelistAuthority = anchor.web3.Keypair.generate();
let whitelistAdmin = anchor.web3.Keypair.generate();
const [mockCpiCallerAuth] = anchor.web3.PublicKey.findProgramAddressSync(
[messageBufferProgram.programId.toBuffer(), Buffer.from("cpi")],
mockCpiProg.programId
);
const [fundPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("fund")],
messageBufferProgram.programId
);
const pythPriceAccountId = new anchor.BN(1);
const addPriceParams = {
@ -41,12 +43,41 @@ const [pythPriceAccountPk] = anchor.web3.PublicKey.findProgramAddressSync(
mockCpiProg.programId
);
const MESSAGE = Buffer.from("message");
const [accumulatorPdaKey] = anchor.web3.PublicKey.findProgramAddressSync(
[mockCpiCallerAuth.toBuffer(), MESSAGE, pythPriceAccountPk.toBuffer()],
messageBufferProgram.programId
const [accumulatorPdaKey, accumulatorPdaBump] =
anchor.web3.PublicKey.findProgramAddressSync(
[mockCpiCallerAuth.toBuffer(), MESSAGE, pythPriceAccountPk.toBuffer()],
messageBufferProgram.programId
);
const pythPriceAccountId2 = new anchor.BN(2);
const [pythPriceAccountPk2] = anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("pyth"),
Buffer.from("price"),
pythPriceAccountId2.toArrayLike(Buffer, "le", 8),
],
mockCpiProg.programId
);
const [accumulatorPdaKey2, accumulatorPdaBump2] =
anchor.web3.PublicKey.findProgramAddressSync(
[mockCpiCallerAuth.toBuffer(), MESSAGE, pythPriceAccountPk2.toBuffer()],
messageBufferProgram.programId
);
const accumulatorPdaMeta2 = {
pubkey: accumulatorPdaKey2,
isSigner: false,
isWritable: true,
};
console.log("3");
let fundBalance = 100 * anchor.web3.LAMPORTS_PER_SOL;
const discriminator = BorshAccountsCoder.accountDiscriminator("MessageBuffer");
const messageBufferDiscriminator = bs58.encode(discriminator);
describe("accumulator_updater", () => {
// Configure the client to use the local cluster.
let provider = anchor.AnchorProvider.env();
@ -58,14 +89,26 @@ describe("accumulator_updater", () => {
messageBufferProgram.programId
);
before("transfer lamports to the fund", async () => {
await provider.connection.requestAirdrop(fundPda, fundBalance);
before("transfer lamports to needed accounts", async () => {
const airdropTxnSig = await provider.connection.requestAirdrop(
whitelistAdmin.publicKey,
fundBalance
);
await provider.connection.confirmTransaction({
signature: airdropTxnSig,
...(await provider.connection.getLatestBlockhash()),
});
const whitelistAuthorityBalance = await provider.connection.getBalance(
whitelistAdmin.publicKey
);
assert.isTrue(whitelistAuthorityBalance === fundBalance);
});
it("Is initialized!", async () => {
// Add your test here.
const tx = await messageBufferProgram.methods
.initialize(whitelistAuthority.publicKey)
.initialize(whitelistAdmin.publicKey)
.accounts({})
.rpc();
console.log("Your transaction signature", tx);
@ -74,7 +117,7 @@ describe("accumulator_updater", () => {
whitelistPubkey
);
assert.strictEqual(whitelist.bump, whitelistBump);
assert.isTrue(whitelist.authority.equals(whitelistAuthority.publicKey));
assert.isTrue(whitelist.admin.equals(whitelistAdmin.publicKey));
console.info(`whitelist: ${JSON.stringify(whitelist)}`);
});
@ -83,9 +126,9 @@ describe("accumulator_updater", () => {
await messageBufferProgram.methods
.setAllowedPrograms(allowedProgramAuthorities)
.accounts({
authority: whitelistAuthority.publicKey,
admin: whitelistAdmin.publicKey,
})
.signers([whitelistAuthority])
.signers([whitelistAdmin])
.rpc();
const whitelist = await messageBufferProgram.account.whitelist.fetch(
whitelistPubkey
@ -100,29 +143,131 @@ describe("accumulator_updater", () => {
);
});
it("Updates the whitelist authority", async () => {
const newWhitelistAuthority = anchor.web3.Keypair.generate();
it("Creates a buffer", async () => {
const accumulatorPdaMetas = [
{
pubkey: accumulatorPdaKey,
isSigner: false,
isWritable: true,
},
];
await messageBufferProgram.methods
.updateWhitelistAuthority(newWhitelistAuthority.publicKey)
.createBuffer(mockCpiCallerAuth, pythPriceAccountPk, 1024 * 8)
.accounts({
authority: whitelistAuthority.publicKey,
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whitelistAuthority])
.signers([whitelistAdmin])
.remainingAccounts(accumulatorPdaMetas)
.rpc({ skipPreflight: true });
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey
);
const messageBufferHeader = deserializeMessageBufferHeader(
messageBufferProgram,
messageBufferAccountData
);
assert.equal(messageBufferHeader.version, 1);
assert.equal(messageBufferHeader.bump, accumulatorPdaBump);
});
it("Creates a buffer even if the account already has lamports", async () => {
const minimumEmptyRent =
await provider.connection.getMinimumBalanceForRentExemption(0);
await provider.sendAndConfirm(
(() => {
const tx = new anchor.web3.Transaction();
tx.add(
anchor.web3.SystemProgram.transfer({
fromPubkey: provider.wallet.publicKey,
toPubkey: accumulatorPdaKey2,
lamports: minimumEmptyRent,
})
);
return tx;
})()
);
const accumulatorPdaBalance = await provider.connection.getBalance(
accumulatorPdaKey2
);
console.log(`accumulatorPdaBalance: ${accumulatorPdaBalance}`);
assert.isTrue(accumulatorPdaBalance === minimumEmptyRent);
await messageBufferProgram.methods
.createBuffer(mockCpiCallerAuth, pythPriceAccountPk2, 1000)
.accounts({
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whitelistAdmin])
.remainingAccounts([accumulatorPdaMeta2])
.rpc({ skipPreflight: true });
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey2
);
const minimumMessageBufferRent =
await provider.connection.getMinimumBalanceForRentExemption(
messageBufferAccountData.length
);
const accumulatorPdaBalanceAfter = await provider.connection.getBalance(
accumulatorPdaKey2
);
assert.isTrue(accumulatorPdaBalanceAfter === minimumMessageBufferRent);
const messageBufferHeader = deserializeMessageBufferHeader(
messageBufferProgram,
messageBufferAccountData
);
console.log(`header: ${JSON.stringify(messageBufferHeader)}`);
assert.equal(messageBufferHeader.bump, accumulatorPdaBump2);
assert.equal(messageBufferAccountData[8], accumulatorPdaBump2);
assert.equal(messageBufferHeader.version, 1);
});
it("Updates the whitelist authority", async () => {
const newWhitelistAdmin = anchor.web3.Keypair.generate();
await messageBufferProgram.methods
.updateWhitelistAdmin(newWhitelistAdmin.publicKey)
.accounts({
admin: whitelistAdmin.publicKey,
})
.signers([whitelistAdmin])
.rpc();
const whitelist = await messageBufferProgram.account.whitelist.fetch(
let whitelist = await messageBufferProgram.account.whitelist.fetch(
whitelistPubkey
);
assert.isTrue(whitelist.authority.equals(newWhitelistAuthority.publicKey));
assert.isTrue(whitelist.admin.equals(newWhitelistAdmin.publicKey));
whitelistAuthority = newWhitelistAuthority;
// swap back to original authority
await messageBufferProgram.methods
.updateWhitelistAdmin(whitelistAdmin.publicKey)
.accounts({
admin: newWhitelistAdmin.publicKey,
})
.signers([newWhitelistAdmin])
.rpc();
whitelist = await messageBufferProgram.account.whitelist.fetch(
whitelistPubkey
);
assert.isTrue(whitelist.admin.equals(whitelistAdmin.publicKey));
});
it("Mock CPI program - AddPrice", async () => {
const mockCpiCallerAddPriceTxPubkeys = await mockCpiProg.methods
.addPrice(addPriceParams)
.accounts({
fund: fundPda,
systemProgram: anchor.web3.SystemProgram.programId,
auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey,
@ -183,10 +328,14 @@ describe("accumulator_updater", () => {
mockCpiCallerAddPriceTxPubkeys.pythPriceAccount
);
const messageBuffer =
await messageBufferProgram.account.messageBuffer.fetch(accumulatorPdaKey);
const messageBufferAccount = await provider.connection.getAccountInfo(
accumulatorPdaKey
);
const accumulatorPriceMessages = parseMessageBuffer(messageBuffer);
const accumulatorPriceMessages = parseMessageBuffer(
messageBufferProgram,
messageBufferAccount.data
);
console.log(
`accumulatorPriceMessages: ${JSON.stringify(
@ -200,29 +349,11 @@ describe("accumulator_updater", () => {
assert.isTrue(pm.price.eq(addPriceParams.price));
assert.isTrue(pm.priceExpo.eq(addPriceParams.priceExpo));
});
const fundBalanceAfter = await provider.connection.getBalance(fundPda);
assert.isTrue(fundBalance > fundBalanceAfter);
});
it("Fetches MessageBuffer using getProgramAccounts with discriminator", async () => {
let discriminator =
BorshAccountsCoder.accountDiscriminator("MessageBuffer");
let messageBufferDiscriminator = bs58.encode(discriminator);
// fetch using `getProgramAccounts` and memcmp filter
const messageBufferAccounts = await provider.connection.getProgramAccounts(
messageBufferProgram.programId,
{
filters: [
{
memcmp: {
offset: 0,
bytes: messageBufferDiscriminator,
},
},
],
}
const messageBufferAccounts = await getProgramAccountsForMessageBuffers(
provider.connection
);
const msgBufferAcctKeys = messageBufferAccounts.map((ai) =>
ai.pubkey.toString()
@ -231,7 +362,7 @@ describe("accumulator_updater", () => {
`messageBufferAccounts: ${JSON.stringify(msgBufferAcctKeys, null, 2)}`
);
assert.isTrue(messageBufferAccounts.length === 1);
assert.isTrue(messageBufferAccounts.length === 2);
msgBufferAcctKeys.includes(accumulatorPdaKey.toString());
});
@ -250,7 +381,6 @@ describe("accumulator_updater", () => {
await mockCpiProg.methods
.updatePrice(updatePriceParams)
.accounts({
fund: fundPda,
pythPriceAccount: pythPriceAccountPk,
auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey,
@ -271,11 +401,16 @@ describe("accumulator_updater", () => {
assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
const messageBuffer =
await messageBufferProgram.account.messageBuffer.fetch(
accumulatorPdaMeta.pubkey
);
const updatedAccumulatorPriceMessages = parseMessageBuffer(messageBuffer);
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey
);
const updatedAccumulatorPriceMessages = parseMessageBuffer(
messageBufferProgram,
messageBufferAccountData
);
console.log(
`updatedAccumulatorPriceMessages: ${JSON.stringify(
@ -299,11 +434,12 @@ describe("accumulator_updater", () => {
let testCase = testCases[i];
console.info(`testCase: ${testCase}`);
const updatePriceParams = {
price: new anchor.BN(10 * i + 5),
priceExpo: new anchor.BN(10 & (i + 6)),
price: new anchor.BN(10 * (i + 5)),
priceExpo: new anchor.BN(10 * (i + 6)),
ema: new anchor.BN(10 * i + 7),
emaExpo: new anchor.BN(10 * i + 8),
};
console.log(`updatePriceParams: ${JSON.stringify(updatePriceParams)}`);
let accumulatorPdaMeta = getAccumulatorPdaMeta(
mockCpiCallerAuth,
@ -312,7 +448,6 @@ describe("accumulator_updater", () => {
await mockCpiProg.methods
.cpiMaxTest(updatePriceParams, testCase)
.accounts({
fund: fundPda,
pythPriceAccount: pythPriceAccountPk,
auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey,
@ -333,28 +468,36 @@ describe("accumulator_updater", () => {
assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
const messageBuffer =
await messageBufferProgram.account.messageBuffer.fetch(
accumulatorPdaMeta.pubkey
);
const updatedAccumulatorPriceMessages = parseMessageBuffer(messageBuffer);
console.log(
`updatedAccumulatorPriceMessages: ${JSON.stringify(
updatedAccumulatorPriceMessages,
null,
2
)}`
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey
);
updatedAccumulatorPriceMessages.forEach((pm) => {
assert.isTrue(pm.id.eq(addPriceParams.id));
assert.isTrue(pm.price.eq(updatePriceParams.price));
assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo));
});
const messageBufferHeader = deserializeMessageBufferHeader(
messageBufferProgram,
messageBufferAccountData
);
console.log(`header: ${JSON.stringify(messageBufferHeader)}`);
let mockCpiMessageHeaderLen = 7;
let currentExpectedOffset = 0;
for (let j = 0; j < testCase.length; j++) {
currentExpectedOffset += testCase[j];
currentExpectedOffset += mockCpiMessageHeaderLen;
console.log(`
header.endOffsets[${j}]: ${messageBufferHeader.endOffsets[j]}
currentExpectedOffset: ${currentExpectedOffset}
`);
assert.isTrue(
messageBufferHeader.endOffsets[j] === currentExpectedOffset
);
}
}
});
it("Mock CPI Program - CPI Max Test Fail", async () => {
it("Mock CPI Program - Exceed CPI Max Test ", async () => {
// with loosen CPI feature activated, max cpi instruction size len is 10KB
let testCases = [[1024, 2048, 4096, 8192]];
// for (let i = 1; i < 8; i++) {
@ -363,7 +506,7 @@ describe("accumulator_updater", () => {
console.info(`testCase: ${testCase}`);
const updatePriceParams = {
price: new anchor.BN(10 * i + 5),
priceExpo: new anchor.BN(10 & (i + 6)),
priceExpo: new anchor.BN(10 * (i + 6)),
ema: new anchor.BN(10 * i + 7),
emaExpo: new anchor.BN(10 * i + 8),
};
@ -377,7 +520,6 @@ describe("accumulator_updater", () => {
await mockCpiProg.methods
.cpiMaxTest(updatePriceParams, testCase)
.accounts({
fund: fundPda,
pythPriceAccount: pythPriceAccountPk,
auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey,
@ -396,6 +538,192 @@ describe("accumulator_updater", () => {
assert.ok(errorThrown);
}
});
it("Resizes a buffer to a valid larger size", async () => {
const messageBufferAccountDataBefore = await getMessageBuffer(
provider.connection,
accumulatorPdaKey2
);
const messageBufferAccountDataLenBefore =
messageBufferAccountDataBefore.length;
// check that header is stil the same as before
const messageBufferHeaderBefore = deserializeMessageBufferHeader(
messageBufferProgram,
messageBufferAccountDataBefore
);
const whitelistAuthorityBalanceBefore =
await provider.connection.getBalance(whitelistAdmin.publicKey);
console.log(
`whitelistAuthorityBalance: ${whitelistAuthorityBalanceBefore}`
);
const targetSize = 10 * 1024;
await messageBufferProgram.methods
.resizeBuffer(
mockCpiCallerAuth,
pythPriceAccountPk2,
accumulatorPdaBump2,
targetSize
)
.accounts({
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whitelistAdmin])
.remainingAccounts([accumulatorPdaMeta2])
.rpc({ skipPreflight: true });
const whitelistAuthorityBalanceAfter = await provider.connection.getBalance(
whitelistAdmin.publicKey
);
assert.isTrue(
whitelistAuthorityBalanceAfter < whitelistAuthorityBalanceBefore
);
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey2
);
assert.equal(messageBufferAccountData.length, targetSize);
// check that header is still the same as before
const messageBufferHeader = deserializeMessageBufferHeader(
messageBufferProgram,
messageBufferAccountData
);
assert.deepEqual(
messageBufferHeader.endOffsets,
messageBufferHeaderBefore.endOffsets
);
assert.deepEqual(
messageBufferAccountData.subarray(0, messageBufferAccountDataLenBefore),
messageBufferAccountDataBefore
);
});
it("Resizes a buffer to a smaller size", async () => {
const targetSize = 4 * 1024;
await messageBufferProgram.methods
.resizeBuffer(
mockCpiCallerAuth,
pythPriceAccountPk2,
accumulatorPdaBump2,
targetSize
)
.accounts({
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whitelistAdmin])
.remainingAccounts([accumulatorPdaMeta2])
.rpc({ skipPreflight: true });
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey2
);
assert.equal(messageBufferAccountData.length, targetSize);
});
it("Fails to resize buffers to invalid sizes", async () => {
// resize more than 10KB in one txn and less than header.header_len should be fail
const testCases = [20 * 1024, 2];
for (const testCase of testCases) {
let errorThrown = false;
try {
await messageBufferProgram.methods
.resizeBuffer(
mockCpiCallerAuth,
pythPriceAccountPk2,
accumulatorPdaBump2,
testCase
)
.accounts({
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whitelistAdmin])
.remainingAccounts([accumulatorPdaMeta2])
.rpc({ skipPreflight: true });
} catch (_err) {
errorThrown = true;
}
assert.ok(errorThrown);
}
});
it("Deletes a buffer", async () => {
await messageBufferProgram.methods
.deleteBuffer(mockCpiCallerAuth, pythPriceAccountPk2, accumulatorPdaBump2)
.accounts({
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
})
.signers([whitelistAdmin])
.remainingAccounts([accumulatorPdaMeta2])
.rpc({ skipPreflight: true });
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey2
);
if (messageBufferAccountData != null) {
assert.fail("messageBufferAccountData should be null");
}
const messageBufferAccounts = await getProgramAccountsForMessageBuffers(
provider.connection
);
assert.equal(messageBufferAccounts.length, 1);
assert.isFalse(
messageBufferAccounts
.map((a) => a.pubkey.toString())
.includes(accumulatorPdaKey2.toString())
);
});
it("Can recreate a buffer after it's been deleted", async () => {
await messageBufferProgram.methods
.createBuffer(mockCpiCallerAuth, pythPriceAccountPk2, 1000)
.accounts({
whitelist: whitelistPubkey,
admin: whitelistAdmin.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([whitelistAdmin])
.remainingAccounts([accumulatorPdaMeta2])
.rpc({ skipPreflight: true });
const messageBufferAccountData = await getMessageBuffer(
provider.connection,
accumulatorPdaKey2
);
const minimumMessageBufferRent =
await provider.connection.getMinimumBalanceForRentExemption(
messageBufferAccountData.length
);
const accumulatorPdaBalanceAfter = await provider.connection.getBalance(
accumulatorPdaKey2
);
assert.isTrue(accumulatorPdaBalanceAfter === minimumMessageBufferRent);
const messageBufferHeader = deserializeMessageBufferHeader(
messageBufferProgram,
messageBufferAccountData
);
console.log(`header: ${JSON.stringify(messageBufferHeader)}`);
assert.equal(messageBufferHeader.bump, accumulatorPdaBump2);
assert.equal(messageBufferAccountData[8], accumulatorPdaBump2);
assert.equal(messageBufferHeader.version, 1);
});
});
export const getAccumulatorPdaMeta = (
@ -413,23 +741,35 @@ export const getAccumulatorPdaMeta = (
};
};
type BufferHeader = IdlTypes<MessageBuffer>["BufferHeader"];
async function getMessageBuffer(
connection: anchor.web3.Connection,
accountKey: anchor.web3.PublicKey
): Promise<Buffer | null> {
let accountInfo = await connection.getAccountInfo(accountKey);
return accountInfo ? accountInfo.data : null;
}
// Parses MessageBuffer.data into a PriceAccount or PriceOnly object based on the
// accountType and accountSchema.
function parseMessageBuffer({
header,
messages,
}: {
header: BufferHeader;
messages: number[];
}): AccumulatorPriceMessage[] {
const accumulatorMessages = [];
let dataBuffer = Buffer.from(messages);
function parseMessageBuffer(
messageBufferProgram: Program<MessageBuffer>,
accountData: Buffer
): AccumulatorPriceMessage[] {
const msgBufferHeader = deserializeMessageBufferHeader(
messageBufferProgram,
accountData
);
const accumulatorMessages = [];
// let dataBuffer = Buffer.from(messages);
let dataBuffer = accountData.subarray(
msgBufferHeader.headerLen,
accountData.length
);
let start = 0;
for (let i = 0; i < header.endOffsets.length; i++) {
const endOffset = header.endOffsets[i];
for (let i = 0; i < msgBufferHeader.endOffsets.length; i++) {
const endOffset = msgBufferHeader.endOffsets[i];
if (endOffset == 0) {
console.log(`endOffset = 0. breaking`);
@ -459,12 +799,22 @@ type MessageHeader = {
size: number;
};
type MessageBuffer = {
type MessageBufferType = {
header: MessageHeader;
data: Buffer;
};
function parseMessageBytes(data: Buffer): MessageBuffer {
function deserializeMessageBufferHeader(
messageBufferProgram: Program<MessageBuffer>,
accountData: Buffer
): IdlAccounts<MessageBuffer>["messageBuffer"] {
return messageBufferProgram.coder.accounts.decode(
"MessageBuffer",
accountData
);
}
function parseMessageBytes(data: Buffer): MessageBufferType {
let offset = 0;
const schema = data.readInt8(offset);
@ -488,8 +838,6 @@ function parseMessageBytes(data: Buffer): MessageBuffer {
};
}
//TODO: follow wormhole sdk parsing structure?
// - https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/vaa/generic.ts
type AccumulatorPriceMessage = FullPriceMessage | CompactPriceMessage;
type FullPriceMessage = {
@ -522,3 +870,19 @@ function parseCompactPriceMessage(data: Uint8Array): CompactPriceMessage {
priceExpo: new anchor.BN(data.subarray(16, 24), "be"),
};
}
// fetch MessageBuffer accounts using `getProgramAccounts` and memcmp filter
async function getProgramAccountsForMessageBuffers(
connection: anchor.web3.Connection
) {
return await connection.getProgramAccounts(messageBufferProgram.programId, {
filters: [
{
memcmp: {
offset: 0,
bytes: messageBufferDiscriminator,
},
},
],
});
}