use serde_json::Value;
use trust_tasks_rs::TrustTask;
use vti_specs::notifications::notify::v0_1 as notify;
use crate::trust_task_proof::{TrustTaskVmResolver, verify_trust_task_proof_value};
use crate::trust_tasks::{TASK_NOTIFICATIONS_NOTIFY_0_1, TASK_NOTIFICATIONS_RELAY_0_1};
pub const MAX_RELAYS: usize = 4;
pub const KIND_ELECTION: &str = "election";
pub const ELECTION_OPEN_WITH: &str = "https://trusttasks.org/spec/vtc/elections/show/0.6";
#[must_use]
pub fn is_recognised_kind(kind: &str) -> bool {
kind == KIND_ELECTION
}
#[must_use]
pub fn kind_may_be_relayed(kind: &str) -> bool {
let _ = kind;
true
}
#[must_use]
pub fn relay_disclosure(kind: &str) -> Option<&'static str> {
match kind {
KIND_ELECTION => Some(
"Relaying this notification shows each party it is relayed to that you are a \
member of the community holding the election — which your own listing in that \
community's directory may otherwise keep from them (VTI-NTF-046, VTI-DIR-040).",
),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ChainError {
#[error("not a notification: {0}")]
NotANotification(String),
#[error("layer {layer} does not validate as a document of its type: {reason}")]
Invalid { layer: usize, reason: String },
#[error("layer {layer}'s proof does not hold: {reason}")]
Proof { layer: usize, reason: String },
#[error("layer {layer} is not addressed to the relayer that forwarded it")]
Misaddressed { layer: usize },
#[error("the relay carries more than {MAX_RELAYS} relays")]
TooDeep,
#[error("the notification may not be relayed")]
NotRelayable,
}
impl ChainError {
#[must_use]
pub fn is_chain_invalid(&self) -> bool {
match self {
ChainError::Invalid { layer, .. } | ChainError::Proof { layer, .. } => *layer > 0,
ChainError::Misaddressed { .. } | ChainError::TooDeep | ChainError::NotRelayable => {
true
}
ChainError::NotANotification(_) => false,
}
}
}
#[derive(Debug, Clone)]
pub struct VerifiedDelivery {
originator: String,
addressee: String,
relayed_by: Vec<String>,
notification: notify::Notification,
notification_json: Value,
document: Value,
}
impl VerifiedDelivery {
#[must_use]
pub fn originator(&self) -> &str {
&self.originator
}
#[must_use]
pub fn addressee(&self) -> &str {
&self.addressee
}
#[must_use]
pub fn relayed_by(&self) -> &[String] {
&self.relayed_by
}
#[must_use]
pub fn delivered_by(&self) -> &str {
self.relayed_by.last().unwrap_or(&self.originator)
}
#[must_use]
pub fn notification(&self) -> ¬ify::Notification {
&self.notification
}
#[must_use]
pub fn notification_json(&self) -> &Value {
&self.notification_json
}
#[must_use]
pub fn document(&self) -> &Value {
&self.document
}
#[must_use]
pub fn innermost(&self) -> &Value {
let mut layer = &self.document;
while layer["type"].as_str() == Some(TASK_NOTIFICATIONS_RELAY_0_1) {
layer = &layer["payload"]["forwarded"];
}
layer
}
#[must_use]
pub fn is_relayed(&self) -> bool {
!self.relayed_by.is_empty()
}
}
#[must_use]
pub fn identity(did: &str) -> String {
crate::did_scid::identity(did)
}
pub async fn verify_delivery(
document: &Value,
resolver: &TrustTaskVmResolver,
) -> Result<VerifiedDelivery, ChainError> {
let outer_type = document["type"].as_str().unwrap_or_default();
if outer_type != TASK_NOTIFICATIONS_NOTIFY_0_1 && outer_type != TASK_NOTIFICATIONS_RELAY_0_1 {
return Err(ChainError::NotANotification(format!(
"`{outer_type}` is neither notifications/notify/0.1 nor notifications/relay/0.1"
)));
}
let addressee = document["recipient"]
.as_str()
.map(str::to_string)
.ok_or_else(|| ChainError::Invalid {
layer: 0,
reason: "the document names no recipient".into(),
})?;
let mut relayers_outer_first: Vec<String> = Vec::new();
let mut layer = document;
let mut depth = 0usize;
loop {
let doc = check_layer(layer, depth, resolver).await?;
let issuer = doc.issuer.clone().unwrap_or_default();
if doc.type_uri.to_string() == TASK_NOTIFICATIONS_NOTIFY_0_1 {
let notification_json = layer["payload"]["notification"].clone();
let notification: notify::Notification =
serde_json::from_value(notification_json.clone()).map_err(|e| {
ChainError::Invalid {
layer: depth,
reason: e.to_string(),
}
})?;
if depth > 0 && (!notification.relayable || !kind_may_be_relayed(¬ification.kind)) {
return Err(ChainError::NotRelayable);
}
relayers_outer_first.reverse();
return Ok(VerifiedDelivery {
originator: issuer,
addressee,
relayed_by: relayers_outer_first,
notification,
notification_json,
document: document.clone(),
});
}
relayers_outer_first.push(issuer.clone());
if relayers_outer_first.len() > MAX_RELAYS {
return Err(ChainError::TooDeep);
}
let forwarded = &layer["payload"]["forwarded"];
let inner_type = forwarded["type"].as_str().unwrap_or_default();
if inner_type != TASK_NOTIFICATIONS_NOTIFY_0_1 && inner_type != TASK_NOTIFICATIONS_RELAY_0_1
{
return Err(ChainError::Invalid {
layer: depth + 1,
reason: format!("a relay may forward only a notify or a relay, not `{inner_type}`"),
});
}
let inner_recipient = forwarded["recipient"].as_str().unwrap_or_default();
if identity(inner_recipient) != identity(&issuer) {
return Err(ChainError::Misaddressed { layer: depth + 1 });
}
layer = forwarded;
depth += 1;
}
}
async fn check_layer(
layer: &Value,
depth: usize,
resolver: &TrustTaskVmResolver,
) -> Result<TrustTask<Value>, ChainError> {
let invalid = |reason: String| ChainError::Invalid {
layer: depth,
reason,
};
let doc: TrustTask<Value> =
serde_json::from_value(layer.clone()).map_err(|e| invalid(e.to_string()))?;
let type_uri = doc.type_uri.to_string();
let schema = vti_specs::schema_index::schema_for(&type_uri)
.ok_or_else(|| invalid(format!("`{type_uri}` is not pinned by this build")))?;
trust_tasks_rs::validate::against_schema(schema, &doc.payload)
.map_err(|e| invalid(e.to_string()))?;
let issuer = doc
.issuer
.as_deref()
.ok_or_else(|| invalid("the document names no issuer".into()))?;
if doc.proof.is_none() {
return Err(ChainError::Proof {
layer: depth,
reason: "the document carries no proof".into(),
});
}
let signer = verify_trust_task_proof_value(layer, resolver)
.await
.map_err(|e| ChainError::Proof {
layer: depth,
reason: e.to_string(),
})?;
if identity(&signer) != identity(issuer) {
return Err(ChainError::Proof {
layer: depth,
reason: "the proof verifies as a DID other than the document's issuer".into(),
});
}
Ok(doc)
}