1use crate::sqlite::DatabaseConnection;
13use crate::write_encoding::{decoded, encoded};
14use crate::write_schema::WriteSchema;
15use crate::DbError;
16use coven_crypto::AuthorshipHasher;
17use coven_format::authorship::{CellKind, KeptLeaf, Leaf, Multiproof, WriteEvidence};
18use coven_format::merge_fields;
19use coven_format::write::WriteDisposition;
20use coven_format::write_stream::WriteHeaderFrame;
21use coven_foundation::id_source::{KeyId, MemberId};
22use coven_merge::{Audience, ColumnValue, RowId, WriteId};
23use rusqlite::params;
24use std::collections::{BTreeMap, BTreeSet};
25
26pub trait Authorship: Send + Sync {
28 fn header(&self, evidence: &WriteEvidence) -> Result<WriteHeaderFrame, DbError>;
32
33 fn hasher(&self, audience: &Audience, key: KeyId) -> Result<AuthorshipHasher, DbError>;
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct HeaderEvidence {
41 pub prefix: Vec<u8>,
43 pub header: Vec<u8>,
45 pub roots: Vec<[u8; 32]>,
47 pub signature: [u8; coven_crypto::SIGNATURE_LEN],
49}
50
51fn hash_bytes(hashes: &[[u8; 32]]) -> Vec<u8> {
54 hashes.iter().flatten().copied().collect()
55}
56
57fn hash_list(bytes: &[u8]) -> Result<Vec<[u8; 32]>, DbError> {
60 let hashes = bytes.chunks_exact(32);
61 if !hashes.remainder().is_empty() {
62 return Err(DbError::DamagedDatabase);
63 }
64 Ok(hashes.map(|hash| hash.try_into().expect("a 32-byte chunk")).collect())
65}
66
67pub(crate) fn retain_header(
68 db: &DatabaseConnection,
69 write: WriteId,
70 evidence: &HeaderEvidence,
71) -> Result<(), DbError> {
72 let roots = hash_bytes(&evidence.roots);
73 db.internal_execute(
74 "INSERT INTO _coven_write_evidence(write_id,prefix,header,roots,signature) VALUES(?1,?2,?3,?4,?5) ON CONFLICT(write_id) DO NOTHING",
75 params![
76 encoded(merge_fields::encode_write_id(&write))?,
77 evidence.prefix,
78 evidence.header,
79 roots,
80 evidence.signature.as_slice()
81 ],
82 )?;
83 Ok(())
84}
85
86fn header(db: &DatabaseConnection, write: WriteId) -> Result<Option<HeaderEvidence>, DbError> {
89 let damaged = |column| {
90 crate::write_encoding::decode_failure(
91 column,
92 rusqlite::types::Type::Blob,
93 DbError::DamagedDatabase,
94 )
95 };
96 Ok(db
97 .query(
98 "SELECT prefix,header,roots,signature FROM _coven_write_evidence WHERE write_id=?1",
99 [encoded(merge_fields::encode_write_id(&write))?],
100 |r| {
101 let roots: Vec<u8> = r.get(2)?;
102 let signature: Vec<u8> = r.get(3)?;
103 Ok(HeaderEvidence {
104 prefix: r.get(0)?,
105 header: r.get(1)?,
106 roots: hash_list(&roots).map_err(|_damaged| damaged(2))?,
107 signature: signature.try_into().map_err(|_wrong_width| damaged(3))?,
108 })
109 },
110 )?
111 .pop())
112}
113
114pub(crate) fn retain_leaves(
116 db: &DatabaseConnection,
117 write: WriteId,
118 audience: &Audience,
119 hashes: &[[u8; 32]],
120) -> Result<(), DbError> {
121 let hashes = hash_bytes(hashes);
122 db.internal_execute(
123 "INSERT INTO _coven_part_leaves(write_id,audience,hashes) VALUES(?1,?2,?3) ON CONFLICT(write_id,audience) DO NOTHING",
124 params![
125 encoded(merge_fields::encode_write_id(&write))?,
126 audience,
127 hashes
128 ],
129 )?;
130 Ok(())
131}
132
133pub(crate) fn holds_leaves(
136 db: &DatabaseConnection,
137 write: WriteId,
138 audience: &Audience,
139) -> Result<bool, DbError> {
140 db.query_row(
141 "SELECT EXISTS(SELECT 1 FROM _coven_part_leaves WHERE write_id=?1 AND audience=?2)",
142 params![encoded(merge_fields::encode_write_id(&write))?, audience],
143 |r| r.get(0),
144 )
145}
146
147pub(crate) fn forget_part(
151 db: &DatabaseConnection,
152 write: WriteId,
153 audience: &Audience,
154) -> Result<(), DbError> {
155 let id = encoded(merge_fields::encode_write_id(&write))?;
156 db.internal_execute(
157 "DELETE FROM _coven_part_leaves WHERE write_id=?1 AND audience=?2",
158 params![id, audience],
159 )?;
160 db.internal_execute(
161 "DELETE FROM _coven_write_evidence WHERE write_id=?1
162 AND NOT EXISTS(SELECT 1 FROM _coven_part_leaves WHERE write_id=?1)
163 AND NOT EXISTS(SELECT 1 FROM _coven_part_proofs WHERE write_id=?1)",
164 [id],
165 )?;
166 Ok(())
167}
168
169#[derive(Default)]
171struct Needs {
172 creations: BTreeSet<RowId>,
173 cells: Vec<(RowId, String, ColumnValue<coven_format::value::Value>, u64)>,
175}
176
177fn missing(write: WriteId) -> DbError {
180 tracing::error!(?write, "another member's write lacks retained authorship evidence");
181 DbError::Invariant { what: "another member's write that set an owned row keeps its evidence" }
182}
183
184fn written(
186 db: &DatabaseConnection,
187 write: WriteId,
188) -> Result<Option<coven_format::write::WriteHeader>, DbError> {
189 if let Some(frame) = crate::write_headers::read(db, write)? {
190 return Ok(Some(frame.header));
191 }
192 let header: Option<Vec<u8>> = db
193 .query(
194 "SELECT header FROM _coven_authored_parts WHERE write_id=?1 LIMIT 1",
195 [encoded(merge_fields::encode_write_id(&write))?],
196 |r| r.get(0),
197 )?
198 .pop();
199 header.map(|bytes| Ok(merge_fields::decode_write_header(&bytes)?)).transpose()
200}
201
202#[derive(Clone, Copy, Debug)]
205pub(crate) struct Author {
206 pub(crate) device: coven_foundation::id_source::DeviceId,
207 pub(crate) member: Option<MemberId>,
208}
209
210fn by_another_member(
213 db: &DatabaseConnection,
214 author: Author,
215 write: WriteId,
216) -> Result<bool, DbError> {
217 Ok(write.device != author.device
218 && (author.member.is_none()
219 || crate::store_state_tables::device_member(db, write.device)? != author.member))
220}
221
222pub(crate) fn keeps_evidence(db: &DatabaseConnection, write: WriteId) -> Result<bool, DbError> {
228 let device = db
229 .local_device()
230 .ok_or(DbError::Invariant { what: "a writer connection observes this device" })?;
231 by_another_member(
232 db,
233 Author { device, member: crate::store_state_tables::device_member(db, device)? },
234 write,
235 )
236}
237
238fn same_row(
240 db: &DatabaseConnection,
241 version: u32,
242 current: u32,
243 leaf: &RowId,
244 row: &RowId,
245) -> Result<bool, DbError> {
246 Ok(leaf.key == row.key
247 && leaf.audience == row.audience
248 && crate::source_names::table(db, version, &leaf.table, current)?.as_deref()
249 == Some(row.table.as_str()))
250}
251
252fn proves(
255 db: &DatabaseConnection,
256 version: u32,
257 current: u32,
258 leaf: &Leaf,
259 needs: &Needs,
260 creators: &BTreeMap<RowId, Option<coven_format::write_sources::ChangeId>>,
261) -> Result<bool, DbError> {
262 match leaf {
263 Leaf::Creation { row, generation: 0, creator } => {
264 for needed in &needs.creations {
265 if same_row(db, version, current, row, needed)?
266 && creators.get(needed) == Some(creator)
267 {
268 return Ok(true);
269 }
270 }
271 Ok(false)
272 }
273 Leaf::Cell { row, generation, kind, column, value } => {
274 let incarnation = match kind {
275 CellKind::Insert => generation + 1,
276 CellKind::Update => *generation,
277 };
278 for (needed, name, needed_value, present) in &needs.cells {
279 if *present == incarnation
280 && value == needed_value
281 && same_row(db, version, current, row, needed)?
282 && crate::source_names::column(
283 db,
284 version,
285 &row.table,
286 column,
287 current,
288 &needed.table,
289 )?
290 .as_deref()
291 == Some(name.as_str())
292 {
293 return Ok(true);
294 }
295 }
296 Ok(false)
297 }
298 _ => Ok(false),
299 }
300}
301
302pub(crate) fn evidence(
307 db: &DatabaseConnection,
308 schema: &WriteSchema,
309 audience: &Audience,
310 author: Author,
311) -> Result<Vec<WriteEvidence>, DbError> {
312 let mut needs = BTreeMap::<WriteId, Needs>::new();
313 let mut creators = BTreeMap::new();
314 let other = |write: WriteId| by_another_member(db, author, write);
315 crate::snapshot_view::creations::<DbError>(db, audience, |creation| {
316 let row = creation.row.clone();
317 if crate::ownership::owner_table(schema, &row.table) && other(creation.write)? {
318 let claim =
319 (creation.creator.write != creation.write).then(|| creation.creator.clone());
320 creators.insert(row.clone(), claim);
321 needs.entry(creation.write).or_default().creations.insert(row);
322 }
323 Ok(())
324 })?;
325 crate::snapshot_view::rows(db, schema, audience, |merged| {
326 let state = merged.state;
327 let row = state.row();
328 if crate::ownership::owner_table(schema, &row.table) && state.present() {
329 for (column, cell) in state.cells() {
330 if other(cell.write)? {
331 needs.entry(cell.write).or_default().cells.push((
332 row.clone(),
333 column.clone(),
334 cell.value.clone(),
335 state.generation(),
336 ));
337 }
338 }
339 }
340 Ok::<_, DbError>(())
341 })?;
342 let current = db.schema_version()?;
343 let mut evidence = Vec::new();
344 for (write, needs) in needs {
345 let original = header(db, write)?.ok_or(missing(write))?;
346 let plain = written(db, write)?.ok_or(missing(write))?;
347 let proof = if plain.disposition == WriteDisposition::Migration {
348 Multiproof { leaves: 0, revealed: Vec::new(), hidden: Vec::new() }
349 } else {
350 open(db, write, audience, plain.schema_version, current, &needs, &creators)?
351 };
352 evidence.push(WriteEvidence {
353 write,
354 prefix: original.prefix,
355 header: original.header,
356 roots: original.roots,
357 signature: original.signature,
358 proof,
359 });
360 }
361 Ok(evidence)
362}
363
364fn open(
369 db: &DatabaseConnection,
370 write: WriteId,
371 audience: &Audience,
372 version: u32,
373 current: u32,
374 needs: &Needs,
375 creators: &BTreeMap<RowId, Option<coven_format::write_sources::ChangeId>>,
376) -> Result<Multiproof, DbError> {
377 let id = encoded(merge_fields::encode_write_id(&write))?;
378 let hashes: Option<Vec<u8>> = db.query_row(
379 "SELECT (SELECT hashes FROM _coven_part_leaves WHERE write_id=?1 AND audience=?2)",
380 params![id, audience],
381 |r| r.get(0),
382 )?;
383 if let Some(hashes) = hashes {
384 let hashes = hash_list(&hashes)?;
385 let mut leaves = Vec::new();
386 db.for_each::<_, DbError>(
387 "SELECT record FROM _coven_authored_changes WHERE write_id=?1 AND audience=?2 ORDER BY table_name,key",
388 params![id, audience],
389 |r| {
390 let record = coven_format::snapshot_rows::KeptRecord::decode(
391 &r.get::<_, Vec<u8>>(0)?,
392 )?;
393 leaves.extend(coven_format::authorship::kept_leaves(&record));
394 Ok(())
395 },
396 )?;
397 if leaves.len() != hashes.len() {
398 return Err(DbError::DamagedDatabase);
399 }
400 let mut opened = Vec::with_capacity(leaves.len());
401 for leaf in &leaves {
402 opened.push(opening(db, version, current, leaf, needs, creators)?);
403 }
404 return Ok(Multiproof::from_hashes(&hashes, opened));
405 }
406 let (proof, hashes) = db
407 .query(
408 "SELECT proof,hashes FROM _coven_part_proofs WHERE write_id=?1 AND audience=?2",
409 params![id, audience],
410 |r| Ok((r.get::<_, Vec<u8>>(0)?, r.get::<_, Vec<u8>>(1)?)),
411 )?
412 .pop()
413 .ok_or_else(|| missing(write))?;
414 let proof = decoded(merge_fields::decode_multiproof(&proof))?;
415 let hashes = hash_list(&hashes)?;
416 let keep = proof
417 .opened()?
418 .iter()
419 .map(|leaf| proves(db, version, current, leaf, needs, creators))
420 .collect::<Result<Vec<_>, _>>()?;
421 Ok(proof.retain_opened(&hashes, &keep))
422}
423
424fn opening(
427 db: &DatabaseConnection,
428 version: u32,
429 current: u32,
430 leaf: &KeptLeaf,
431 needs: &Needs,
432 creators: &BTreeMap<RowId, Option<coven_format::write_sources::ChangeId>>,
433) -> Result<Option<Vec<u8>>, DbError> {
434 let candidates = match leaf {
435 KeptLeaf::Known(leaf) => vec![leaf.clone()],
436 KeptLeaf::Digest { .. } => {
437 let mut candidates = Vec::new();
438 for (_, _, value, _) in &needs.cells {
439 candidates.extend(leaf.with_value(value)?);
440 }
441 candidates
442 }
443 KeptLeaf::Step => Vec::new(),
444 };
445 for candidate in candidates {
446 if proves(db, version, current, &candidate, needs, creators)? {
447 return Ok(Some(candidate.encode()?));
448 }
449 }
450 Ok(None)
451}
452
453pub(crate) struct AuthenticatedEvidence {
457 pub(crate) opened: WriteHeaderFrame,
458 pub(crate) leaves: Vec<Leaf>,
459 pub(crate) hashes: Vec<[u8; 32]>,
460 pub(crate) part: bool,
461}
462
463pub(crate) fn authenticate(
464 authorship: &dyn Authorship,
465 audience: &Audience,
466 evidence: &WriteEvidence,
467) -> Result<AuthenticatedEvidence, DbError> {
468 let opened = authorship.header(evidence)?;
469 if opened.header.position != evidence.write {
470 return Err(crate::snapshot_error::invalid("write evidence names another write"));
471 }
472 let prefix = coven_format::sealed_write::WriteObjectPrefix::decode(&evidence.prefix)?;
473 let part = opened.parts.iter().position(|part| part.audience == *audience);
474 let (leaves, hashes) = match part {
475 Some(index) => {
476 let hasher = authorship.hasher(audience, prefix.part_keys[index])?;
477 let (root, hashes) = evidence.proof.root_with_leaf_hashes(&hasher)?;
478 if root != evidence.roots[index] {
479 return Err(crate::snapshot_error::invalid(
480 "write evidence opening differs from its signed root",
481 ));
482 }
483 (evidence.proof.opened()?, hashes)
484 }
485 None if evidence.proof.leaves == 0 => (Vec::new(), Vec::new()),
486 None => {
487 return Err(crate::snapshot_error::invalid(
488 "write evidence opens a part the write does not have",
489 ))
490 }
491 };
492 Ok(AuthenticatedEvidence { opened, leaves, hashes, part: part.is_some() })
493}
494
495pub(crate) fn load(
499 db: &DatabaseConnection,
500 audience: &Audience,
501 evidence: &WriteEvidence,
502 verified: &AuthenticatedEvidence,
503) -> Result<(), DbError> {
504 crate::write_headers::retain(db, &verified.opened)?;
505 if !keeps_evidence(db, evidence.write)? {
506 return Ok(());
507 }
508 retain_header(
509 db,
510 evidence.write,
511 &HeaderEvidence {
512 prefix: evidence.prefix.clone(),
513 header: evidence.header.clone(),
514 roots: evidence.roots.clone(),
515 signature: evidence.signature,
516 },
517 )?;
518 if verified.part {
519 let hashes = hash_bytes(&verified.hashes);
520 db.internal_execute(
521 "INSERT INTO _coven_part_proofs(write_id,audience,proof,hashes) VALUES(?1,?2,?3,?4) ON CONFLICT(write_id,audience) DO UPDATE SET proof=excluded.proof,hashes=excluded.hashes",
522 params![
523 encoded(merge_fields::encode_write_id(&evidence.write))?,
524 audience,
525 encoded(merge_fields::encode_multiproof(&evidence.proof))?,
526 hashes
527 ],
528 )?;
529 }
530 Ok(())
531}
532
533pub(crate) fn check(
537 db: &DatabaseConnection,
538 schema: &WriteSchema,
539 audience: &Audience,
540 author: Author,
541 authenticated: &BTreeMap<WriteId, AuthenticatedEvidence>,
542) -> Result<(), DbError> {
543 let current = db.schema_version()?;
544 let refused = || {
545 crate::snapshot_error::invalid(
546 "snapshot names another member's authorship without its write's evidence",
547 )
548 };
549 let other = |write: WriteId| by_another_member(db, author, write);
550 let mut needs = BTreeMap::<WriteId, Needs>::new();
551 let mut creators = BTreeMap::new();
552 crate::ownership::visit::<DbError>(db, audience, |row, creation| {
553 if crate::ownership::owner_table(schema, &row.table) && other(creation.write)? {
554 let claim =
555 (creation.creator.write != creation.write).then(|| creation.creator.clone());
556 creators.insert(row.clone(), claim);
557 needs.entry(creation.write).or_default().creations.insert(row);
558 }
559 Ok(())
560 })?;
561 let loaded = BTreeSet::from([audience.clone()]);
563 let store = crate::merge_store::MergeStore::from_snapshot(db, &schema.schema, &loaded);
564 for declaration in schema.declarations.iter().filter(|d| d.owner_only) {
565 let rows = db.query(
566 "SELECT DISTINCT table_name,key,audience FROM _coven_rows WHERE audience=?1 AND table_name=?2",
567 params![audience, declaration.name],
568 crate::row_queries::read_identity,
569 )?;
570 for row in rows {
571 let state = store.row_without_losses(&row)?.state;
572 if !state.present() {
573 continue;
574 }
575 for (column, cell) in state.cells() {
576 if other(cell.write)? {
577 needs.entry(cell.write).or_default().cells.push((
578 row.clone(),
579 column.clone(),
580 cell.value.clone(),
581 state.generation(),
582 ));
583 }
584 }
585 }
586 }
587 for (write, needs) in needs {
588 let Some(verified) = authenticated.get(&write) else {
589 return Err(refused());
590 };
591 if verified.opened.header.disposition == WriteDisposition::Migration {
592 continue;
593 }
594 let version = verified.opened.header.schema_version;
595 let leaves = &verified.leaves;
596 for row in &needs.creations {
597 let single = Needs { creations: BTreeSet::from([row.clone()]), cells: Vec::new() };
598 let mut found = false;
599 for leaf in leaves {
600 found |= proves(db, version, current, leaf, &single, &creators)?;
601 }
602 if !found {
603 return Err(refused());
604 }
605 }
606 for cell in &needs.cells {
607 let single = Needs { creations: BTreeSet::new(), cells: vec![cell.clone()] };
608 let mut found = false;
609 for leaf in leaves {
610 found |= proves(db, version, current, leaf, &single, &creators)?;
611 }
612 if !found {
613 return Err(refused());
614 }
615 }
616 }
617 Ok(())
618}
619
620pub(crate) struct PartRoot {
623 hasher: AuthorshipHasher,
624 root: [u8; 32],
625 hashes: Vec<[u8; 32]>,
626}
627
628impl PartRoot {
629 pub(crate) fn new(hasher: AuthorshipHasher, root: [u8; 32]) -> Self {
630 Self { hasher, root, hashes: Vec::new() }
631 }
632
633 pub(crate) fn record(
635 &mut self,
636 record: coven_format::write::PartRecord<'_>,
637 ) -> Result<(), DbError> {
638 for leaf in coven_format::authorship::record_leaves(record) {
639 self.hashes.push(self.hasher.leaf(&leaf.encode()?));
640 }
641 Ok(())
642 }
643
644 pub(crate) fn finish(self) -> Result<Vec<[u8; 32]>, DbError> {
646 if coven_format::authorship::root_of_hashes(&self.hashes) != self.root {
647 return Err(crate::snapshot_error::invalid(
648 "write part differs from its signed authorship root",
649 ));
650 }
651 Ok(self.hashes)
652 }
653}
654
655#[cfg(test)]
656#[path = "authorship_tests.rs"]
657pub(crate) mod tests;