Skip to main content

torna_sdk/
lib.rs

1//! Torna client SDK -- the PathPlanner.
2//!
3//! Integrators call insert/update/delete/find with a 32-byte key; the planner reads
4//! the tree off-chain (via an `AccountReader`) and produces a ready `Instruction`
5//! with the exact account set. node_idx, bumps, paths, and spares never leak out.
6//!
7//! Layout constants mirror the FROZEN ABI (torna_docs/abi.md). If the engine layout
8//! changes, change it here too -- the cpitest/inttest in torna/integration will catch
9//! a mismatch because the SDK-built instructions run against the real torna.so.
10
11use solana_sdk::{
12    instruction::{AccountMeta, Instruction},
13    pubkey::Pubkey,
14};
15
16// ---- instruction discriminators ----
17pub const IX_INIT_TREE: u8 = 0;
18pub const IX_INSERT: u8 = 2;
19pub const IX_FIND: u8 = 3;
20pub const IX_DELETE: u8 = 8;
21pub const IX_INSERT_FAST: u8 = 16;
22pub const IX_UPDATE_FAST: u8 = 17;
23pub const IX_DELETE_FAST: u8 = 18;
24
25// engine error codes a client classifies for retry/fallback (see torna.c)
26pub const ERR_NEED_SPLIT_SLOT: u32 = 102; // InsertFast hit a full leaf -> use insert (cold)
27pub const ERR_DUPLICATE_KEY: u32 = 103;
28pub const ERR_KEY_NOT_FOUND: u32 = 104;
29pub const ERR_BAD_PATH: u32 = 105; // a node_idx/tree_uid mismatch -> path went stale, re-resolve
30
31// ---- frozen layout (abi.md) ----
32pub const KEY_SIZE: usize = 32;
33pub const NODE_HDR: usize = 44;
34pub const TREE_HEADER_SIZE: usize = 146;
35pub const ALLOC_SIZE: usize = 32;
36
37// header field offsets
38const H_VALUE_SIZE: usize = 46;
39const H_FANOUT: usize = 48;
40const H_NODE_SIZE: usize = 50;
41const H_ROOT: usize = 54;
42const H_HEIGHT: usize = 62;
43const H_LEFTMOST: usize = 66;
44const H_RIGHTMOST: usize = 74;
45const H_EPOCH: usize = 82;
46const H_AUTHORITY: usize = 90;
47// node field offsets
48const N_KEY_COUNT: usize = 2;
49const N_NEXT_LEAF: usize = 20;
50// allocator
51const A_HIGH_WATER: usize = 8;
52
53fn rd_u16(d: &[u8], o: usize) -> u16 { u16::from_le_bytes(d[o..o + 2].try_into().unwrap()) }
54fn rd_u32(d: &[u8], o: usize) -> u32 { u32::from_le_bytes(d[o..o + 4].try_into().unwrap()) }
55fn rd_u64(d: &[u8], o: usize) -> u64 { u64::from_le_bytes(d[o..o + 8].try_into().unwrap()) }
56
57/// Anything that can fetch raw account data (LiteSVM, RPC, a cache).
58pub trait AccountReader {
59    fn account_data(&self, key: &Pubkey) -> Option<Vec<u8>>;
60}
61
62/// Parsed, cache-relevant header fields.
63#[derive(Clone, Copy, Debug)]
64pub struct Header {
65    pub value_size: u16,
66    pub fanout: u16,
67    pub node_size: u32,
68    pub root: u64,
69    pub height: u32,
70    pub leftmost: u64,
71    pub rightmost: u64,
72    /// bumped ONLY on a structural change (split/merge/root). A stable epoch between
73    /// resolve and submit means the cached path is still valid; a change means re-resolve.
74    pub structure_epoch: u64,
75    pub authority: Pubkey,
76}
77
78impl Header {
79    fn parse(d: &[u8]) -> Header {
80        Header {
81            value_size: rd_u16(d, H_VALUE_SIZE),
82            fanout: rd_u16(d, H_FANOUT),
83            node_size: rd_u32(d, H_NODE_SIZE),
84            root: rd_u64(d, H_ROOT),
85            height: rd_u32(d, H_HEIGHT),
86            leftmost: rd_u64(d, H_LEFTMOST),
87            rightmost: rd_u64(d, H_RIGHTMOST),
88            structure_epoch: rd_u64(d, H_EPOCH),
89            authority: Pubkey::new_from_array(d[H_AUTHORITY..H_AUTHORITY + 32].try_into().unwrap()),
90        }
91    }
92}
93
94/// A handle to one tree: (program, creator, tree_id). All PDA/seed logic lives here.
95#[derive(Clone, Copy, Debug)]
96pub struct Tree {
97    pub program: Pubkey,
98    pub creator: Pubkey,
99    pub tree_id: u32,
100}
101
102impl Tree {
103    pub fn new(program: Pubkey, creator: Pubkey, tree_id: u32) -> Self {
104        Tree { program, creator, tree_id }
105    }
106
107    pub fn header_pda(&self) -> (Pubkey, u8) {
108        Pubkey::find_program_address(&[b"thdr", self.creator.as_ref(), &self.tree_id.to_le_bytes()], &self.program)
109    }
110    pub fn alloc_pda(&self) -> (Pubkey, u8) {
111        Pubkey::find_program_address(&[b"talloc", self.creator.as_ref(), &self.tree_id.to_le_bytes()], &self.program)
112    }
113    pub fn node_pda(&self, idx: u64) -> (Pubkey, u8) {
114        Pubkey::find_program_address(
115            &[b"tnode", self.creator.as_ref(), &self.tree_id.to_le_bytes(), &idx.to_le_bytes()], &self.program)
116    }
117
118    pub fn header(&self, r: &dyn AccountReader) -> Option<Header> {
119        r.account_data(&self.header_pda().0).map(|d| Header::parse(&d))
120    }
121    fn high_water(&self, r: &dyn AccountReader) -> Option<u64> {
122        r.account_data(&self.alloc_pda().0).map(|d| rd_u64(&d, A_HIGH_WATER))
123    }
124
125    /// Descent path root..leaf (node_idx list) for `key`. Empty if the tree is empty.
126    /// Mirrors the engine's descent: lower_bound, then "key == separator -> go right".
127    pub fn path(&self, r: &dyn AccountReader, key: &[u8; 32]) -> Option<Vec<u64>> {
128        let h = self.header(r)?;
129        if h.height == 0 { return Some(vec![]); }
130        let f = h.fanout as usize;
131        let kids_off = NODE_HDR + (f + 1) * KEY_SIZE;
132        let mut cur = h.root;
133        let mut path = vec![cur];
134        for _ in 0..h.height - 1 {
135            let d = r.account_data(&self.node_pda(cur).0)?;
136            let cnt = rd_u16(&d, N_KEY_COUNT) as usize;
137            // lower_bound: first i with keys[i] >= key
138            let (mut lo, mut hi) = (0usize, cnt);
139            while lo < hi {
140                let m = (lo + hi) / 2;
141                let mk = &d[NODE_HDR + m * KEY_SIZE..NODE_HDR + m * KEY_SIZE + KEY_SIZE];
142                if mk < &key[..] { lo = m + 1; } else { hi = m; }
143            }
144            let eq = lo < cnt && &d[NODE_HDR + lo * KEY_SIZE..NODE_HDR + lo * KEY_SIZE + KEY_SIZE] == &key[..];
145            let slot = if eq { lo + 1 } else { lo };
146            cur = rd_u64(&d, kids_off + slot * 8);
147            path.push(cur);
148        }
149        Some(path)
150    }
151
152    fn path_metas(&self, path: &[u64], leaf_writable: bool) -> Vec<AccountMeta> {
153        path.iter().enumerate().map(|(i, &n)| {
154            let pk = self.node_pda(n).0;
155            if leaf_writable && i == path.len() - 1 { AccountMeta::new(pk, false) }
156            else { AccountMeta::new_readonly(pk, false) }
157        }).collect()
158    }
159
160    // ---- hot path: header read-only, only the leaf writable (parallelizable) ----
161
162    /// InsertFast: place a new key/value into an existing leaf (fails if the leaf is
163    /// full -> caller falls back to `insert` for the cold split path).
164    pub fn insert_fast_ix(&self, r: &dyn AccountReader, authority: Pubkey, key: &[u8; 32], value: &[u8]) -> Option<Instruction> {
165        let path = self.path(r, key)?;
166        let mut data = vec![IX_INSERT_FAST];
167        data.extend_from_slice(key); data.extend_from_slice(value); data.push(path.len() as u8);
168        Some(self.fast_ix(authority, data, &path))
169    }
170    /// UpdateFast: overwrite the value of an existing key in place.
171    pub fn update_fast_ix(&self, r: &dyn AccountReader, authority: Pubkey, key: &[u8; 32], value: &[u8]) -> Option<Instruction> {
172        let path = self.path(r, key)?;
173        let mut data = vec![IX_UPDATE_FAST];
174        data.extend_from_slice(key); data.extend_from_slice(value); data.push(path.len() as u8);
175        Some(self.fast_ix(authority, data, &path))
176    }
177    /// DeleteFast: remove a key without rebalancing (a leaf may drop below MIN).
178    pub fn delete_fast_ix(&self, r: &dyn AccountReader, authority: Pubkey, key: &[u8; 32]) -> Option<Instruction> {
179        let path = self.path(r, key)?;
180        let mut data = vec![IX_DELETE_FAST];
181        data.extend_from_slice(key); data.push(path.len() as u8);
182        Some(self.fast_ix(authority, data, &path))
183    }
184    fn fast_ix(&self, authority: Pubkey, data: Vec<u8>, path: &[u64]) -> Instruction {
185        let mut metas = vec![
186            AccountMeta::new_readonly(self.header_pda().0, false),
187            AccountMeta::new_readonly(authority, true),
188        ];
189        metas.extend(self.path_metas(path, true));
190        Instruction::new_with_bytes(self.program, &data, metas)
191    }
192
193    /// Find: returns the instruction; the caller reads return_data [found u8, value..].
194    pub fn find_ix(&self, r: &dyn AccountReader, key: &[u8; 32]) -> Option<Instruction> {
195        let path = self.path(r, key)?;
196        let mut data = vec![IX_FIND]; data.extend_from_slice(key); data.push(path.len() as u8);
197        let mut metas = vec![AccountMeta::new_readonly(self.header_pda().0, false)];
198        metas.extend(self.path_metas(&path, false));
199        Some(Instruction::new_with_bytes(self.program, &data, metas))
200    }
201
202    // ---- cold path: Insert (descends, may split via CPI-created spares) ----
203
204    /// Insert (cold path): handles the empty-tree first insert and splits. Resolves the
205    /// spare node PDAs (height+2 of them) the engine may need. `rent_node` = rent-exempt
206    /// lamports for one node account (caller computes from its client).
207    pub fn insert_ix(&self, r: &dyn AccountReader, payer: Pubkey, key: &[u8; 32], value: &[u8], rent_node: u64) -> Option<Instruction> {
208        let h = self.header(r)?;
209        let hw = self.high_water(r)?;
210        let path = self.path(r, key)?;
211        let spare_n = h.height as usize + 2;
212        let mut data = vec![IX_INSERT];
213        data.extend_from_slice(key); data.extend_from_slice(value);
214        data.push(path.len() as u8); data.push(spare_n as u8);
215        data.extend_from_slice(&rent_node.to_le_bytes());
216        let mut spares = Vec::with_capacity(spare_n);
217        for i in 0..spare_n as u64 {
218            let (pk, b) = self.node_pda(hw + 1 + i);
219            data.push(b); spares.push(pk);
220        }
221        let mut metas = vec![
222            AccountMeta::new(self.header_pda().0, false),
223            AccountMeta::new(payer, true),
224            AccountMeta::new(self.alloc_pda().0, false),
225            AccountMeta::new_readonly(Pubkey::default(), false),
226        ];
227        metas.extend(self.path_metas(&path, false).into_iter().map(|m| AccountMeta::new(m.pubkey, false)));
228        for s in spares { metas.push(AccountMeta::new(s, false)); }
229        Some(Instruction::new_with_bytes(self.program, &data, metas))
230    }
231
232    /// Delete (cold path): removes a key and rebalances (borrow/merge), reclaiming
233    /// rent to `payer`. Resolves, for each non-root level, which sibling the engine
234    /// needs (right if our child is not the last, else left) by reading each parent.
235    /// `payer` must be the tree authority (Delete is primary-only).
236    pub fn delete_ix(&self, r: &dyn AccountReader, payer: Pubkey, key: &[u8; 32]) -> Option<Instruction> {
237        let h = self.header(r)?;
238        if h.height == 0 { return None; }
239        let path = self.path(r, key)?;
240        let height = path.len();
241        let ko = NODE_HDR + (h.fanout as usize + 1) * KEY_SIZE;
242        let mut sides = vec![0u8; height];
243        let mut sib_idxs: Vec<u64> = Vec::new();
244        for level in 1..height {
245            let node_idx = path[level];
246            let pd = r.account_data(&self.node_pda(path[level - 1]).0)?;
247            let pcnt = rd_u16(&pd, N_KEY_COUNT) as usize;
248            let kid = |i: usize| rd_u64(&pd, ko + i * 8);
249            let mut our = 0usize;
250            for i in 0..=pcnt { if kid(i) == node_idx { our = i; break; } }
251            if our < pcnt { sides[level] = 1; sib_idxs.push(kid(our + 1)); } // right sibling
252            else { sides[level] = 2; sib_idxs.push(kid(our - 1)); }          // left sibling
253        }
254        let mut data = vec![IX_DELETE];
255        data.extend_from_slice(key);
256        data.push(height as u8);
257        data.extend_from_slice(&sides);
258        let mut metas = vec![
259            AccountMeta::new(self.header_pda().0, false),
260            AccountMeta::new(payer, true),
261        ];
262        for &n in &path { metas.push(AccountMeta::new(self.node_pda(n).0, false)); }
263        for &s in &sib_idxs { metas.push(AccountMeta::new(self.node_pda(s).0, false)); }
264        Some(Instruction::new_with_bytes(self.program, &data, metas))
265    }
266
267    /// Resolve the cold-path Insert plan for `key`: the descent path and the spare node
268    /// PDAs (height+2 of them, with bumps) the engine may consume on a split. Used to
269    /// build a cold place when InsertFast hits a full leaf (ERR_NEED_SPLIT_SLOT).
270    pub fn cold_plan(&self, r: &dyn AccountReader, key: &[u8; 32]) -> Option<(Vec<u64>, Vec<(Pubkey, u8)>)> {
271        let h = self.header(r)?;
272        let hw = self.high_water(r)?;
273        let path = self.path(r, key)?;
274        let spare_n = h.height as u64 + 2;
275        let spares = (0..spare_n).map(|i| self.node_pda(hw + 1 + i)).collect();
276        Some((path, spares))
277    }
278
279    /// InitTree. `rent_hdr`/`rent_alloc` from the caller's client.
280    pub fn init_tree_ix(&self, payer: Pubkey, value_size: u16, fanout: u16, rent_hdr: u64, rent_alloc: u64) -> Instruction {
281        let (hdr, hb) = self.header_pda();
282        let (alc, ab) = self.alloc_pda();
283        let mut data = vec![IX_INIT_TREE];
284        data.extend_from_slice(&self.tree_id.to_le_bytes());
285        data.push(hb); data.push(ab);
286        data.extend_from_slice(&value_size.to_le_bytes());
287        data.extend_from_slice(&fanout.to_le_bytes());
288        data.extend_from_slice(&rent_hdr.to_le_bytes());
289        data.extend_from_slice(&rent_alloc.to_le_bytes());
290        Instruction::new_with_bytes(self.program, &data, vec![
291            AccountMeta::new(payer, true),
292            AccountMeta::new(hdr, false),
293            AccountMeta::new(alc, false),
294            AccountMeta::new_readonly(Pubkey::default(), false),
295        ])
296    }
297
298    // ---- client-side reads (walk the tree off-chain; no transaction needed) ----
299
300    /// In-order scan from the smallest key, up to `max` entries: (key, value) pairs.
301    /// Walks the forward leaf chain via the AccountReader. For an orderbook this is
302    /// the book in price-time order (best price first); take(1) = top of book.
303    pub fn scan(&self, r: &dyn AccountReader, max: usize) -> Vec<([u8; 32], Vec<u8>)> {
304        let h = match self.header(r) { Some(h) if h.height > 0 => h, _ => return vec![] };
305        let (f, vs) = (h.fanout as usize, h.value_size as usize);
306        let voff = NODE_HDR + (f + 1) * KEY_SIZE;
307        let mut idx = h.leftmost;
308        let mut out = Vec::new();
309        while idx != 0 && out.len() < max {
310            let d = match r.account_data(&self.node_pda(idx).0) { Some(d) => d, None => break };
311            let cnt = rd_u16(&d, N_KEY_COUNT) as usize;
312            for i in 0..cnt {
313                if out.len() >= max { break; }
314                let key: [u8; 32] = d[NODE_HDR + i * KEY_SIZE..NODE_HDR + i * KEY_SIZE + KEY_SIZE].try_into().unwrap();
315                let val = d[voff + i * vs..voff + i * vs + vs].to_vec();
316                out.push((key, val));
317            }
318            idx = rd_u64(&d, N_NEXT_LEAF);
319        }
320        out
321    }
322
323    /// The smallest entry (top of book), or None if empty.
324    pub fn best(&self, r: &dyn AccountReader) -> Option<([u8; 32], Vec<u8>)> {
325        self.scan(r, 1).into_iter().next()
326    }
327
328    /// The value stored at `key`, or None if absent.
329    pub fn get(&self, r: &dyn AccountReader, key: &[u8; 32]) -> Option<Vec<u8>> {
330        let h = self.header(r)?;
331        if h.height == 0 { return None; }
332        let leaf = *self.path(r, key)?.last()?;
333        let d = r.account_data(&self.node_pda(leaf).0)?;
334        let (f, vs) = (h.fanout as usize, h.value_size as usize);
335        let cnt = rd_u16(&d, N_KEY_COUNT) as usize;
336        let (mut lo, mut hi) = (0usize, cnt);
337        while lo < hi {
338            let m = (lo + hi) / 2;
339            if &d[NODE_HDR + m * KEY_SIZE..NODE_HDR + m * KEY_SIZE + KEY_SIZE] < &key[..] { lo = m + 1; } else { hi = m; }
340        }
341        if lo < cnt && &d[NODE_HDR + lo * KEY_SIZE..NODE_HDR + lo * KEY_SIZE + KEY_SIZE] == &key[..] {
342            let voff = NODE_HDR + (f + 1) * KEY_SIZE;
343            Some(d[voff + lo * vs..voff + lo * vs + vs].to_vec())
344        } else { None }
345    }
346
347    /// The header + the leaf account pubkeys a forward scan of up to `max_entries`
348    /// would traverse. A K-order match tx references these, so they go in an Address
349    /// Lookup Table (build the v0 message + ALT with solana-sdk's address_lookup_table;
350    /// the ALT program calls are standard Solana, not Torna-specific).
351    pub fn scan_accounts(&self, r: &dyn AccountReader, max_entries: usize) -> Vec<Pubkey> {
352        let h = match self.header(r) { Some(h) if h.height > 0 => h, _ => return vec![] };
353        let mut out = vec![self.header_pda().0];
354        let (mut idx, mut seen) = (h.leftmost, 0usize);
355        while idx != 0 && seen < max_entries {
356            let pk = self.node_pda(idx).0;
357            out.push(pk);
358            let d = match r.account_data(&pk) { Some(d) => d, None => break };
359            seen += rd_u16(&d, N_KEY_COUNT) as usize;
360            idx = rd_u64(&d, N_NEXT_LEAF);
361        }
362        out
363    }
364}
365
366/// One resolve+submit attempt for [`retry`].
367pub enum Attempt<T, E> {
368    Done(T),   // success
369    Stale,     // the cached path went stale (ERR_BAD_PATH after a concurrent split/merge)
370    Fatal(E),  // a real error -> stop retrying
371}
372
373/// Re-resolve + resubmit up to `attempts` times. `f` should resolve the instruction
374/// from FRESH state (the planner reads live accounts each call) and submit it,
375/// returning `Attempt::Stale` to retry. This is the SDK's staleness model: splits are
376/// rare (leaf-full), but between resolving a path and the tx landing a concurrent
377/// writer may have split/merged a node, invalidating node_idx -> ERR_BAD_PATH; the
378/// client just re-resolves. Compare Header::structure_epoch to detect it cheaply.
379pub fn retry<T, E>(attempts: u32, mut f: impl FnMut() -> Attempt<T, E>) -> Option<Result<T, E>> {
380    for _ in 0..attempts {
381        match f() {
382            Attempt::Done(t) => return Some(Ok(t)),
383            Attempt::Fatal(e) => return Some(Err(e)),
384            Attempt::Stale => continue,
385        }
386    }
387    None
388}
389
390/// Orderbook key convention (CLOB-specific; the core Tree is key-agnostic).
391///
392/// A 32-byte order key that sorts to price-time priority: asks ascending price, bids
393/// descending price; ties broken by slot (approximate FIFO), then a WRITER-UNIQUE
394/// (maker, nonce) tail so two parallel makers never collide on a key. Strict global
395/// FIFO is intentionally NOT used -- a shared sequence counter would serialize every
396/// placement and destroy the parallelism (see orderbook-requirements.md D1).
397pub mod keys {
398    use solana_sdk::pubkey::Pubkey;
399
400    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
401    pub enum Side { Ask, Bid }
402
403    pub fn order_key(side: Side, price: u64, slot: u64, maker: &Pubkey, nonce: u64) -> [u8; 32] {
404        let p = match side { Side::Ask => price, Side::Bid => u64::MAX - price };
405        let mut k = [0u8; 32];
406        k[0..8].copy_from_slice(&p.to_be_bytes());
407        k[8..16].copy_from_slice(&slot.to_be_bytes());
408        k[16..24].copy_from_slice(&maker.to_bytes()[0..8]);
409        k[24..32].copy_from_slice(&nonce.to_be_bytes());
410        k
411    }
412    pub fn price_of(side: Side, key: &[u8; 32]) -> u64 {
413        let p = u64::from_be_bytes(key[0..8].try_into().unwrap());
414        match side { Side::Ask => p, Side::Bid => u64::MAX - p }
415    }
416    pub fn slot_of(key: &[u8; 32]) -> u64 { u64::from_be_bytes(key[8..16].try_into().unwrap()) }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::keys::*;
422    use solana_sdk::pubkey::Pubkey;
423
424    #[test]
425    fn order_key_price_time_priority() {
426        let m = Pubkey::new_unique();
427        // asks: lower price sorts first (better ask = lower)
428        assert!(order_key(Side::Ask, 100, 5, &m, 0) < order_key(Side::Ask, 200, 5, &m, 0));
429        // bids: higher price sorts first (better bid = higher)
430        assert!(order_key(Side::Bid, 200, 5, &m, 0) < order_key(Side::Bid, 100, 5, &m, 0));
431        // within a price: earlier slot (FIFO) sorts first
432        assert!(order_key(Side::Ask, 100, 1, &m, 0) < order_key(Side::Ask, 100, 9, &m, 0));
433        // roundtrip
434        let a = order_key(Side::Ask, 12345, 7, &m, 3);
435        assert_eq!(price_of(Side::Ask, &a), 12345);
436        assert_eq!(slot_of(&a), 7);
437        assert_eq!(price_of(Side::Bid, &order_key(Side::Bid, 999, 0, &m, 0)), 999);
438    }
439
440    #[test]
441    fn retry_resolves_after_stale() {
442        use super::{retry, Attempt};
443        // succeeds on the 3rd attempt after two stale re-resolves
444        let mut n = 0;
445        let r: Option<Result<i32, ()>> = retry(5, || { n += 1; if n < 3 { Attempt::Stale } else { Attempt::Done(n) } });
446        assert_eq!(r, Some(Ok(3)));
447        // a fatal error stops immediately
448        let r: Option<Result<(), &str>> = retry(5, || Attempt::Fatal("boom"));
449        assert_eq!(r, Some(Err("boom")));
450        // exhausting attempts returns None
451        let r: Option<Result<(), ()>> = retry(2, || Attempt::Stale);
452        assert!(r.is_none());
453    }
454}
455