[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:
parent
6a5b1c434a
commit
794bd84c6f
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue