Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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