2024-05-30 13:51:32 +01:00
|
|
|
|
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
|
2024-05-30 14:52:02 +01:00
|
|
|
|
// also untested! currently we keep an unbounded message queue
|
2024-05-30 13:51:32 +01:00
|
|
|
|
// 5. block actual messages from honest actors (eclipse attack)
|
|
|
|
|
|
|
|
|
|
|
|
#[add_crdt_fields]
|
|
|
|
|
|
#[derive(Clone, CrdtNode)]
|
|
|
|
|
|
struct ListExample {
|
|
|
|
|
|
list: ListCrdt<char>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-11 19:16:36 +01:00
|
|
|
|
// case 1 - send valid updates
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_valid_updates() {
|
|
|
|
|
|
// Insert to crdt.doc on local node, test applying the same operation to a remote node
|
|
|
|
|
|
// and check that the view is the same
|
|
|
|
|
|
let key = make_keypair();
|
|
|
|
|
|
let mut crdt = BaseCrdt::<ListExample>::new(&key);
|
|
|
|
|
|
let _a = crdt.doc.list.insert(ROOT_ID, 'a').sign(&key);
|
|
|
|
|
|
let _b = crdt.doc.list.insert(_a.id(), 'b').sign(&key);
|
|
|
|
|
|
let _c = crdt.doc.list.insert(_b.id(), 'c').sign(&key);
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(crdt.doc.list.view(), vec!['a', 'b', 'c']);
|
|
|
|
|
|
|
|
|
|
|
|
let key2 = make_keypair();
|
|
|
|
|
|
let mut crdt2 = BaseCrdt::<ListExample>::new(&key2);
|
|
|
|
|
|
crdt2.apply(_a.clone());
|
|
|
|
|
|
crdt2.apply(_b);
|
2024-06-12 15:07:24 +01:00
|
|
|
|
crdt2.apply(_c.clone());
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
crdt2.doc.list.view(),
|
|
|
|
|
|
crdt.doc.list.view(),
|
|
|
|
|
|
"views should be equal"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
crdt2.apply(_a.clone());
|
2024-06-11 19:16:36 +01:00
|
|
|
|
crdt2.apply(_a);
|
|
|
|
|
|
|
2024-06-12 15:07:24 +01:00
|
|
|
|
assert_eq!(
|
|
|
|
|
|
crdt.doc.list.view(),
|
|
|
|
|
|
crdt2.doc.list.view(),
|
|
|
|
|
|
"views are still equal after repeated applies"
|
|
|
|
|
|
);
|
2024-06-11 19:16:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-05-30 13:51:32 +01:00
|
|
|
|
// case 2a + 2b
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_equivocation() {
|
|
|
|
|
|
let key = make_keypair();
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let test_key = make_keypair();
|
2024-05-30 13:51:32 +01:00
|
|
|
|
let mut crdt = BaseCrdt::<ListExample>::new(&key);
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let mut test_crdt = BaseCrdt::<ListExample>::new(&test_key);
|
2024-05-30 13:51:32 +01:00
|
|
|
|
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);
|
|
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
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);
|
2024-05-30 13:51:32 +01:00
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
// make sure it doesn't accept either of the fake operations
|
2024-05-30 13:51:32 +01:00
|
|
|
|
assert_eq!(crdt.doc.list.view(), vec!['a', 'b']);
|
2024-05-30 14:52:02 +01:00
|
|
|
|
assert_eq!(crdt.doc.list.view(), test_crdt.doc.list.view());
|
2024-05-30 13:51:32 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// case 2c
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_forge_update() {
|
|
|
|
|
|
let key = make_keypair();
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let test_key = make_keypair();
|
2024-05-30 13:51:32 +01:00
|
|
|
|
let mut crdt = BaseCrdt::<ListExample>::new(&key);
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let mut test_crdt = BaseCrdt::<ListExample>::new(&test_key);
|
2024-05-30 13:51:32 +01:00
|
|
|
|
let _a = crdt.doc.list.insert(ROOT_ID, 'a').sign(&key);
|
|
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let fake_key = make_keypair(); // generate a new keypair as we don't have private key of list.our_id
|
2024-05-30 13:51:32 +01:00
|
|
|
|
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);
|
2024-05-30 14:52:02 +01:00
|
|
|
|
assert_eq!(test_crdt.apply(signed), OpState::ErrHashMismatch);
|
|
|
|
|
|
assert_eq!(test_crdt.apply(_a), OpState::Ok);
|
2024-05-30 13:51:32 +01:00
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
// make sure it doesn't accept fake operation
|
2024-05-30 13:51:32 +01:00
|
|
|
|
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<bool>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_path_update() {
|
|
|
|
|
|
let key = make_keypair();
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let test_key = make_keypair();
|
2024-05-30 13:51:32 +01:00
|
|
|
|
let mut crdt = BaseCrdt::<Nested>::new(&key);
|
2024-05-30 14:52:02 +01:00
|
|
|
|
let mut test_crdt = BaseCrdt::<Nested>::new(&test_key);
|
2024-05-30 13:51:32 +01:00
|
|
|
|
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),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
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![
|
2024-05-30 13:51:32 +01:00
|
|
|
|
PathSegment::Field("a".to_string()),
|
|
|
|
|
|
PathSegment::Field("b".to_string()),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
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
|
|
|
|
|
|
);
|
2024-05-30 13:51:32 +01:00
|
|
|
|
|
2024-05-30 14:52:02 +01:00
|
|
|
|
// make sure it doesn't accept fake operation
|
2024-05-30 13:51:32 +01:00
|
|
|
|
assert_eq!(crdt.doc.a.b.view(), json!(false).into());
|
2024-05-30 14:52:02 +01:00
|
|
|
|
assert_eq!(test_crdt.doc.a.b.view(), json!(null).into());
|
2024-05-30 13:51:32 +01:00
|
|
|
|
}
|