Skip to content

Commit 44948a1

Browse files
Marcus Pousettemarcus-pousette
authored andcommitted
fix(sqlite): persist local proofs atomically
1 parent f50f698 commit 44948a1

3 files changed

Lines changed: 390 additions & 36 deletions

File tree

packages/treecrdt-sqlite-ext/src/extension/functions/local_ops.rs

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,15 @@ struct JsonLocalOpTransactionError {
5555
enum LocalOpMode {
5656
Immediate,
5757
Prepare,
58-
Commit(String),
58+
Commit {
59+
precondition: String,
60+
proof: LocalOpProof,
61+
},
62+
}
63+
64+
struct LocalOpProof {
65+
sig: Vec<u8>,
66+
proof_ref: Vec<u8>,
5967
}
6068

6169
enum LocalOpDispatchResult {
@@ -109,20 +117,37 @@ fn parse_local_op_mode(
109117
args: &[*mut sqlite3_value],
110118
base_argc: usize,
111119
) -> Result<LocalOpMode, ()> {
112-
if argc as usize == base_argc {
113-
return Ok(LocalOpMode::Immediate);
120+
match argc as usize {
121+
n if n == base_argc => return Ok(LocalOpMode::Immediate),
122+
n if n == base_argc + 1 => {
123+
return (read_text(args[base_argc]) == "prepare")
124+
.then_some(LocalOpMode::Prepare)
125+
.ok_or(());
126+
}
127+
n if n == base_argc + 3 => {}
128+
_ => return Err(()),
114129
}
115-
if argc as usize != base_argc + 1 {
130+
131+
let precondition = read_text(args[base_argc]);
132+
if !precondition.starts_with("v1:") {
116133
return Err(());
117134
}
118-
let phase = read_text(args[base_argc]);
119-
if phase == "prepare" {
120-
Ok(LocalOpMode::Prepare)
121-
} else if phase.starts_with("v1:") {
122-
Ok(LocalOpMode::Commit(phase))
123-
} else {
124-
Err(())
135+
let sig = read_required_blob(args[base_argc + 1])?;
136+
if sig.len() != 64 {
137+
return Err(());
138+
}
139+
let proof_ref = read_required_blob(args[base_argc + 2])?;
140+
if proof_ref.len() != 16 {
141+
return Err(());
125142
}
143+
Ok(LocalOpMode::Commit {
144+
precondition,
145+
proof: LocalOpProof { sig, proof_ref },
146+
})
147+
}
148+
149+
fn valid_local_op_argc(argc: c_int, base_argc: usize) -> bool {
150+
matches!(argc as usize, n if n == base_argc || n == base_argc + 1 || n == base_argc + 3)
126151
}
127152

128153
fn sqlite_busy_or_locked(rc: c_int) -> bool {
@@ -285,6 +310,72 @@ fn local_precondition(
285310
Ok(Some(format!("v1:{}", hasher.finalize().to_hex())))
286311
}
287312

313+
fn persist_local_op_proof(
314+
db: *mut sqlite3,
315+
doc_id: &[u8],
316+
op: &Operation,
317+
proof: &LocalOpProof,
318+
) -> Result<(), c_int> {
319+
let sql = CString::new(
320+
"INSERT OR REPLACE INTO treecrdt_sync_op_auth \
321+
(doc_id, op_ref, sig, proof_ref, created_at_ms) \
322+
VALUES (?1, ?2, ?3, ?4, CAST(strftime('%s','now') AS INTEGER) * 1000)",
323+
)
324+
.expect("local op proof sql");
325+
let mut stmt: *mut sqlite3_stmt = null_mut();
326+
let rc = sqlite_prepare_v2(db, sql.as_ptr(), -1, &mut stmt, null_mut());
327+
if rc != SQLITE_OK as c_int {
328+
return Err(rc);
329+
}
330+
331+
let op_ref = derive_op_ref_v0(doc_id, op.meta.id.replica.as_bytes(), op.meta.id.counter);
332+
let mut bind_err = false;
333+
unsafe {
334+
bind_err |= sqlite_bind_text(
335+
stmt,
336+
1,
337+
doc_id.as_ptr() as *const c_char,
338+
doc_id.len() as c_int,
339+
None,
340+
) != SQLITE_OK as c_int;
341+
bind_err |= sqlite_bind_blob(
342+
stmt,
343+
2,
344+
op_ref.as_ptr() as *const c_void,
345+
op_ref.len() as c_int,
346+
None,
347+
) != SQLITE_OK as c_int;
348+
bind_err |= sqlite_bind_blob(
349+
stmt,
350+
3,
351+
proof.sig.as_ptr() as *const c_void,
352+
proof.sig.len() as c_int,
353+
None,
354+
) != SQLITE_OK as c_int;
355+
bind_err |= sqlite_bind_blob(
356+
stmt,
357+
4,
358+
proof.proof_ref.as_ptr() as *const c_void,
359+
proof.proof_ref.len() as c_int,
360+
None,
361+
) != SQLITE_OK as c_int;
362+
}
363+
if bind_err {
364+
unsafe { sqlite_finalize(stmt) };
365+
return Err(SQLITE_ERROR as c_int);
366+
}
367+
368+
let step_rc = unsafe { sqlite_step(stmt) };
369+
let finalize_rc = unsafe { sqlite_finalize(stmt) };
370+
if step_rc != SQLITE_DONE as c_int {
371+
return Err(step_rc);
372+
}
373+
if finalize_rc != SQLITE_OK as c_int {
374+
return Err(finalize_rc);
375+
}
376+
Ok(())
377+
}
378+
288379
fn begin_local_core_op(
289380
db: *mut sqlite3,
290381
doc_id: &[u8],
@@ -493,7 +584,10 @@ where
493584
};
494585
finish_local_core_op(session, op, plan).map(LocalOpDispatchResult::Committed)
495586
}
496-
LocalOpMode::Commit(expected_precondition) => {
587+
LocalOpMode::Commit {
588+
precondition: expected_precondition,
589+
proof,
590+
} => {
497591
let mut session = match begin_optimistic_local_core_op(db, &replica, savepoint_name) {
498592
Ok(v) => v,
499593
Err(OptimisticBeginError::Conflict) => return Ok(LocalOpDispatchResult::Conflict),
@@ -525,6 +619,9 @@ where
525619
Ok(v) => v,
526620
Err(err) => return Err(session.rollback(sqlite_err_from_core(err))),
527621
};
622+
if let Err(rc) = persist_local_op_proof(session.db, &session.doc_id, &op, &proof) {
623+
return Err(session.rollback(rc));
624+
}
528625
finish_local_core_op(session, op, plan).map(LocalOpDispatchResult::Committed)
529626
}
530627
}
@@ -556,10 +653,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_insert(
556653
return;
557654
}
558655

559-
if argc != 6 && argc != 7 {
656+
if !valid_local_op_argc(argc, 6) {
560657
sqlite_result_error(
561658
ctx,
562-
b"treecrdt_local_insert expects 6 args plus optional prepare/precondition\0".as_ptr()
659+
b"treecrdt_local_insert expects 6 args, 7 for prepare, or 9 for commit\0".as_ptr()
563660
as *const c_char,
564661
);
565662
return;
@@ -571,7 +668,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_insert(
571668
Err(_) => {
572669
sqlite_result_error(
573670
ctx,
574-
b"treecrdt_local_insert: invalid prepare/precondition argument\0".as_ptr()
671+
b"treecrdt_local_insert: invalid prepare or commit arguments\0".as_ptr()
575672
as *const c_char,
576673
);
577674
return;
@@ -655,10 +752,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_move(
655752
return;
656753
}
657754

658-
if argc != 5 && argc != 6 {
755+
if !valid_local_op_argc(argc, 5) {
659756
sqlite_result_error(
660757
ctx,
661-
b"treecrdt_local_move expects 5 args plus optional prepare/precondition\0".as_ptr()
758+
b"treecrdt_local_move expects 5 args, 6 for prepare, or 8 for commit\0".as_ptr()
662759
as *const c_char,
663760
);
664761
return;
@@ -670,7 +767,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_move(
670767
Err(_) => {
671768
sqlite_result_error(
672769
ctx,
673-
b"treecrdt_local_move: invalid prepare/precondition argument\0".as_ptr()
770+
b"treecrdt_local_move: invalid prepare or commit arguments\0".as_ptr()
674771
as *const c_char,
675772
);
676773
return;
@@ -752,10 +849,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_delete(
752849
return;
753850
}
754851

755-
if argc != 2 && argc != 3 {
852+
if !valid_local_op_argc(argc, 2) {
756853
sqlite_result_error(
757854
ctx,
758-
b"treecrdt_local_delete expects 2 args plus optional prepare/precondition\0".as_ptr()
855+
b"treecrdt_local_delete expects 2 args, 3 for prepare, or 5 for commit\0".as_ptr()
759856
as *const c_char,
760857
);
761858
return;
@@ -767,7 +864,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_delete(
767864
Err(_) => {
768865
sqlite_result_error(
769866
ctx,
770-
b"treecrdt_local_delete: invalid prepare/precondition argument\0".as_ptr()
867+
b"treecrdt_local_delete: invalid prepare or commit arguments\0".as_ptr()
771868
as *const c_char,
772869
);
773870
return;
@@ -818,10 +915,10 @@ pub(super) unsafe extern "C" fn treecrdt_local_payload(
818915
return;
819916
}
820917

821-
if argc != 3 && argc != 4 {
918+
if !valid_local_op_argc(argc, 3) {
822919
sqlite_result_error(
823920
ctx,
824-
b"treecrdt_local_payload expects 3 args plus optional prepare/precondition\0".as_ptr()
921+
b"treecrdt_local_payload expects 3 args, 4 for prepare, or 6 for commit\0".as_ptr()
825922
as *const c_char,
826923
);
827924
return;
@@ -833,7 +930,7 @@ pub(super) unsafe extern "C" fn treecrdt_local_payload(
833930
Err(_) => {
834931
sqlite_result_error(
835932
ctx,
836-
b"treecrdt_local_payload: invalid prepare/precondition argument\0".as_ptr()
933+
b"treecrdt_local_payload: invalid prepare or commit arguments\0".as_ptr()
837934
as *const c_char,
838935
);
839936
return;

packages/treecrdt-sqlite-ext/src/extension/functions/schema.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,16 @@ CREATE TABLE IF NOT EXISTS tree_payload (
306306
last_replica BLOB NOT NULL,
307307
last_counter INTEGER NOT NULL
308308
);
309+
"#;
310+
const OP_AUTH: &str = r#"
311+
CREATE TABLE IF NOT EXISTS treecrdt_sync_op_auth (
312+
doc_id TEXT NOT NULL,
313+
op_ref BLOB NOT NULL,
314+
sig BLOB NOT NULL CHECK(length(sig) = 64),
315+
proof_ref BLOB NOT NULL CHECK(length(proof_ref) = 16),
316+
created_at_ms INTEGER NOT NULL,
317+
PRIMARY KEY (doc_id, op_ref)
318+
);
309319
"#;
310320

311321
let rc_meta = {
@@ -352,6 +362,13 @@ CREATE TABLE IF NOT EXISTS tree_payload (
352362
return Err(rc_tree_payload);
353363
}
354364

365+
let rc_op_auth = {
366+
let sql = CString::new(OP_AUTH).expect("op auth schema");
367+
sqlite_exec(db, sql.as_ptr(), None, null_mut(), null_mut())
368+
};
369+
if rc_op_auth != SQLITE_OK as c_int {
370+
return Err(rc_op_auth);
371+
}
355372
const INDEXES: &str = r#"
356373
CREATE INDEX IF NOT EXISTS idx_ops_lamport ON ops(lamport, replica, counter);
357374
CREATE INDEX IF NOT EXISTS idx_ops_op_ref ON ops(op_ref);

0 commit comments

Comments
 (0)