Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

/// Direction of the trade.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
#[repr(u8)]
pub enum OrderKind {
#[default]
Sell = 0,
Buy = 1,
}

#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct OrderIntent {
/// Account authorized to create and invalidate this order and whose
/// signature authenticates it. For off-chain orders this is the Ed25519
Expand Down
97 changes: 96 additions & 1 deletion interface/src/data/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ use core::mem::size_of;

use arrayref::{array_refs, mut_array_refs};
use derive_more::Deref;
use solana_account_view::AccountView;
use solana_address::Address;
use solana_hash::Hash;
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

use crate::data::intent::{self, EncodedOrderIntent, OrderIntent};
use crate::pda::order::order_pda_signer_seeds;
use crate::SettlementError;

/// Idiomatic representation of an order PDA's body.
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct OrderAccount {
/// `false` = the order is still active and can be filled; `true` = the
/// order has been cancelled by the owner and must not be filled.
Expand All @@ -51,6 +55,31 @@ pub struct OrderAccount {
pub intent: OrderIntent,
}

impl OrderAccount {
/// Load and decode the order at the given PDA, and confirm its canonical
pub fn load_from_pda(
order_pda: &AccountView,
program_id: &Address,
bump: u8,
) -> Result<Self, ProgramError> {
let data = order_pda.try_borrow()?;
let bytes: &[u8; EncodedOrderAccount::SIZE] = (&*data)
.try_into()
.map_err(|_| ProgramError::InvalidAccountData)?;
let (account, uid) = EncodedOrderAccount::decode_and_hash(bytes)?;
drop(data);

let expected =
Address::create_program_address(&order_pda_signer_seeds(&uid, &[bump]), program_id)
.map_err(|_| SettlementError::OrderNotCanonical)?;
if &expected != order_pda.address() {
return Err(SettlementError::OrderNotCanonical.into());
}

Ok(account)
}
}

/// Canonical 199-byte representation of an [`OrderAccount`]. The bytes
/// written to/read from the order PDA's data area.
///
Expand Down Expand Up @@ -359,6 +388,72 @@ mod tests {
assert_eq!(err, ProgramError::InvalidAccountData);
}

mod load_from_pda {
use super::*;
use crate::instruction::fixtures::fake_account_with_data;
use crate::pda::order::find_order_pda;

const PROGRAM_ID: Address = Address::new_from_array([9; 32]);

#[test]
fn accepts_the_canonical_pda() {
let account = sample_account(false);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
let order_pda = fake_account_with_data(
pda_address,
&EncodedOrderAccount::from(account.clone())[..],
);

let loaded = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, bump)
.expect("canonical PDA must load");
assert_eq!(loaded, account);
}

#[test]
fn rejects_a_non_canonical_address() {
let account = sample_account(false);
let (_, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
// An address unrelated to the intent's canonical seeds.
let wrong_address = Pubkey::new_from_array([0x42; 32]);
let order_pda =
fake_account_with_data(wrong_address, &EncodedOrderAccount::from(account)[..]);

let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, bump)
.expect_err("a non-canonical address must be rejected");
assert_eq!(err, SettlementError::OrderNotCanonical.into());
}

#[test]
fn rejects_a_wrong_bump() {
let account = sample_account(false);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
let order_pda =
fake_account_with_data(pda_address, &EncodedOrderAccount::from(account)[..]);

// Any bump other than the canonical one either derives a
// different address or fails to derive one at all (falling on
// curve); either way, the PDA can no longer be proven canonical.
let wrong_bump = bump.wrapping_sub(1);
let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, wrong_bump)
.expect_err("a non-canonical bump must be rejected");
assert_eq!(err, SettlementError::OrderNotCanonical.into());
}

#[test]
fn propagates_decode_errors() {
let account = sample_account(false);
let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(account).into();
bytes[CANCELLED_OFFSET] = 0xff;
let order_pda = fake_account_with_data(pda_address, &bytes);

let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID, bump)
.expect_err("a corrupt account must fail to decode");
assert_eq!(err, ProgramError::InvalidAccountData);
}
}

