diff --git a/client/src/instructions.rs b/client/src/instructions.rs index 9f2a87f..ef301e0 100644 --- a/client/src/instructions.rs +++ b/client/src/instructions.rs @@ -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); } } } diff --git a/interface/src/data/intent.rs b/interface/src/data/intent.rs index 3686f6d..fbab54c 100644 --- a/interface/src/data/intent.rs +++ b/interface/src/data/intent.rs @@ -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 ─────┐ @@ -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; @@ -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, @@ -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)); } @@ -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) diff --git a/interface/src/data/mod.rs b/interface/src/data/mod.rs index 47a4a0f..8365c12 100644 --- a/interface/src/data/mod.rs +++ b/interface/src/data/mod.rs @@ -4,3 +4,4 @@ pub mod intent; pub mod order; +pub mod state; diff --git a/interface/src/data/order.rs b/interface/src/data/order.rs index 2130523..343afed 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -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::(); const W_CANCELLED: usize = size_of::(); const W_AMOUNT_WITHDRAWN: usize = size_of::(); const W_AMOUNT_RECEIVED: usize = size_of::(); const W_CREATED_BY: usize = size_of::(); 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 @@ -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; } @@ -151,8 +165,9 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount { type Error = ProgramError; fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result { - 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, @@ -160,14 +175,18 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount { 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)?, }) @@ -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 { @@ -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}, @@ -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] = @@ -416,6 +445,7 @@ mod tests { kind in arb_order_kind(), partially_fillable in any::(), ) { + 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; diff --git a/interface/src/data/state.rs b/interface/src/data/state.rs new file mode 100644 index 0000000..61a7f4a --- /dev/null +++ b/interface/src/data/state.rs @@ -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); + } +} diff --git a/interface/src/instruction/initialize.rs b/interface/src/instruction/initialize.rs index ce37dfd..16d0fea 100644 --- a/interface/src/instruction/initialize.rs +++ b/interface/src/instruction/initialize.rs @@ -54,7 +54,7 @@ impl From 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> { diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 9026ff1..f7550f2 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -33,8 +33,8 @@ pub struct Pull { /// the builder. /// /// Wire format (grouped, with `n` orders and `T` total transfers): -/// `[discriminator=0][finalize_ix_index: u16 BE][n: u8][bump×n][transfer_count×n] -/// [amount: u64 BE ×T]`. +/// `[discriminator=0][finalize_ix_index: u16 LE][n: u8][bump×n][transfer_count×n] +/// [amount: u64 LE ×T]`. /// Required accounts: `[instructions_sysvar (R), state_pda (R), token_program /// (R)]` followed, per order, by `[order_pda (R), sell_token_account (W), /// destination (W)...]`. @@ -75,11 +75,11 @@ impl From> for Instruction { let amounts: Vec = order .iter() .flat_map(|&i| pulls[i].iter()) - .flat_map(|pull| pull.amount.to_be_bytes()) + .flat_map(|pull| pull.amount.to_le_bytes()) .collect(); let data = [ &[SettlementInstruction::BeginSettle.discriminator()][..], - &finalize_ix_index.to_be_bytes()[..], + &finalize_ix_index.to_le_bytes()[..], &[order_pdas.len() as u8][..], &order .iter() @@ -124,7 +124,7 @@ pub struct SettledOrder<'a> { pub bump: u8, /// Destination accounts for this order's transfers. pub destinations: &'a [AccountView], - /// Transfer amounts (big-endian `u64`), one per destination. + /// Transfer amounts (little-endian `u64`), one per destination. pub amounts: &'a [[u8; 8]], } @@ -141,7 +141,7 @@ pub struct SettledOrders<'a> { bumps: &'a [u8], /// One transfer count per order, parallel to `bumps`. counts: &'a [u8], - /// Transfer amounts (big-endian `u64`), shared across orders and + /// Transfer amounts (little-endian `u64`), shared across orders and /// handed out `count` at a time. amounts: &'a [[u8; 8]], } @@ -309,7 +309,7 @@ mod tests { data, ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - hex!("1337"), // counterpart index + hex!("3713"), // counterpart index, little endian [0], // order count ], ); @@ -353,7 +353,7 @@ mod tests { data, ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - hex!("1337"), // counterpart index + hex!("3713"), // counterpart index, little endian [2], // order count [low_bump, high_bump], // bumps [0, 0], // transfer counts (both zero) @@ -428,14 +428,14 @@ mod tests { data, ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - hex!("1337"), // counterpart index + hex!("3713"), // counterpart index, little endian [2], // order count [0xa1, 0xb1], // bumps [2, 1], // counts - // amounts - hex!("0000000000000102"), - hex!("0000000000000304"), - hex!("0000000000000506"), + // amounts, little endian + hex!("0201000000000000"), + hex!("0403000000000000"), + hex!("0605000000000000"), ], ); @@ -477,7 +477,7 @@ mod tests { ]; let data = ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - [0x13, 0x37], // finalize index + [0x37, 0x13], // finalize index, little-endian [0x00], // order count ]; let BeginSettleInput { @@ -536,7 +536,7 @@ mod tests { ]; let data = ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - [0x13, 0x37], // finalize index + [0x37, 0x13], // finalize index, little-endian [0x01], // order count [0xab], // one order's bump [0x00], // that order's transfer count @@ -582,12 +582,12 @@ mod tests { ]; let data = ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - [0x13, 0x37], // finalize index + [0x37, 0x13], // finalize index, little-endian [0x01], // order count [0xab], // bump [0x02], // transfer count - 0x1122u64.to_be_bytes(), - 0x3344u64.to_be_bytes(), + 0x1122u64.to_le_bytes(), + 0x3344u64.to_le_bytes(), ]; let BeginSettleInput { orders, .. } = @@ -602,7 +602,7 @@ mod tests { .destinations .iter() .zip(order.amounts) - .map(|(destination, amount)| (destination.address(), u64::from_be_bytes(*amount))) + .map(|(destination, amount)| (destination.address(), u64::from_le_bytes(*amount))) .collect(); assert_eq!(transfers, vec![(&dest0, 0x1122), (&dest1, 0x3344)]); assert!(orders.next().is_none()); @@ -637,7 +637,7 @@ mod tests { // then all transfer counts (every order has zero transfers). let data = ix_data![ [SettlementInstruction::BeginSettle.discriminator()], - [0x13, 0x37], // finalize index + [0x37, 0x13], // finalize index, little-endian [ORDER_COUNT as u8], // order count bumps, [0u8; ORDER_COUNT], @@ -687,8 +687,8 @@ mod tests { [0x01], // order count [0xab], // bump [0x01], // count says one, but two amounts/destinations exist - 0u64.to_be_bytes(), - 0u64.to_be_bytes(), + 0u64.to_le_bytes(), + 0u64.to_le_bytes(), ]; assert_eq!( BeginSettleInput::parse(&data, &mut accounts).err(), diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index ddef36e..886854a 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -35,8 +35,8 @@ pub const FINALIZE_FIXED_ACCOUNTS: usize = 3; /// the builder. /// /// Wire format (with `n` total pushes): -/// `[discriminator=1][begin_ix_index: u16 BE][bump: u8 ×n][amount: u64 BE ×n]`. -/// Accounts: +/// `[discriminator=1][begin_ix_index: u16 LE][bump: u8 ×n][amount: u64 LE ×n]`. +/// Required accounts: /// `[instructions_sysvar (R), state_pda (R), token_program (R)]` followed, per /// push, by `[source_buffer (W), destination (W)]`. /// @@ -69,9 +69,9 @@ impl From> for Instruction { } = builder; let data: Vec = core::iter::once(SettlementInstruction::FinalizeSettle.discriminator()) - .chain(begin_ix_index.to_be_bytes()) + .chain(begin_ix_index.to_le_bytes()) .chain(bumps.iter().copied()) - .chain(amounts.iter().flat_map(|amount| amount.to_be_bytes())) + .chain(amounts.iter().flat_map(|amount| amount.to_le_bytes())) .collect(); let mut accounts = vec![ @@ -92,7 +92,7 @@ impl From> for Instruction { } } -/// A single fund push parsed from `FinalizeSettle`: move `amount` (big-endian +/// A single fund push parsed from `FinalizeSettle`: move `amount` (little-endian /// `u64`) from `source_buffer` to `destination`. `bump` is `source_buffer`'s /// claimed canonical buffer bump, which the program re-derives against. pub struct Push<'a> { @@ -111,7 +111,7 @@ pub struct Pushes<'a> { /// `[source_buffer, destination]` per push, flattened. push_accounts: &'a [AccountView], bumps: &'a [u8], - /// One push amount (big-endian `u64`) per push, parallel to `bumps`. + /// One push amount (little-endian `u64`) per push, parallel to `bumps`. amounts: &'a [[u8; 8]], } @@ -181,7 +181,7 @@ impl<'a> InstructionInputParsing<'a> for FinalizeSettleInput<'a> { }; // The body after the begin index is, per push, a bump byte (all `n` - // first) then a big-endian `u64` amount: `9 * n` bytes. Unlike + // first) then a little-endian `u64` amount: `9 * n` bytes. Unlike // `BeginSettle`, there's no explicit count byte, so `n` is recovered as // `body.len() / 9`; a body that isn't a whole number of these 9-byte // pushes can't be parsed into the push layout at all (and would otherwise @@ -267,7 +267,7 @@ mod tests { data, ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], - hex!("1337"), // counterpart index + hex!("3713"), // counterpart index (little-endian) ], ); // No pushes: the three fixed accounts (sysvar, state PDA, token program). @@ -303,11 +303,11 @@ mod tests { ix.data, ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], - hex!("1337"), // counterpart index + hex!("3713"), // counterpart index (little-endian) [0xa1, 0xb1], // bumps - // amounts - hex!("0000000001020304"), - hex!("0000000005060708"), + // amounts (little-endian) + hex!("0403020100000000"), + hex!("0807060500000000"), ], ); @@ -349,7 +349,7 @@ mod tests { ]; let data = ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], - [0x13, 0x37], // begin index + [0x37, 0x13], // begin index, little-endian ]; let FinalizeSettleInput { begin_ix_index, @@ -388,8 +388,8 @@ mod tests { [SettlementInstruction::FinalizeSettle.discriminator()], [0x13, 0x37], // begin index [0xfe, 0xfd], // bumps - 0x1122u64.to_be_bytes(), - 0x3344u64.to_be_bytes(), + 0x1122u64.to_le_bytes(), + 0x3344u64.to_le_bytes(), ]; let FinalizeSettleInput { pushes, .. } = @@ -402,7 +402,7 @@ mod tests { push.source_buffer.address(), push.destination.address(), push.bump, - u64::from_be_bytes(*push.amount), + u64::from_le_bytes(*push.amount), ) }) .collect(); @@ -430,7 +430,7 @@ mod tests { let source = Address::new_from_array([i as u8; 32]); let dest = Address::new_from_array([(i + PUSH_COUNT) as u8; 32]); let bump = (i + 2 * PUSH_COUNT) as u8; - let amount = u64::from_be_bytes([(i + 3 * PUSH_COUNT) as u8; 8]); + let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]); expected.push(ExpectedPush { source, dest, @@ -452,7 +452,7 @@ mod tests { accounts.push(fake_account(push.source)); accounts.push(fake_account(push.dest)); bump_bytes.push(push.bump); - amount_bytes.extend_from_slice(&push.amount.to_be_bytes()); + amount_bytes.extend_from_slice(&push.amount.to_le_bytes()); } let data = ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], @@ -470,7 +470,7 @@ mod tests { assert_eq!(push.source_buffer.address(), &expected.source); assert_eq!(push.destination.address(), &expected.dest); assert_eq!(push.bump, expected.bump); - assert_eq!(u64::from_be_bytes(*push.amount), expected.amount); + assert_eq!(u64::from_le_bytes(*push.amount), expected.amount); } } @@ -508,7 +508,7 @@ mod tests { [SettlementInstruction::FinalizeSettle.discriminator()], [13, 37], // begin index [0xff], // the push's bump - 31337u64.to_be_bytes(), + 31337u64.to_le_bytes(), ]; // Too few: only one push account follows the fixed accounts. @@ -533,7 +533,7 @@ mod tests { let mut accounts = fake_sequential_accounts::(); let data = ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], - [13, 37], // begin index + [37, 13], // begin index [0xff], // the push's bump [0x11, 0x22, 0x33, 0x44], // a partial push (4 bytes) ]; diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 107cdba..9563a42 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -13,16 +13,16 @@ pub use begin::{BeginSettle, BeginSettleInput, Pull, SettledOrder}; pub use finalize::{FinalizeSettle, FinalizeSettleInput, Push, Pushes, FINALIZE_FIXED_ACCOUNTS}; /// Reads the first two bytes of a byte slice (instruction data) and -/// interprets them as a big-endian u16, returning it together with the +/// interprets them as a little-endian u16, returning it together with the /// remaining bytes to parse. /// It's meant to be used for BeginSettle and FinalizeSettle to extract the /// counterpart index, that is, the index linking that instruction to the /// opposite instruction which is encoded as the first -/// 2 bytes of the instruction data: `[0x13, 0x37]` → `0x1337`. +/// 2 bytes of the instruction data: `[0x37, 0x13]` → `0x1337`. /// Returns `InvalidInstructionData` if fewer than two bytes are provided. pub fn recover_counterpart(instruction_data: &[u8]) -> Result<(u16, &[u8]), ProgramError> { match instruction_data { - [b1, b2, rest @ ..] => Ok((u16::from_be_bytes([*b1, *b2]), rest)), + [b1, b2, rest @ ..] => Ok((u16::from_le_bytes([*b1, *b2]), rest)), _ => Err(ProgramError::InvalidInstructionData), } } @@ -35,7 +35,7 @@ mod tests { /// Builds an instruction-data byte vector from a list of field chunks, so a /// test can spell out the wire layout one field per line without repeating /// the `&[..][..]` slicing. Each chunk is anything sliceable to `[u8]` (a - /// byte array, a `Vec`, the result of `to_be_bytes()`, ...). + /// byte array, a `Vec`, the result of `to_le_bytes()`, ...). macro_rules! ix_data { ($($chunk:expr),* $(,)?) => { [$(&$chunk[..]),*].concat() @@ -64,7 +64,7 @@ mod tests { assert_eq!( recover_counterpart( &[ - &hex!("1337")[..], // counterpart index + &hex!("3713")[..], // counterpart index, little-endian &[42][..], // trailing ] .concat() diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 595677f..018abc0 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -34,6 +34,30 @@ impl SettlementInstruction { } } +/// Identifies the account type a given account's data belongs to, via the +/// single discriminator byte stored at its front. Starts at 128 to keep +/// account discriminators visually distinct from instruction discriminators. +#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)] +#[repr(u8)] +#[num_enum(error_type( + name = ProgramError, + constructor = SettlementAccount::unknown_discriminator, +))] +pub enum SettlementAccount { + OrderAccount = 128, + SettlementState = 129, +} + +impl SettlementAccount { + pub const fn discriminator(self) -> u8 { + self as u8 + } + + fn unknown_discriminator(_: u8) -> ProgramError { + ProgramError::InvalidAccountData + } +} + /// Recover the discriminator from the first byte of the payload and the /// remaining bytes to parse. /// Returns `InvalidInstructionData` for an insufficient length or an @@ -187,4 +211,22 @@ mod tests { Ok(SettlementInstruction::BeginSettle) ); } + + #[test] + fn settlement_account_try_from_partitions_all_bytes() { + for i in u8::MIN..=u8::MAX { + match SettlementAccount::try_from(i) { + Ok(account) => assert_eq!(account as u8, i), + Err(err) => assert_eq!(err, ProgramError::InvalidAccountData), + } + } + } + + #[test] + fn settlement_account_discriminators_are_distinct() { + assert_ne!( + SettlementAccount::OrderAccount.discriminator(), + SettlementAccount::SettlementState.discriminator(), + ); + } } diff --git a/programs/settlement/src/initialize.rs b/programs/settlement/src/initialize.rs index 2ccf19e..9adfd4f 100644 --- a/programs/settlement/src/initialize.rs +++ b/programs/settlement/src/initialize.rs @@ -1,7 +1,8 @@ //! `Initialize` instruction handler. -use pinocchio::{AccountView, Address, ProgramResult}; +use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult}; use settlement_interface::{ + data::state, instruction::{initialize::InitializeInput, InstructionInputParsing}, pda::state::state_pda_seeds, }; @@ -25,12 +26,18 @@ pub fn process_initialize( program_id, payer, pda: state_pda, - size: 0, + size: state::SIZE as u64, owner: program_id, seeds: state_pda_seeds(), } .create()?; + let mut buffer = state_pda.try_borrow_mut()?; + let buffer: &mut [u8; state::SIZE] = (&mut *buffer) + .try_into() + .map_err(|_| ProgramError::AccountDataTooSmall)?; + state::write_account(buffer); + Ok(()) } diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 632c6fe..62b9f3f 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -300,7 +300,7 @@ fn process_order( sell_token_account, destination, state_account, - u64::from_be_bytes(*amount), + u64::from_le_bytes(*amount), ) .invoke_signed(core::slice::from_ref(state_pda_signer))?; } diff --git a/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index 860908e..59f131e 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -389,7 +389,7 @@ fn rejects_orders_in_wrong_address_order() { // Lay out the two distinct orders strictly decreasing by PDA address, which // the program rejects. The interface builders would sort them, so build both // instructions by hand in the current wire format. Begin data is - // `[discriminator, finalize_ix_index (BE), order_count, bump×n, transfer_count×n]` + // `[discriminator, finalize_ix_index (LE), order_count, bump×n, transfer_count×n]` // (no transfers here) and begin accounts are `[instructions_sysvar, state_pda, // token_program, (order_pda, sell_token_account)...]`. The finalize's push // destinations are laid out in the same decreasing order, so the first order's @@ -411,7 +411,7 @@ fn rejects_orders_in_wrong_address_order() { orders.sort_by_key(|&(pda, ..)| std::cmp::Reverse(pda)); let mut data = vec![SettlementInstruction::BeginSettle.discriminator()]; - data.extend_from_slice(&u16::from(FINALIZE_INDEX).to_be_bytes()); + data.extend_from_slice(&u16::from(FINALIZE_INDEX).to_le_bytes()); data.push(orders.len() as u8); data.extend(orders.iter().map(|&(_, _, _, bump)| bump)); // No transfers: one zero transfer-count byte per order. diff --git a/programs/settlement/tests/initialize.rs b/programs/settlement/tests/initialize.rs index 0f10753..cc3cd25 100644 --- a/programs/settlement/tests/initialize.rs +++ b/programs/settlement/tests/initialize.rs @@ -1,6 +1,6 @@ use settlement_client::instructions::Initialize; use settlement_client::settlement_interface::{ - instruction::initialize::Initialize as InitializeRaw, pda::state::find_state_pda, + data::state, instruction::initialize::Initialize as InitializeRaw, pda::state::find_state_pda, }; use solana_sdk::{ pubkey::Pubkey, @@ -10,7 +10,7 @@ use solana_sdk::{ mod common; #[test] -fn happy_path_initializes_empty_state_pda() { +fn happy_path_initializes_state_pda_with_discriminator() { let (mut svm, program_id, payer) = common::setup(); let (state_pda, _bump) = find_state_pda(&program_id); @@ -30,9 +30,13 @@ fn happy_path_initializes_empty_state_pda() { account.owner, program_id, "state PDA must be owned by the settlement program" ); - assert!(account.data.is_empty(), "state PDA must be empty"); + assert_eq!( + account.data, + state::DISCRIMINATOR, + "state PDA must hold only the discriminator" + ); - let rent = svm.minimum_balance_for_rent_exemption(0); + let rent = svm.minimum_balance_for_rent_exemption(state::SIZE); assert_eq!( account.lamports, rent, "state PDA must hold exactly the rent minimum: {} != {}", @@ -59,7 +63,7 @@ fn funding_payer_can_differ_from_fee_payer() { // The rent came out of the funder, not the fee payer: the funder paid no // transaction fee, so its balance dropped by exactly the PDA rent. - let rent = svm.minimum_balance_for_rent_exemption(0); + let rent = svm.minimum_balance_for_rent_exemption(state::SIZE); assert_eq!( common::lamports(&svm, &funder.pubkey()), funder_airdrop - rent,