Files
bft-crdt-experiment/crates/bft-json-crdt/tests/byzantine.rs

138 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<char>,
}
// case 2a + 2b
#[test]
fn test_equivocation() {
let key = make_keypair();
let test_key = make_keypair();
let mut crdt = BaseCrdt::<ListExample>::new(&key);
let mut test_crdt = BaseCrdt::<ListExample>::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::<ListExample>::new(&key);
let mut test_crdt = BaseCrdt::<ListExample>::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<bool>,
}
#[test]
fn test_path_update() {
let key = make_keypair();
let test_key = make_keypair();
let mut crdt = BaseCrdt::<Nested>::new(&key);
let mut test_crdt = BaseCrdt::<Nested>::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());
}