#[test]
fn direct_write_account_matches_order_account_decoding() {
let cancelled = true;
Expand Down
78 changes: 73 additions & 5 deletions interface/src/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{recover_discriminator, SettlementInstruction};
pub mod create_buffer;
pub mod create_order;
pub mod initialize;
pub mod reclaim_order;
pub mod settle;

/// Shared components for parsing generic instruction input.
Expand Down Expand Up @@ -49,8 +50,9 @@ pub trait InstructionInputParsing<'a>: Sized {
/// duplicating the unsafe initializer below.
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use solana_account_view::{AccountView, RuntimeAccount};
use solana_account_view::{AccountView, RuntimeAccount, MAX_PERMITTED_DATA_INCREASE};
use solana_address::Address;
use std::{mem, ptr};

/// Build an `AccountView` based on the input `RuntimeAccount` and whose
/// data region is empty.
Expand Down Expand Up @@ -85,11 +87,77 @@ pub mod fixtures {
unsafe { AccountView::new_unchecked(backing as *mut RuntimeAccount) }
}

/// Adapted from pinocchio's crate-private test helper; kept structurally
/// close for comparison:
/// https://docs.rs/crate/pinocchio/0.11.1/source/src/sysvars/slot_hashes/test_utils.rs#120-160
///
/// Allocate a heap-backed `AccountView` whose data region is initialized with
/// `data`, whose address is `address`, and whose borrow flag is `borrow_state`.
///
/// The function also returns the backing `Vec<u64>` so the caller can keep it
/// alive for the duration of the test (otherwise the memory would be freed and
/// the raw pointer inside `AccountView` would dangle).
///
/// # Safety
/// The caller must ensure the returned `AccountView` is used only for reading
/// or according to borrow rules because the Solana runtime invariants are not
/// fully enforced in this hand-rolled representation.
#[allow(
clippy::arithmetic_side_effects,
reason = "the function is mostly vendored and don't want to introduce unnecessary changes"
)]
pub unsafe fn make_account_view(
address: Address,
data: &[u8],
borrow_state: u8,
) -> (AccountView, Vec<u64>) {
// pinocchio writes a hand-rolled `AccountLayout` mirror and casts the
// pointer to `*mut RuntimeAccount`, because inside that crate the
// account struct's fields are private and its tests cannot set them by
// name. Here we instead write a real `RuntimeAccount`, since
// `solana_account_view::RuntimeAccount` has public fields and a
// `Default` impl.
let hdr_size = mem::size_of::<RuntimeAccount>();
// we over-allocate by MAX_PERMITTED_DATA_INCREASE to prevent memory
//corruption in the unlikely case a runtime increases the account data size
let total = hdr_size + data.len() + MAX_PERMITTED_DATA_INCREASE;
let words = total.div_ceil(mem::size_of::<u64>());
let mut backing: Vec<u64> = vec![0u64; words];
assert!(
mem::align_of::<u64>() >= mem::align_of::<RuntimeAccount>(),
"`backing` should be properly aligned to store a `RuntimeAccount` instance"
);

let hdr_ptr = backing.as_mut_ptr() as *mut RuntimeAccount;
ptr::write(
hdr_ptr,
RuntimeAccount {
address,
borrow_state,
data_len: data.len() as u64,
..Default::default()
},
);

ptr::copy_nonoverlapping(
data.as_ptr(),
(hdr_ptr as *mut u8).add(hdr_size),
data.len(),
);

(AccountView::new_unchecked(hdr_ptr), backing)
}

/// Create an account view storing the input data.
pub fn fake_account_with_data(address: Address, data: &[u8]) -> AccountView {
let (account, backing) =
unsafe { make_account_view(address, data, solana_account_view::NOT_BORROWED) };
core::mem::forget(backing);
account
}

pub fn fake_account(address: Address) -> AccountView {
fake_account_from(RuntimeAccount {
address,
..Default::default()
})
fake_account_with_data(address, &[])
}

Comment thread
kaze-cow marked this conversation as resolved.
pub fn fake_account_from_array(address_array: [u8; 32]) -> AccountView {
Expand Down
Loading