#![allow(clippy::too_long_first_doc_paragraph)]
use polyc_crypto::approval::ApprovalSigner;
use polyc_mmr::{SignedRoot, VerifiableLog, verify_root_signature};
use crate::Event;
pub const MMR_SIGNED_ROOT_KIND: &str = "__mmr_signed_root__";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum IntegrityError {
#[error("mmr: {0}")]
Mmr(#[from] polyc_mmr::MmrError),
#[error("malformed signed-root marker at leaf count {leaf_count_hint}: {source}")]
MalformedRoot {
leaf_count_hint: u64,
source: serde_json::Error,
},
#[error("signed root at leaf count {leaf_count} does not verify under the expected signer")]
SignatureInvalid {
leaf_count: u64,
},
#[error(
"integrity violation: at leaf count {leaf_count}, replay computed root {computed_root_hex} \
but the signed marker recorded {expected_root_hex}"
)]
RootMismatch {
leaf_count: u64,
expected_root_hex: String,
computed_root_hex: String,
},
}
pub fn extend_and_sign(
log: &VerifiableLog,
new_events: &[Event],
signer: &ApprovalSigner,
) -> Result<Event, IntegrityError> {
for event in new_events {
log.append(&event.kind, &event.payload)?;
}
let root = log.sign_root(signer)?;
let payload = serde_json::to_vec(&root).expect("SignedRoot serializes");
Ok(Event::new(MMR_SIGNED_ROOT_KIND, payload))
}
pub fn rebuild_from_events(events: &[Event]) -> Result<VerifiableLog, IntegrityError> {
let log = VerifiableLog::rebuild(
events
.iter()
.filter(|e| e.kind != MMR_SIGNED_ROOT_KIND)
.map(|e| (e.kind.as_str(), e.payload.as_slice())),
)?;
Ok(log)
}
pub fn verify_replay(events: &[Event], expected_signer_pk_hex: &str) -> Result<(), IntegrityError> {
let log = VerifiableLog::new();
for event in events {
if event.kind == MMR_SIGNED_ROOT_KIND {
let leaf_count = log.leaf_count()?;
let root: SignedRoot = serde_json::from_slice(&event.payload).map_err(|source| {
IntegrityError::MalformedRoot {
leaf_count_hint: leaf_count,
source,
}
})?;
let sig_ok = verify_root_signature(&root, expected_signer_pk_hex).unwrap_or(false);
if !sig_ok {
return Err(IntegrityError::SignatureInvalid { leaf_count });
}
let computed_root = log.root()?;
let computed_root_hex = hex::encode(computed_root.as_ref());
if root.leaf_count != leaf_count || root.root_hex != computed_root_hex {
return Err(IntegrityError::RootMismatch {
leaf_count,
expected_root_hex: root.root_hex,
computed_root_hex,
});
}
} else {
log.append(&event.kind, &event.payload)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
fn signer() -> ApprovalSigner {
ApprovalSigner::from_seed(7)
}
fn pk_hex(signer: &ApprovalSigner) -> String {
hex::encode(signer.public_key_bytes())
}
#[test]
fn mmr_verify_replay_detects_tampered_event() {
let log = VerifiableLog::new();
let s = signer();
let turn_events = vec![
Event::new("user_msg", b"what is 2+2?".to_vec()),
Event::new("output_msg", b"4".to_vec()),
];
let marker = extend_and_sign(&log, &turn_events, &s).expect("sign");
let mut persisted = turn_events.clone();
persisted.push(marker);
verify_replay(&persisted, &pk_hex(&s)).expect("untampered replay must verify");
persisted[1].payload[0] ^= 0xFF;
let err = verify_replay(&persisted, &pk_hex(&s))
.expect_err("tampered replay must report an integrity violation");
assert!(
matches!(err, IntegrityError::RootMismatch { .. }),
"expected a root mismatch, got {err:?}"
);
}
#[test]
fn mmr_verify_replay_accepts_multi_turn_untampered_log() {
let log = VerifiableLog::new();
let s = signer();
let mut persisted = Vec::new();
for turn in 0..3u8 {
let events = vec![
Event::new("user_msg", vec![turn]),
Event::new("output_msg", vec![turn, turn]),
];
let marker = extend_and_sign(&log, &events, &s).expect("sign");
persisted.extend(events);
persisted.push(marker);
}
verify_replay(&persisted, &pk_hex(&s)).expect("three untampered turns must verify");
}
#[test]
fn mmr_verify_replay_rejects_root_signed_by_a_different_key() {
let log = VerifiableLog::new();
let s = signer();
let events = vec![Event::new("user_msg", b"hi".to_vec())];
let marker = extend_and_sign(&log, &events, &s).expect("sign");
let mut persisted = events;
persisted.push(marker);
let other = ApprovalSigner::from_seed(999);
let err = verify_replay(&persisted, &pk_hex(&other))
.expect_err("a root signed under a different key must not verify");
assert!(matches!(err, IntegrityError::SignatureInvalid { .. }));
}
#[test]
fn mmr_verify_replay_accepts_partition_with_no_signed_roots_yet() {
let events = vec![Event::new("user_msg", b"no marker yet".to_vec())];
verify_replay(&events, &pk_hex(&signer())).expect("no markers is trivially fine");
}
#[test]
fn mmr_rebuild_from_events_skips_marker_events() {
let log = VerifiableLog::new();
let s = signer();
let events = vec![
Event::new("user_msg", b"a".to_vec()),
Event::new("output_msg", b"b".to_vec()),
];
let marker = extend_and_sign(&log, &events, &s).expect("sign");
let mut persisted = events;
persisted.push(marker);
let rebuilt = rebuild_from_events(&persisted).expect("rebuild");
assert_eq!(rebuilt.leaf_count().unwrap(), 2, "markers are not leaves");
assert_eq!(rebuilt.root().unwrap(), log.root().unwrap());
}
}