use serde::{Deserialize, Serialize};
use super::kind::EventType;
use super::payload::EventPayload;
use super::signature::EventSignature;
use super::type_code::type_code;
use crate::crypto::SignerId;
use crate::model::EventId;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InclusionProof {
pub scheme: String,
pub proof: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventSignatureRecordedPayload {
pub target_event_id: EventId,
pub target_event_record_hash: String,
pub attesting_signer: SignerId,
pub attestation: EventSignature,
#[serde(skip_serializing_if = "Option::is_none")]
pub inclusion_proof: Option<InclusionProof>,
}
impl EventSignatureRecordedPayload {
pub fn idempotency_key(
target_event_record_hash: &str,
attesting_signer: &SignerId,
signature_encoding: &str,
) -> String {
format!(
"{}:{}:{}:{}",
type_code(EventType::EventSignatureRecorded),
target_event_record_hash,
attesting_signer.as_str(),
signature_encoding,
)
}
}
impl EventPayload for EventSignatureRecordedPayload {
fn event_type(&self) -> EventType {
EventType::EventSignatureRecorded
}
}
#[cfg(test)]
mod tests {
use crate::crypto::SignerId;
use crate::model::{EventId, JournalId};
use crate::session::event::{
EventPayload, EventSignature, EventSignatureRecordedPayload, EventTarget, EventType,
InclusionProof, ShoreEvent, Writer, type_code,
};
const FRIENDLY_SIGNER: &str = "did:key:z6MkehRgf7yJbgaGfYsdoAsKdBPE3dj2CYhowQdcjqSJgvVd";
fn carrier_event(idempotency_key: &str, payload: EventSignatureRecordedPayload) -> ShoreEvent {
ShoreEvent::new(
EventType::EventSignatureRecorded,
idempotency_key,
EventTarget::for_journal(JournalId::new("journal:fixture")),
Writer::shore_local("test"),
payload,
"2026-06-04T00:00:00Z",
)
.expect("carrier event builds")
}
fn payload_for(
record_hash: &str,
signer: &SignerId,
sig: &str,
) -> EventSignatureRecordedPayload {
EventSignatureRecordedPayload {
target_event_id: EventId::new("evt:sha256:target"),
target_event_record_hash: record_hash.to_owned(),
attesting_signer: signer.clone(),
attestation: EventSignature::new_ed25519_v1(sig).unwrap(),
inclusion_proof: None,
}
}
#[test]
fn event_signature_idempotency_key_is_full_attestation_triple() {
let signer = SignerId::parse(FRIENDLY_SIGNER).unwrap();
let key =
EventSignatureRecordedPayload::idempotency_key("sha256:rec", &signer, "SIG_BASE64");
assert_eq!(
key,
format!(
"{}:sha256:rec:did:key:z6MkehRgf7yJbgaGfYsdoAsKdBPE3dj2CYhowQdcjqSJgvVd:SIG_BASE64",
type_code(EventType::EventSignatureRecorded)
)
);
}
#[test]
fn two_distinct_signatures_by_one_signer_yield_distinct_event_ids() {
let signer = SignerId::parse(FRIENDLY_SIGNER).unwrap();
let key_a = EventSignatureRecordedPayload::idempotency_key("sha256:rec", &signer, "SIGAAA");
let key_b = EventSignatureRecordedPayload::idempotency_key("sha256:rec", &signer, "SIGBBB");
let event_a = carrier_event(&key_a, payload_for("sha256:rec", &signer, "SIGAAA"));
let event_b = carrier_event(&key_b, payload_for("sha256:rec", &signer, "SIGBBB"));
assert_ne!(
event_a.event_id, event_b.event_id,
"two distinct signatures by one signer are two distinct members, never two slot claimants"
);
}
#[test]
fn identical_attestation_triple_is_idempotent() {
let signer = SignerId::parse(FRIENDLY_SIGNER).unwrap();
let key_a = EventSignatureRecordedPayload::idempotency_key("sha256:rec", &signer, "SIGAAA");
let key_b = EventSignatureRecordedPayload::idempotency_key("sha256:rec", &signer, "SIGAAA");
assert_eq!(key_a, key_b);
let event_a = carrier_event(&key_a, payload_for("sha256:rec", &signer, "SIGAAA"));
let event_b = carrier_event(&key_b, payload_for("sha256:rec", &signer, "SIGAAA"));
assert_eq!(event_a.event_id, event_b.event_id);
}
#[test]
fn populating_inclusion_proof_does_not_change_idempotency_key_or_event_id() {
let signer = SignerId::parse(FRIENDLY_SIGNER).unwrap();
let key = EventSignatureRecordedPayload::idempotency_key("sha256:rec", &signer, "SIGAAA");
let without = carrier_event(&key, payload_for("sha256:rec", &signer, "SIGAAA"));
let mut proof_payload = payload_for("sha256:rec", &signer, "SIGAAA");
proof_payload.inclusion_proof = Some(InclusionProof {
scheme: "reserved".to_owned(),
proof: "opaque".to_owned(),
});
let with = carrier_event(&key, proof_payload);
assert_eq!(without.idempotency_key, with.idempotency_key);
assert_eq!(without.event_id, with.event_id);
assert_ne!(without.payload_hash, with.payload_hash);
}
#[test]
fn event_signature_payload_round_trips_and_skips_absent_inclusion_proof() {
let signer = SignerId::parse(FRIENDLY_SIGNER).unwrap();
let payload = payload_for("sha256:rec", &signer, "SIGAAA");
let json = serde_json::to_value(&payload).unwrap();
assert!(json.get("inclusionProof").is_none());
assert_eq!(json["targetEventRecordHash"], "sha256:rec");
assert_eq!(json["attestingSigner"], FRIENDLY_SIGNER);
let round: EventSignatureRecordedPayload = serde_json::from_value(json).unwrap();
assert_eq!(round, payload);
let mut with_proof = payload;
with_proof.inclusion_proof = Some(InclusionProof {
scheme: "reserved".to_owned(),
proof: "opaque".to_owned(),
});
let json = serde_json::to_value(&with_proof).unwrap();
assert_eq!(json["inclusionProof"]["scheme"], "reserved");
let round: EventSignatureRecordedPayload = serde_json::from_value(json).unwrap();
assert_eq!(round, with_proof);
}
#[test]
fn payload_event_type_matches_discriminant() {
let signer = SignerId::parse(FRIENDLY_SIGNER).unwrap();
let payload = payload_for("sha256:rec", &signer, "SIGAAA");
assert_eq!(payload.event_type(), EventType::EventSignatureRecorded);
}
}