Skip to main content

hypercall_state_commitment/
keys.rs

1use jmt::KeyHash;
2use sha3::{Digest, Keccak256};
3
4/// Compound key derivation for the state tree.
5///
6/// All state lives in a single JMT with compound keys:
7///   keccak256(address || namespace || sub_key)
8///
9/// Keys are derived using the Keccak256 hasher directly (no allocation)
10/// to avoid Vec overhead on the hot path.
11pub struct StateKey;
12
13impl StateKey {
14    /// Account metadata: cash, nonce, margin_mode, liquidation_state
15    pub fn account(address: &[u8; 20]) -> KeyHash {
16        let mut h = Keccak256::new();
17        h.update(address);
18        h.update(b"account");
19        KeyHash(h.finalize().into())
20    }
21
22    /// Option position for (address, symbol)
23    pub fn option_position(address: &[u8; 20], symbol: &str) -> KeyHash {
24        let mut h = Keccak256::new();
25        h.update(address);
26        h.update(b"pos");
27        h.update(symbol.as_bytes());
28        KeyHash(h.finalize().into())
29    }
30
31    /// Perp position for (address, coin)
32    pub fn perp_position(address: &[u8; 20], coin: &str) -> KeyHash {
33        let mut h = Keccak256::new();
34        h.update(address);
35        h.update(b"perp");
36        h.update(coin.as_bytes());
37        KeyHash(h.finalize().into())
38    }
39
40    /// Open order for (address, order_id)
41    pub fn order(address: &[u8; 20], order_id: u64) -> KeyHash {
42        let mut h = Keccak256::new();
43        h.update(address);
44        h.update(b"order");
45        h.update(order_id.to_be_bytes());
46        KeyHash(h.finalize().into())
47    }
48
49    /// MMP config for (address, currency)
50    pub fn mmp_config(address: &[u8; 20], currency: &str) -> KeyHash {
51        let mut h = Keccak256::new();
52        h.update(address);
53        h.update(b"mmp");
54        h.update(currency.as_bytes());
55        KeyHash(h.finalize().into())
56    }
57
58    /// Instrument metadata (global, not per-account)
59    pub fn instrument(symbol: &str) -> KeyHash {
60        let mut h = Keccak256::new();
61        h.update(b"instrument");
62        h.update(symbol.as_bytes());
63        KeyHash(h.finalize().into())
64    }
65
66    /// Oracle anchor (global)
67    pub fn oracle(underlying: &str) -> KeyHash {
68        let mut h = Keccak256::new();
69        h.update(b"oracle");
70        h.update(underlying.as_bytes());
71        KeyHash(h.finalize().into())
72    }
73
74    /// Global counters (singleton)
75    pub fn global() -> KeyHash {
76        let mut h = Keccak256::new();
77        h.update(b"global");
78        KeyHash(h.finalize().into())
79    }
80
81    /// Risk tree key (separate JMT, keyed by address only)
82    pub fn risk(address: &[u8; 20]) -> KeyHash {
83        let mut h = Keccak256::new();
84        h.update(address);
85        KeyHash(h.finalize().into())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn different_namespaces_produce_different_keys() {
95        let addr = [0xABu8; 20];
96        let account = StateKey::account(&addr);
97        let pos = StateKey::option_position(&addr, "BTC-20261231-100000-C");
98        let perp = StateKey::perp_position(&addr, "BTC");
99        let mmp = StateKey::mmp_config(&addr, "BTC");
100
101        assert_ne!(account, pos);
102        assert_ne!(account, perp);
103        assert_ne!(account, mmp);
104        assert_ne!(pos, perp);
105        assert_ne!(pos, mmp);
106        assert_ne!(perp, mmp);
107    }
108
109    #[test]
110    fn same_inputs_produce_same_key() {
111        let addr = [0x42u8; 20];
112        let a = StateKey::option_position(&addr, "ETH-20261231-5000-P");
113        let b = StateKey::option_position(&addr, "ETH-20261231-5000-P");
114        assert_eq!(a, b);
115    }
116
117    #[test]
118    fn different_addresses_produce_different_keys() {
119        let addr_a = [0x01u8; 20];
120        let addr_b = [0x02u8; 20];
121        assert_ne!(StateKey::account(&addr_a), StateKey::account(&addr_b));
122    }
123
124    #[test]
125    fn global_key_is_deterministic() {
126        assert_eq!(StateKey::global(), StateKey::global());
127    }
128
129    #[test]
130    fn namespace_prefix_prevents_cross_type_collision() {
131        let addr = [0x00; 20];
132        // "pos" + "BTC" vs "perp" + "TC" -- different namespace prefixes
133        let pos_btc = StateKey::option_position(&addr, "BTC");
134        let perp_tc = StateKey::perp_position(&addr, "TC");
135        assert_ne!(pos_btc, perp_tc);
136    }
137
138    #[test]
139    fn empty_symbol_produces_valid_key() {
140        let addr = [0x01; 20];
141        let key = StateKey::option_position(&addr, "");
142        assert_ne!(key.0, [0u8; 32]);
143    }
144
145    #[test]
146    fn long_symbol_produces_valid_key() {
147        let addr = [0x01; 20];
148        let long_symbol = "A".repeat(1000);
149        let key = StateKey::option_position(&addr, &long_symbol);
150        assert_ne!(key.0, [0u8; 32]);
151    }
152
153    #[test]
154    fn fast_risk_key_differs_from_account_key() {
155        let addr = [0x42; 20];
156        assert_ne!(StateKey::account(&addr), StateKey::risk(&addr));
157    }
158
159    #[test]
160    fn all_zero_address_works() {
161        let addr = [0x00; 20];
162        let key = StateKey::account(&addr);
163        assert_ne!(key.0, [0u8; 32]);
164    }
165}
166
167#[cfg(kani)]
168mod kani_proofs {
169    use super::*;
170
171    /// Prove: account key derivation is deterministic for all addresses.
172    #[kani::proof]
173    fn bounded_account_key_deterministic() {
174        let addr: [u8; 20] = kani::any();
175        let a = StateKey::account(&addr);
176        let b = StateKey::account(&addr);
177        assert_eq!(a.0, b.0);
178    }
179
180    /// Prove: different addresses always produce different account keys.
181    /// (This is a collision resistance property of keccak256.)
182    #[kani::proof]
183    #[kani::unwind(1)]
184    fn bounded_account_key_no_panic() {
185        let addr: [u8; 20] = kani::any();
186        let _ = StateKey::account(&addr);
187    }
188
189    /// Prove: order key derivation is deterministic.
190    #[kani::proof]
191    fn bounded_order_key_deterministic() {
192        let addr: [u8; 20] = kani::any();
193        let order_id: u64 = kani::any();
194        let a = StateKey::order(&addr, order_id);
195        let b = StateKey::order(&addr, order_id);
196        assert_eq!(a.0, b.0);
197    }
198
199    /// Prove: risk key differs from account key for all addresses.
200    /// (Different namespace prefix guarantees different hash input.)
201    #[kani::proof]
202    fn bounded_risk_key_differs_from_account_key() {
203        let addr: [u8; 20] = kani::any();
204        let account = StateKey::account(&addr);
205        let risk = StateKey::risk(&addr);
206        // These hash different inputs (addr||"account" vs addr alone),
207        // so they should always differ. Kani verifies this exhaustively.
208        assert_ne!(account.0, risk.0);
209    }
210
211    /// Prove: global key never panics and produces non-zero output.
212    #[kani::proof]
213    fn bounded_global_key_no_panic() {
214        let key = StateKey::global();
215        // keccak256("global") is a constant -- just verify no panic
216        let _ = key.0;
217    }
218}