iop_coeus_node/
state.rs

1use super::*;
2
3pub type Version = u64;
4
5#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
6pub struct TxnStatus {
7    pub version_before_txn: Version,
8    pub success: bool,
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
12#[serde(rename_all = "camelCase")]
13pub struct State {
14    corrupted: bool,
15    root: Domain,
16    last_seen_height: BlockHeight,
17    version_of_first_undo_operation: Version,
18    undo_operations: Vec<UndoOperation>,
19    nonces: HashMap<MPublicKey, Nonce>,
20    txn_statuses: HashMap<String, TxnStatus>,
21}
22
23impl Default for State {
24    fn default() -> Self {
25        Self {
26            corrupted: false,
27            root: Domain::new_root(),
28            last_seen_height: Default::default(),
29            version_of_first_undo_operation: Default::default(),
30            undo_operations: Default::default(),
31            nonces: Default::default(),
32            txn_statuses: Default::default(),
33        }
34    }
35}
36
37impl State {
38    pub fn new() -> Self {
39        Default::default()
40    }
41
42    pub fn is_corrupted(&self) -> bool {
43        self.corrupted
44    }
45
46    pub fn ensure_not_corrupted(&self) -> Result<()> {
47        if self.corrupted {
48            bail!("Coeus state is corrupt. All incoming changes will be ignored.");
49        }
50        Ok(())
51    }
52
53    pub fn root(&self) -> &Domain {
54        &self.root
55    }
56
57    pub fn last_seen_height(&self) -> BlockHeight {
58        self.last_seen_height
59    }
60
61    pub(crate) fn set_last_seen_height(&mut self, height: BlockHeight) {
62        self.last_seen_height = height;
63    }
64
65    pub fn version(&self) -> Version {
66        self.undo_operations.len() as Version + self.version_of_first_undo_operation
67    }
68
69    pub fn block_applying(&mut self, height: BlockHeight) -> Result<()> {
70        self.ensure_not_corrupted()?;
71        self.apply_operations(vec![SystemOperation::start_block(height)]).map(|_version| ())
72    }
73
74    pub fn block_reverted(&mut self, height: BlockHeight) -> Result<()> {
75        self.ensure_not_corrupted()?;
76        let height_before_revert = self.last_seen_height;
77        self.set_corrupted_on_err(|state| {
78            ensure!(
79                height_before_revert == height,
80                "Cannot revert block at height {}, because currently the state is at height {}",
81                height,
82                height_before_revert,
83            );
84            state.undo_operation(state.version() - 1)?;
85            ensure!(height_before_revert > state.last_seen_height, "Cannot revert block at height {}, because the operation undone was not reducing the block height.", height);
86            Ok(())
87        })
88    }
89
90    pub fn apply_transaction(&mut self, txid: &str, asset: CoeusAsset) -> Result<()> {
91        self.ensure_not_corrupted()?;
92
93        let version_before_txn = self.version();
94
95        for bundle in asset.bundles {
96            if let Err(e) = self.apply_signed_bundle(bundle) {
97                self.undo_operations(version_before_txn)?;
98                self.txn_statuses
99                    .insert(txid.to_owned(), TxnStatus { version_before_txn, success: false });
100                return Err(e);
101            }
102        }
103        self.txn_statuses.insert(txid.to_owned(), TxnStatus { version_before_txn, success: true });
104        Ok(())
105    }
106
107    pub fn revert_transaction(&mut self, txid: &str, asset: CoeusAsset) -> Result<()> {
108        self.ensure_not_corrupted()?;
109        self.set_corrupted_on_err(|state| match state.txn_statuses.remove(txid) {
110            None => {
111                bail!("Transaction has not been applied previously.");
112            }
113            Some(status) => {
114                let current_version = state.version();
115                let version_before_txn = status.version_before_txn;
116                let operation_count: usize =
117                    asset.bundles.iter().map(|signed| signed.bundle.operations.len()).sum();
118                ensure!(
119                    version_before_txn + operation_count as u64 == current_version,
120                    "Number of operations in transaction do not match our previous records."
121                );
122                state.undo_operations(version_before_txn)?;
123                Ok(())
124            }
125        })
126    }
127
128    pub(crate) fn apply_signed_bundle(&mut self, ops: SignedBundle) -> Result<Version> {
129        ensure!(ops.verify(), "Invalid signature or the operations were tampered with");
130        self.authorize_operations(&ops.bundle.operations, &ops.public_key)?;
131        self.apply_nonced_bundle(ops.bundle, ops.public_key)
132    }
133
134    fn authorize_operations(&mut self, ops: &[UserOperation], pk: &MPublicKey) -> Result<()> {
135        ops.iter().try_for_each(|op| op.validate_auth(self, pk))
136    }
137
138    pub fn validate_domain_owner(&self, name: &DomainName, pk: &MPublicKey) -> Result<()> {
139        let domain = self.domain(name)?;
140        domain.owner().validate_impersonation(pk)
141    }
142
143    fn apply_nonced_bundle(&mut self, bundle: NoncedBundle, pk: MPublicKey) -> Result<Version> {
144        let old_nonce = self.nonces.get(&pk).copied().unwrap_or_default();
145        ensure!(
146            bundle.nonce == old_nonce + 1,
147            "Invalid nonce {}, expected {}",
148            bundle.nonce,
149            old_nonce + 1
150        );
151
152        let version = self.apply_operations(bundle.operations)?;
153
154        self.nonces.insert(pk, old_nonce + 1);
155
156        Ok(version)
157    }
158
159    pub(crate) fn apply_operations(&mut self, mut ops: Vec<impl Command>) -> Result<Version> {
160        let mut undos = vec![];
161        let res = ops.drain(..).try_fold(&mut undos, |undos, op| {
162            let undo = op.execute(self)?;
163            undos.push(undo);
164            Ok(undos)
165        });
166        match res {
167            Err(e) => {
168                // TODO Corrupt state if next line fails
169                undos.drain(..).rev().try_for_each(|op| op.execute(self))?;
170                Err(e)
171            }
172            Ok(_) => {
173                self.undo_operations.extend_from_slice(&undos);
174                Ok(self.version())
175            }
176        }
177    }
178
179    pub fn domain(&self, name: &DomainName) -> Result<&Domain> {
180        name.iter()
181            .try_fold(&self.root, |dom, e| dom.child(e))
182            .with_context(|| format!("Cannot find domain with name {}", name))
183    }
184
185    pub(crate) fn domain_mut(&mut self, name: &DomainName) -> Result<&mut Domain> {
186        name.iter()
187            .try_fold(&mut self.root, |dom, e| dom.child_mut(e))
188            .with_context(|| format!("Cannot find domain with name {}", name))
189    }
190
191    pub fn nonce(&self, pk: &MPublicKey) -> Nonce {
192        self.nonces.get(pk).copied().unwrap_or(0)
193    }
194
195    pub fn get_txn_status(&self, txid: &str) -> Result<&TxnStatus> {
196        self.txn_statuses.get(txid).with_context(|| format!("Cannot find txn with id {}", txid))
197    }
198
199    pub fn resolve_data(&self, name: &DomainName) -> Result<&DynamicContent> {
200        let domain = name.iter().try_fold(self.root(), |dom, edge| {
201            dom.child(edge)
202                .with_context(|| format!("Edge {} was not found for domain {}", edge, name))
203                .and_then(|child| {
204                    if child.is_expired_at(self.last_seen_height) {
205                        bail!("Edge {} in domain {} expired", edge, name)
206                    } else {
207                        Ok(child)
208                    }
209                })
210        })?;
211        Ok(domain.data())
212    }
213
214    pub fn validate_subtree_policies(&self, domain_name: &DomainName) -> Result<()> {
215        let domain_after_op = self.domain(domain_name)?;
216        let mut policy_domain = self.root();
217        policy_domain.validate_subtree_policies(self, domain_after_op)?;
218
219        for edge in domain_name.edges() {
220            policy_domain = policy_domain
221                .child(edge)
222                .expect("Implementation error: validating nonexisting domain data");
223            policy_domain.validate_subtree_policies(self, domain_after_op)?;
224        }
225
226        Ok(())
227    }
228
229    fn set_corrupted_on_err<R>(&mut self, func: impl FnOnce(&mut Self) -> Result<R>) -> Result<R> {
230        match func(self) {
231            Err(e) => {
232                self.corrupted = true;
233                Err(e)
234            }
235            Ok(r) => Ok(r),
236        }
237    }
238
239    fn undo_operations(&mut self, to_version: Version) -> Result<()> {
240        for version in (to_version..self.version()).rev() {
241            self.undo_operation(version)?;
242        }
243        Ok(())
244    }
245
246    fn undo_operation(&mut self, to_version: Version) -> Result<()> {
247        self.set_corrupted_on_err(|state| {
248            let undo_op = state
249                .undo_operations
250                .pop()
251                .with_context(|| format!("Cannot undo to version {} anymore", to_version))?;
252            undo_op.execute(state)
253        })
254    }
255}
256
257#[cfg(test)]
258mod test {
259    use super::*;
260
261    use iop_keyvault::{multicipher::MPrivateKey, PrivateKey};
262
263    trait StateExt {
264        fn apply_operation(&mut self, op: impl Command) -> Result<Version>;
265    }
266
267    impl StateExt for State {
268        fn apply_operation(&mut self, op: impl Command) -> Result<Version> {
269            self.apply_operations(vec![op])
270        }
271    }
272
273    fn ark_sk_from(passphrase: &str) -> MPrivateKey {
274        let secp_sk =
275            iop_keyvault::secp256k1::SecpPrivateKey::from_ark_passphrase(passphrase).unwrap();
276        MPrivateKey::from(secp_sk)
277    }
278
279    fn ark_sk() -> MPrivateKey {
280        let passphrase = "scout try doll stuff cake welcome random taste load town clerk ostrich";
281        ark_sk_from(passphrase)
282    }
283
284    #[test]
285    fn empty_state() {
286        let state = State::new();
287
288        let root = state.root();
289        assert!(root.name().is_root());
290        assert_eq!(root.owner(), &Principal::system());
291
292        let child_names = root.child_names();
293        let schema_edge = Edge::new("schema").unwrap();
294        assert_eq!(child_names.len(), 1);
295        assert_eq!(child_names[0], &schema_edge);
296
297        let schema_domain = root.child(&schema_edge).unwrap();
298        assert!(schema_domain.child_names().is_empty());
299        assert!(!schema_domain.name().is_root());
300        assert_eq!(schema_domain.owner(), &Principal::system());
301
302        assert!(state.undo_operations.is_empty());
303        assert_eq!(state.last_seen_height(), 0);
304        assert_eq!(state.version(), 0);
305    }
306
307    fn name_resolves_to(
308        state: &State, name: &DomainName, expected_data: &DynamicContent,
309    ) -> Result<()> {
310        let data = state.resolve_data(name)?;
311        assert_eq!(data, expected_data);
312        Ok(())
313    }
314
315    fn name_does_not_resolve(state: &State, domain_name: &DomainName) -> Result<()> {
316        let last_edge = domain_name.last_edge().unwrap();
317        let err_msg = format!("Edge {} in domain {} expired", last_edge, domain_name);
318
319        let err = state.resolve_data(domain_name).unwrap_err();
320        assert_eq!(err.to_string(), err_msg);
321        Ok(())
322    }
323
324    #[test]
325    fn expiry() {
326        let mut state = State::new();
327
328        let domain_name = domain_name(".schema.decentralizers");
329        let domain_data = json!({"data": "We're gonna rule the world"});
330        state
331            .apply_operation(UserOperation::register(
332                domain_name.clone(),
333                domain_owner(),
334                no_policies(),
335                Default::default(),
336                domain_data.clone(),
337                42,
338            ))
339            .unwrap();
340
341        name_resolves_to(&state, &domain_name, &domain_data).unwrap();
342
343        state.block_applying(41).unwrap();
344        name_resolves_to(&state, &domain_name, &domain_data).unwrap();
345
346        state.block_applying(42).unwrap();
347        name_does_not_resolve(&state, &domain_name).unwrap();
348
349        state.apply_operation(UserOperation::renew(domain_name.clone(), 43)).unwrap();
350        name_resolves_to(&state, &domain_name, &domain_data).unwrap();
351
352        state.block_applying(1234567890).unwrap();
353        name_does_not_resolve(&state, &domain_name).unwrap();
354        assert_eq!(state.version(), 5);
355
356        state.block_reverted(1234567890).unwrap(); // undo block 1234567890
357        name_resolves_to(&state, &domain_name, &domain_data).unwrap();
358
359        state.undo_operation(3).unwrap(); // undo renew
360        name_does_not_resolve(&state, &domain_name).unwrap();
361
362        state.block_reverted(42).unwrap(); // undo block 42
363        name_resolves_to(&state, &domain_name, &domain_data).unwrap();
364    }
365
366    fn no_policies() -> SubtreePolicies {
367        SubtreePolicies::new() // json!({})
368    }
369
370    fn data(data: &str) -> serde_json::Value {
371        json!({ "data": data })
372    }
373
374    fn domain_name(name: &str) -> DomainName {
375        name.parse().unwrap()
376    }
377
378    fn domain_owner_pk() -> MPublicKey {
379        let sk = ark_sk();
380        sk.public_key()
381    }
382
383    fn domain_owner() -> Principal {
384        Principal::PublicKey(domain_owner_pk())
385    }
386
387    fn sign_ops(ops: NoncedBundle) -> SignedBundle {
388        let sk =
389            ark_sk_from("scout try doll stuff cake welcome random taste load town clerk ostrich");
390        ops.sign(&sk).unwrap()
391    }
392
393    fn sign_ops_by_wrong_key(ops: NoncedBundle) -> SignedBundle {
394        let sk = ark_sk_from(
395            "not scout try doll stuff cake welcome random taste load town clerk ostrich",
396        );
397        ops.sign(&sk).unwrap()
398    }
399
400    fn check_domain_exists(
401        state: &State, name: &DomainName, expected_data: &serde_json::Value,
402        expected_owner: &Principal,
403    ) {
404        let edge = name.last_edge().unwrap();
405        let parent = state.domain(&name.parent().unwrap()).unwrap();
406        assert!(parent.child_names().contains(&edge));
407
408        let registered_domain = parent.child(edge).unwrap();
409        assert_eq!(registered_domain.name(), name);
410        assert_eq!(registered_domain.owner(), expected_owner);
411        assert_eq!(registered_domain.subtree_policies(), &no_policies());
412        assert_eq!(registered_domain.data(), expected_data);
413        assert!(registered_domain.child_names().is_empty());
414    }
415
416    fn check_domain_missing(state: &State, name: &DomainName) {
417        let edge = name.last_edge().unwrap();
418        let parent = state.domain(&name.parent().unwrap()).unwrap();
419        assert!(!parent.child_names().contains(&edge));
420    }
421
422    fn execute_tld_register_system_domain() -> State {
423        let mut state = State::new();
424
425        let register_operation = UserOperation::register(
426            domain_name(".wallet"),
427            Principal::system(),
428            no_policies(),
429            RegistrationPolicy::Owner,
430            data("a"),
431            ExpirationPolicy::YEAR,
432        );
433        let version = state.apply_operation(register_operation).unwrap();
434
435        assert_eq!(version, 1);
436
437        state
438    }
439
440    #[test]
441    fn execute_checks_registration_policy() {
442        let mut state = execute_tld_register_system_domain();
443
444        let pk = "pezDj6ea4tVfNRUTMyssVDepAAzPW67Fe3yHtuHL6ZNtcfJ".parse().unwrap();
445        let register_operation = UserOperation::register(
446            domain_name(".wallet.wigy"),
447            Principal::public_key(&pk),
448            no_policies(),
449            Default::default(),
450            data("a"),
451            ExpirationPolicy::YEAR,
452        );
453        let err = state.apply_operation(register_operation).unwrap_err();
454
455        assert_eq!(err.to_string(), "Only system can register a child of .wallet");
456    }
457
458    #[test]
459    fn signed_register_cannot_impersonate_system() {
460        let mut state = State::new();
461        let sk = ark_sk();
462
463        let register_operation = UserOperation::register(
464            domain_name(".schema.system"),
465            Principal::system(),
466            no_policies(),
467            Default::default(),
468            data("a"),
469            ExpirationPolicy::YEAR,
470        );
471        let signed_ops = NoncedBundle::new(vec![register_operation], 42).sign(&sk).unwrap();
472        let err = state.apply_signed_bundle(signed_ops).unwrap_err();
473
474        assert_eq!(err.to_string(), "System principal cannot be impersonated");
475    }
476
477    #[test]
478    fn serde_roundtrip() {
479        let mut state = State::new();
480
481        let register_operation = UserOperation::register(
482            domain_name(".schema.a"),
483            domain_owner(),
484            no_policies(),
485            Default::default(),
486            data("a"),
487            ExpirationPolicy::YEAR,
488        );
489        state.apply_operation(register_operation).unwrap();
490
491        let update_operation = UserOperation::update(domain_name(".schema.a"), data("b"));
492        state.apply_operation(update_operation).unwrap();
493
494        let register_operation = UserOperation::register(
495            domain_name(".schema.ageover"),
496            domain_owner(),
497            no_policies(),
498            Default::default(),
499            data("c"),
500            ExpirationPolicy::YEAR,
501        );
502        state.apply_operation(register_operation).unwrap();
503
504        let serialized = serde_json::to_string(&state).unwrap();
505
506        // println!("{}", serde_json::to_string_pretty(&state).unwrap());
507
508        let deserialized: State = serde_json::from_str(&serialized).unwrap();
509
510        assert_eq!(deserialized, state);
511    }
512
513    #[test]
514    fn register_update_transfer_delete_domain() {
515        let name = ".schema.a";
516        let domain_name = || domain_name(name);
517
518        let mut state = State::new();
519
520        let register_operation = UserOperation::register(
521            domain_name(),
522            domain_owner(),
523            no_policies(),
524            Default::default(),
525            data("top level"),
526            ExpirationPolicy::YEAR,
527        );
528        state.apply_operation(register_operation).unwrap();
529
530        assert_eq!(state.version(), 1);
531        check_domain_exists(&state, &domain_name(), &data("top level"), &domain_owner());
532
533        let update_operation = UserOperation::update(domain_name(), data("cool, heh?"));
534        state.apply_operation(update_operation).unwrap();
535
536        assert_eq!(state.version(), 2);
537        check_domain_exists(&state, &domain_name(), &data("cool, heh?"), &domain_owner());
538
539        let pk = "pezDj6ea4tVfNRUTMyssVDepAAzPW67Fe3yHtuHL6ZNtcfJ".parse().unwrap();
540        let transfer_to = Principal::public_key(&pk);
541        let transfer_operation = UserOperation::transfer(domain_name(), transfer_to.clone());
542        state.apply_operation(transfer_operation).unwrap();
543
544        assert_eq!(state.version(), 3);
545        check_domain_exists(&state, &domain_name(), &data("cool, heh?"), &transfer_to);
546
547        state.apply_operation(UserOperation::delete(domain_name())).unwrap();
548
549        check_domain_missing(&state, &domain_name());
550        assert_eq!(state.last_seen_height(), 0);
551        assert_eq!(state.version(), 4);
552
553        state.undo_operation(3).unwrap(); // undo delete
554
555        check_domain_exists(&state, &domain_name(), &data("cool, heh?"), &transfer_to);
556
557        state.undo_operation(2).unwrap(); // undo transfer
558
559        check_domain_exists(&state, &domain_name(), &data("cool, heh?"), &domain_owner());
560
561        state.undo_operation(1).unwrap(); // undo update
562
563        check_domain_exists(&state, &domain_name(), &data("top level"), &domain_owner());
564        assert_eq!(state.version(), 1);
565
566        state.undo_operation(0).unwrap(); // undo register
567
568        check_domain_missing(&state, &domain_name());
569        assert_eq!(state.version(), 0);
570    }
571
572    fn schema_policy(schema: Schema) -> SubtreePolicies {
573        SubtreePolicies::new().with_schema(schema)
574    }
575
576    #[test]
577    fn schema_validation() {
578        let mut state = State::new();
579
580        let reg_badschema = UserOperation::register(
581            domain_name(".schema.badschema"),
582            domain_owner(),
583            schema_policy(json!({"properties": "invalid"})),
584            Default::default(),
585            json!({}),
586            ExpirationPolicy::YEAR,
587        );
588        assert_eq!(
589            state.apply_operation(reg_badschema).unwrap_err().to_string(),
590            "Domain .schema.badschema has invalid schema"
591        );
592
593        let reg_baddata = UserOperation::register(
594            domain_name(".schema.baddata"),
595            domain_owner(),
596            schema_policy(json!( {
597                "properties": {
598                    "someProperty": {
599                        "type": "string",
600                    },
601                },
602                "additionalProperties": false,
603            })),
604            Default::default(),
605            json!({"data": "notmatching"}),
606            ExpirationPolicy::YEAR,
607        );
608        assert_eq!(
609            state.apply_operation(reg_baddata).unwrap_err().to_string(),
610            "Domain .schema.baddata data does not match schema of .schema.baddata"
611        );
612
613        let reg_schema_empty = UserOperation::register(
614            domain_name(".schema.empty"),
615            domain_owner(),
616            schema_policy(json!({"additionalProperties": false,})),
617            Default::default(),
618            json!({}),
619            ExpirationPolicy::YEAR,
620        );
621        state.apply_operation(reg_schema_empty).unwrap();
622
623        let upd_schema_empty =
624            UserOperation::update(domain_name(".schema.empty"), json!({ "bad": "data"}));
625        assert_eq!(
626            state.apply_operation(upd_schema_empty).unwrap_err().to_string(),
627            "Domain .schema.empty data does not match schema of .schema.empty"
628        );
629    }
630
631    #[test]
632    fn authorization() {
633        let name = ".schema.a";
634        let domain_name = || domain_name(name);
635
636        let mut state = State::new();
637
638        let reg_op = UserOperation::register(
639            domain_name(),
640            domain_owner(),
641            no_policies(),
642            Default::default(),
643            data("top level"),
644            10,
645        );
646        // TODO Need to implement domain policy first to register under system domains
647        state.apply_operation(reg_op).unwrap();
648
649        assert_eq!(state.version(), 1);
650        assert_eq!(state.nonce(&domain_owner_pk()), 0);
651        check_domain_exists(&state, &domain_name(), &data("top level"), &domain_owner());
652        name_resolves_to(&state, &domain_name(), &data("top level")).unwrap();
653
654        state.block_applying(10).unwrap();
655        assert_eq!(state.version(), 2);
656        name_does_not_resolve(&state, &domain_name()).unwrap();
657
658        let update_op = UserOperation::update(domain_name(), data("cool, heh?"));
659        let renew_op = UserOperation::renew(domain_name(), 20);
660        let nonced_ops = NoncedBundle::new(vec![renew_op.clone(), update_op.clone()], 1);
661
662        let bad_nonce_ops = NoncedBundle::new(vec![update_op.clone(), renew_op.clone()], 2);
663        let signed_ops_bad_nonce = sign_ops(bad_nonce_ops);
664        let bad_nonce_err = state.apply_signed_bundle(signed_ops_bad_nonce).unwrap_err();
665        assert_eq!(bad_nonce_err.to_string(), "Invalid nonce 2, expected 1");
666        assert_eq!(state.version(), 2);
667        name_does_not_resolve(&state, &domain_name()).unwrap();
668
669        let bad_signed_ops = sign_ops_by_wrong_key(nonced_ops.clone());
670        let bad_signer_err = state.apply_signed_bundle(bad_signed_ops).unwrap_err();
671        assert_eq!(
672            bad_signer_err.to_string(),
673            "PublicKey principal psz291QGsvwafGPkKMu6MUsXThWRcBRzRf6pcVPM1Pst6WgW cannot be impersonated by pszcYyCB1iBEWSD9xFGzFYYQnJvYyvaENgRS9TnjJPNqfkz"
674        );
675        name_does_not_resolve(&state, &domain_name()).unwrap();
676
677        let bad_order_ops = NoncedBundle::new(vec![update_op, renew_op], 1);
678        let bad_order_signed_ops = sign_ops(bad_order_ops);
679        let bad_order_err = state.apply_signed_bundle(bad_order_signed_ops).unwrap_err();
680        assert_eq!(bad_order_err.to_string(), "Domain .schema.a expired");
681
682        let signed_ops = sign_ops(nonced_ops);
683        state.apply_signed_bundle(signed_ops).unwrap();
684
685        assert_eq!(state.version(), 4);
686        assert_eq!(state.nonce(&domain_owner_pk()), 1);
687        check_domain_exists(&state, &domain_name(), &data("cool, heh?"), &domain_owner());
688        name_resolves_to(&state, &domain_name(), &data("cool, heh?")).unwrap();
689    }
690}