[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" language: "rust"
entry: cargo +nightly fmt --manifest-path ./message_buffer/Cargo.toml --all -- --config-path rustfmt.toml entry: cargo +nightly fmt --manifest-path ./message_buffer/Cargo.toml --all -- --config-path rustfmt.toml
pass_filenames: false pass_filenames: false
files: accumulator_updater files: message_buffer
- id: cargo-clippy-message-buffer - id: cargo-clippy-message-buffer
name: Cargo clippy for message buffer contract name: Cargo clippy for message buffer contract
language: "rust" language: "rust"
entry: cargo +nightly clippy --manifest-path ./message_buffer/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings entry: cargo +nightly clippy --manifest-path ./message_buffer/Cargo.toml --tests --fix --allow-dirty --allow-staged -- -D warnings
pass_filenames: false pass_filenames: false
files: accumulator_updater files: message_buffer
# Hooks for solana receiver contract # Hooks for solana receiver contract
- id: cargo-fmt-solana-receiver - id: cargo-fmt-solana-receiver
name: Cargo format for solana target chain contract 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 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 { use {
crate::{ crate::{
instructions::verify_message_buffer,
state::*, state::*,
AccumulatorUpdaterError, MessageBufferError,
},
anchor_lang::{
prelude::*,
system_program::{
self,
CreateAccount,
},
}, },
anchor_lang::prelude::*,
std::mem,
}; };
pub const MESSAGE: &str = "message";
pub const FUND: &str = "fund";
pub fn put_all<'info>( pub fn put_all<'info>(
ctx: Context<'_, '_, '_, 'info, PutAll<'info>>, ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
base_account_key: Pubkey, base_account_key: Pubkey,
messages: Vec<Vec<u8>>, messages: Vec<Vec<u8>>,
) -> Result<()> { ) -> Result<()> {
let cpi_caller_auth = ctx.accounts.whitelist_verifier.is_allowed()?; let cpi_caller_auth = ctx.accounts.whitelist_verifier.is_allowed()?;
let accumulator_input_ai = ctx let message_buffer_account_info = ctx
.remaining_accounts .remaining_accounts
.first() .first()
.ok_or(AccumulatorUpdaterError::MessageBufferNotProvided)?; .ok_or(MessageBufferError::MessageBufferNotProvided)?;
let loader; verify_message_buffer(message_buffer_account_info)?;
{ let account_data = &mut message_buffer_account_info.try_borrow_mut_data()?;
let accumulator_input = &mut (if is_uninitialized_account(accumulator_input_ai) { let header_end_index = mem::size_of::<MessageBuffer>() + 8;
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 (header_bytes, body_bytes) = account_data.split_at_mut(header_end_index);
let (num_msgs, num_bytes) = accumulator_input.put_all(&messages); let message_buffer: &mut MessageBuffer = bytemuck::from_bytes_mut(&mut header_bytes[8..]);
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); 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(()) Ok(())
} }
pub fn is_uninitialized_account(ai: &AccountInfo) -> bool {
ai.data_is_empty() && ai.owner == &system_program::ID
}
#[derive(Accounts)] #[derive(Accounts)]
#[instruction( base_account_key: Pubkey)] #[instruction( base_account_key: Pubkey)]
pub struct PutAll<'info> { 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 whitelist_verifier: WhitelistVerifier<'info>,
pub system_program: Program<'info, System>,
// remaining_accounts: - [AccumulatorInput PDA] // 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::*; 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. /// Once initialized, the authority must sign all further changes to the whitelist.
pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> Result<()> { pub fn initialize(ctx: Context<Initialize>, admin: Pubkey) -> Result<()> {
require_keys_neq!(authority, Pubkey::default()); require_keys_neq!(admin, Pubkey::default());
let whitelist = &mut ctx.accounts.whitelist; let whitelist = &mut ctx.accounts.whitelist;
whitelist.bump = *ctx.bumps.get("whitelist").unwrap(); whitelist.bump = *ctx.bumps.get("whitelist").unwrap();
whitelist.authority = authority; whitelist.admin = admin;
Ok(()) Ok(())
} }
@ -40,23 +40,19 @@ pub mod message_buffer {
Ok(()) Ok(())
} }
/// Sets the new authority for the whitelist /// Sets the new admin for the whitelist
pub fn update_whitelist_authority( pub fn update_whitelist_admin(ctx: Context<UpdateWhitelist>, new_admin: Pubkey) -> Result<()> {
ctx: Context<UpdateWhitelist>,
new_authority: Pubkey,
) -> Result<()> {
let whitelist = &mut ctx.accounts.whitelist; let whitelist = &mut ctx.accounts.whitelist;
whitelist.validate_new_authority(new_authority)?; whitelist.validate_new_admin(new_admin)?;
whitelist.authority = new_authority; whitelist.admin = new_admin;
Ok(()) Ok(())
} }
/// Insert messages/inputs for the Accumulator. All inputs derived from the /// Put messages into the Accumulator. All messages put for the same
/// `base_account_key` will go into the same PDA. The PDA is derived with /// `base_account_key` go into the same buffer PDA. The PDA's address is
/// seeds = [cpi_caller_auth, b"accumulator", base_account_key] /// `[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 /// * `base_account_key` - Pubkey of the original account the
/// `MessageBuffer` is derived from /// `MessageBuffer` is derived from
@ -74,7 +70,6 @@ pub mod message_buffer {
/// any existing contents. /// any existing contents.
/// ///
/// TODO: /// TODO:
/// - try handling re-allocation of the accumulator_input space
/// - handle updates ("paging/batches of messages") /// - handle updates ("paging/batches of messages")
/// ///
pub fn put_all<'info>( pub fn put_all<'info>(
@ -84,6 +79,74 @@ pub mod message_buffer {
) -> Result<()> { ) -> Result<()> {
instructions::put_all(ctx, base_account_key, messages) 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)] #[derive(Accounts)]
@ -107,19 +170,19 @@ pub struct UpdateWhitelist<'info> {
#[account(mut)] #[account(mut)]
pub payer: Signer<'info>, pub payer: Signer<'info>,
pub authority: Signer<'info>, pub admin: Signer<'info>,
#[account( #[account(
mut, mut,
seeds = [b"message".as_ref(), b"whitelist".as_ref()], seeds = [b"message".as_ref(), b"whitelist".as_ref()],
bump = whitelist.bump, bump = whitelist.bump,
has_one = authority has_one = admin
)] )]
pub whitelist: Account<'info, Whitelist>, pub whitelist: Account<'info, Whitelist>,
} }
#[error_code] #[error_code]
pub enum AccumulatorUpdaterError { pub enum MessageBufferError {
#[msg("CPI Caller not allowed")] #[msg("CPI Caller not allowed")]
CallerNotAllowed, CallerNotAllowed,
#[msg("Whitelist already contains program")] #[msg("Whitelist already contains program")]
@ -140,6 +203,14 @@ pub enum AccumulatorUpdaterError {
CurrentDataLengthExceeded, CurrentDataLengthExceeded,
#[msg("Message Buffer not provided")] #[msg("Message Buffer not provided")]
MessageBufferNotProvided, MessageBufferNotProvided,
#[msg("Message Buffer is not sufficiently large")]
MessageBufferTooSmall,
#[msg("Fund Bump not found")] #[msg("Fund Bump not found")]
FundBumpNotFound, 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(), $cpi_caller_pid.as_ref(),
b"message".as_ref(), b"message".as_ref(),
$base_account.as_ref(), $base_account.as_ref(),
&[$accumulator_input.header.bump], &[$accumulator_input.bump],
] ]
}; };
} }

