use std::collections::{BTreeMap, VecDeque};
use prikk_error::{PrikkError, Result};
use prikk_object::{
BlockPayload, ObjectEnvelope, ObjectId, ObjectType, RecognitionClaimPayload, Signature,
SignatureAlgorithm, SignerRole,
};
use crate::layout::RepositoryLayout;
use crate::object_store::ObjectReader;
use crate::trust::{MaintainerTrustPolicy, load_maintainer_trust_policy};
use crate::trust_index::read_current_trust_policy_snapshot;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContradictedField {
PatchIds,
ParentBlockIds,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecognitionClaimConsistency {
Consistent,
BlockAbsent,
Contradicted {
field: ContradictedField,
claimed: Vec<ObjectId>,
actual: Vec<ObjectId>,
},
}
pub fn check_recognition_claim_consistency(
object_store: &impl ObjectReader,
claim: &RecognitionClaimPayload,
) -> Result<RecognitionClaimConsistency> {
let Some(block_envelope) = object_store.read_typed(claim.block_id, ObjectType::Block)? else {
return Ok(RecognitionClaimConsistency::BlockAbsent);
};
let block_payload = BlockPayload::decode_canonical(&block_envelope.canonical_payload)?;
if block_payload.patch_ids != claim.patch_ids {
return Ok(RecognitionClaimConsistency::Contradicted {
field: ContradictedField::PatchIds,
claimed: claim.patch_ids.clone(),
actual: block_payload.patch_ids,
});
}
if block_payload.parent_block_ids != claim.parent_block_ids {
return Ok(RecognitionClaimConsistency::Contradicted {
field: ContradictedField::ParentBlockIds,
claimed: claim.parent_block_ids.clone(),
actual: block_payload.parent_block_ids,
});
}
Ok(RecognitionClaimConsistency::Consistent)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClaimSignatureVerification {
Sound {
key_id: String,
},
Unverifiable {
key_id: String,
},
}
pub(crate) fn verify_claim_signature(
envelope: &ObjectEnvelope,
trust_policy: &MaintainerTrustPolicy,
) -> Result<ClaimSignatureVerification> {
let claim_id = envelope.object_id();
let Some(signature) = envelope
.signatures
.iter()
.find(|signature| signature.signer_role == SignerRole::Maintainer)
else {
return Err(PrikkError::Integrity(format!(
"recognition claim {claim_id} carries no MAINTAINER signature -- a claim is, by \
definition, signed by the sender's maintainer key"
)));
};
if signature.algorithm != SignatureAlgorithm::Ed25519 {
return Err(PrikkError::InvalidSignature(format!(
"recognition claim {claim_id} MAINTAINER signature is not Ed25519"
)));
}
match trust_policy
.keys
.iter()
.find(|adopted| adopted.key_id == signature.key_id)
{
None => Ok(ClaimSignatureVerification::Unverifiable {
key_id: signature.key_id.clone(),
}),
Some(adopted) => {
let preimage = Signature::signed_bytes(
SignatureAlgorithm::Ed25519,
envelope.object_type,
claim_id,
SignerRole::Maintainer,
&signature.key_id,
)?;
if prikk_crypto::verify_ed25519(
&adopted.public_key,
&preimage,
&signature.signature_bytes,
)
.is_err()
{
return Err(PrikkError::InvalidSignature(format!(
"recognition claim {claim_id} MAINTAINER signature does not verify against \
adopted key {}",
signature.key_id
)));
}
Ok(ClaimSignatureVerification::Sound {
key_id: signature.key_id.clone(),
})
}
}
}
pub(crate) fn maintainer_trust_policy_or_empty(
layout: &RepositoryLayout,
) -> Result<MaintainerTrustPolicy> {
match read_current_trust_policy_snapshot(layout)? {
Some(_) => load_maintainer_trust_policy(layout),
None => Ok(MaintainerTrustPolicy { keys: Vec::new() }),
}
}
pub fn order_claims_for_sealing(
object_store: &impl ObjectReader,
claim_ids: &[ObjectId],
) -> Result<Vec<ObjectId>> {
let mut claims: BTreeMap<ObjectId, RecognitionClaimPayload> = BTreeMap::new();
for &claim_id in claim_ids {
let envelope = object_store
.read_typed(claim_id, ObjectType::RecognitionClaim)?
.ok_or_else(|| {
PrikkError::Integrity(format!("recognition claim {claim_id} does not exist"))
})?;
let payload = RecognitionClaimPayload::decode_canonical(&envelope.canonical_payload)?;
claims.insert(claim_id, payload);
}
let mut block_to_claim: BTreeMap<ObjectId, ObjectId> = BTreeMap::new();
for (&claim_id, payload) in &claims {
if let Some(&existing) = block_to_claim.get(&payload.block_id) {
return Err(PrikkError::Integrity(format!(
"claims {existing} and {claim_id} both name block {} -- refusing to order an \
ambiguous batch",
payload.block_id
)));
}
block_to_claim.insert(payload.block_id, claim_id);
}
let mut remaining_parents: BTreeMap<ObjectId, usize> = BTreeMap::new();
let mut children: BTreeMap<ObjectId, Vec<ObjectId>> = BTreeMap::new();
for (&claim_id, payload) in &claims {
let count = payload
.parent_block_ids
.iter()
.filter(|parent_block_id| block_to_claim.contains_key(*parent_block_id))
.count();
remaining_parents.insert(claim_id, count);
for parent_block_id in &payload.parent_block_ids {
if let Some(&parent_claim_id) = block_to_claim.get(parent_block_id) {
children.entry(parent_claim_id).or_default().push(claim_id);
}
}
}
let mut ready: Vec<ObjectId> = remaining_parents
.iter()
.filter(|(_, count)| **count == 0)
.map(|(id, _)| *id)
.collect();
ready.sort_unstable();
let mut queue: VecDeque<ObjectId> = ready.into();
let mut order = Vec::with_capacity(claims.len());
while let Some(claim_id) = queue.pop_front() {
order.push(claim_id);
for &child in children.get(&claim_id).into_iter().flatten() {
let entry = remaining_parents.get_mut(&child).ok_or_else(|| {
PrikkError::Integrity(
"claim ordering lost a tracked child -- internal inconsistency".to_string(),
)
})?;
*entry -= 1;
if *entry == 0 {
queue.push_back(child);
}
}
}
if order.len() != claims.len() {
let stuck: Vec<String> = remaining_parents
.keys()
.filter(|id| !order.contains(id))
.map(ObjectId::to_string)
.collect();
return Err(PrikkError::Integrity(format!(
"recognition claims for sealing contain a cycle -- refusing to order: {}",
stuck.join(", ")
)));
}
Ok(order)
}
#[cfg(test)]
mod tests;