diff --git a/knot2/crates/knot-cob/src/lib.rs b/knot2/crates/knot-cob/src/lib.rs
index 578df86aa..75454b3da 100644
--- a/knot2/crates/knot-cob/src/lib.rs
+++ b/knot2/crates/knot-cob/src/lib.rs
@@ -6,17 +6,22 @@ mod graph;
pub mod instrument;
mod object;
-pub use change::{Change, ChangePayload, CobHome, Payload};
-pub use error::{CobError, PayloadError};
+pub use change::{Change, ChangePayload, CobHome, Emits, Payload, Record, decode_payload};
+pub use error::{AddressMove, CobError, PayloadError};
pub use graph::{ChangeGraph, History};
pub use knot_types::{ActorId, ChangeId, CobId, TypeName};
-pub use object::{Checkpoint, Evaluate, HistoryModel, Object, SnapshotStride, StateSize};
+pub use object::{
+ Append, Authored, Authorize, Checkpoint, Evaluate, HistoryModel, KnotAuthored, Object,
+ SnapshotStride, StateSize, WriteContext,
+};
pub use backend::parse_cob_ref;
-use knot_git::{RefUpdate, Repo};
+use std::collections::BTreeSet;
+
+use knot_git::{Haves, RefUpdate, Repo, Wants};
use knot_runtime::Signer;
-use knot_types::{Oid, UnixSeconds};
+use knot_types::{Decision, Oid, RecordAddress, UnixSeconds};
use serde::Serialize;
use serde::de::DeserializeOwned;
@@ -40,16 +45,42 @@ pub struct Created {
#[derive(Debug, Clone)]
pub struct Materialized
{
pub change: P,
- pub record: Option>,
+ pub record: Record,
}
impl Materialized
{
pub fn bare(change: P) -> Self {
Self {
change,
- record: None,
+ record: Record::Bare,
+ }
+ }
+}
+
+pub struct Approved<'cx, C> {
+ cx: &'cx WriteContext<'cx>,
+ materialized: Materialized,
+}
+
+impl<'cx, C: Authored> Approved<'cx, C> {
+ pub fn signed_by(
+ cx: &'cx WriteContext<'cx>,
+ materialized: Materialized,
+ ) -> Result {
+ let author = materialized.change.author();
+ if author == cx.actor() {
+ Ok(Self { cx, materialized })
+ } else {
+ Err(CobError::Impersonation {
+ actor: cx.actor().clone(),
+ author: author.clone(),
+ })
}
}
+
+ pub const fn context(&self) -> &'cx WriteContext<'cx> {
+ self.cx
+ }
}
#[derive(Debug)]
@@ -58,7 +89,7 @@ pub struct Delta {
pub tip: ChangeId,
}
-const CHECKPOINT_FORMAT: u16 = 4;
+const CHECKPOINT_FORMAT: u16 = 5;
#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct CheckpointDigest(Oid);
@@ -113,7 +144,7 @@ impl<'r> CobStore<'r> {
self.repo
}
- pub fn create(
+ pub fn create(
&self,
home: &CobHome,
payload: &P,
@@ -121,10 +152,28 @@ impl<'r> CobStore<'r> {
timestamp: UnixSeconds,
) -> Result {
let bytes = payload.encode()?;
- self.create_from_bytes(home, &P::type_name(), &bytes, None, signer, timestamp)
+ Self::check_payload(payload, &Record::Bare, timestamp)?;
+ self.create_from_bytes(
+ home,
+ &P::type_name(),
+ &bytes,
+ &Record::Bare,
+ signer,
+ timestamp,
+ )
}
- pub fn create_materialized(
+ pub fn create_materialized(
+ &self,
+ home: &CobHome,
+ payload: &Materialized,
+ signer: &dyn Signer,
+ timestamp: UnixSeconds,
+ ) -> Result {
+ self.open(home, payload, signer, timestamp)
+ }
+
+ fn open(
&self,
home: &CobHome,
payload: &Materialized,
@@ -132,22 +181,80 @@ impl<'r> CobStore<'r> {
timestamp: UnixSeconds,
) -> Result {
let bytes = payload.change.encode()?;
+ Self::check_payload(&payload.change, &payload.record, timestamp)?;
self.create_from_bytes(
home,
&P::type_name(),
&bytes,
- payload.record.as_deref(),
+ &payload.record,
signer,
timestamp,
)
}
+ pub fn create_approved(
+ &self,
+ home: &CobHome,
+ approved: &Approved<'_, E::Change>,
+ signer: &dyn Signer,
+ timestamp: UnixSeconds,
+ ) -> Result
+ where
+ E: Authorize,
+ E::Change: ChangePayload + Authored,
+ {
+ let cx = approved.context();
+ match E::may_create(cx) {
+ Decision::Allow => self.open(home, &approved.materialized, signer, timestamp),
+ Decision::Deny => Err(CobError::CreateRejected {
+ actor: cx.actor().clone(),
+ type_name: E::Change::type_name(),
+ permission: cx.permission(),
+ }),
+ }
+ }
+
+ pub fn append_approved(
+ &self,
+ home: &CobHome,
+ object: CobId,
+ approved: &Approved<'_, E::Change>,
+ signer: &dyn Signer,
+ timestamp: UnixSeconds,
+ ) -> Result
+ where
+ E: Authorize + Checkpoint,
+ E::State: Serialize + DeserializeOwned,
+ E::Change: ChangePayload + Authored + Clone,
+ {
+ let cx = approved.context();
+ self.retrying_append::(home, object, signer, timestamp, |state| {
+ match E::may_append(cx, state, &approved.materialized.change) {
+ Append::Allow => Ok(Some(approved.materialized.clone())),
+ Append::Deny => Err(CobError::AppendRejected {
+ actor: cx.actor().clone(),
+ object,
+ permission: cx.permission(),
+ }),
+ Append::Stale { current } => Err(CobError::StaleRecord {
+ object,
+ uri: self
+ .root_address::(object)?
+ .ok_or(CobError::Unaddressed { object })?
+ .at_uri(cx.repo()),
+ current,
+ }),
+ }
+ })
+ .map(|tip| tip.expect("Approved append always yields its change"))
+ }
+
fn create_from_bytes(
&self,
home: &CobHome,
type_name: &TypeName,
bytes: &[u8],
- record: Option<&[u8]>,
+ record: &Record,
signer: &dyn Signer,
timestamp: UnixSeconds,
) -> Result {
@@ -164,14 +271,24 @@ impl<'r> CobStore<'r> {
)?;
let object = CobId::new(tip.oid());
let name = backend::cob_ref_name(type_name, object)?;
- self.repo.update_ref(&RefUpdate::Create {
- name,
- new: tip.oid(),
- })?;
- Ok(Created { object, tip })
+ match backend::resolve_tip(self.repo, type_name, object)? {
+ Some(current) if current == tip.oid() => Ok(Created { object, tip }),
+ Some(_) => Err(CobError::StaleTip {
+ object,
+ expected: tip,
+ }),
+ None => self
+ .repo
+ .update_ref(&RefUpdate::Create {
+ name,
+ new: tip.oid(),
+ })
+ .map(|()| Created { object, tip })
+ .map_err(CobError::from),
+ }
}
- pub fn update(
+ pub fn update(
&self,
home: &CobHome,
object: CobId,
@@ -188,7 +305,7 @@ impl<'r> CobStore<'r> {
)
}
- pub fn extend<'p, P: ChangePayload + 'p>(
+ pub fn extend<'p, P: KnotAuthored + 'p>(
&self,
home: &CobHome,
object: CobId,
@@ -206,7 +323,7 @@ impl<'r> CobStore<'r> {
expected,
changes
.into_iter()
- .map(|(payload, timestamp)| (payload, None, timestamp)),
+ .map(|(payload, timestamp)| (payload, &Record::Bare, timestamp)),
signer,
)
}
@@ -227,7 +344,7 @@ impl<'r> CobStore<'r> {
object,
type_name,
expected,
- std::iter::once((payload, None, timestamp)),
+ std::iter::once((payload, &Record::Bare, timestamp)),
signer,
)
.map(|tip| tip.expect("one change chains onto one tip"))
@@ -249,19 +366,96 @@ impl<'r> CobStore<'r> {
object,
type_name,
expected,
- std::iter::once((&payload.change, payload.record.as_deref(), timestamp)),
+ std::iter::once((&payload.change, &payload.record, timestamp)),
signer,
)
.map(|tip| tip.expect("one change chains onto one tip"))
}
+ fn check_payload(
+ payload: &P,
+ record: &Record,
+ timestamp: UnixSeconds,
+ ) -> Result<(), CobError> {
+ Self::check_collection(payload)?;
+ Self::check_stamp(payload, timestamp)?;
+ Self::check_claim(payload, record)
+ }
+
+ fn check_collection(payload: &P) -> Result<(), CobError> {
+ match (payload.address(), P::collection()) {
+ (Some(found), Some(collection)) if *found.collection() != collection => {
+ Err(CobError::AddressForeign {
+ type_name: P::type_name(),
+ found,
+ })
+ }
+ _ => Ok(()),
+ }
+ }
+
+ fn check_stamp(payload: &P, timestamp: UnixSeconds) -> Result<(), CobError> {
+ match payload.stamped_at() {
+ Some(stamped) if stamped != timestamp => Err(CobError::StampMoved {
+ stamped,
+ signed: timestamp,
+ }),
+ _ => Ok(()),
+ }
+ }
+
+ fn check_claim(payload: &P, record: &Record) -> Result<(), CobError> {
+ match (payload.emits(), record) {
+ (Emits::Version(declared), Record::Version(body)) => {
+ let found = body.cid();
+ if found == declared {
+ Ok(())
+ } else {
+ Err(CobError::RecordMisclaimed { declared, found })
+ }
+ }
+ (Emits::Version(declared), Record::Bare | Record::Tombstone) => {
+ Err(CobError::RecordUnwritten { declared })
+ }
+ (Emits::Tombstone, Record::Tombstone) => Ok(()),
+ (Emits::Tombstone, Record::Bare | Record::Version(_)) => {
+ Err(CobError::TombstoneUnwritten)
+ }
+ (Emits::Unclaimed, Record::Tombstone) => Err(CobError::TombstoneUnclaimed),
+ (Emits::Unclaimed, Record::Bare | Record::Version(_)) => Ok(()),
+ }
+ }
+
+ fn root_address(
+ &self,
+ object: CobId,
+ ) -> Result