1use jmt::storage::{TreeReader, TreeUpdateBatch, TreeWriter};
17use jmt::{KeyHash, RootHash, Version};
18
19use crate::batch_root::compute_batch_root;
20use crate::keys::StateKey;
21use crate::leaves::*;
22use crate::mmr::{
23 HypercallMmr, MmrHash, MmrMetadataStore, PrepareMmrStore, PreparedMmrAppend, SupportedMmrStore,
24};
25#[cfg(feature = "rocksdb")]
26use crate::rocks_store::{RocksDbMmrStore, RocksDbStore};
27use crate::state_tree::new_jmt;
28
29#[derive(Debug, Default, Clone)]
37pub struct StateDelta {
38 pub commands: Vec<[u8; 32]>,
39 pub obligations: Vec<[u8; 32]>,
40 pub intents: Vec<[u8; 32]>,
41 pub accounts: Vec<AccountUpdate>,
42 pub option_positions: Vec<OptionPositionUpdate>,
43 pub perp_positions: Vec<PerpPositionUpdate>,
44 pub orders: Vec<OrderUpdate>,
45 pub instruments: Vec<InstrumentUpdate>,
46 pub oracles: Vec<OracleUpdate>,
47 pub mmp_configs: Vec<MmpConfigUpdate>,
48 pub risk: Vec<RiskUpdate>,
49 pub global: Option<GlobalLeaf>,
50}
51
52#[derive(Debug, Clone)]
53pub struct AccountUpdate {
54 pub address: [u8; 20],
55 pub leaf: AccountLeaf,
56}
57
58#[derive(Debug, Clone)]
59pub struct OptionPositionUpdate {
60 pub address: [u8; 20],
61 pub symbol: String,
62 pub leaf: Option<OptionPositionLeaf>,
64}
65
66#[derive(Debug, Clone)]
67pub struct PerpPositionUpdate {
68 pub address: [u8; 20],
69 pub coin: String,
70 pub leaf: Option<PerpPositionLeaf>,
72}
73
74#[derive(Debug, Clone)]
75pub struct OrderUpdate {
76 pub address: [u8; 20],
77 pub order_id: u64,
78 pub leaf: Option<OrderLeaf>,
80}
81
82#[derive(Debug, Clone)]
83pub struct InstrumentUpdate {
84 pub symbol: String,
85 pub leaf: InstrumentLeaf,
86}
87
88#[derive(Debug, Clone)]
89pub struct OracleUpdate {
90 pub underlying: String,
91 pub leaf: OracleLeaf,
92}
93
94#[derive(Debug, Clone)]
95pub struct MmpConfigUpdate {
96 pub address: [u8; 20],
97 pub currency: String,
98 pub leaf: MmpConfigLeaf,
99}
100
101#[derive(Debug, Clone)]
102pub struct RiskUpdate {
103 pub address: [u8; 20],
104 pub leaf: RiskLeaf,
105}
106
107#[derive(Debug, Clone)]
109pub struct BatchCommitment {
110 pub version: Version,
111 pub state_root: RootHash,
112 pub risk_root: RootHash,
113 pub command_mmr_root: MmrHash,
114 pub obligation_mmr_root: MmrHash,
115 pub intent_mmr_root: MmrHash,
116 pub batch_root: [u8; 32],
117 pub state_leaves_updated: usize,
118 pub risk_leaves_updated: usize,
119}
120
121#[derive(Debug)]
122pub struct PreparedBatchCommitment {
123 pub commitment: BatchCommitment,
124 state_update: Option<TreeUpdateBatch>,
125 risk_update: Option<TreeUpdateBatch>,
126 command_mmr: PreparedMmrAppend,
127 obligation_mmr: PreparedMmrAppend,
128 intent_mmr: PreparedMmrAppend,
129}
130
131pub struct CommitmentPipeline<S, M = crate::mmr::MemoryMmrStore> {
142 state_store: S,
143 risk_store: S,
144 command_mmr: HypercallMmr<M>,
145 obligation_mmr: HypercallMmr<M>,
146 intent_mmr: HypercallMmr<M>,
147 version: Version,
148 prune_window: u64,
149}
150
151#[cfg(feature = "rocksdb")]
152impl CommitmentPipeline<RocksDbStore, RocksDbMmrStore> {
153 pub fn checkpoint_inherited_state_root(&self, version: Version) -> anyhow::Result<bool> {
154 self.state_store.copy_latest_root_to_version(version)
155 }
156
157 pub fn checkpoint_inherited_risk_root(&self, version: Version) -> anyhow::Result<bool> {
158 self.risk_store.copy_latest_root_to_version(version)
159 }
160
161 pub fn prune_stores(&self, prune_to: Version) -> anyhow::Result<(usize, usize)> {
162 let (state_pruned, _) = self.state_store.prune(prune_to)?;
163 let (risk_pruned, _) = self.risk_store.prune(prune_to)?;
164 Ok((state_pruned, risk_pruned))
165 }
166
167 pub fn compact_stores(&self) -> anyhow::Result<()> {
168 self.state_store.compact_all()?;
169 self.risk_store.compact_all()?;
170 Ok(())
171 }
172}
173
174impl<S: TreeReader + TreeWriter> CommitmentPipeline<S, crate::mmr::MemoryMmrStore> {
175 pub fn new(state_store: S, risk_store: S, prune_window: u64) -> Self {
177 Self {
178 state_store,
179 risk_store,
180 command_mmr: HypercallMmr::new(),
181 obligation_mmr: HypercallMmr::new(),
182 intent_mmr: HypercallMmr::new(),
183 version: 0,
184 prune_window,
185 }
186 }
187}
188
189impl<S, M> CommitmentPipeline<S, M>
190where
191 S: TreeReader + TreeWriter,
192 M: MmrMetadataStore + SupportedMmrStore + PrepareMmrStore,
193 for<'a> &'a M: ckb_merkle_mountain_range::MMRStoreReadOps<MmrHash>
194 + ckb_merkle_mountain_range::MMRStoreWriteOps<MmrHash>,
195{
196 pub fn new_with_mmrs(
197 state_store: S,
198 risk_store: S,
199 command_mmr_store: M,
200 obligation_mmr_store: M,
201 intent_mmr_store: M,
202 prune_window: u64,
203 ) -> anyhow::Result<Self> {
204 Ok(Self {
205 state_store,
206 risk_store,
207 command_mmr: HypercallMmr::from_store(command_mmr_store)?,
208 obligation_mmr: HypercallMmr::from_store(obligation_mmr_store)?,
209 intent_mmr: HypercallMmr::from_store(intent_mmr_store)?,
210 version: 0,
211 prune_window,
212 })
213 }
214
215 pub fn with_version(mut self, version: Version) -> Self {
217 self.version = version;
218 self
219 }
220
221 pub fn version(&self) -> Version {
223 self.version
224 }
225
226 pub fn prune_window(&self) -> u64 {
228 self.prune_window
229 }
230
231 pub fn prepare_commit(&self, delta: &StateDelta) -> anyhow::Result<PreparedBatchCommitment> {
233 let version = self.version;
234
235 let (state_root, state_count, state_update) = self.prepare_state_delta(delta, version)?;
236 let (risk_root, risk_count, risk_update) = self.prepare_risk_delta(&delta.risk, version)?;
237 let command_mmr = self.command_mmr.prepare_append_many(&delta.commands)?;
238 let obligation_mmr = self
239 .obligation_mmr
240 .prepare_append_many(&delta.obligations)?;
241 let intent_mmr = self.intent_mmr.prepare_append_many(&delta.intents)?;
242
243 let command_mmr_root = command_mmr.root();
244 let obligation_mmr_root = obligation_mmr.root();
245 let intent_mmr_root = intent_mmr.root();
246
247 let batch_root = compute_batch_root(
248 &state_root,
249 &risk_root,
250 &command_mmr_root,
251 &obligation_mmr_root,
252 &intent_mmr_root,
253 );
254
255 let commitment = BatchCommitment {
256 version,
257 state_root,
258 risk_root,
259 command_mmr_root,
260 obligation_mmr_root,
261 intent_mmr_root,
262 batch_root,
263 state_leaves_updated: state_count,
264 risk_leaves_updated: risk_count,
265 };
266
267 Ok(PreparedBatchCommitment {
268 commitment,
269 state_update,
270 risk_update,
271 command_mmr,
272 obligation_mmr,
273 intent_mmr,
274 })
275 }
276
277 pub fn apply_prepared_commit(
279 &mut self,
280 prepared: PreparedBatchCommitment,
281 ) -> anyhow::Result<BatchCommitment> {
282 if prepared.commitment.version != self.version {
283 anyhow::bail!(
284 "prepared commitment version {} does not match pipeline version {}",
285 prepared.commitment.version,
286 self.version
287 );
288 }
289
290 if let Some(update) = prepared.state_update {
291 self.state_store.write_node_batch(&update.node_batch)?;
292 }
293 if let Some(update) = prepared.risk_update {
294 self.risk_store.write_node_batch(&update.node_batch)?;
295 }
296 self.command_mmr
297 .apply_prepared_append(prepared.command_mmr)?;
298 self.obligation_mmr
299 .apply_prepared_append(prepared.obligation_mmr)?;
300 self.intent_mmr.apply_prepared_append(prepared.intent_mmr)?;
301
302 self.version += 1;
303 Ok(prepared.commitment)
304 }
305
306 pub fn commit(&mut self, delta: &StateDelta) -> anyhow::Result<BatchCommitment> {
311 let prepared = self.prepare_commit(delta)?;
312 self.apply_prepared_commit(prepared)
313 }
314
315 pub fn append_command(&mut self, command_hash: [u8; 32]) -> anyhow::Result<()> {
317 self.command_mmr.append(command_hash)
318 }
319
320 pub fn append_obligation(&mut self, obligation_hash: [u8; 32]) -> anyhow::Result<()> {
322 self.obligation_mmr.append(obligation_hash)
323 }
324
325 pub fn append_intent(&mut self, intent_hash: [u8; 32]) -> anyhow::Result<()> {
327 self.intent_mmr.append(intent_hash)
328 }
329
330 fn latest_root(&self, store: &S, version: Version) -> anyhow::Result<RootHash> {
331 if version == 0 {
332 return Ok(RootHash([0u8; 32]));
333 }
334 let tree = new_jmt(store);
335 match tree.get_root_hash_option(version - 1)? {
336 Some(root) => Ok(root),
337 None => Ok(RootHash([0u8; 32])),
338 }
339 }
340
341 fn prepare_state_delta(
342 &self,
343 delta: &StateDelta,
344 version: Version,
345 ) -> anyhow::Result<(RootHash, usize, Option<TreeUpdateBatch>)> {
346 let mut entries: Vec<(KeyHash, Option<Vec<u8>>)> = Vec::new();
347
348 for u in &delta.accounts {
349 entries.push((StateKey::account(&u.address), Some(leaf_to_bytes(&u.leaf))));
350 }
351
352 for u in &delta.option_positions {
353 let key = StateKey::option_position(&u.address, &u.symbol);
354 entries.push((key, u.leaf.as_ref().map(leaf_to_bytes)));
355 }
356
357 for u in &delta.perp_positions {
358 let key = StateKey::perp_position(&u.address, &u.coin);
359 entries.push((key, u.leaf.as_ref().map(leaf_to_bytes)));
360 }
361
362 for u in &delta.orders {
363 let key = StateKey::order(&u.address, u.order_id);
364 entries.push((key, u.leaf.as_ref().map(leaf_to_bytes)));
365 }
366
367 for u in &delta.instruments {
368 entries.push((
369 StateKey::instrument(&u.symbol),
370 Some(leaf_to_bytes(&u.leaf)),
371 ));
372 }
373
374 for u in &delta.oracles {
375 entries.push((
376 StateKey::oracle(&u.underlying),
377 Some(leaf_to_bytes(&u.leaf)),
378 ));
379 }
380
381 for u in &delta.mmp_configs {
382 entries.push((
383 StateKey::mmp_config(&u.address, &u.currency),
384 Some(leaf_to_bytes(&u.leaf)),
385 ));
386 }
387
388 if let Some(ref g) = delta.global {
389 entries.push((StateKey::global(), Some(leaf_to_bytes(g))));
390 }
391
392 let count = entries.len();
393 if count == 0 {
394 return Ok((self.latest_root(&self.state_store, version)?, 0, None));
395 }
396
397 let tree = new_jmt(&self.state_store);
398 let (root, batch) = tree.put_value_set(entries, version)?;
399
400 Ok((root, count, Some(batch)))
401 }
402
403 fn prepare_risk_delta(
404 &self,
405 risk_updates: &[RiskUpdate],
406 version: Version,
407 ) -> anyhow::Result<(RootHash, usize, Option<TreeUpdateBatch>)> {
408 if risk_updates.is_empty() {
409 return Ok((self.latest_root(&self.risk_store, version)?, 0, None));
410 }
411
412 let entries: Vec<(KeyHash, Option<Vec<u8>>)> = risk_updates
413 .iter()
414 .map(|u| (StateKey::risk(&u.address), Some(leaf_to_bytes(&u.leaf))))
415 .collect();
416
417 let count = entries.len();
418 let tree = new_jmt(&self.risk_store);
419 let (root, batch) = tree.put_value_set(entries, version)?;
420
421 Ok((root, count, Some(batch)))
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::store::FastMemoryStore;
429
430 fn test_pipeline() -> CommitmentPipeline<FastMemoryStore> {
431 CommitmentPipeline::new(FastMemoryStore::new(), FastMemoryStore::new(), 100)
432 }
433
434 #[test]
435 fn empty_commit_produces_deterministic_root() {
436 let mut pipeline = test_pipeline();
437 let c1 = pipeline.commit(&StateDelta::default()).unwrap();
438 assert_eq!(c1.version, 0);
439 assert_eq!(c1.state_leaves_updated, 0);
440 assert_eq!(c1.risk_leaves_updated, 0);
441 }
442
443 #[test]
444 fn single_account_commit() {
445 let mut pipeline = test_pipeline();
446 let delta = StateDelta {
447 accounts: vec![AccountUpdate {
448 address: [0x42; 20],
449 leaf: AccountLeaf {
450 cash: 1_000_000 * PRICE_SCALE,
451 nonce: 0,
452 margin_mode: 0,
453 liquidation_state: 0,
454 },
455 }],
456 ..Default::default()
457 };
458
459 let c = pipeline.commit(&delta).unwrap();
460 assert_eq!(c.version, 0);
461 assert_eq!(c.state_leaves_updated, 1);
462 assert_ne!(c.state_root.0, [0u8; 32]);
463 assert_ne!(c.batch_root, [0u8; 32]);
464 }
465
466 #[test]
467 fn prepare_commit_does_not_mutate_until_apply() {
468 let mut pipeline = test_pipeline();
469 let delta = StateDelta {
470 commands: vec![[0xAA; 32]],
471 accounts: vec![AccountUpdate {
472 address: [0x42; 20],
473 leaf: AccountLeaf {
474 cash: 1_000_000 * PRICE_SCALE,
475 nonce: 0,
476 margin_mode: 0,
477 liquidation_state: 0,
478 },
479 }],
480 ..Default::default()
481 };
482
483 let prepared = pipeline.prepare_commit(&delta).unwrap();
484 assert_eq!(prepared.commitment.version, 0);
485 assert_eq!(pipeline.version(), 0);
486 assert_eq!(pipeline.command_mmr.size(), 0);
487 assert!(
488 new_jmt(&pipeline.state_store)
489 .get_root_hash_option(0)
490 .unwrap()
491 .is_none(),
492 "prepare must not write the state root before apply"
493 );
494
495 let applied = pipeline.apply_prepared_commit(prepared).unwrap();
496 assert_eq!(applied.version, 0);
497 assert_eq!(pipeline.version(), 1);
498 assert_eq!(pipeline.command_mmr.size(), 1);
499 assert_eq!(
500 new_jmt(&pipeline.state_store)
501 .get_root_hash_option(0)
502 .unwrap(),
503 Some(applied.state_root)
504 );
505 }
506
507 #[test]
508 fn version_increments() {
509 let mut pipeline = test_pipeline();
510 let delta = StateDelta {
511 accounts: vec![AccountUpdate {
512 address: [0x01; 20],
513 leaf: AccountLeaf {
514 cash: 100,
515 nonce: 0,
516 margin_mode: 0,
517 liquidation_state: 0,
518 },
519 }],
520 ..Default::default()
521 };
522
523 let c0 = pipeline.commit(&delta).unwrap();
524 assert_eq!(c0.version, 0);
525 assert_eq!(pipeline.version(), 1);
526
527 let c1 = pipeline.commit(&delta).unwrap();
528 assert_eq!(c1.version, 1);
529 assert_eq!(pipeline.version(), 2);
530 }
531
532 #[test]
533 fn state_root_changes_on_balance_update() {
534 let mut pipeline = test_pipeline();
535 let addr = [0x42; 20];
536
537 let d1 = StateDelta {
538 accounts: vec![AccountUpdate {
539 address: addr,
540 leaf: AccountLeaf {
541 cash: 1000,
542 nonce: 0,
543 margin_mode: 0,
544 liquidation_state: 0,
545 },
546 }],
547 ..Default::default()
548 };
549 let c1 = pipeline.commit(&d1).unwrap();
550
551 let d2 = StateDelta {
552 accounts: vec![AccountUpdate {
553 address: addr,
554 leaf: AccountLeaf {
555 cash: 2000,
556 nonce: 1,
557 margin_mode: 0,
558 liquidation_state: 0,
559 },
560 }],
561 ..Default::default()
562 };
563 let c2 = pipeline.commit(&d2).unwrap();
564
565 assert_ne!(c1.state_root, c2.state_root);
566 assert_ne!(c1.batch_root, c2.batch_root);
567 }
568
569 #[test]
570 fn risk_root_changes_independently() {
571 let mut pipeline = test_pipeline();
572 let addr = [0x01; 20];
573
574 let d1 = StateDelta {
575 accounts: vec![AccountUpdate {
576 address: addr,
577 leaf: AccountLeaf {
578 cash: 1000,
579 nonce: 0,
580 margin_mode: 0,
581 liquidation_state: 0,
582 },
583 }],
584 risk: vec![RiskUpdate {
585 address: addr,
586 leaf: RiskLeaf {
587 equity: 1000,
588 initial_margin: 500,
589 maintenance_margin: 250,
590 scanning_risk: 400,
591 health_nonce: 0,
592 },
593 }],
594 ..Default::default()
595 };
596 let c1 = pipeline.commit(&d1).unwrap();
597
598 let d2 = StateDelta {
599 risk: vec![RiskUpdate {
600 address: addr,
601 leaf: RiskLeaf {
602 equity: 800,
603 initial_margin: 500,
604 maintenance_margin: 250,
605 scanning_risk: 400,
606 health_nonce: 1,
607 },
608 }],
609 ..Default::default()
610 };
611 let c2 = pipeline.commit(&d2).unwrap();
612
613 assert_eq!(c1.state_root, c2.state_root);
614 assert_ne!(c1.risk_root, c2.risk_root);
615 assert_ne!(c1.batch_root, c2.batch_root);
616 }
617
618 #[test]
619 fn command_mmr_affects_batch_root() {
620 let mut pipeline = test_pipeline();
621
622 let delta = StateDelta {
623 accounts: vec![AccountUpdate {
624 address: [0x01; 20],
625 leaf: AccountLeaf {
626 cash: 100,
627 nonce: 0,
628 margin_mode: 0,
629 liquidation_state: 0,
630 },
631 }],
632 ..Default::default()
633 };
634
635 let c1 = pipeline.commit(&delta).unwrap();
636 pipeline.append_command([0xAA; 32]).unwrap();
637 let c2 = pipeline.commit(&delta).unwrap();
638
639 assert_ne!(c1.batch_root, c2.batch_root);
640 }
641
642 #[test]
643 fn full_trading_scenario() {
644 let mut pipeline = test_pipeline();
645
646 let alice = [0x01; 20];
647 let bob = [0x02; 20];
648 let symbol = "BTC-20261231-100000-C";
649
650 let d0 = StateDelta {
652 accounts: vec![
653 AccountUpdate {
654 address: alice,
655 leaf: AccountLeaf {
656 cash: 100_000 * PRICE_SCALE,
657 nonce: 0,
658 margin_mode: 1,
659 liquidation_state: 0,
660 },
661 },
662 AccountUpdate {
663 address: bob,
664 leaf: AccountLeaf {
665 cash: 500_000 * PRICE_SCALE,
666 nonce: 0,
667 margin_mode: 1,
668 liquidation_state: 0,
669 },
670 },
671 ],
672 instruments: vec![InstrumentUpdate {
673 symbol: symbol.to_string(),
674 leaf: InstrumentLeaf {
675 expired: false,
676 trading_mode: 2,
677 expiry: 1798761600,
678 strike: 100_000 * PRICE_SCALE,
679 option_type: 0,
680 },
681 }],
682 oracles: vec![OracleUpdate {
683 underlying: "BTC".to_string(),
684 leaf: OracleLeaf {
685 spot_price: 95_000 * PRICE_SCALE,
686 iv_surface_hash: [0xAA; 32],
687 },
688 }],
689 global: Some(GlobalLeaf {
690 next_order_id: 1,
691 next_trade_id: 1,
692 command_chain_root: [0; 32],
693 command_chain_seq: 0,
694 }),
695 risk: vec![
696 RiskUpdate {
697 address: alice,
698 leaf: RiskLeaf {
699 equity: 100_000 * PRICE_SCALE,
700 initial_margin: 0,
701 maintenance_margin: 0,
702 scanning_risk: 0,
703 health_nonce: 0,
704 },
705 },
706 RiskUpdate {
707 address: bob,
708 leaf: RiskLeaf {
709 equity: 500_000 * PRICE_SCALE,
710 initial_margin: 0,
711 maintenance_margin: 0,
712 scanning_risk: 0,
713 health_nonce: 0,
714 },
715 },
716 ],
717 ..Default::default()
718 };
719 pipeline.append_command([0x01; 32]).unwrap();
720 let c0 = pipeline.commit(&d0).unwrap();
721 assert_eq!(c0.state_leaves_updated, 5);
722 assert_eq!(c0.risk_leaves_updated, 2);
723
724 let d1 = StateDelta {
726 accounts: vec![
727 AccountUpdate {
728 address: alice,
729 leaf: AccountLeaf {
730 cash: 95_000 * PRICE_SCALE,
731 nonce: 1,
732 margin_mode: 1,
733 liquidation_state: 0,
734 },
735 },
736 AccountUpdate {
737 address: bob,
738 leaf: AccountLeaf {
739 cash: 505_000 * PRICE_SCALE,
740 nonce: 1,
741 margin_mode: 1,
742 liquidation_state: 0,
743 },
744 },
745 ],
746 option_positions: vec![
747 OptionPositionUpdate {
748 address: alice,
749 symbol: symbol.to_string(),
750 leaf: Some(OptionPositionLeaf {
751 quantity: 10 * PRICE_SCALE,
752 entry_price: 5_000 * PRICE_SCALE,
753 }),
754 },
755 OptionPositionUpdate {
756 address: bob,
757 symbol: symbol.to_string(),
758 leaf: Some(OptionPositionLeaf {
759 quantity: -10 * PRICE_SCALE,
760 entry_price: 5_000 * PRICE_SCALE,
761 }),
762 },
763 ],
764 global: Some(GlobalLeaf {
765 next_order_id: 3,
766 next_trade_id: 2,
767 command_chain_root: [0x11; 32],
768 command_chain_seq: 2,
769 }),
770 risk: vec![
771 RiskUpdate {
772 address: alice,
773 leaf: RiskLeaf {
774 equity: 95_000 * PRICE_SCALE,
775 initial_margin: 20_000 * PRICE_SCALE,
776 maintenance_margin: 10_000 * PRICE_SCALE,
777 scanning_risk: 15_000 * PRICE_SCALE,
778 health_nonce: 1,
779 },
780 },
781 RiskUpdate {
782 address: bob,
783 leaf: RiskLeaf {
784 equity: 505_000 * PRICE_SCALE,
785 initial_margin: 50_000 * PRICE_SCALE,
786 maintenance_margin: 25_000 * PRICE_SCALE,
787 scanning_risk: 40_000 * PRICE_SCALE,
788 health_nonce: 1,
789 },
790 },
791 ],
792 ..Default::default()
793 };
794 pipeline.append_command([0x02; 32]).unwrap();
795 pipeline.append_command([0x03; 32]).unwrap();
796 let c1 = pipeline.commit(&d1).unwrap();
797 assert_ne!(c0.batch_root, c1.batch_root);
798 assert_eq!(c1.state_leaves_updated, 5);
799
800 let d2 = StateDelta {
802 oracles: vec![OracleUpdate {
803 underlying: "BTC".to_string(),
804 leaf: OracleLeaf {
805 spot_price: 100_000 * PRICE_SCALE,
806 iv_surface_hash: [0xBB; 32],
807 },
808 }],
809 risk: vec![
810 RiskUpdate {
811 address: alice,
812 leaf: RiskLeaf {
813 equity: 110_000 * PRICE_SCALE,
814 initial_margin: 22_000 * PRICE_SCALE,
815 maintenance_margin: 11_000 * PRICE_SCALE,
816 scanning_risk: 16_000 * PRICE_SCALE,
817 health_nonce: 2,
818 },
819 },
820 RiskUpdate {
821 address: bob,
822 leaf: RiskLeaf {
823 equity: 490_000 * PRICE_SCALE,
824 initial_margin: 55_000 * PRICE_SCALE,
825 maintenance_margin: 27_500 * PRICE_SCALE,
826 scanning_risk: 44_000 * PRICE_SCALE,
827 health_nonce: 2,
828 },
829 },
830 ],
831 ..Default::default()
832 };
833 let c2 = pipeline.commit(&d2).unwrap();
834 assert_ne!(c1.batch_root, c2.batch_root);
835 assert_eq!(pipeline.version(), 3);
836 }
837
838 #[test]
839 fn position_delete_changes_root() {
840 let mut pipeline = test_pipeline();
841 let addr = [0x01; 20];
842
843 let d1 = StateDelta {
844 option_positions: vec![OptionPositionUpdate {
845 address: addr,
846 symbol: "BTC-C".to_string(),
847 leaf: Some(OptionPositionLeaf {
848 quantity: 100,
849 entry_price: 5000,
850 }),
851 }],
852 ..Default::default()
853 };
854 let c1 = pipeline.commit(&d1).unwrap();
855
856 let d2 = StateDelta {
857 option_positions: vec![OptionPositionUpdate {
858 address: addr,
859 symbol: "BTC-C".to_string(),
860 leaf: None,
861 }],
862 ..Default::default()
863 };
864 let c2 = pipeline.commit(&d2).unwrap();
865
866 assert_ne!(c1.state_root, c2.state_root);
867 }
868}