pub trait WellShaped {
fn well_shaped(&self) -> bool;
}
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Digest(pub String);
impl Digest {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
#[inline]
pub fn as_inner(&self) -> &str {
&self.0
}
}
pub type Blake3Hash = Digest;
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ReplayHint(pub String);
impl ReplayHint {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
#[inline]
pub fn as_inner(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceiptShape {
pub witness: String,
pub digest: Digest,
pub replay_hint: ReplayHint,
}
impl ReceiptShape {
pub fn new(witness: impl Into<String>, digest: Digest, replay_hint: ReplayHint) -> Self {
Self {
witness: witness.into(),
digest,
replay_hint,
}
}
pub fn is_well_shaped(&self) -> bool {
!self.witness.is_empty() && !self.digest.0.is_empty() && !self.replay_hint.0.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceiptEnvelope {
pub subject: String,
pub witness: String,
pub digest: Digest,
pub replay_hint: ReplayHint,
}
impl ReceiptEnvelope {
pub fn new(
subject: impl Into<String>,
witness: impl Into<String>,
digest: Digest,
replay_hint: ReplayHint,
) -> Self {
Self {
subject: subject.into(),
witness: witness.into(),
digest,
replay_hint,
}
}
pub fn is_well_shaped(&self) -> bool {
!self.subject.is_empty()
&& !self.witness.is_empty()
&& !self.digest.0.is_empty()
&& !self.replay_hint.0.is_empty()
}
pub fn try_from_parts(
subject: impl Into<String>,
witness: impl Into<String>,
digest: Digest,
replay_hint: ReplayHint,
) -> Result<Self, ReceiptRefusal> {
let subject = subject.into();
let witness = witness.into();
if subject.is_empty() {
return Err(ReceiptRefusal::MissingSubject);
}
if witness.is_empty() {
return Err(ReceiptRefusal::MissingWitness);
}
if digest.0.is_empty() {
return Err(ReceiptRefusal::MissingDigest);
}
if replay_hint.0.is_empty() {
return Err(ReceiptRefusal::MissingReplayHint);
}
Ok(Self {
subject,
witness,
digest,
replay_hint,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReceiptRefusal {
MissingSubject,
MissingWitness,
MissingDigest,
MissingReplayHint,
UnreplayableClaim,
BrokenChainLink(usize),
EmptyChain,
}
impl core::fmt::Display for ReceiptRefusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ReceiptRefusal::MissingSubject => write!(f, "receipt refused: MissingSubject"),
ReceiptRefusal::MissingWitness => write!(f, "receipt refused: MissingWitness"),
ReceiptRefusal::MissingDigest => write!(f, "receipt refused: MissingDigest"),
ReceiptRefusal::MissingReplayHint => write!(f, "receipt refused: MissingReplayHint"),
ReceiptRefusal::UnreplayableClaim => write!(f, "receipt refused: UnreplayableClaim"),
ReceiptRefusal::BrokenChainLink(idx) => {
write!(f, "receipt refused: BrokenChainLink at index {idx}")
}
ReceiptRefusal::EmptyChain => write!(f, "receipt refused: EmptyChain"),
}
}
}
pub struct ReceiptBuilder<W> {
subject: Option<String>,
digest: Option<String>,
replay_hint: Option<String>,
_witness: core::marker::PhantomData<W>,
}
impl<W: crate::witness::Witness> ReceiptBuilder<W> {
#[must_use]
pub fn new() -> Self {
ReceiptBuilder {
subject: None,
digest: None,
replay_hint: None,
_witness: core::marker::PhantomData,
}
}
#[must_use]
pub fn subject(mut self, s: impl Into<String>) -> Self {
self.subject = Some(s.into());
self
}
#[must_use]
pub fn digest(mut self, d: impl Into<String>) -> Self {
self.digest = Some(d.into());
self
}
#[must_use]
pub fn replay_hint(mut self, h: impl Into<String>) -> Self {
self.replay_hint = Some(h.into());
self
}
#[must_use = "check the shape-check result"]
pub fn build(self) -> Result<ReceiptEnvelope, ReceiptRefusal> {
let subject = self.subject.unwrap_or_default();
let digest = Digest::new(self.digest.unwrap_or_default());
let replay_hint = ReplayHint::new(self.replay_hint.unwrap_or_default());
ReceiptEnvelope::try_from_parts(subject, W::KEY, digest, replay_hint)
}
}
impl<W: crate::witness::Witness> Default for ReceiptBuilder<W> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceiptChain {
pub chain_id: String,
links: Vec<ReceiptEnvelope>,
}
impl ReceiptChain {
pub fn try_new(
chain_id: impl Into<String>,
links: Vec<ReceiptEnvelope>,
) -> Result<Self, ReceiptRefusal> {
if links.is_empty() {
return Err(ReceiptRefusal::EmptyChain);
}
for (i, link) in links.iter().enumerate() {
if !link.is_well_shaped() {
return Err(ReceiptRefusal::BrokenChainLink(i));
}
}
Ok(Self {
chain_id: chain_id.into(),
links,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.links.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.links.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &ReceiptEnvelope> {
self.links.iter()
}
#[must_use]
pub fn root(&self) -> &ReceiptEnvelope {
&self.links[0]
}
#[must_use]
pub fn tip(&self) -> &ReceiptEnvelope {
&self.links[self.links.len() - 1]
}
#[must_use = "check whether the link was accepted"]
pub fn extend_with(&mut self, link: ReceiptEnvelope) -> Result<(), ReceiptRefusal> {
if !link.is_well_shaped() {
return Err(ReceiptRefusal::BrokenChainLink(self.links.len()));
}
self.links.push(link);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GraduationReceipt {
pub envelope: ReceiptEnvelope,
pub reason_tag: &'static str,
}
impl GraduationReceipt {
#[must_use]
pub fn new(envelope: ReceiptEnvelope, reason_tag: &'static str) -> Self {
Self {
envelope,
reason_tag,
}
}
#[must_use]
pub fn is_well_shaped(&self) -> bool {
self.envelope.is_well_shaped() && !self.reason_tag.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReceiptVerdict {
Admitted,
Refused(ReceiptRefusal),
}
impl ReceiptVerdict {
#[must_use]
pub fn from_shape_check(ok: bool, reason: Option<ReceiptRefusal>) -> Self {
if ok {
Self::Admitted
} else {
Self::Refused(reason.unwrap_or(ReceiptRefusal::MissingWitness))
}
}
#[must_use]
pub fn is_admitted(&self) -> bool {
matches!(self, Self::Admitted)
}
#[must_use]
pub fn is_refused(&self) -> bool {
matches!(self, Self::Refused(_))
}
#[must_use]
pub fn refusal(&self) -> Option<&ReceiptRefusal> {
match self {
Self::Refused(r) => Some(r),
Self::Admitted => None,
}
}
}
impl core::fmt::Display for ReceiptVerdict {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ReceiptVerdict::Admitted => write!(f, "receipt verdict: Admitted"),
ReceiptVerdict::Refused(r) => write!(f, "receipt verdict: Refused({r})"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceiptChainConst<const N: usize> {
pub chain_id: String,
links: [ReceiptEnvelope; N],
}
impl<const N: usize> ReceiptChainConst<N> {
pub fn try_new(
chain_id: impl Into<String>,
links: [ReceiptEnvelope; N],
) -> Result<Self, ReceiptRefusal> {
if N == 0 {
return Err(ReceiptRefusal::EmptyChain);
}
for (i, link) in links.iter().enumerate() {
if !link.is_well_shaped() {
return Err(ReceiptRefusal::BrokenChainLink(i));
}
}
Ok(Self {
chain_id: chain_id.into(),
links,
})
}
#[must_use]
pub const fn arity(&self) -> usize {
N
}
#[must_use]
pub fn root(&self) -> &ReceiptEnvelope {
&self.links[0]
}
#[must_use]
pub fn tip(&self) -> &ReceiptEnvelope {
&self.links[N - 1]
}
pub fn iter(&self) -> impl Iterator<Item = &ReceiptEnvelope> {
self.links.iter()
}
}
impl<const N: usize> WellShaped for ReceiptChainConst<N> {
fn well_shaped(&self) -> bool {
N > 0 && self.iter().all(|link| link.is_well_shaped())
}
}
impl core::fmt::Display for Digest {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
impl core::fmt::Display for ReplayHint {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
impl core::fmt::Display for ReceiptShape {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"receipt[{}] digest={} replay={}",
self.witness, self.digest, self.replay_hint
)
}
}
impl core::fmt::Display for ReceiptEnvelope {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"receipt[{}] subject={} digest={} replay={}",
self.witness, self.subject, self.digest, self.replay_hint
)
}
}
impl core::fmt::Display for ReceiptChain {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "chain[{}] links={}", self.chain_id, self.links.len())
}
}
impl core::fmt::Display for GraduationReceipt {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"graduation[{}] subject={} witness={}",
self.reason_tag, self.envelope.subject, self.envelope.witness
)
}
}
impl<const N: usize> core::fmt::Display for ReceiptChainConst<N> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "chain-const[{}] arity={N}", self.chain_id)
}
}
impl From<String> for Digest {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Digest {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<Digest> for String {
fn from(d: Digest) -> Self {
d.0
}
}
impl AsRef<str> for Digest {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for ReplayHint {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ReplayHint {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<ReplayHint> for String {
fn from(h: ReplayHint) -> Self {
h.0
}
}
impl AsRef<str> for ReplayHint {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<ReceiptRefusal> for ReceiptVerdict {
fn from(r: ReceiptRefusal) -> Self {
ReceiptVerdict::Refused(r)
}
}
impl WellShaped for ReceiptShape {
fn well_shaped(&self) -> bool {
self.is_well_shaped()
}
}
impl WellShaped for ReceiptEnvelope {
fn well_shaped(&self) -> bool {
self.is_well_shaped()
}
}
impl WellShaped for ReceiptChain {
fn well_shaped(&self) -> bool {
!self.is_empty() && self.iter().all(|link| link.is_well_shaped())
}
}
impl WellShaped for GraduationReceipt {
fn well_shaped(&self) -> bool {
self.is_well_shaped()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProvenanceChain {
pub input_hash: String,
pub config_hash: String,
pub plan_hash: String,
pub output_hash: String,
pub combined_hash: String,
pub algorithm_id: String,
pub algorithm_version: String,
pub backend_id: String,
pub kernel_version: String,
pub wasm_build_hash: String,
}
use blake3::Hasher;
use serde::{Deserialize, Serialize};
mod signature_serde {
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(sig: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
sig[..].serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 64], D::Error>
where
D: Deserializer<'de>,
{
let v = Vec::<u8>::deserialize(deserializer)?;
let mut array = [0u8; 64];
if v.len() == 64 {
array.copy_from_slice(&v);
Ok(array)
} else {
Err(D::Error::custom("expected 64 bytes for signature"))
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Receipt {
pub model_id: [u8; 32],
pub final_hash_chain: [u8; 32],
#[serde(with = "signature_serde")]
pub authority_signature: [u8; 64],
pub verdict: ConformanceVerdict,
pub timestamp_epoch: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConformanceVerdict {
PerfectAlignment,
FitnessDeficit,
DeadlockEncountered,
}
pub struct TraceChainer {
pub hasher: Hasher,
}
impl TraceChainer {
pub fn new() -> Self {
let mut hasher = Hasher::new();
hasher.update(b"W4PM_RECEIPT_SALT_V1");
Self { hasher }
}
pub fn chain_step(&mut self, event_id: &[u8], transition_id: &[u8], marking_state: &[u8]) {
self.hasher.update(event_id);
self.hasher.update(transition_id);
self.hasher.update(marking_state);
}
pub fn finalize(self) -> [u8; 32] {
let hash = self.hasher.finalize();
hash.into()
}
}
impl Default for TraceChainer {
fn default() -> Self {
Self::new()
}
}