use crate::parsed_coz::VerifiedCoz;
use crate::state::{AuthRoot, PrincipalRoot, StateRoot, TaggedCzd};
#[derive(Debug, Clone)]
pub struct Commit {
pub(crate) transactions: Vec<crate::transaction::Transaction>,
pub(crate) commit_tx: crate::transaction::CommitTransaction,
tr: crate::transaction_root::TransactionRoot,
ar: AuthRoot,
sr: StateRoot,
pr: PrincipalRoot,
}
impl Commit {
pub(crate) fn new(
transactions: Vec<crate::transaction::Transaction>,
commit_tx: crate::transaction::CommitTransaction,
tr: crate::transaction_root::TransactionRoot,
ar: AuthRoot,
sr: StateRoot,
pr: PrincipalRoot,
) -> crate::error::Result<Self> {
if transactions.is_empty() && commit_tx.0.is_empty() {
return Err(crate::error::Error::EmptyCommit);
}
Ok(Self {
transactions,
commit_tx,
tr,
ar,
sr,
pr,
})
}
pub fn transactions(&self) -> &[crate::transaction::Transaction] {
&self.transactions
}
pub fn commit_tx(&self) -> &crate::transaction::CommitTransaction {
&self.commit_tx
}
pub fn all_cozies(&self) -> Vec<VerifiedCoz> {
self.iter_all_cozies().cloned().collect()
}
pub fn iter_all_cozies(&self) -> impl Iterator<Item = &VerifiedCoz> {
self.transactions
.iter()
.flat_map(|tx| tx.0.iter())
.chain(self.commit_tx.0.iter())
}
pub fn tr(&self) -> &crate::transaction_root::TransactionRoot {
&self.tr
}
pub fn sr(&self) -> &StateRoot {
&self.sr
}
pub fn auth_root(&self) -> &AuthRoot {
&self.ar
}
pub fn pr(&self) -> &PrincipalRoot {
&self.pr
}
pub fn len(&self) -> usize {
self.iter_all_cozies().count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Default)]
pub struct PendingCommit {
pub(crate) transactions: Vec<crate::transaction::Transaction>,
pub(crate) commit_tx: Option<crate::transaction::CommitTransaction>,
}
impl PendingCommit {
pub fn new() -> Self {
Self::default()
}
pub fn push_tx(&mut self, tx: crate::transaction::Transaction) {
if tx.0.is_empty() {
return;
}
let is_commit = tx.0.iter().any(|cz| cz.arrow().is_some());
if is_commit {
match &mut self.commit_tx {
Some(ctx) => ctx.0.extend(tx.0),
None => self.commit_tx = Some(crate::transaction::CommitTransaction(tx.0)),
}
} else {
self.transactions.push(tx);
}
}
pub fn transactions(&self) -> &[crate::transaction::Transaction] {
&self.transactions
}
pub fn commit_tx(&self) -> Option<&crate::transaction::CommitTransaction> {
self.commit_tx.as_ref()
}
pub fn all_cozies(&self) -> Vec<VerifiedCoz> {
self.iter_all_cozies().cloned().collect()
}
pub fn iter_all_cozies(&self) -> impl Iterator<Item = &VerifiedCoz> {
self.transactions
.iter()
.flat_map(|tx| tx.0.iter())
.chain(self.commit_tx.iter().flat_map(|ctx| ctx.0.iter()))
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn len(&self) -> usize {
self.iter_all_cozies().count()
}
pub fn compute_roots(
&self,
algs: &[coz::HashAlg],
) -> (
Option<crate::transaction_root::TransactionMutationRoot>,
Option<crate::transaction_root::TransactionCommitRoot>,
Option<crate::transaction_root::TransactionRoot>,
) {
if self.is_empty() {
return (None, None, None);
}
let mut tx_roots = Vec::new();
for tx in &self.transactions {
let tx_czds: Vec<TaggedCzd<'_>> =
tx.0.iter()
.map(|t| TaggedCzd::new(t.czd(), t.hash_alg()))
.collect();
if let Some(mh) = crate::transaction_root::compute_tx(&tx_czds, algs) {
tx_roots.push(mh);
}
}
let tx_refs: Vec<&crate::multihash::MultihashDigest> = tx_roots.iter().collect();
let tmr = crate::transaction_root::compute_tmr(&tx_refs, algs);
if let Some(ctx) = &self.commit_tx {
let ctx_czds: Vec<TaggedCzd<'_>> = ctx
.0
.iter()
.map(|t| TaggedCzd::new(t.czd(), t.hash_alg()))
.collect();
if let Some(tcr) = crate::transaction_root::compute_tcr(&ctx_czds, algs) {
let tr = crate::transaction_root::compute_tr(tmr.as_ref(), &tcr, algs);
return (tmr, Some(tcr), tr);
}
}
(tmr, None, None)
}
pub fn compute_tr(
&self,
algs: &[coz::HashAlg],
) -> Option<crate::transaction_root::TransactionRoot> {
self.compute_roots(algs).2
}
pub fn finalize(
self,
ar: AuthRoot,
sr: StateRoot,
pr: PrincipalRoot,
tx_algs: &[coz::HashAlg],
) -> crate::error::Result<Commit> {
if self.is_empty() {
return Err(crate::error::Error::EmptyCommit);
}
let commit_tx = self
.commit_tx
.clone()
.ok_or(crate::error::Error::MalformedPayload)?;
let tr = self
.compute_tr(tx_algs)
.ok_or(crate::error::Error::EmptyCommit)?;
Commit::new(self.transactions, commit_tx, tr, ar, sr, pr)
}
pub fn into_transactions(self) -> Vec<VerifiedCoz> {
self.iter_all_cozies().cloned().collect()
}
}
#[must_use = "a CommitScope must be finalized via .finalize() to produce a Commit"]
pub struct CommitScope<'a> {
principal: &'a mut crate::principal::Principal,
pending: PendingCommit,
pre_commit_keys: std::collections::BTreeMap<String, crate::key::Key>,
}
impl<'a> CommitScope<'a> {
pub(crate) fn new(principal: &'a mut crate::principal::Principal) -> Self {
let _hash_alg = principal.hash_alg();
let pre_commit_keys: std::collections::BTreeMap<String, crate::key::Key> = principal
.active_keys()
.map(|k| (k.tmb.to_b64(), k.clone()))
.collect();
Self {
principal,
pending: PendingCommit::new(),
pre_commit_keys,
}
}
pub fn apply(&mut self, vtx: VerifiedCoz) -> crate::error::Result<()> {
self.principal.apply_verified_internal(vtx.clone())?;
self.pending
.push_tx(crate::transaction::Transaction(vec![vtx]));
Ok(())
}
pub fn apply_tx(&mut self, vts: Vec<VerifiedCoz>) -> crate::error::Result<()> {
let mut tx = Vec::with_capacity(vts.len());
for vt in vts {
self.principal.apply_verified_internal(vt.clone())?;
tx.push(vt);
}
self.pending.push_tx(crate::transaction::Transaction(tx));
Ok(())
}
pub fn finalize(self) -> crate::error::Result<&'a Commit> {
self.principal.finalize_commit(self.pending)
}
pub fn verify_and_apply(
&mut self,
pay_json: &[u8],
sig: &[u8],
czd: coz::Czd,
new_key: Option<crate::key::Key>,
) -> crate::error::Result<()> {
use crate::parsed_coz::verify_coz;
let pay: coz::Pay =
serde_json::from_slice(pay_json).map_err(|_| crate::error::Error::MalformedPayload)?;
let signer_tmb = pay
.tmb
.as_ref()
.ok_or(crate::error::Error::MalformedPayload)?;
let tmb_str = signer_tmb.to_b64();
let signer_key = if let Some(key) = self.pre_commit_keys.get(&tmb_str) {
key
} else if self.principal.is_key_active(signer_tmb) {
self.principal
.get_key(signer_tmb)
.ok_or(crate::error::Error::UnknownKey)?
} else if self.principal.is_key_revoked(signer_tmb) {
return Err(crate::error::Error::KeyRevoked);
} else {
return Err(crate::error::Error::UnknownKey);
};
let vtx = verify_coz(pay_json, sig, signer_key, czd, new_key)?;
self.apply(vtx)
}
pub fn principal_hash_alg(&self) -> crate::state::HashAlg {
self.principal.hash_alg()
}
pub fn len(&self) -> usize {
self.pending.len()
}
pub fn is_empty(&self) -> bool {
self.pending.is_empty()
}
pub fn finalize_with_arrow(
mut self,
alg: &str,
prv_key: &[u8],
pub_key: &[u8],
tmb: &coz::Thumbprint,
now: i64,
authority: &str,
) -> crate::error::Result<&'a Commit> {
use crate::parsed_coz::{ParsedCoz, VerifiedCoz};
use crate::state::{hash_alg_from_str, hash_sorted_concat_bytes};
use coz::base64ct::{Base64UrlUnpadded, Encoding};
use serde_json::json;
if self.is_empty() {
return Err(crate::error::Error::EmptyCommit);
}
let signer_hash_alg = hash_alg_from_str(alg)?;
let key_refs: Vec<&crate::key::Key> = self.principal.auth.keys.values().collect();
let active_algs = crate::state::derive_hash_algs(&key_refs);
let thumbprints: Vec<&coz::Thumbprint> =
self.principal.auth.keys.values().map(|k| &k.tmb).collect();
let (_kr, _ar, sr) = crate::state::derive_auth_state(
&thumbprints,
self.principal.dr.as_ref(),
&active_algs,
)?;
let (tmr, _, _) = self.pending.compute_roots(&[signer_hash_alg]);
let tmr = tmr.ok_or(crate::error::Error::EmptyCommit)?;
let pre = &self.principal.pr;
let pre_bytes = pre.0.get_or_err(signer_hash_alg)?;
let sr_bytes = sr.0.get_or_err(signer_hash_alg)?;
let tmr_bytes = tmr.0.get_or_err(signer_hash_alg)?;
let arrow_digest =
hash_sorted_concat_bytes(signer_hash_alg, &[pre_bytes, sr_bytes, tmr_bytes]);
let arrow_tagged = format!(
"{}:{}",
signer_hash_alg,
Base64UrlUnpadded::encode_string(&arrow_digest)
);
let commit_typ = format!("{authority}/{}", crate::parsed_coz::typ::COMMIT_CREATE);
let mut pay = serde_json::Map::new();
pay.insert("alg".to_string(), json!(alg));
pay.insert("arrow".to_string(), json!(arrow_tagged));
pay.insert("now".to_string(), json!(now));
pay.insert("tmb".to_string(), json!(tmb.to_b64()));
pay.insert("typ".to_string(), json!(commit_typ));
let mut pay_obj = serde_json::Value::Object(pay);
if let Some(obj) = pay_obj.as_object_mut() {
obj.sort_keys();
}
let pay_vec =
serde_json::to_vec(&pay_obj).map_err(|_| crate::error::Error::MalformedPayload)?;
let (sig, cad) = coz::sign_json(&pay_vec, alg, prv_key, pub_key)
.ok_or(crate::error::Error::MalformedPayload)?;
let czd = coz::czd_for_alg(&cad, &sig, alg).ok_or(crate::error::Error::MalformedPayload)?;
let raw = coz::CozJson {
pay: pay_obj.clone(),
sig: sig.clone(),
};
let parsed_pay: coz::Pay = serde_json::from_value(pay_obj.clone())
.map_err(|_| crate::error::Error::MalformedPayload)?;
let arrow_tx = ParsedCoz::from_pay(&parsed_pay, czd, signer_hash_alg, raw)?;
let arrow_vtx = VerifiedCoz::from_parts(arrow_tx, None);
self.pending
.push_tx(crate::transaction::Transaction(vec![arrow_vtx]));
self.finalize()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::multihash::MultihashDigest;
use crate::parsed_coz::{ParsedCoz, VerifiedCoz};
use crate::state::HashAlg;
use coz::{Czd, PayBuilder, Thumbprint};
use serde_json::json;
const TEST_PRE: &str = "SHA-256:U5XUZots-WmQYcQWmsO751Xk0yeVi9XUKWQ2mGz6Aqg";
const TEST_ID: &str = "xrYMu87EXes58PnEACcDW1t0jF2ez4FCN-njTF0MHNo";
fn make_test_tx(is_commit: bool, czd_byte: u8) -> VerifiedCoz {
let typ = if is_commit {
"cyphr.me/cyphr/commit/create"
} else {
"cyphr.me/cyphr/key/create"
};
let mut pay = PayBuilder::new()
.typ(typ)
.alg("ES256")
.now(1000)
.tmb(Thumbprint::from_bytes(vec![0xAA; 32]))
.build();
if !is_commit {
pay.extra.insert("pre".into(), json!(TEST_PRE));
pay.extra.insert("id".into(), json!(TEST_ID));
}
if is_commit {
pay.extra.insert(
"arrow".into(),
json!("SHA-256:U5XUZots-WmQYcQWmsO751Xk0yeVi9XUKWQ2mGz6Aqg"),
);
}
let czd = Czd::from_bytes(vec![czd_byte; 32]);
let raw = coz::CozJson {
pay: serde_json::to_value(&pay).unwrap(),
sig: vec![0; 64],
};
let cz = ParsedCoz::from_pay(&pay, czd, HashAlg::Sha256, raw).unwrap();
VerifiedCoz::from_transaction_unsafe(cz, None)
}
#[test]
fn pending_commit_empty_state() {
let pending = PendingCommit::new();
assert!(pending.is_empty());
assert_eq!(pending.len(), 0);
assert!(pending.compute_tr(&[coz::HashAlg::Sha256]).is_none());
}
#[test]
fn pending_commit_push_adds_transactions() {
let mut pending = PendingCommit::new();
let tx1 = make_test_tx(false, 0x01);
pending.push_tx(crate::transaction::Transaction(vec![tx1]));
assert_eq!(pending.len(), 1);
let tx2 = make_test_tx(true, 0x02);
pending.push_tx(crate::transaction::Transaction(vec![tx2]));
assert_eq!(pending.len(), 2);
}
#[test]
fn pending_commit_compute_tr_returns_merkle_root() {
let mut pending = PendingCommit::new();
let tx1 = make_test_tx(false, 0x01);
pending
.transactions
.push(crate::transaction::Transaction(vec![tx1.clone()]));
let ctx = crate::transaction::CommitTransaction(vec![tx1]);
pending.commit_tx = Some(ctx);
let tr = pending.compute_tr(&[coz::HashAlg::Sha256]);
assert!(tr.is_some());
assert_eq!(
tr.clone().unwrap().0.get(HashAlg::Sha256).unwrap().len(),
32
);
}
#[test]
fn pending_commit_finalize_succeeds_with_finalizer() {
let mut pending = PendingCommit::new();
let cz = make_test_tx(true, 0x01);
pending.push_tx(crate::transaction::Transaction(vec![cz]));
let auth_root = AuthRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xAA; 32],
));
let sr = StateRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xCC; 32],
));
let ps = PrincipalRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xBB; 32],
));
let commit = pending.finalize(
auth_root.clone(),
sr.clone(),
ps.clone(),
&[coz::HashAlg::Sha256],
);
assert!(commit.is_ok());
let commit = commit.unwrap();
assert_eq!(commit.len(), 1);
assert_eq!(commit.auth_root(), &auth_root);
assert_eq!(commit.sr(), &sr);
assert_eq!(commit.pr(), &ps);
}
#[test]
fn pending_commit_finalize_fails_without_finalizer_marker() {
let mut pending = PendingCommit::new();
let cz = make_test_tx(false, 0x01); pending.push_tx(crate::transaction::Transaction(vec![cz]));
let auth_root = AuthRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xAA; 32],
));
let sr = StateRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xCC; 32],
));
let ps = PrincipalRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xBB; 32],
));
let result = pending.finalize(auth_root, sr, ps, &[coz::HashAlg::Sha256]);
assert!(
matches!(result, Err(crate::error::Error::MalformedPayload)),
"finalize should fail without finalizer marker"
);
}
#[test]
fn pending_commit_finalize_fails_when_empty() {
let pending = PendingCommit::new();
let auth_root = AuthRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xAA; 32],
));
let sr = StateRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xCC; 32],
));
let ps = PrincipalRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xBB; 32],
));
let result = pending.finalize(auth_root, sr, ps, &[coz::HashAlg::Sha256]);
assert!(result.is_err(), "should fail when empty");
}
#[test]
fn pending_commit_into_transactions_returns_accumulated() {
let mut pending = PendingCommit::new();
pending.push_tx(crate::transaction::Transaction(vec![make_test_tx(
false, 0x01,
)]));
pending.push_tx(crate::transaction::Transaction(vec![make_test_tx(
true, 0x02,
)]));
let cozies = pending.into_transactions();
assert_eq!(cozies.len(), 2);
}
#[test]
fn commit_accessors_return_correct_values() {
let mut pending = PendingCommit::new();
pending.push_tx(crate::transaction::Transaction(vec![make_test_tx(
true, 0x01,
)]));
let auth_root = AuthRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xAA; 32],
));
let sr = StateRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xCC; 32],
));
let ps = PrincipalRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xBB; 32],
));
let commit = pending
.finalize(
auth_root.clone(),
sr.clone(),
ps.clone(),
&[coz::HashAlg::Sha256],
)
.unwrap();
assert_eq!(commit.iter_all_cozies().count(), 1);
assert!(!commit.is_empty());
assert_eq!(commit.len(), 1);
assert_eq!(commit.auth_root(), &auth_root);
assert_eq!(commit.sr(), &sr);
assert_eq!(commit.pr(), &ps);
assert_eq!(commit.tr().0.get(HashAlg::Sha256).unwrap().len(), 32);
}
#[test]
fn commit_multi_transaction_computes_correct_tr() {
let mut pending = PendingCommit::new();
pending.push_tx(crate::transaction::Transaction(vec![make_test_tx(
false, 0x01,
)]));
pending.push_tx(crate::transaction::Transaction(vec![make_test_tx(
false, 0x02,
)]));
pending.push_tx(crate::transaction::Transaction(vec![make_test_tx(
true, 0x03,
)]));
let auth_root = AuthRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xAA; 32],
));
let sr = StateRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xCC; 32],
));
let ps = PrincipalRoot(MultihashDigest::from_single(
HashAlg::Sha256,
vec![0xBB; 32],
));
let commit = pending
.finalize(auth_root, sr, ps, &[coz::HashAlg::Sha256])
.unwrap();
assert_eq!(commit.len(), 3);
let cid = &commit.tr().0;
assert_eq!(cid.get(HashAlg::Sha256).unwrap().len(), 32);
}
#[test]
fn test_cozjson_serialization() {
let mut pay = json!({"typ": "test", "now": 1234});
pay.as_object_mut()
.unwrap()
.insert("commit".to_string(), json!("SHA-256:abc"));
let raw = coz::CozJson {
pay: pay.clone(),
sig: vec![0, 1, 2],
};
let out = serde_json::to_string(&raw).unwrap();
assert!(
out.contains("commit"),
"coz::CozJson serialization dropped 'commit'! Output: {}",
out
);
}
}