use bft_json_crdt::{ json_crdt::{add_crdt_fields, BaseCrdt, CrdtNode, IntoCrdtNode, OpState}, keypair::make_keypair, list_crdt::ListCrdt, lww_crdt::LwwRegisterCrdt, op::{Op, PathSegment, ROOT_ID}, }; use serde_json::json; // What is potentially Byzantine behaviour? // 1. send valid updates // 2. send a mix of valid and invalid updates // a) messages with duplicate ID (attempt to overwrite old entries) // b) send incorrect sequence number to multiple nodes (which could lead to divergent state) -- this is called equivocation // c) ‘forge’ updates from another author (could happen when forwarding valid messages from peers) // 3. send malformed updates (e.g. missing fields) // this we don't test as we assume transport layer only allows valid messages // 4. overwhelm message queue by sending many updates far into the future // also untested! currently we keep an unbounded message queue // 5. block actual messages from honest actors (eclipse attack) #[add_crdt_fields] #[derive(Clone, CrdtNode)] struct ListExample { list: ListCrdt, } // case 2a + 2b #[test] fn test_equivocation() { let key = make_keypair(); let test_key = make_keypair(); let mut crdt = BaseCrdt::::new(&key); let mut test_crdt = BaseCrdt::::new(&test_key); let _a = crdt.doc.list.insert(ROOT_ID, 'a').sign(&key); let _b = crdt.doc.list.insert(_a.id(), 'b').sign(&key); // make a fake operation with same id as _b but different content let mut fake_op = _b.clone(); fake_op.inner.content = Some('c'.into()); // also try modifying the sequence number let mut fake_op_seq = _b.clone(); fake_op_seq.inner.seq = 99; fake_op_seq.inner.is_deleted = true; assert_eq!(crdt.apply(fake_op.clone()), OpState::ErrHashMismatch); assert_eq!(crdt.apply(fake_op_seq.clone()), OpState::ErrHashMismatch); assert_eq!(test_crdt.apply(fake_op_seq), OpState::ErrHashMismatch); assert_eq!(test_crdt.apply(fake_op), OpState::ErrHashMismatch); assert_eq!(test_crdt.apply(_a), OpState::Ok); assert_eq!(test_crdt.apply(_b), OpState::Ok); // make sure it doesn't accept either of the fake operations assert_eq!(crdt.doc.list.view(), vec!['a', 'b']); assert_eq!(crdt.doc.list.view(), test_crdt.doc.list.view()); } // case 2c #[test] fn test_forge_update() { let key = make_keypair(); let test_key = make_keypair(); let mut crdt = BaseCrdt::::new(&key); let mut test_crdt = BaseCrdt::::new(&test_key); let _a = crdt.doc.list.insert(ROOT_ID, 'a').sign(&key); let fake_key = make_keypair(); // generate a new keypair as we don't have private key of list.our_id let mut op = Op { origin: _a.inner.id, author: crdt.doc.id, // pretend to be the owner of list content: Some('b'), path: vec![PathSegment::Field("list".to_string())], seq: 1, is_deleted: false, id: ROOT_ID, // placeholder, to be generated }; // this is a completely valid hash and digest, just signed by the wrong person // as keypair.public != list.public op.id = op.hash_to_id(); let signed = op.sign(&fake_key); assert_eq!(crdt.apply(signed.clone()), OpState::ErrHashMismatch); assert_eq!(test_crdt.apply(signed), OpState::ErrHashMismatch); assert_eq!(test_crdt.apply(_a), OpState::Ok); // make sure it doesn't accept fake operation assert_eq!(crdt.doc.list.view(), vec!['a']); } #[add_crdt_fields] #[derive(Clone, CrdtNode)] struct Nested { a: Nested2, } #[add_crdt_fields] #[derive(Clone, CrdtNode)] struct Nested2 { b: LwwRegisterCrdt, } #[test] fn test_path_update() { let key = make_keypair(); let test_key = make_keypair(); let mut crdt = BaseCrdt::::new(&key); let mut test_crdt = BaseCrdt::::new(&test_key); let mut _true = crdt.doc.a.b.set(true); _true.path = vec![PathSegment::Field("x".to_string())]; let mut _false = crdt.doc.a.b.set(false); _false.path = vec![ PathSegment::Field("a".to_string()), PathSegment::Index(_false.id), ]; let signed_true = _true.sign(&key); let signed_false = _false.sign(&key); let mut signed_false_fake_path = signed_false.clone(); signed_false_fake_path.inner.path = vec![ PathSegment::Field("a".to_string()), PathSegment::Field("b".to_string()), ]; assert_eq!(test_crdt.apply(signed_true), OpState::ErrPathMismatch); assert_eq!(test_crdt.apply(signed_false), OpState::ErrPathMismatch); assert_eq!( test_crdt.apply(signed_false_fake_path), OpState::ErrDigestMismatch ); // make sure it doesn't accept fake operation assert_eq!(crdt.doc.a.b.view(), json!(false).into()); assert_eq!(test_crdt.doc.a.b.view(), json!(null).into()); }