From bf3d4a1bec4fbbcd3e0b72a8bf71de0f6fb1c14a Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:04:43 -0700 Subject: [PATCH 1/8] Architect Seat data struct --- interface/src/seat/mod.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/interface/src/seat/mod.rs b/interface/src/seat/mod.rs index 57e7b09..0f6082c 100644 --- a/interface/src/seat/mod.rs +++ b/interface/src/seat/mod.rs @@ -1,4 +1,21 @@ +use crate::order::Order; use dropset_macros::svm_data; +use pinocchio::Address as Pubkey; + +pub const MAX_ORDERS_PER_SIDE: usize = 5; #[svm_data] -pub struct Seat {} +pub struct Seat { + pub parent: *mut Seat, + pub left: *mut Seat, + pub right: *mut Seat, + pub user: Pubkey, + pub base_total: u64, + pub base_locked: u64, + pub quote_total: u64, + pub quote_locked: u64, + pub lamports_total: u64, + pub lamports_locked: u64, + pub asks: [*mut Order; MAX_ORDERS_PER_SIDE], + pub bids: [*mut Order; MAX_ORDERS_PER_SIDE], +} From 94c5f7ca11b8b878b19892f898f07970c5273344 Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:41:00 -0700 Subject: [PATCH 2/8] Add more data structure layout --- interface/src/common/memory.rs | 15 +++++++++++++- interface/src/groups.rs | 1 + interface/src/order/mod.rs | 4 +++- interface/src/seat/mod.rs | 10 ++++----- interface/src/stack/mod.rs | 1 + macros/src/lib.rs | 32 ++++++++++++++++++++--------- program/src/dropset/common/memory.s | 7 +++++++ 7 files changed, 52 insertions(+), 18 deletions(-) diff --git a/interface/src/common/memory.rs b/interface/src/common/memory.rs index 4b83132..064531d 100644 --- a/interface/src/common/memory.rs +++ b/interface/src/common/memory.rs @@ -2,9 +2,22 @@ use crate::common::account::EmptyAccount; use crate::common::token::InitializeAccount2; use crate::market::MarketHeader; use crate::market::register::CreateAccountData; -use dropset_macros::{constant_group, size_of_group}; +use dropset_macros::{constant_group, discriminant_enum, size_of_group}; use pinocchio::Address as Pubkey; +// region: node_tag +/// Discriminant tag for nodes in the market memory map. +#[discriminant_enum("common/memory", "NODE_TAG")] +pub enum NodeTag { + /// Seat node. + Seat, + /// Order node. + Order, + /// Stack node (free list). + StackNode, +} +// endregion: node_tag + constant_group! { #[prefix("DATA")] #[inject("common/memory")] diff --git a/interface/src/groups.rs b/interface/src/groups.rs index 54f2f35..60045f4 100644 --- a/interface/src/groups.rs +++ b/interface/src/groups.rs @@ -12,6 +12,7 @@ pub const INJECTION_GROUPS: &[&dropset_build::ConstantGroup] = &[ &crate::common::account::cpi::GROUP, &crate::common::memory::size_of::GROUP, &crate::common::memory::constants::GROUP, + &crate::common::memory::node_tag::GROUP, &crate::common::pubkey::constants::GROUP, &crate::common::token::constants::GROUP, ]; diff --git a/interface/src/order/mod.rs b/interface/src/order/mod.rs index c0ce667..ca817fc 100644 --- a/interface/src/order/mod.rs +++ b/interface/src/order/mod.rs @@ -1,4 +1,6 @@ use dropset_macros::svm_data; #[svm_data] -pub struct Order {} +pub struct Order { + pub tag: u8, +} diff --git a/interface/src/seat/mod.rs b/interface/src/seat/mod.rs index 0f6082c..2e86762 100644 --- a/interface/src/seat/mod.rs +++ b/interface/src/seat/mod.rs @@ -6,16 +6,14 @@ pub const MAX_ORDERS_PER_SIDE: usize = 5; #[svm_data] pub struct Seat { + pub tag: u8, pub parent: *mut Seat, pub left: *mut Seat, pub right: *mut Seat, pub user: Pubkey, - pub base_total: u64, - pub base_locked: u64, - pub quote_total: u64, - pub quote_locked: u64, - pub lamports_total: u64, - pub lamports_locked: u64, + pub base_available: u64, + pub quote_available: u64, + pub lamports_available: u64, pub asks: [*mut Order; MAX_ORDERS_PER_SIDE], pub bids: [*mut Order; MAX_ORDERS_PER_SIDE], } diff --git a/interface/src/stack/mod.rs b/interface/src/stack/mod.rs index 6727aaf..4fbd997 100644 --- a/interface/src/stack/mod.rs +++ b/interface/src/stack/mod.rs @@ -2,5 +2,6 @@ use dropset_macros::svm_data; #[svm_data] pub struct StackNode { + pub tag: u8, pub next: *mut StackNode, } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index b7f7ca2..ba695c5 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -86,30 +86,42 @@ pub fn cpi_accounts(input: TokenStream) -> TokenStream { TokenStream::from(cpi_accounts::expand(&input)) } -/// Attribute macro for instruction discriminant enums. +/// Attribute macro for discriminant enums. /// /// Re-emits the enum with `#[repr(u8)]` and explicit discriminant values, /// numbered from 0. Generates a `From for u8` impl and a hidden module -/// with `DISC_`-prefixed assembly constants. +/// with assembly constants prefixed by the given prefix (defaults to `DISC`). /// /// ```ignore -/// #[discriminant_enum("discriminant")] +/// #[discriminant_enum("entrypoint")] /// pub enum Discriminant { /// /// Register a new market. /// RegisterMarket, /// } +/// +/// #[discriminant_enum("market", "DISC_NODE")] +/// pub enum NodeTag { +/// /// Seat node. +/// Seat, +/// } /// ``` #[proc_macro_attribute] pub fn discriminant_enum(attr: TokenStream, item: TokenStream) -> TokenStream { - let target = parse_macro_input!(attr as LitStr); + let parser = |input: syn::parse::ParseStream| { + let target: LitStr = input.parse()?; + let prefix = if input.peek(syn::Token![,]) { + let _: syn::Token![,] = input.parse()?; + let p: LitStr = input.parse()?; + p.value() + } else { + "DISC".to_string() + }; + Ok((target.value(), prefix)) + }; + let (target, prefix) = parse_macro_input!(attr with parser); let input = parse_macro_input!(item as syn::ItemEnum); TokenStream::from(enum_to_asm::expand( - &target.value(), - "DISC", - 0, - "u8", - &input, - false, + &target, &prefix, 0, "u8", &input, false, )) } diff --git a/program/src/dropset/common/memory.s b/program/src/dropset/common/memory.s index 5b8fb3d..6dbef27 100644 --- a/program/src/dropset/common/memory.s +++ b/program/src/dropset/common/memory.s @@ -16,3 +16,10 @@ .equ DATA_BOOL_FALSE, 0 # Boolean false value. .equ DATA_BOOL_TRUE, 1 # Boolean true value. # ------------------------------------------------------------------------- + +# Discriminant tag for nodes in the market memory map. +# ------------------------------------------------------------------------- +.equ NODE_TAG_SEAT, 0 # Seat node. +.equ NODE_TAG_ORDER, 1 # Order node. +.equ NODE_TAG_STACK_NODE, 2 # Stack node (free list). +# ------------------------------------------------------------------------- From 54c875378149622f81572ec5b32433192623d2b7 Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:45:36 -0700 Subject: [PATCH 3/8] Update layout --- Cargo.lock | 1 + interface/Cargo.toml | 1 + interface/src/common/memory.rs | 38 +++++++++++++++++-- interface/src/entrypoint.rs | 36 ++++++++++-------- macros/src/constant_group/expand/immediate.rs | 26 +++++++++++++ macros/src/constant_group/expand/mod.rs | 5 +++ macros/src/constant_group/mod.rs | 2 + macros/src/constant_group/parse/mod.rs | 6 +++ program/src/dropset/common/memory.s | 3 +- program/src/dropset/entrypoint.s | 22 ++++++----- program/src/dropset/market/init_base_vault.s | 4 +- .../init_market_pda/create_market_account.s | 10 ++--- program/src/dropset/market/init_quote_vault.s | 4 +- tests/tests/cases/register_market.rs | 4 +- 14 files changed, 121 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03e3128..f908f51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,6 +893,7 @@ dependencies = [ "pinocchio", "pinocchio-token", "pinocchio-token-2022", + "solana-sbpf 0.16.0", ] [[package]] diff --git a/interface/Cargo.toml b/interface/Cargo.toml index e08c17a..b2eb907 100644 --- a/interface/Cargo.toml +++ b/interface/Cargo.toml @@ -4,6 +4,7 @@ dropset-macros = {path = "../macros"} pinocchio = {workspace = true} pinocchio-token = {workspace = true} pinocchio-token-2022 = {workspace = true} +solana-sbpf = {workspace = true} [package] name = "dropset-interface" diff --git a/interface/src/common/memory.rs b/interface/src/common/memory.rs index 064531d..6899ce6 100644 --- a/interface/src/common/memory.rs +++ b/interface/src/common/memory.rs @@ -2,7 +2,10 @@ use crate::common::account::EmptyAccount; use crate::common::token::InitializeAccount2; use crate::market::MarketHeader; use crate::market::register::CreateAccountData; -use dropset_macros::{constant_group, discriminant_enum, size_of_group}; +use crate::order::Order; +use crate::seat::Seat; +use crate::stack::StackNode; +use dropset_macros::{constant_group, discriminant_enum, size_of_group, svm_data}; use pinocchio::Address as Pubkey; // region: node_tag @@ -13,8 +16,8 @@ pub enum NodeTag { Seat, /// Order node. Order, - /// Stack node (free list). - StackNode, + /// Stack node. + Stack, } // endregion: node_tag @@ -36,12 +39,39 @@ constant_group! { BOOL_FALSE = immediate!(0), /// Boolean true value. BOOL_TRUE = immediate!(1), + } } +/// Sector-sized byte buffer (largest of Order, Seat, StackNode). +#[svm_data] +pub struct Sector( + [u8; { + const ORDER: usize = core::mem::size_of::(); + const SEAT: usize = core::mem::size_of::(); + const STACK: usize = core::mem::size_of::(); + if ORDER >= SEAT && ORDER >= STACK { + ORDER + } else if SEAT >= STACK { + SEAT + } else { + STACK + } + }], +); + // region: size_of_group_example size_of_group! { #[inject("common/memory")] - [u8, u64, Pubkey, EmptyAccount, MarketHeader, CreateAccountData, InitializeAccount2] + [ + u8, + u64, + Pubkey, + EmptyAccount, + MarketHeader, + CreateAccountData, + InitializeAccount2, + Sector, + ] } // endregion: size_of_group_example diff --git a/interface/src/entrypoint.rs b/interface/src/entrypoint.rs index 7bc4beb..e087dd2 100644 --- a/interface/src/entrypoint.rs +++ b/interface/src/entrypoint.rs @@ -2,6 +2,7 @@ use crate::common::account::EmptyAccount; use crate::market::MarketHeader; use dropset_macros::{constant_group, discriminant_enum, svm_data}; use pinocchio::account::RuntimeAccount; +use solana_sbpf::ebpf::MM_INPUT_START; // region: discriminant_enum /// Instruction discriminants. @@ -34,9 +35,9 @@ pub struct InputBufferHeader { pub n_accounts: u64, pub user: EmptyAccount, pub market: RuntimeAccount, - pub market_data_header: MarketHeader, - /// MarketHeader.next initializes to this offset. - pub market_data_bytes: u8, + pub market_header: MarketHeader, + /// MarketHeader.next initializes to an absolute pointer to this byte. + pub market_sectors_start: u8, } // endregion: input_buffer_header @@ -66,20 +67,25 @@ constant_group! { USER_DATA_TO_MARKET_ADDRESS = relative_offset!( InputBufferHeader, user.data, market.address ), - /// From input buffer to market data next pointer. - MARKET_DATA_NEXT = offset!(InputBufferHeader.market_data_header.next), - /// From input buffer to market data bump. - MARKET_DATA_BUMP = offset!(InputBufferHeader.market_data_header.bump), - /// From input buffer to market data base vault bump. - MARKET_DATA_BASE_VAULT_BUMP = offset!( - InputBufferHeader.market_data_header.base_vault_bump + /// From input buffer to market header next pointer. + MARKET_HEADER_NEXT = offset!(InputBufferHeader.market_header.next), + /// From input buffer to market header bump. + MARKET_HEADER_BUMP = offset!(InputBufferHeader.market_header.bump), + /// From input buffer to market header base vault bump. + MARKET_HEADER_BASE_VAULT_BUMP = offset!( + InputBufferHeader.market_header.base_vault_bump ), - /// From input buffer to market data quote vault bump. - MARKET_DATA_QUOTE_VAULT_BUMP = offset!( - InputBufferHeader.market_data_header.quote_vault_bump + /// From input buffer to market header quote vault bump. + MARKET_HEADER_QUOTE_VAULT_BUMP = offset!( + InputBufferHeader.market_header.quote_vault_bump + ), + /// From input buffer to first sector in market memory map. + MARKET_SECTORS_START = offset!(InputBufferHeader.market_sectors_start), + /// Absolute SBPF pointer to first sector in market memory map. + MARKET_SECTORS_START_PTR = wide!( + MM_INPUT_START as i64 + + core::mem::offset_of!(InputBufferHeader, market_sectors_start) as i64 ), - /// From input buffer to first byte after market data header. - MARKET_DATA_BYTES = offset!(InputBufferHeader.market_data_bytes), } } // endregion: constant_group_example diff --git a/macros/src/constant_group/expand/immediate.rs b/macros/src/constant_group/expand/immediate.rs index 31bb7d5..aec2af9 100644 --- a/macros/src/constant_group/expand/immediate.rs +++ b/macros/src/constant_group/expand/immediate.rs @@ -27,3 +27,29 @@ pub fn expand_immediate( (def, meta_ident) } + +/// Expand `wide!(expr)` into an i64 constant for `lddw`. Name gets `_WD` suffix. +pub fn expand_wide( + base_name: &Ident, + asm_name: &str, + doc: &str, + expr: &syn::Expr, +) -> (proc_macro2::TokenStream, Ident) { + let rust_name = Ident::new(&format!("{}_WD", base_name), base_name.span()); + let asm_name = format!("{}_WD", asm_name); + let meta_ident = codegen::meta_ident(&asm_name, base_name.span()); + + let meta = codegen::wide_meta(&meta_ident, &asm_name, doc, quote! { #rust_name }); + + let def = quote! { + #[doc = #doc] + pub const #rust_name: i64 = { + use super::*; + (#expr) as i64 + }; + + #meta + }; + + (def, meta_ident) +} diff --git a/macros/src/constant_group/expand/mod.rs b/macros/src/constant_group/expand/mod.rs index 4124e6d..ef3b4f5 100644 --- a/macros/src/constant_group/expand/mod.rs +++ b/macros/src/constant_group/expand/mod.rs @@ -93,6 +93,11 @@ pub fn expand(input: &ConstantGroupInput) -> proc_macro2::TokenStream { const_defs.push(def); meta_idents.push(meta); } + ConstantKind::Wide { expr } => { + let (def, meta) = immediate::expand_wide(base_name, &asm_name, doc, expr); + const_defs.push(def); + meta_idents.push(meta); + } ConstantKind::Pubkey { expr } => { pubkey::expand_pubkey(&asm_name, doc, expr, &mut const_defs, &mut meta_idents); } diff --git a/macros/src/constant_group/mod.rs b/macros/src/constant_group/mod.rs index e452f68..c2b2eaa 100644 --- a/macros/src/constant_group/mod.rs +++ b/macros/src/constant_group/mod.rs @@ -23,6 +23,8 @@ pub(crate) enum ConstantKind { }, /// `immediate!(expr)`: value must fit i32, exposed as i32 in Rust. Immediate { expr: Expr }, + /// `wide!(expr)`: 64-bit immediate for `lddw`, exposed as i64 in Rust. + Wide { expr: Expr }, /// `pubkey!(expr)`: splits a pubkey into four 8-byte chunks, emitting /// `_CHUNK_{0..3}_LO` and `_CHUNK_{0..3}_HI` i32 immediates. Pubkey { expr: Expr }, diff --git a/macros/src/constant_group/parse/mod.rs b/macros/src/constant_group/parse/mod.rs index 9f267da..ef711ec 100644 --- a/macros/src/constant_group/parse/mod.rs +++ b/macros/src/constant_group/parse/mod.rs @@ -72,6 +72,12 @@ impl Parse for ConstantGroupInput { let expr: Expr = inner.parse()?; ConstantKind::Immediate { expr } } + "wide" => { + let inner; + syn::parenthesized!(inner in content); + let expr: Expr = inner.parse()?; + ConstantKind::Wide { expr } + } "pubkey" => { let inner; syn::parenthesized!(inner in content); diff --git a/program/src/dropset/common/memory.s b/program/src/dropset/common/memory.s index 6dbef27..a131e84 100644 --- a/program/src/dropset/common/memory.s +++ b/program/src/dropset/common/memory.s @@ -5,6 +5,7 @@ .equ SIZE_OF_MARKET_HEADER, 43 # Size of MarketHeader in bytes. .equ SIZE_OF_CREATE_ACCOUNT_DATA, 52 # Size of CreateAccountData in bytes. .equ SIZE_OF_INITIALIZE_ACCOUNT2, 33 # Size of InitializeAccount2 in bytes. +.equ SIZE_OF_SECTOR, 161 # Size of Sector in bytes. # Common data-related constants. # ------------------------------------------------------------------------- @@ -21,5 +22,5 @@ # ------------------------------------------------------------------------- .equ NODE_TAG_SEAT, 0 # Seat node. .equ NODE_TAG_ORDER, 1 # Order node. -.equ NODE_TAG_STACK_NODE, 2 # Stack node (free list). +.equ NODE_TAG_STACK, 2 # Stack node. # ------------------------------------------------------------------------- diff --git a/program/src/dropset/entrypoint.s b/program/src/dropset/entrypoint.s index ce965a6..bc242f9 100644 --- a/program/src/dropset/entrypoint.s +++ b/program/src/dropset/entrypoint.s @@ -46,16 +46,18 @@ .equ IB_LAMPORTS_TO_DATA_REL_OFF_IMM, 16 # From user data to market address in the input buffer. .equ IB_USER_DATA_TO_MARKET_ADDRESS_REL_OFF_IMM, 10256 -# From input buffer to market data next pointer. -.equ IB_MARKET_DATA_NEXT_OFF, 10464 -# From input buffer to market data bump. -.equ IB_MARKET_DATA_BUMP_OFF, 10472 -# From input buffer to market data base vault bump. -.equ IB_MARKET_DATA_BASE_VAULT_BUMP_OFF, 10473 -# From input buffer to market data quote vault bump. -.equ IB_MARKET_DATA_QUOTE_VAULT_BUMP_OFF, 10474 -# From input buffer to first byte after market data header. -.equ IB_MARKET_DATA_BYTES_OFF, 10475 +# From input buffer to market header next pointer. +.equ IB_MARKET_HEADER_NEXT_OFF, 10464 +# From input buffer to market header bump. +.equ IB_MARKET_HEADER_BUMP_OFF, 10472 +# From input buffer to market header base vault bump. +.equ IB_MARKET_HEADER_BASE_VAULT_BUMP_OFF, 10473 +# From input buffer to market header quote vault bump. +.equ IB_MARKET_HEADER_QUOTE_VAULT_BUMP_OFF, 10474 +# From input buffer to first sector in market memory map. +.equ IB_MARKET_SECTORS_START_OFF, 10475 +# Absolute SBPF pointer to first sector in market memory map. +.equ IB_MARKET_SECTORS_START_PTR_WD, 17179879659 # ------------------------------------------------------------------------- entrypoint: diff --git a/program/src/dropset/market/init_base_vault.s b/program/src/dropset/market/init_base_vault.s index 5ed71b1..4c1c046 100644 --- a/program/src/dropset/market/init_base_vault.s +++ b/program/src/dropset/market/init_base_vault.s @@ -91,9 +91,9 @@ init_base_vault_advance: # if result != entrypoint::RETURN_SUCCESS # return result jne r0, RETURN_SUCCESS, init_base_vault_failed - # input.market.data.base_vault_bump = frame.bump + # input.market_header.base_vault_bump = frame.bump ldxb r7, [r10 + RM_FM_BUMP_OFF] - stxb [r8 + IB_MARKET_DATA_BASE_VAULT_BUMP_OFF], r7 + stxb [r8 + IB_MARKET_HEADER_BASE_VAULT_BUMP_OFF], r7 # acct += EmptyAccount.size add64 r9, SIZE_OF_EMPTY_ACCOUNT ja init_base_vault_return diff --git a/program/src/dropset/market/init_market_pda/create_market_account.s b/program/src/dropset/market/init_market_pda/create_market_account.s index 9f48a23..eb5cf87 100644 --- a/program/src/dropset/market/init_market_pda/create_market_account.s +++ b/program/src/dropset/market/init_market_pda/create_market_account.s @@ -72,12 +72,12 @@ create_market_account: # syscall.seeds_len = market::register::N_PDA_SIGNERS mov64 r5, RM_N_PDA_SIGNERS call sol_invoke_signed_c - # input.market.data.next = input + entrypoint::input_buffer::MARKET_DATA_BYTES + # input.market_header.next = input + entrypoint::input_buffer::MARKET_SECTORS_START ldxdw r6, [r10 + RM_FM_INPUT_OFF] mov64 r7, r6 - add64 r7, IB_MARKET_DATA_BYTES_OFF - stxdw [r6 + IB_MARKET_DATA_NEXT_OFF], r7 - # input.market.data.bump = frame.bump + add64 r7, IB_MARKET_SECTORS_START_OFF + stxdw [r6 + IB_MARKET_HEADER_NEXT_OFF], r7 + # input.market_header.bump = frame.bump ldxb r7, [r10 + RM_FM_BUMP_OFF] - stxb [r6 + IB_MARKET_DATA_BUMP_OFF], r7 + stxb [r6 + IB_MARKET_HEADER_BUMP_OFF], r7 ja create_market_account_return diff --git a/program/src/dropset/market/init_quote_vault.s b/program/src/dropset/market/init_quote_vault.s index 6bc60e7..de6843e 100644 --- a/program/src/dropset/market/init_quote_vault.s +++ b/program/src/dropset/market/init_quote_vault.s @@ -102,7 +102,7 @@ init_quote_vault_done_token_program: mov64 r1, r9 mov64 r2, r10 call init_vault - # input.market.data.quote_vault_bump = frame.bump + # input.market_header.quote_vault_bump = frame.bump ldxb r7, [r10 + RM_FM_BUMP_OFF] - stxb [r8 + IB_MARKET_DATA_QUOTE_VAULT_BUMP_OFF], r7 + stxb [r8 + IB_MARKET_HEADER_QUOTE_VAULT_BUMP_OFF], r7 ja init_quote_vault_return diff --git a/tests/tests/cases/register_market.rs b/tests/tests/cases/register_market.rs index 80b559f..2e1f554 100644 --- a/tests/tests/cases/register_market.rs +++ b/tests/tests/cases/register_market.rs @@ -1,6 +1,6 @@ use dropset_interface::common::pubkey::constants::CHUNK_3_OFF; use dropset_interface::common::pubkey::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID}; -use dropset_interface::entrypoint::input_buffer::MARKET_DATA_BYTES_OFF; +use dropset_interface::entrypoint::input_buffer::MARKET_SECTORS_START_OFF; use dropset_interface::market::MarketHeader; use dropset_interface::market::constants::{VAULT_INDEX_BASE, VAULT_INDEX_QUOTE}; use dropset_interface::market::register::Accounts; @@ -251,7 +251,7 @@ fn check_market_header_bumps( ) { let header: &MarketHeader = unsafe { &*(data.as_ptr() as *const MarketHeader) }; - let expected_next = MM_INPUT_START + MARKET_DATA_BYTES_OFF as u64; + let expected_next = MM_INPUT_START + MARKET_SECTORS_START_OFF as u64; let actual_next = header.next as u64; if actual_next != expected_next { errors.push(format!( From 466bc76f4c152aee6e6a92b75e53f2f242d9c647 Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:56:21 -0700 Subject: [PATCH 4/8] Update algos for absolute header --- docs/algorithms/market/INIT-BASE-VAULT.tex | 4 +-- docs/algorithms/market/INIT-QUOTE-VAULT.tex | 4 +-- .../init-market-pda/CREATE-MARKET-ACCOUNT.tex | 8 ++--- docs/src/program/markets.md | 29 +++++++++++++++++++ interface/src/common/memory.rs | 2 ++ interface/src/market/register.rs | 2 ++ interface/src/order/mod.rs | 2 ++ interface/src/seat/mod.rs | 2 ++ interface/src/stack/mod.rs | 2 ++ .../init_market_pda/create_market_account.s | 5 ++-- 10 files changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/algorithms/market/INIT-BASE-VAULT.tex b/docs/algorithms/market/INIT-BASE-VAULT.tex index 82ce44e..d3b01f0 100644 --- a/docs/algorithms/market/INIT-BASE-VAULT.tex +++ b/docs/algorithms/market/INIT-BASE-VAULT.tex @@ -23,7 +23,7 @@ \ENSURE $r_9$ = acct \ENSURE frame.token\_program\_id \ENSURE frame.token\_program\_is\_2022 - \ENSURE input.market.data.base\_vault\_bump = frame.bump + \ENSURE input.market\_header.base\_vault\_bump = frame.bump \PROCEDURE{INIT-BASE-VAULT}{acct, frame} \COMMENT{Retrieve input buffer pointers and advance to base token program account.} \STATE input = frame.input @@ -64,7 +64,7 @@ \RETURN result \ENDIF \COMMENT{Store derived bump in market account data.} - \STATE input.market.data.base\_vault\_bump = frame.bump + \STATE input.market\_header.base\_vault\_bump = frame.bump \COMMENT{Advance to quote token program account.} \STATE acct += \texttt{EmptyAccount.size} \ENDPROCEDURE diff --git a/docs/algorithms/market/INIT-QUOTE-VAULT.tex b/docs/algorithms/market/INIT-QUOTE-VAULT.tex index 1889804..127e62c 100644 --- a/docs/algorithms/market/INIT-QUOTE-VAULT.tex +++ b/docs/algorithms/market/INIT-QUOTE-VAULT.tex @@ -17,7 +17,7 @@ \REQUIRE frame.pda\_seeds[2].addr = \&frame.bump \REQUIRE frame.pda\_seeds[2].len = \texttt{u8.size} \REQUIRE frame.rent - \ENSURE input.market.data.quote\_vault\_bump = frame.bump + \ENSURE input.market\_header.quote\_vault\_bump = frame.bump \PROCEDURE{INIT-QUOTE-VAULT}{acct, frame, input, input\_shifted} \COMMENT{Check quote token program account.} \IF{acct.duplicate == \texttt{common::account::NON\_DUP\_MARKER}} @@ -62,7 +62,7 @@ \STATE frame.mint = \&input\_shifted.quote\_mint \STATE \CALL{INIT-VAULT}{acct, frame} \COMMENT{Store derived bump in market account data.} - \STATE input.market.data.quote\_vault\_bump = frame.bump + \STATE input.market\_header.quote\_vault\_bump = frame.bump \ENDPROCEDURE \end{algorithmic} \end{algorithm} diff --git a/docs/algorithms/market/init-market-pda/CREATE-MARKET-ACCOUNT.tex b/docs/algorithms/market/init-market-pda/CREATE-MARKET-ACCOUNT.tex index d4be91e..a58c61f 100644 --- a/docs/algorithms/market/init-market-pda/CREATE-MARKET-ACCOUNT.tex +++ b/docs/algorithms/market/init-market-pda/CREATE-MARKET-ACCOUNT.tex @@ -13,8 +13,8 @@ \REQUIRE frame.cpi[1].info.data\_len = \texttt{common::memory::LEN\_ZERO} \ENSURE frame.signers\_seeds.addr = \&frame.pda\_seeds \ENSURE frame.signers\_seeds.len = \texttt{frame.pda\_seeds.n\_seeds} - \ENSURE input.market.data.next = input + \texttt{entrypoint::input\_buffer::MARKET\_DATA\_BYTES} - \ENSURE input.market.data.bump = frame.bump + \ENSURE input.market\_header.next = \texttt{\&entrypoint::input\_buffer::MARKET\_SECTORS\_START} + \ENSURE input.market\_header.bump = frame.bump \PROCEDURE{CREATE-MARKET-ACCOUNT}{frame} \COMMENT{Assign CPI account fields via immediates.} \STATE frame.cpi[0].info.is\_signer = \texttt{true} @@ -55,8 +55,8 @@ \STATE syscall.seeds\_len = \texttt{market::register::N\_PDA\_SIGNERS} \STATE \CALL{sol-invoke-signed-c}{system-program::CreateAccount} \COMMENT{Initialize market header next pointer and store derived bump.} - \STATE input.market.data.next = input + \texttt{entrypoint::input\_buffer::MARKET\_DATA\_BYTES} - \STATE input.market.data.bump = frame.bump + \STATE input.market\_header.next = \texttt{\&entrypoint::input\_buffer::MARKET\_SECTORS\_START} + \STATE input.market\_header.bump = frame.bump \ENDPROCEDURE \end{algorithmic} \end{algorithm} diff --git a/docs/src/program/markets.md b/docs/src/program/markets.md index 6246096..84ed57b 100644 --- a/docs/src/program/markets.md +++ b/docs/src/program/markets.md @@ -21,6 +21,34 @@ All relevant information is derived from the accounts: +### Input buffer + +The registration `InputBuffer` extends the base +[input buffer] header with the base and quote mint +accounts: + + + +All fields through `market` sit at compile-time-known +offsets from `r1` (see [InputBufferHeader]). +The mint accounts that follow have variable-length data, +so their positions cannot be determined statically. +The assembly handles this with a dynamic offset: after +reading `base_mint.data_len`, it computes + +``` +input_shifted = input + padded(base_mint.data_len) +``` + +and stores the result in `Frame.input_shifted`. From that +shifted base, all `quote_mint` fields are accessible at +static offsets (the `RM_QUOTE_*` constants derived from the +`InputBuffer` layout, which assumes zero-length mint data +via `EmptyAccount`). Accounts after `quote_mint` (System +Program, Rent sysvar, token programs, vaults) are located +by walking forward from `input_shifted` using each +account's padded data length. + The entrypoint dispatches to [REGISTER-MARKET](#register-market), which validates the provided accounts, derives and creates the market PDA, then initializes the base and quote @@ -119,6 +147,7 @@ account for the given mint, with the market as the account owner. [input buffer]: inputs#input-buffer +[InputBufferHeader]: inputs#input-buffer [System Program]: https://solana.com/docs/core/programs/builtin-programs#the-system-program [Rent]: https://docs.rs/pinocchio/0.11.0/pinocchio/sysvars/rent/struct.Rent.html [Token Program]: https://github.com/solana-program/token diff --git a/interface/src/common/memory.rs b/interface/src/common/memory.rs index 6899ce6..bac13ee 100644 --- a/interface/src/common/memory.rs +++ b/interface/src/common/memory.rs @@ -43,6 +43,7 @@ constant_group! { } } +// region: sector /// Sector-sized byte buffer (largest of Order, Seat, StackNode). #[svm_data] pub struct Sector( @@ -59,6 +60,7 @@ pub struct Sector( } }], ); +// endregion: sector // region: size_of_group_example size_of_group! { diff --git a/interface/src/market/register.rs b/interface/src/market/register.rs index 235e3d4..3498379 100644 --- a/interface/src/market/register.rs +++ b/interface/src/market/register.rs @@ -38,6 +38,7 @@ pub struct Data { } // endregion: register_market_data +// region: register_input_buffer #[svm_data] pub struct InputBuffer { pub n_accounts: u64, @@ -47,6 +48,7 @@ pub struct InputBuffer { pub base_mint: EmptyAccount, pub quote_mint: EmptyAccount, } +// endregion: register_input_buffer constant_group! { #[prefix("RM")] diff --git a/interface/src/order/mod.rs b/interface/src/order/mod.rs index ca817fc..9cc6437 100644 --- a/interface/src/order/mod.rs +++ b/interface/src/order/mod.rs @@ -1,6 +1,8 @@ use dropset_macros::svm_data; +// region: order #[svm_data] pub struct Order { pub tag: u8, } +// endregion: order diff --git a/interface/src/seat/mod.rs b/interface/src/seat/mod.rs index 2e86762..1cde948 100644 --- a/interface/src/seat/mod.rs +++ b/interface/src/seat/mod.rs @@ -2,6 +2,7 @@ use crate::order::Order; use dropset_macros::svm_data; use pinocchio::Address as Pubkey; +// region: seat pub const MAX_ORDERS_PER_SIDE: usize = 5; #[svm_data] @@ -17,3 +18,4 @@ pub struct Seat { pub asks: [*mut Order; MAX_ORDERS_PER_SIDE], pub bids: [*mut Order; MAX_ORDERS_PER_SIDE], } +// endregion: seat diff --git a/interface/src/stack/mod.rs b/interface/src/stack/mod.rs index 4fbd997..be2e044 100644 --- a/interface/src/stack/mod.rs +++ b/interface/src/stack/mod.rs @@ -1,7 +1,9 @@ use dropset_macros::svm_data; +// region: stack_node #[svm_data] pub struct StackNode { pub tag: u8, pub next: *mut StackNode, } +// endregion: stack_node diff --git a/program/src/dropset/market/init_market_pda/create_market_account.s b/program/src/dropset/market/init_market_pda/create_market_account.s index eb5cf87..e1272ab 100644 --- a/program/src/dropset/market/init_market_pda/create_market_account.s +++ b/program/src/dropset/market/init_market_pda/create_market_account.s @@ -72,10 +72,9 @@ create_market_account: # syscall.seeds_len = market::register::N_PDA_SIGNERS mov64 r5, RM_N_PDA_SIGNERS call sol_invoke_signed_c - # input.market_header.next = input + entrypoint::input_buffer::MARKET_SECTORS_START + # input.market_header.next = &entrypoint::input_buffer::MARKET_SECTORS_START ldxdw r6, [r10 + RM_FM_INPUT_OFF] - mov64 r7, r6 - add64 r7, IB_MARKET_SECTORS_START_OFF + lddw r7, IB_MARKET_SECTORS_START_PTR_WD stxdw [r6 + IB_MARKET_HEADER_NEXT_OFF], r7 # input.market_header.bump = frame.bump ldxb r7, [r10 + RM_FM_BUMP_OFF] From 2fabd98ffe792760249b6f7213b090ee34368f47 Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:27:26 -0700 Subject: [PATCH 5/8] Add docs about new data structures --- CLAUDE.md | 3 ++ docs/.vitepress/config.js | 3 ++ docs/src/program/markets.md | 32 +++++++-------- docs/src/program/orders.md | 5 +++ docs/src/program/seats.md | 8 ++++ docs/src/program/sectors.md | 77 +++++++++++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 docs/src/program/orders.md create mode 100644 docs/src/program/seats.md create mode 100644 docs/src/program/sectors.md diff --git a/CLAUDE.md b/CLAUDE.md index 2d095a2..0f16d12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,9 @@ development topics: - `docs/src/program/layout.md` program memory layout - `docs/src/program/inputs.md` input format specs - `docs/src/program/markets.md` market structure +- `docs/src/program/sectors.md` market memory sectors +- `docs/src/program/seats.md` seat data structure +- `docs/src/program/orders.md` order data structure - `docs/src/program/algorithm-index.md` algorithm documentation diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 7e501e7..9ca11c8 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -115,6 +115,9 @@ export default { { text: "Layout", link: "/program/layout" }, { text: "Inputs", link: "/program/inputs" }, { text: "Markets", link: "/program/markets" }, + { text: "Sectors", link: "/program/sectors" }, + { text: "Seats", link: "/program/seats" }, + { text: "Orders", link: "/program/orders" }, { text: "Algorithm Index", link: "/program/algorithm-index" }, ], }, diff --git a/docs/src/program/markets.md b/docs/src/program/markets.md index 84ed57b..0eba1ea 100644 --- a/docs/src/program/markets.md +++ b/docs/src/program/markets.md @@ -29,31 +29,25 @@ accounts: -All fields through `market` sit at compile-time-known -offsets from `r1` (see [InputBufferHeader]). -The mint accounts that follow have variable-length data, +All fields through `Market` sit at compile-time-known +offsets from `r1` (see [`InputBufferHeader`]). +`BaseMint` and `QuoteMint` have variable-length data, so their positions cannot be determined statically. -The assembly handles this with a dynamic offset: after -reading `base_mint.data_len`, it computes +[MARKET-PDA-PRELUDE](#market-pda-prelude) handles this +with a dynamic offset: after reading +`input.base_mint.data_len`, it computes ``` -input_shifted = input + padded(base_mint.data_len) +frame.input_shifted = input + padded(input.base_mint.data_len) ``` -and stores the result in `Frame.input_shifted`. From that -shifted base, all `quote_mint` fields are accessible at -static offsets (the `RM_QUOTE_*` constants derived from the -`InputBuffer` layout, which assumes zero-length mint data -via `EmptyAccount`). Accounts after `quote_mint` (System -Program, Rent sysvar, token programs, vaults) are located -by walking forward from `input_shifted` using each +From `frame.input_shifted`, all `QuoteMint` fields are +accessible at static offsets. Accounts after `QuoteMint` +(`SystemProgram`, `RentSysvar`, `BaseTokenProgram`, +`BaseVault`, `QuoteTokenProgram`, `QuoteVault`) are located +by walking forward from `frame.input_shifted` using each account's padded data length. -The entrypoint dispatches to -[REGISTER-MARKET](#register-market), which validates the provided accounts, -derives and creates the market PDA, then initializes the base and quote -token vaults. - ## REGISTER-MARKET REGISTER-MARKET is the top-level orchestrator for market creation. It sequences @@ -147,7 +141,7 @@ account for the given mint, with the market as the account owner. [input buffer]: inputs#input-buffer -[InputBufferHeader]: inputs#input-buffer +[`InputBufferHeader`]: inputs#input-buffer [System Program]: https://solana.com/docs/core/programs/builtin-programs#the-system-program [Rent]: https://docs.rs/pinocchio/0.11.0/pinocchio/sysvars/rent/struct.Rent.html [Token Program]: https://github.com/solana-program/token diff --git a/docs/src/program/orders.md b/docs/src/program/orders.md new file mode 100644 index 0000000..ce4d4af --- /dev/null +++ b/docs/src/program/orders.md @@ -0,0 +1,5 @@ +# Orders + +An `Order` represents an entry on the order book. + + diff --git a/docs/src/program/seats.md b/docs/src/program/seats.md new file mode 100644 index 0000000..af708bb --- /dev/null +++ b/docs/src/program/seats.md @@ -0,0 +1,8 @@ +# Seats + +A `Seat` is a market maker's trading account within a +market. +It holds tree pointers for lookup, the user's address, +token balances, and per-side order arrays. + + diff --git a/docs/src/program/sectors.md b/docs/src/program/sectors.md new file mode 100644 index 0000000..81f17a5 --- /dev/null +++ b/docs/src/program/sectors.md @@ -0,0 +1,77 @@ +# Sectors + +Market account data begins with a [MarketHeader](markets) +followed by a contiguous array of fixed-size sectors. Each +sector holds one of three node types ([Order](orders), +[Seat](seats), or [StackNode](#stacknode)), but a node does not +necessarily occupy the entire sector. + +Because the market account is at a [fixed position] in the +input buffer, its data offsets are persisted across +transactions. `MarketHeader` stores absolute SBPF pointers +into the sector array (`seats`, `asks`, `bids`, `top`, +`next`) that remain valid without recomputation. + +[fixed position]: inputs#input-buffer + +```txt ++----------------+----------+----------+----------+-----+ +| MarketHeader | Sector 0 | Sector 1 | Sector 2 | ... | ++----------------+----------+----------+----------+-----+ +``` + +A `Sector` is a byte buffer sized to the largest node type: + + + +The first byte of every sector is a `NodeTag` discriminant +that identifies its contents: + + + +## Order + +A sector holding an [Order](orders) is an active node in +one of the market's order trees. + +```txt ++-----+--------------------------------------------------+ +| tag | (unused) | ++-----+--------------------------------------------------+ +``` + + + +## Seat + +A sector holding a [Seat](seats) is an active node in +the market's seat tree. + +```txt ++-----+--------+------+-------+------+-----+------+------+ +| tag | parent | left | right | user | ... | asks | bids | ++-----+--------+------+-------+------+-----+------+------+ +``` + + + +## StackNode + +A sector holding a `StackNode` is a freed sector on the +free sector stack. + +```txt ++-----+------+-------------------------------------------+ +| tag | next | (unused) | ++-----+------+-------------------------------------------+ +``` + + + +## Allocation + +`MarketHeader.next` points to the next unallocated sector +in the memory map. `MarketHeader.top` points to the top of +the free sector stack. When a sector is freed, it becomes a +`StackNode` pushed onto the stack. New allocations pop from +the stack first; when the stack is empty, `next` advances. From d204d36c64373e4262b0f017be2defb5e02e199b Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:30:17 -0700 Subject: [PATCH 6/8] Update skill to check ASCII --- .claude/skills/docs-review/SKILL.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.claude/skills/docs-review/SKILL.md b/.claude/skills/docs-review/SKILL.md index c744a49..b50b390 100644 --- a/.claude/skills/docs-review/SKILL.md +++ b/.claude/skills/docs-review/SKILL.md @@ -53,6 +53,17 @@ gaps, outdated content, and stale comments. - `` and `` component attributes should resolve correctly. +1. Verify that sector layout diagrams in + `docs/src/program/sectors.md` are consistent with + their data structures. For each node type (Order, + Seat, StackNode), compare the fields shown in the + ASCII diagram against the fields in the corresponding + struct definition (in `interface/src/order/mod.rs`, + `interface/src/seat/mod.rs`, + `interface/src/stack/mod.rs`). Flag any field that + is missing from the diagram, present in the diagram + but not the struct, or in the wrong order. + 1. Verify that all directory trees in the docs are current. For each tree, list the actual files on disk and compare against the rendered tree. Flag From 988811a6d384ea7bb7ab515de5b890d44aa5adf6 Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:35:27 -0700 Subject: [PATCH 7/8] Update reference links --- .claude/skills/docs-review/SKILL.md | 8 +++++--- docs/src/program/sectors.md | 18 +++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.claude/skills/docs-review/SKILL.md b/.claude/skills/docs-review/SKILL.md index b50b390..c750571 100644 --- a/.claude/skills/docs-review/SKILL.md +++ b/.claude/skills/docs-review/SKILL.md @@ -47,9 +47,11 @@ gaps, outdated content, and stale comments. should be valid. - Cross-page markdown links must use reference-style definitions (`[text][ref]` or `[text]` with a - `[text]: url` at the bottom of the file). Inline - links (`[text](url)`) are only acceptable for - same-page anchors (e.g. `[label](#anchor)`). + `[text]: url` at the bottom of the file). + Reference definitions must be placed at the end + of the file, not inline near their first usage. + Inline links (`[text](url)`) are only acceptable + for same-page anchors (e.g. `[label](#anchor)`). - `` and `` component attributes should resolve correctly. diff --git a/docs/src/program/sectors.md b/docs/src/program/sectors.md index 81f17a5..ae59255 100644 --- a/docs/src/program/sectors.md +++ b/docs/src/program/sectors.md @@ -1,9 +1,9 @@ # Sectors -Market account data begins with a [MarketHeader](markets) +Market account data begins with a [`MarketHeader`] followed by a contiguous array of fixed-size sectors. Each -sector holds one of three node types ([Order](orders), -[Seat](seats), or [StackNode](#stacknode)), but a node does not +sector holds one of three node types ([`Order`], +[`Seat`], or [`StackNode`]), but a node does not necessarily occupy the entire sector. Because the market account is at a [fixed position] in the @@ -12,8 +12,6 @@ transactions. `MarketHeader` stores absolute SBPF pointers into the sector array (`seats`, `asks`, `bids`, `top`, `next`) that remain valid without recomputation. -[fixed position]: inputs#input-buffer - ```txt +----------------+----------+----------+----------+-----+ | MarketHeader | Sector 0 | Sector 1 | Sector 2 | ... | @@ -31,7 +29,7 @@ that identifies its contents: ## Order -A sector holding an [Order](orders) is an active node in +A sector holding an [`Order`] is an active node in one of the market's order trees. ```txt @@ -44,7 +42,7 @@ one of the market's order trees. ## Seat -A sector holding a [Seat](seats) is an active node in +A sector holding a [`Seat`] is an active node in the market's seat tree. ```txt @@ -75,3 +73,9 @@ in the memory map. `MarketHeader.top` points to the top of the free sector stack. When a sector is freed, it becomes a `StackNode` pushed onto the stack. New allocations pop from the stack first; when the stack is empty, `next` advances. + +[`MarketHeader`]: markets +[`Order`]: orders +[`Seat`]: seats +[`StackNode`]: #stacknode +[fixed position]: inputs#input-buffer From e4a3b8240c19c2e6a7587f5849b87bcba38319a6 Mon Sep 17 00:00:00 2001 From: Alex Kahn <43892045+alnoki@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:37:04 -0700 Subject: [PATCH 8/8] Add code block --- docs/src/program/markets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/program/markets.md b/docs/src/program/markets.md index 0eba1ea..78c70fb 100644 --- a/docs/src/program/markets.md +++ b/docs/src/program/markets.md @@ -37,7 +37,7 @@ so their positions cannot be determined statically. with a dynamic offset: after reading `input.base_mint.data_len`, it computes -``` +```rs frame.input_shifted = input + padded(input.base_mint.data_len) ```