View File

@ -1,36 +1,31 @@
use { use {
crate::{ crate::{
accumulator_input_seeds, accumulator_input_seeds,
AccumulatorUpdaterError, MessageBufferError,
}, },
anchor_lang::prelude::*, anchor_lang::prelude::*,
}; };
/// A MessageBuffer will have the following structure
/// `MessageBuffer` is an arbitrary set of bytes /// ```ignore
/// that will be included in the AccumulatorSysvar /// 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 /// A `MessageBuffer` AccountInfo.data will look like:
/// the CPI calling program (e.g. Pyth Oracle) /// [ <discrimintator>, <buffer_header>, <messages> ]
/// /// (0..8) (8..header_len) (header_len...accountInfo.data.len)
/// TODO: implement custom serialization & set alignment
#[account(zero_copy)] #[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)] #[derive(InitSpace, Debug)]
pub struct BufferHeader { pub struct MessageBuffer {
/* header */
pub bump: u8, // 1 pub bump: u8, // 1
pub version: u8, // 1 pub version: u8, // 1
// byte offset of accounts where data starts // byte offset of accounts where data starts
@ -41,14 +36,19 @@ pub struct BufferHeader {
/// => msg1 = account_info.data[(header_len + 0)..(header_len + 10)] /// => msg1 = account_info.data[(header_len + 0)..(header_len + 10)]
/// => msg2 = account_info.data[(header_len + 10)..(header_len + 14)] /// => msg2 = account_info.data[(header_len + 10)..(header_len + 14)]
pub end_offsets: [u16; 255], // 510 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. // 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 // this is the number of bytes from the beginning of the account_info.data
// to the start of the `AccumulatorInput` 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; 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; self.version = Self::CURRENT_VERSION;
} self.end_offsets = [0u16; u8::MAX as usize];
}
impl MessageBuffer {
pub fn new(bump: u8) -> Self {
let header = BufferHeader::new(bump);
Self {
header,
messages: [0u8; 9_718],
}
} }
/// `put_all` writes all the messages to the `AccumulatorInput` account /// `put_all` writes all the messages to the `AccumulatorInput` account
/// and updates the `end_offsets` array. /// 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 /// Returns tuple of the number of messages written and the end_offset
/// of the last message /// of the last message
/// ///
// TODO: add a end_offsets index parameter for "continuation" // TODO: add a end_offsets index parameter for "continuation"
// TODO: test max size of parameters that can be passed into CPI call // 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; let mut offset = 0u16;
for (i, v) in values.iter().enumerate() { for (i, v) in values.iter().enumerate() {
let start = offset; let start = offset;
let len = u16::try_from(v.len()); let len = u16::try_from(v.len());
if len.is_err() { if len.is_err() {
msg!("len err");
return (i, start); return (i, start);
} }
let end = offset.checked_add(len.unwrap()); let end = offset.checked_add(len.unwrap());
@ -96,23 +97,22 @@ impl MessageBuffer {
return (i, start); return (i, start);
} }
let end = end.unwrap(); let end = end.unwrap();
if end > self.messages.len() as u16 { if end > destination.len() as u16 {
return (i, start); return (i, start);
} }
self.header.end_offsets[i] = end; self.end_offsets[i] = end;
self.messages[(start as usize)..(end as usize)].copy_from_slice(v); destination[(start as usize)..(end as usize)].copy_from_slice(v);
offset = end offset = end
} }
(values.len(), offset) (values.len(), offset)
} }
fn derive_pda(&self, cpi_caller: Pubkey, base_account: Pubkey) -> Result<Pubkey> { fn derive_pda(&self, cpi_caller: Pubkey, base_account: Pubkey) -> Result<Pubkey> {
let res = Pubkey::create_program_address( let res = Pubkey::create_program_address(
accumulator_input_seeds!(self, cpi_caller, base_account), accumulator_input_seeds!(self, cpi_caller, base_account),
&crate::ID, &crate::ID,
) )
.map_err(|_| AccumulatorUpdaterError::InvalidPDA)?; .map_err(|_| MessageBufferError::InvalidPDA)?;
Ok(res) Ok(res)
} }
@ -123,15 +123,18 @@ impl MessageBuffer {
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use { use {
super::*, super::*,
bytemuck::bytes_of, anchor_lang::solana_program::keccak::hashv,
std::mem::{ bytemuck::bytes_of_mut,
align_of, std::{
size_of, io::Write,
mem::{
align_of,
size_of,
},
}, },
}; };
@ -143,18 +146,22 @@ mod test {
bytes 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] #[test]
fn test_sizes_and_alignments() { fn test_sizes_and_alignments() {
let (header_idx_size, header_idx_align) = let (message_buffer_size, message_buffer_align) =
(size_of::<BufferHeader>(), align_of::<BufferHeader>()); (size_of::<MessageBuffer>(), align_of::<MessageBuffer>());
let (input_size, input_align) = (size_of::<MessageBuffer>(), align_of::<MessageBuffer>()); assert_eq!(message_buffer_size, 514);
assert_eq!(message_buffer_align, 2);
assert_eq!(header_idx_size, 514);
assert_eq!(header_idx_align, 2);
assert_eq!(input_size, 10_232);
assert_eq!(input_align, 2);
} }
#[test] #[test]
@ -162,35 +169,50 @@ mod test {
let data = vec![vec![12, 34], vec![56, 78, 90]]; let data = vec![vec![12, 34], vec![56, 78, 90]];
let data_bytes: Vec<Vec<u8>> = data.into_iter().map(data_bytes).collect(); 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_msgs, 2);
assert_eq!(num_bytes, 5); assert_eq!(num_bytes, 5);
assert_eq!(accumulator_input.header.end_offsets[0], 2); assert_eq!(message_buffer.end_offsets[0], 2);
assert_eq!(accumulator_input.header.end_offsets[1], 5); assert_eq!(message_buffer.end_offsets[1], 5);
let message_buffer_bytes = bytes_of(accumulator_input); // let account_data = 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 iter = accumulator_input // // The header_len field represents the size of all data prior to the message bytes.
.header // // This includes the account discriminator, which is not part of the header struct.
.end_offsets // // Subtract the size of the discriminator (8 bytes) to compensate
.iter() // let header_len = message_buffer.header_len as usize - 8;
.take_while(|x| **x != 0); 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 start = header_len;
let mut data_iter = data_bytes.iter(); let mut data_iter = data_bytes.iter();
for offset in iter { for offset in iter {
let end_offset = header_len + *offset as usize; 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(); let expected_data = data_iter.next().unwrap();
assert_eq!(message_buffer_data, expected_data.as_slice()); assert_eq!(message_buffer_data, expected_data.as_slice());
start = end_offset; start = end_offset;
@ -202,40 +224,48 @@ mod test {
let data = vec![vec![0u8; 9_718 - 2], vec![0u8], vec![0u8; 2]]; 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 data_bytes: Vec<Vec<u8>> = data.into_iter().map(data_bytes).collect();
let message_buffer = &mut MessageBuffer::new(0); 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_msgs, 2);
assert_eq!( assert_eq!(
num_bytes, num_bytes,
data_bytes[0..2].iter().map(|x| x.len()).sum::<usize>() as u16 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. let iter = message_buffer.end_offsets.iter().take_while(|x| **x != 0);
// 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 mut start = header_len; let mut start = header_len;
let mut data_iter = data_bytes.iter(); let mut data_iter = data_bytes.iter();
for offset in iter { for offset in iter {
let end_offset = header_len + *offset as usize; 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(); let expected_data = data_iter.next().unwrap();
assert_eq!(message_buffer_data, expected_data.as_slice()); assert_eq!(message_buffer_data, expected_data.as_slice());
start = end_offset; start = end_offset;
} }
assert_eq!(message_buffer.header.end_offsets[2], 0); assert_eq!(message_buffer.end_offsets[2], 0);
} }
//
#[test] #[test]
fn test_put_all_long_vec() { fn test_put_all_long_vec() {
let data = vec![ let data = vec![
@ -247,40 +277,52 @@ mod test {
]; ];
let data_bytes: Vec<Vec<u8>> = data.into_iter().map(data_bytes).collect(); 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 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_msgs, 3);
assert_eq!( assert_eq!(
num_bytes, num_bytes,
data_bytes[0..3].iter().map(|x| x.len()).sum::<usize>() as u16 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 let iter = message_buffer.end_offsets.iter().take_while(|x| **x != 0);
// `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 mut start = header_len; let mut start = header_len;
let mut data_iter = data_bytes.iter(); let mut data_iter = data_bytes.iter();
for offset in iter { for offset in iter {
let end_offset = header_len + *offset as usize; 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(); let expected_data = data_iter.next().unwrap();
assert_eq!(message_buffer_data, expected_data.as_slice()); assert_eq!(message_buffer_data, expected_data.as_slice());
start = end_offset; start = end_offset;
} }
assert_eq!(message_buffer.header.end_offsets[0], 9_715); assert_eq!(message_buffer.end_offsets[0], 9_715);
assert_eq!(message_buffer.header.end_offsets[1], 9_716); assert_eq!(message_buffer.end_offsets[1], 9_716);
assert_eq!(message_buffer.header.end_offsets[2], 9_717); assert_eq!(message_buffer.end_offsets[2], 9_717);
assert_eq!(message_buffer.header.end_offsets[3], 0); assert_eq!(message_buffer.end_offsets[3], 0);
assert_eq!(message_buffer.header.end_offsets[4], 0); assert_eq!(message_buffer.end_offsets[4], 0);
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,10 @@
import * as anchor from "@coral-xyz/anchor"; 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 { MessageBuffer } from "../target/types/message_buffer";
import { MockCpiCaller } from "../target/types/mock_cpi_caller"; import { MockCpiCaller } from "../target/types/mock_cpi_caller";
import lumina from "@lumina-dev/test"; 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 // Enables tool that runs in local browser for easier debugging of
// transactions in this test - https://lumina.fyi/debug // transactions in this test - https://lumina.fyi/debug
lumina(); // lumina();
const messageBufferProgram = anchor.workspace const messageBufferProgram = anchor.workspace
.MessageBuffer as Program<MessageBuffer>; .MessageBuffer as Program<MessageBuffer>;
const mockCpiProg = anchor.workspace.MockCpiCaller as Program<MockCpiCaller>; 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( const [mockCpiCallerAuth] = anchor.web3.PublicKey.findProgramAddressSync(
[messageBufferProgram.programId.toBuffer(), Buffer.from("cpi")], [messageBufferProgram.programId.toBuffer(), Buffer.from("cpi")],
mockCpiProg.programId mockCpiProg.programId
); );
const [fundPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("fund")],
messageBufferProgram.programId
);
const pythPriceAccountId = new anchor.BN(1); const pythPriceAccountId = new anchor.BN(1);
const addPriceParams = { const addPriceParams = {
@ -41,12 +43,41 @@ const [pythPriceAccountPk] = anchor.web3.PublicKey.findProgramAddressSync(
mockCpiProg.programId mockCpiProg.programId
); );
const MESSAGE = Buffer.from("message"); const MESSAGE = Buffer.from("message");
const [accumulatorPdaKey] = anchor.web3.PublicKey.findProgramAddressSync( const [accumulatorPdaKey, accumulatorPdaBump] =
[mockCpiCallerAuth.toBuffer(), MESSAGE, pythPriceAccountPk.toBuffer()], anchor.web3.PublicKey.findProgramAddressSync(
messageBufferProgram.programId [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; let fundBalance = 100 * anchor.web3.LAMPORTS_PER_SOL;
const discriminator = BorshAccountsCoder.accountDiscriminator("MessageBuffer");
const messageBufferDiscriminator = bs58.encode(discriminator);
describe("accumulator_updater", () => { describe("accumulator_updater", () => {
// Configure the client to use the local cluster. // Configure the client to use the local cluster.
let provider = anchor.AnchorProvider.env(); let provider = anchor.AnchorProvider.env();
@ -58,14 +89,26 @@ describe("accumulator_updater", () => {
messageBufferProgram.programId messageBufferProgram.programId
); );
before("transfer lamports to the fund", async () => { before("transfer lamports to needed accounts", async () => {
await provider.connection.requestAirdrop(fundPda, fundBalance); 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 () => { it("Is initialized!", async () => {
// Add your test here. // Add your test here.
const tx = await messageBufferProgram.methods const tx = await messageBufferProgram.methods
.initialize(whitelistAuthority.publicKey) .initialize(whitelistAdmin.publicKey)
.accounts({}) .accounts({})
.rpc(); .rpc();
console.log("Your transaction signature", tx); console.log("Your transaction signature", tx);
@ -74,7 +117,7 @@ describe("accumulator_updater", () => {
whitelistPubkey whitelistPubkey
); );
assert.strictEqual(whitelist.bump, whitelistBump); 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)}`); console.info(`whitelist: ${JSON.stringify(whitelist)}`);
}); });
@ -83,9 +126,9 @@ describe("accumulator_updater", () => {
await messageBufferProgram.methods await messageBufferProgram.methods
.setAllowedPrograms(allowedProgramAuthorities) .setAllowedPrograms(allowedProgramAuthorities)
.accounts({ .accounts({
authority: whitelistAuthority.publicKey, admin: whitelistAdmin.publicKey,
}) })
.signers([whitelistAuthority]) .signers([whitelistAdmin])
.rpc(); .rpc();
const whitelist = await messageBufferProgram.account.whitelist.fetch( const whitelist = await messageBufferProgram.account.whitelist.fetch(
whitelistPubkey whitelistPubkey
@ -100,29 +143,131 @@ describe("accumulator_updater", () => {
); );
}); });
it("Updates the whitelist authority", async () => { it("Creates a buffer", async () => {
const newWhitelistAuthority = anchor.web3.Keypair.generate(); const accumulatorPdaMetas = [
{
pubkey: accumulatorPdaKey,
isSigner: false,
isWritable: true,
},
];
await messageBufferProgram.methods await messageBufferProgram.methods
.updateWhitelistAuthority(newWhitelistAuthority.publicKey) .createBuffer(mockCpiCallerAuth, pythPriceAccountPk, 1024 * 8)
.accounts({ .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(); .rpc();
const whitelist = await messageBufferProgram.account.whitelist.fetch( let whitelist = await messageBufferProgram.account.whitelist.fetch(
whitelistPubkey 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 () => { it("Mock CPI program - AddPrice", async () => {
const mockCpiCallerAddPriceTxPubkeys = await mockCpiProg.methods const mockCpiCallerAddPriceTxPubkeys = await mockCpiProg.methods
.addPrice(addPriceParams) .addPrice(addPriceParams)
.accounts({ .accounts({
fund: fundPda,
systemProgram: anchor.web3.SystemProgram.programId, systemProgram: anchor.web3.SystemProgram.programId,
auth: mockCpiCallerAuth, auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey, accumulatorWhitelist: whitelistPubkey,
@ -183,10 +328,14 @@ describe("accumulator_updater", () => {
mockCpiCallerAddPriceTxPubkeys.pythPriceAccount mockCpiCallerAddPriceTxPubkeys.pythPriceAccount
); );
const messageBuffer = const messageBufferAccount = await provider.connection.getAccountInfo(
await messageBufferProgram.account.messageBuffer.fetch(accumulatorPdaKey); accumulatorPdaKey
);
const accumulatorPriceMessages = parseMessageBuffer(messageBuffer); const accumulatorPriceMessages = parseMessageBuffer(
messageBufferProgram,
messageBufferAccount.data
);
console.log( console.log(
`accumulatorPriceMessages: ${JSON.stringify( `accumulatorPriceMessages: ${JSON.stringify(
@ -200,29 +349,11 @@ describe("accumulator_updater", () => {
assert.isTrue(pm.price.eq(addPriceParams.price)); assert.isTrue(pm.price.eq(addPriceParams.price));
assert.isTrue(pm.priceExpo.eq(addPriceParams.priceExpo)); 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 () => { it("Fetches MessageBuffer using getProgramAccounts with discriminator", async () => {
let discriminator = const messageBufferAccounts = await getProgramAccountsForMessageBuffers(
BorshAccountsCoder.accountDiscriminator("MessageBuffer"); provider.connection
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 msgBufferAcctKeys = messageBufferAccounts.map((ai) => const msgBufferAcctKeys = messageBufferAccounts.map((ai) =>
ai.pubkey.toString() ai.pubkey.toString()
@ -231,7 +362,7 @@ describe("accumulator_updater", () => {
`messageBufferAccounts: ${JSON.stringify(msgBufferAcctKeys, null, 2)}` `messageBufferAccounts: ${JSON.stringify(msgBufferAcctKeys, null, 2)}`
); );
assert.isTrue(messageBufferAccounts.length === 1); assert.isTrue(messageBufferAccounts.length === 2);
msgBufferAcctKeys.includes(accumulatorPdaKey.toString()); msgBufferAcctKeys.includes(accumulatorPdaKey.toString());
}); });
@ -250,7 +381,6 @@ describe("accumulator_updater", () => {
await mockCpiProg.methods await mockCpiProg.methods
.updatePrice(updatePriceParams) .updatePrice(updatePriceParams)
.accounts({ .accounts({
fund: fundPda,
pythPriceAccount: pythPriceAccountPk, pythPriceAccount: pythPriceAccountPk,
auth: mockCpiCallerAuth, auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey, accumulatorWhitelist: whitelistPubkey,
@ -271,11 +401,16 @@ describe("accumulator_updater", () => {
assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo)); assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema)); assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo)); assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
const messageBuffer =
await messageBufferProgram.account.messageBuffer.fetch( const messageBufferAccountData = await getMessageBuffer(
accumulatorPdaMeta.pubkey provider.connection,
); accumulatorPdaKey
const updatedAccumulatorPriceMessages = parseMessageBuffer(messageBuffer); );
const updatedAccumulatorPriceMessages = parseMessageBuffer(
messageBufferProgram,
messageBufferAccountData
);
console.log( console.log(
`updatedAccumulatorPriceMessages: ${JSON.stringify( `updatedAccumulatorPriceMessages: ${JSON.stringify(
@ -299,11 +434,12 @@ describe("accumulator_updater", () => {
let testCase = testCases[i]; let testCase = testCases[i];
console.info(`testCase: ${testCase}`); console.info(`testCase: ${testCase}`);
const updatePriceParams = { const updatePriceParams = {
price: new anchor.BN(10 * i + 5), 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), ema: new anchor.BN(10 * i + 7),
emaExpo: new anchor.BN(10 * i + 8), emaExpo: new anchor.BN(10 * i + 8),
}; };
console.log(`updatePriceParams: ${JSON.stringify(updatePriceParams)}`);
let accumulatorPdaMeta = getAccumulatorPdaMeta( let accumulatorPdaMeta = getAccumulatorPdaMeta(
mockCpiCallerAuth, mockCpiCallerAuth,
@ -312,7 +448,6 @@ describe("accumulator_updater", () => {
await mockCpiProg.methods await mockCpiProg.methods
.cpiMaxTest(updatePriceParams, testCase) .cpiMaxTest(updatePriceParams, testCase)
.accounts({ .accounts({
fund: fundPda,
pythPriceAccount: pythPriceAccountPk, pythPriceAccount: pythPriceAccountPk,
auth: mockCpiCallerAuth, auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey, accumulatorWhitelist: whitelistPubkey,
@ -333,28 +468,36 @@ describe("accumulator_updater", () => {
assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo)); assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema)); assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo)); assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
const messageBuffer =
await messageBufferProgram.account.messageBuffer.fetch(
accumulatorPdaMeta.pubkey
);
const updatedAccumulatorPriceMessages = parseMessageBuffer(messageBuffer);
console.log( const messageBufferAccountData = await getMessageBuffer(
`updatedAccumulatorPriceMessages: ${JSON.stringify( provider.connection,
updatedAccumulatorPriceMessages, accumulatorPdaKey
null,
2
)}`
); );
updatedAccumulatorPriceMessages.forEach((pm) => {
assert.isTrue(pm.id.eq(addPriceParams.id)); const messageBufferHeader = deserializeMessageBufferHeader(
assert.isTrue(pm.price.eq(updatePriceParams.price)); messageBufferProgram,
assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo)); 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 // with loosen CPI feature activated, max cpi instruction size len is 10KB
let testCases = [[1024, 2048, 4096, 8192]]; let testCases = [[1024, 2048, 4096, 8192]];
// for (let i = 1; i < 8; i++) { // for (let i = 1; i < 8; i++) {
@ -363,7 +506,7 @@ describe("accumulator_updater", () => {
console.info(`testCase: ${testCase}`); console.info(`testCase: ${testCase}`);
const updatePriceParams = { const updatePriceParams = {
price: new anchor.BN(10 * i + 5), 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), ema: new anchor.BN(10 * i + 7),
emaExpo: new anchor.BN(10 * i + 8), emaExpo: new anchor.BN(10 * i + 8),
}; };
@ -377,7 +520,6 @@ describe("accumulator_updater", () => {
await mockCpiProg.methods await mockCpiProg.methods
.cpiMaxTest(updatePriceParams, testCase) .cpiMaxTest(updatePriceParams, testCase)
.accounts({ .accounts({
fund: fundPda,
pythPriceAccount: pythPriceAccountPk, pythPriceAccount: pythPriceAccountPk,
auth: mockCpiCallerAuth, auth: mockCpiCallerAuth,
accumulatorWhitelist: whitelistPubkey, accumulatorWhitelist: whitelistPubkey,
@ -396,6 +538,192 @@ describe("accumulator_updater", () => {
assert.ok(errorThrown); 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 = ( 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 // Parses MessageBuffer.data into a PriceAccount or PriceOnly object based on the
// accountType and accountSchema. // accountType and accountSchema.
function parseMessageBuffer({ function parseMessageBuffer(
header, messageBufferProgram: Program<MessageBuffer>,
messages, accountData: Buffer
}: { ): AccumulatorPriceMessage[] {
header: BufferHeader; const msgBufferHeader = deserializeMessageBufferHeader(
messages: number[]; messageBufferProgram,
}): AccumulatorPriceMessage[] { accountData
const accumulatorMessages = []; );
let dataBuffer = Buffer.from(messages);
const accumulatorMessages = [];
// let dataBuffer = Buffer.from(messages);
let dataBuffer = accountData.subarray(
msgBufferHeader.headerLen,
accountData.length
);
let start = 0; let start = 0;
for (let i = 0; i < header.endOffsets.length; i++) { for (let i = 0; i < msgBufferHeader.endOffsets.length; i++) {
const endOffset = header.endOffsets[i]; const endOffset = msgBufferHeader.endOffsets[i];
if (endOffset == 0) { if (endOffset == 0) {
console.log(`endOffset = 0. breaking`); console.log(`endOffset = 0. breaking`);
@ -459,12 +799,22 @@ type MessageHeader = {
size: number; size: number;
}; };
type MessageBuffer = { type MessageBufferType = {
header: MessageHeader; header: MessageHeader;
data: Buffer; 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; let offset = 0;
const schema = data.readInt8(offset); 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 AccumulatorPriceMessage = FullPriceMessage | CompactPriceMessage;
type FullPriceMessage = { type FullPriceMessage = {
@ -522,3 +870,19 @@ function parseCompactPriceMessage(data: Uint8Array): CompactPriceMessage {
priceExpo: new anchor.BN(data.subarray(16, 24), "be"), 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,
},
},
],
});
}