Skip to content
Merged
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
2 changes: 1 addition & 1 deletion client/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ mod tests {
prop_assert_eq!(push.source_buffer.address(), &expected.buffer);
prop_assert_eq!(push.destination.address(), &expected.destination);
prop_assert_eq!(push.bump, expected.bump);
prop_assert_eq!(u64::from_be_bytes(*push.amount), expected.amount);
prop_assert_eq!(u64::from_le_bytes(*push.amount), expected.amount);
}
}
}
Expand Down
28 changes: 14 additions & 14 deletions interface/src/data/intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub struct OrderIntent {
///
/// Layout: one character per byte, cell widths proportional to field size,
/// each divider belongs to the cell on its right. The byte range is
/// annotated below. Amounts and `valid_to` are big-endian encoded.
/// annotated below. Amounts and `valid_to` are little-endian encoded.
///
/// ```text
/// partially_fillable ─────┐
Expand Down Expand Up @@ -178,9 +178,9 @@ impl From<&OrderIntent> for EncodedOrderIntent {
*owner = intent.owner.to_bytes();
*buy_token = intent.buy_token_account.to_bytes();
*sell_token = intent.sell_token_account.to_bytes();
*sell_amount = intent.sell_amount.to_be_bytes();
*buy_amount = intent.buy_amount.to_be_bytes();
*valid_to = intent.valid_to.to_be_bytes();
*sell_amount = intent.sell_amount.to_le_bytes();
*buy_amount = intent.buy_amount.to_le_bytes();
*valid_to = intent.valid_to.to_le_bytes();
*kind = [intent.kind as u8];
*partially_fillable = [intent.partially_fillable as u8];
*app_data = intent.app_data;
Expand Down Expand Up @@ -226,9 +226,9 @@ impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
owner: Pubkey::new_from_array(*owner),
buy_token_account: Pubkey::new_from_array(*buy_token),
sell_token_account: Pubkey::new_from_array(*sell_token),
sell_amount: u64::from_be_bytes(*sell_amount),
buy_amount: u64::from_be_bytes(*buy_amount),
valid_to: u32::from_be_bytes(*valid_to),
sell_amount: u64::from_le_bytes(*sell_amount),
buy_amount: u64::from_le_bytes(*buy_amount),
valid_to: u32::from_le_bytes(*valid_to),
kind: match kind {
[0] => OrderKind::Sell,
[1] => OrderKind::Buy,
Expand Down Expand Up @@ -457,7 +457,7 @@ mod tests {
#[test]
fn uid_digest_regression() {
let intent = sample_intent(OrderKind::Buy, true);
let expected = hex!("091d7e1959ac6f7a400a91f1dcd9ce436f8f53e2b7a1d968acb08f79d3c1231d");
let expected = hex!("7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00");
assert_eq!(intent.uid(), Hash::from(expected));
}

Expand All @@ -482,12 +482,12 @@ mod tests {
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
// sell_amount (0x0123_4567_89ab_cdef, BE u64)
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
// buy_amount (0xfedc_ba98_7654_3210, BE u64)
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
// valid_to (0xdead_beef, BE u32)
0xde, 0xad, 0xbe, 0xef,
// sell_amount (0x0123_4567_89ab_cdef, LE u64)
0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
// buy_amount (0xfedc_ba98_7654_3210, LE u64)
0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
// valid_to (0xdead_beef, LE u32)
0xef, 0xbe, 0xad, 0xde,
// kind (Buy = 1)
0x01,
// partially_fillable (true = 1)
Expand Down
1 change: 1 addition & 0 deletions interface/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
pub mod intent;
pub mod order;
pub mod state;
76 changes: 53 additions & 23 deletions interface/src/data/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,40 +51,45 @@ pub struct OrderAccount {
pub intent: OrderIntent,
}

/// Canonical 199-byte representation of an [`OrderAccount`]. The bytes
/// Canonical 200-byte representation of an [`OrderAccount`]. The bytes
/// written to/read from the order PDA's data area.
///
/// Layout: one character per byte, cell widths proportional to field size,
/// each divider belongs to the cell on its right. Integers are big-endian.
/// The intent slot holds a verbatim [`EncodedOrderIntent`]; see that
/// type's docs for its inner layout.
/// each divider belongs to the cell on its right. Integers are little-endian
/// (Anchor/Borsh convention). The intent slot holds a verbatim
/// [`EncodedOrderIntent`]; see that type's docs for its inner layout.
///
/// ```text
/// ┌──── cancelled
/// ┌┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐
/// ││amount_│amount_│ │ │
/// ││with- │re- │ created_by │ intent (EncodedOrderIntent) │
/// ││drawn │ceived │ │ │
/// └┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘
/// 0 1 9 17 49 ... 199
/// ┌──── discriminator
/// │┌─── cancelled
/// ┌┬┬───────┬───────┬───────────────────────────────┬─────────────────...─────────────────┐
/// ││|amount_│amount_│ │ │
/// │││with- │re- │ created_by │ intent (EncodedOrderIntent) │
/// │││drawn │ceived │ │ │
/// └┴┴───────┴───────┴───────────────────────────────┴─────────────────...─────────────────┘
/// 0 1 2 10 18 50 ... 200
/// ```
#[derive(Clone, Debug, Deref, Eq, PartialEq)]
pub struct EncodedOrderAccount([u8; Self::SIZE]);

impl EncodedOrderAccount {
// Per-field widths, derived from the `OrderAccount` field types.
const W_DISCRIMINATOR: usize = size_of::<u8>();
const W_CANCELLED: usize = size_of::<bool>();
const W_AMOUNT_WITHDRAWN: usize = size_of::<u64>();
const W_AMOUNT_RECEIVED: usize = size_of::<u64>();
const W_CREATED_BY: usize = size_of::<Pubkey>();
const W_INTENT: usize = EncodedOrderIntent::SIZE;

pub const SIZE: usize = 199;
pub const SIZE: usize = 200;

/// Single-byte account discriminator. See [`crate::SettlementAccount`].
pub const DISCRIMINATOR: u8 = crate::SettlementAccount::OrderAccount.discriminator();

/// Decode the account body and compute the embedded intent's UID in one
/// shot, mirroring [`EncodedOrderIntent::decode_and_hash`]. Decoding
/// validates the intent; returns [`ProgramError::InvalidAccountData`] on a
/// decode error.
/// validates the discriminator and the intent; returns
/// [`ProgramError::InvalidAccountData`] on a decode error.
pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> {
let order_account = OrderAccount::try_from(*bytes)?;
// The order UID is the hash of the intent's canonical bytes. Decoding
Expand All @@ -111,17 +116,26 @@ pub fn write_account(
created_by: &Pubkey,
encoded_intent: &[u8; EncodedOrderIntent::SIZE],
) {
let (cancelled_slot, amount_withdrawn_slot, amount_received_slot, created_by_slot, intent_slot) = mut_array_refs![
let (
discriminator_slot,
cancelled_slot,
amount_withdrawn_slot,
amount_received_slot,
created_by_slot,
intent_slot,
) = mut_array_refs![
buffer,
EncodedOrderAccount::W_DISCRIMINATOR,
EncodedOrderAccount::W_CANCELLED,
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
EncodedOrderAccount::W_AMOUNT_RECEIVED,
EncodedOrderAccount::W_CREATED_BY,
EncodedOrderAccount::W_INTENT
];
*discriminator_slot = [EncodedOrderAccount::DISCRIMINATOR];
*cancelled_slot = [cancelled as u8];
*amount_withdrawn_slot = amount_withdrawn.to_be_bytes();
*amount_received_slot = amount_received.to_be_bytes();
*amount_withdrawn_slot = amount_withdrawn.to_le_bytes();
*amount_received_slot = amount_received.to_le_bytes();
*created_by_slot = created_by.to_bytes();
*intent_slot = *encoded_intent;
}
Expand Down Expand Up @@ -151,23 +165,28 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount {
type Error = ProgramError;

fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result<Self, Self::Error> {
let (cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
let (discriminator, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
&bytes,
EncodedOrderAccount::W_DISCRIMINATOR,
EncodedOrderAccount::W_CANCELLED,
EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
EncodedOrderAccount::W_AMOUNT_RECEIVED,
EncodedOrderAccount::W_CREATED_BY,
EncodedOrderAccount::W_INTENT
];

if *discriminator != [EncodedOrderAccount::DISCRIMINATOR] {
return Err(ProgramError::InvalidAccountData);
}

Ok(OrderAccount {
cancelled: match cancelled {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidAccountData),
},
amount_withdrawn: u64::from_be_bytes(*amount_withdrawn),
amount_received: u64::from_be_bytes(*amount_received),
amount_withdrawn: u64::from_le_bytes(*amount_withdrawn),
amount_received: u64::from_le_bytes(*amount_received),
created_by: Pubkey::new_from_array(*created_by),
intent: OrderIntent::try_from(intent).map_err(|_| ProgramError::InvalidAccountData)?,
})
Expand All @@ -193,8 +212,9 @@ pub mod fixtures {
};

// Hardcoded but verified in a sanity-check test.
pub const CANCELLED_OFFSET: usize = 0;
pub const INTENT_OFFSET: usize = 49;
pub const DISCRIMINATOR_OFFSET: usize = 0;
pub const CANCELLED_OFFSET: usize = 1;
pub const INTENT_OFFSET: usize = 50;

/// Hand-picked example order account wrapping [`sample_intent`].
pub fn sample_account(cancelled: bool) -> OrderAccount {
Expand Down Expand Up @@ -232,7 +252,7 @@ pub mod fixtures {
mod tests {
use core::mem::size_of;

use super::fixtures::{sample_account, CANCELLED_OFFSET, INTENT_OFFSET};
use super::fixtures::{sample_account, CANCELLED_OFFSET, DISCRIMINATOR_OFFSET, INTENT_OFFSET};
use super::*;
use crate::data::intent::{
fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET},
Expand Down Expand Up @@ -321,6 +341,15 @@ mod tests {
);
}

#[test]
fn decode_rejects_wrong_discriminator() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
EncodedOrderAccount::from(sample_account(false)).into();
bytes[0] ^= 0xff;
let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected");
assert_eq!(err, ProgramError::InvalidAccountData);
}

#[test]
fn decode_rejects_non_boolean_cancelled() {
let mut bytes: [u8; EncodedOrderAccount::SIZE] =
Expand Down Expand Up @@ -416,6 +445,7 @@ mod tests {
kind in arb_order_kind(),
partially_fillable in any::<bool>(),
) {
bytes[DISCRIMINATOR_OFFSET] = EncodedOrderAccount::DISCRIMINATOR;
bytes[CANCELLED_OFFSET] = cancelled as u8;
bytes[INTENT_OFFSET + KIND_OFFSET] = kind as u8;
bytes[INTENT_OFFSET + PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
Expand Down
50 changes: 50 additions & 0 deletions interface/src/data/state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Settlement state PDA body.

use solana_program_error::ProgramError;

use crate::SettlementAccount;

/// Canonical size of the settlement state PDA's body: just the discriminator.
pub const SIZE: usize = 1;

/// Single-byte account discriminator. See [`crate::SettlementAccount`].
pub const DISCRIMINATOR: [u8; SIZE] = [SettlementAccount::SettlementState.discriminator()];

/// Validate that `bytes` carries the expected discriminator.
pub fn decode(bytes: &[u8; SIZE]) -> Result<(), ProgramError> {
if *bytes != DISCRIMINATOR {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}

/// Writes the canonical encoding of the settlement state PDA's body into
/// `buffer`. There are no fields beyond the discriminator.
pub fn write_account(buffer: &mut [u8; SIZE]) {
*buffer = DISCRIMINATOR;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn decode_accepts_discriminator() {
assert_eq!(decode(&DISCRIMINATOR), Ok(()));
}

#[test]
fn decode_rejects_wrong_discriminator() {
let mut bytes = DISCRIMINATOR;
bytes[0] ^= 0xff;
assert_eq!(decode(&bytes), Err(ProgramError::InvalidAccountData));
}

#[test]
fn write_account_produces_decodable_bytes() {
let mut buffer = [0u8; SIZE];
write_account(&mut buffer);
assert_eq!(decode(&buffer), Ok(()));
assert_eq!(buffer, DISCRIMINATOR);
}
}
2 changes: 1 addition & 1 deletion interface/src/instruction/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl From<Initialize> for Instruction {
/// Parsed inputs of an `Initialize` instruction.
pub struct InitializeInput<'a> {
pub payer: &'a AccountView,
pub state_pda: &'a AccountView,
pub state_pda: &'a mut AccountView,
}

impl<'a> InstructionInputParsing<'a> for InitializeInput<'a> {
Expand Down
Loading