From f49e0fea676daab18d01552c770a4f06f9633b4b Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:05:17 +0900 Subject: [PATCH 1/5] Encode account and instruction integers as little-endian Switches every hand-rolled u16/u32/u64 field currently stored/sent big-endian to little-endian: OrderIntent's sell_amount/buy_amount/valid_to, OrderAccount's amount_withdrawn/amount_received, and the BeginSettle/ FinalizeSettle counterpart-index fields (finalize_ix_index/begin_ix_index). Little-endian is the Borsh/Anchor convention; this is a first step toward Anchor-compatible on-chain data so external tooling (indexers, explorers) can read it without bespoke decoding. This changes the order UID hash and the wire format of CreateOrder/BeginSettle/FinalizeSettle - not backwards compatible with any existing on-chain state or off-chain signers. Co-Authored-By: Claude Sonnet 5 --- interface/src/data/intent.rs | 28 +++++++++---------- interface/src/data/order.rs | 14 +++++----- interface/src/instruction/settle/begin.rs | 18 ++++++------ interface/src/instruction/settle/finalize.rs | 10 +++---- interface/src/instruction/settle/mod.rs | 10 +++---- .../settlement/tests/begin_settle_orders.rs | 4 +-- 6 files changed, 42 insertions(+), 42 deletions(-) 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/order.rs b/interface/src/data/order.rs index 2130523..81c23a4 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -55,9 +55,9 @@ pub struct OrderAccount { /// 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 @@ -120,8 +120,8 @@ pub fn write_account( EncodedOrderAccount::W_INTENT ]; *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; } @@ -166,8 +166,8 @@ impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount { [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)?, }) diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index c690008..059e4de 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -33,7 +33,7 @@ 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] +/// `[discriminator=0][finalize_ix_index: u16 LE][n: u8][bump×n][transfer_count×n] /// [amount: u64 BE ×T]`. /// Required accounts: `[instructions_sysvar (R), state_pda (R), token_program /// (R)]` followed, per order, by `[order_pda (R), sell_token_account (W), @@ -79,7 +79,7 @@ impl From> for Instruction { .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() @@ -309,7 +309,7 @@ mod tests { data, [ &[SettlementInstruction::BeginSettle.discriminator()][..], - &hex!("1337")[..], // counterpart index + &hex!("3713")[..], // counterpart index, little-endian &[0][..], // order count ] .concat(), @@ -354,7 +354,7 @@ mod tests { 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) @@ -430,7 +430,7 @@ mod tests { data, [ &[SettlementInstruction::BeginSettle.discriminator()][..], - &hex!("1337")[..], // counterpart index + &hex!("3713")[..], // counterpart index, little-endian &[2][..], // order count &[0xa1, 0xb1][..], // bumps &[2, 1][..], // counts @@ -480,7 +480,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 { @@ -539,7 +539,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 @@ -585,7 +585,7 @@ 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 @@ -640,7 +640,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], diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index e6dc737..ae55bad 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -17,7 +17,7 @@ use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID}; /// `begin_ix_index` is the index of the paired `BeginSettle` instruction in the /// same transaction. /// -/// Wire format: `[discriminator=1, begin_ix_index: u16 BE]`, 3 bytes. +/// Wire format: `[discriminator=1, begin_ix_index: u16 LE]`, 3 bytes. /// Required accounts: `[instructions_sysvar (R)]`. pub struct FinalizeSettle { pub program_id: Pubkey, @@ -31,7 +31,7 @@ impl From for Instruction { accounts: vec![AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false)], data: [ &[SettlementInstruction::FinalizeSettle.discriminator()], - &builder.begin_ix_index.to_be_bytes()[..], + &builder.begin_ix_index.to_le_bytes()[..], ] .concat(), } @@ -86,7 +86,7 @@ mod tests { data, [ &[SettlementInstruction::FinalizeSettle.discriminator()][..], - &hex!("1337")[..], // counterpart index + &hex!("3713")[..], // counterpart index, little-endian ] .concat(), ); @@ -104,7 +104,7 @@ mod tests { let mut accounts = [fake_account(address)]; let data = ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], - [0x13, 0x37], // begin index + [0x37, 0x13], // begin index, little-endian ]; let FinalizeSettleInput { begin_ix_index, @@ -147,7 +147,7 @@ mod tests { let mut accounts = [fake_account(first_address), fake_account(second_address)]; let data = ix_data![ [SettlementInstruction::FinalizeSettle.discriminator()], - [0x13, 0x37], // begin index + [0x37, 0x13], // begin index, little-endian [42], // extra ]; let FinalizeSettleInput { diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index d9afa22..c4ac4c4 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, SettledOrders pub use finalize::{FinalizeSettle, FinalizeSettleInput}; /// 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/programs/settlement/tests/begin_settle_orders.rs b/programs/settlement/tests/begin_settle_orders.rs index abf1811..1f5da25 100644 --- a/programs/settlement/tests/begin_settle_orders.rs +++ b/programs/settlement/tests/begin_settle_orders.rs @@ -403,7 +403,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 builder would sort them, so build the // instruction by hand in the current wire format: 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 accounts are `[instructions_sysvar, state_pda, // token_program, (order_pda, sell_token_account)...]`. let mut orders = [ @@ -413,7 +413,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(&1u16.to_be_bytes()); + data.extend_from_slice(&1u16.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. From e52f98dcd539ea8576dbe64eb748dbf4c220cab0 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:47:51 +0900 Subject: [PATCH 2/5] fix le encoding also needed for transfers in begin_settle --- interface/src/instruction/settle/begin.rs | 18 +++++++++--------- programs/settlement/src/settle/begin.rs | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 059e4de..8eceac2 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -75,7 +75,7 @@ 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()][..], @@ -435,9 +435,9 @@ mod tests { &[0xa1, 0xb1][..], // bumps &[2, 1][..], // counts // amounts - &hex!("0000000000000102")[..], - &hex!("0000000000000304")[..], - &hex!("0000000000000506")[..], + &hex!("0201000000000000")[..], + &hex!("0403000000000000")[..], + &hex!("0605000000000000")[..], ] .concat(), ); @@ -589,8 +589,8 @@ mod tests { [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, .. } = @@ -605,7 +605,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()); @@ -690,8 +690,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/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index 3fc2e8e..6958e92 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -234,7 +234,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))?; } From ac18e42a9508f679a8bde7abcc7eb46ff88ebb30 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:55:42 +0900 Subject: [PATCH 3/5] final comments fixes --- interface/src/instruction/settle/begin.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 8168a41..f7550f2 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -34,7 +34,7 @@ pub struct Pull { /// /// Wire format (grouped, with `n` orders and `T` total transfers): /// `[discriminator=0][finalize_ix_index: u16 LE][n: u8][bump×n][transfer_count×n] -/// [amount: u64 BE ×T]`. +/// [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)...]`. @@ -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]], } @@ -432,7 +432,7 @@ mod tests { [2], // order count [0xa1, 0xb1], // bumps [2, 1], // counts - // amounts + // amounts, little endian hex!("0201000000000000"), hex!("0403000000000000"), hex!("0605000000000000"), From efbb63ceb3c2692ed11d9f785d8fcd03b89c1410 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:51:57 +0900 Subject: [PATCH 4/5] Add single-byte account discriminators via a SettlementAccount enum (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Prefixes every account this program owns with real storage with a 1-byte discriminator, mirroring how `SettlementInstruction` already identifies instructions. - New `SettlementAccount` enum (`interface/src/lib.rs`), following the same `num_enum::TryFromPrimitive` pattern as `SettlementInstruction`. Discriminators start at 128 and increment per account type, kept visually/numerically distinct from the instruction discriminators (0-4). - `OrderAccount`: discriminator 128, account grows 199 -> 200 bytes. - Settlement state PDA: new `interface::data::state` module, discriminator 129, account grows 0 -> 1 byte (written on `Initialize`). - An IDL's `discriminator` field can be any byte length — it doesn't have to be Anchor's own 8-byte `sha256("account:...")` convention — so a single byte is enough for IDL-driven tooling (e.g. Solscan) to identify the account type, while costing far less rent than an 8-byte discriminator would. Builds on #63 (little-endian encoding) — based on that branch since decoding an account now also depends on its fields already being little-endian. ## Test plan - [x] `just build-verified` to rebuild the on-chain `.so` - [x] `cargo test --workspace` — all tests pass, including new discriminator-rejection tests and `SettlementAccount` enum tests - [x] `just fmt-check` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com> --- interface/src/data/mod.rs | 1 + interface/src/data/order.rs | 62 ++++++++++++++++++------- interface/src/data/state.rs | 50 ++++++++++++++++++++ interface/src/instruction/initialize.rs | 2 +- interface/src/lib.rs | 42 +++++++++++++++++ programs/settlement/src/initialize.rs | 11 ++++- programs/settlement/tests/initialize.rs | 14 ++++-- 7 files changed, 158 insertions(+), 24 deletions(-) create mode 100644 interface/src/data/state.rs 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 81c23a4..343afed 100644 --- a/interface/src/data/order.rs +++ b/interface/src/data/order.rs @@ -51,7 +51,7 @@ 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, @@ -60,31 +60,36 @@ pub struct OrderAccount { /// [`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,14 +116,23 @@ 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_le_bytes(); *amount_received_slot = amount_received.to_le_bytes(); @@ -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,6 +175,10 @@ 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, @@ -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/lib.rs b/interface/src/lib.rs index 732831d..baccdc7 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 @@ -175,4 +199,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/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, From 8d3fe32f8c56d73f50555f77a6ca1d86553bd637 Mon Sep 17 00:00:00 2001 From: Kaze <230549489+kaze-cow@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:10:00 +0900 Subject: [PATCH 5/5] re-add missing comment --- interface/src/instruction/settle/finalize.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index 8ce5907..886854a 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -39,6 +39,13 @@ pub const FINALIZE_FIXED_ACCOUNTS: usize = 3; /// Required accounts: /// `[instructions_sysvar (R), state_pda (R), token_program (R)]` followed, per /// push, by `[source_buffer (W), destination (W)]`. +/// +/// `FinalizeSettle` validates that each source is the canonical buffer for its +/// destination's mint and executes the transfers; the order correspondence and +/// that each destination is an order's buy token account are `BeginSettle`'s +/// checks. So a push isn't aware of what orders are being paid, just the accounts +/// to move funds between, the source's bump, and the amount. The same buffer may +/// legitimately fund several pushes. pub struct FinalizeSettle<'a> { pub program_id: Pubkey, pub state_pda: Pubkey,