Skip to main content

macula_rust/record/
tombstone.rs

1//! Tombstones: a signer's withdrawal of one of its records, stored in the
2//! withdrawn record's slot and signed with the key that signed it.
3
4use crate::cbor::Value;
5
6use super::payload::slot_field_names;
7use super::{
8    entry, id_field, malformed, text_field, unsigned, Record, RecordError, RecordType,
9    CLOCK_TOLERANCE_MS,
10};
11
12/// Why a tombstone withdraws a record.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Reason {
15    Shutdown,
16    Moved,
17    Revoked,
18}
19
20impl Reason {
21    fn name(self) -> &'static str {
22        match self {
23            Reason::Shutdown => "shutdown",
24            Reason::Moved => "moved",
25            Reason::Revoked => "revoked",
26        }
27    }
28}
29
30/// A tombstone's optional fields: `detail`, left out when empty, and
31/// `ttl_ms`, 0 for the clock tolerance, 5 minutes.
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct TombstoneOptions {
34    pub detail: String,
35    pub ttl_ms: u64,
36}
37
38/// An unsigned tombstone that withdraws `withdrawn`: it names the record's
39/// type, version and slot fields, takes its slot, and lives until the record
40/// has expired plus the clock tolerance, or `ttl_ms` past its own creation
41/// when later, so no replica serves the record again after it lapses.
42pub fn new_tombstone(
43    withdrawn: &Record,
44    reason: Reason,
45    opts: &TombstoneOptions,
46) -> Result<Record, RecordError> {
47    if withdrawn.record_type == RecordType::TOMBSTONE {
48        return Err(RecordError::TombstoneOfATombstone);
49    }
50    let mut entries = vec![
51        entry(
52            "withdrawn_type",
53            Value::Int(i128::from(withdrawn.record_type.0)),
54        ),
55        entry(
56            "withdrawn_version",
57            Value::Bytes(withdrawn.version.to_vec()),
58        ),
59        entry("reason", Value::text(reason.name())),
60    ];
61    entries.extend(slot_fields(withdrawn)?);
62    if !opts.detail.is_empty() {
63        entries.push(entry("detail", Value::text(opts.detail.clone())));
64    }
65    let ttl_ms = if opts.ttl_ms == 0 {
66        CLOCK_TOLERANCE_MS
67    } else {
68        opts.ttl_ms
69    };
70    let mut r = unsigned(RecordType::TOMBSTONE, Value::Map(entries), ttl_ms);
71    r.expires_at = (r.created_at + ttl_ms).max(withdrawn.expires_at + CLOCK_TOLERANCE_MS);
72    Ok(r)
73}
74
75fn slot_fields(withdrawn: &Record) -> Result<Vec<(Value, Value)>, RecordError> {
76    if withdrawn.record_type >= RecordType::DOMAIN_MIN {
77        return Ok(withdrawn
78            .subject
79            .as_ref()
80            .map(|s| vec![entry("subject", Value::Bytes(s.clone()))])
81            .unwrap_or_default());
82    }
83    slot_field_names(i128::from(withdrawn.record_type.0))
84        .iter()
85        .map(|name| {
86            withdrawn
87                .payload
88                .get(name)
89                .map(|v| entry(name, v.clone()))
90                .ok_or_else(|| malformed(format!("the withdrawn record has no {name}")))
91        })
92        .collect()
93}
94
95/// A tombstone's payload: what it withdraws, why, and the withdrawn record's
96/// slot fields.
97#[derive(Debug, Clone, Default, PartialEq, Eq)]
98pub struct Tombstone {
99    pub withdrawn_type: RecordType,
100    pub withdrawn_version: [u8; 16],
101    pub reason: String,
102    pub detail: String,
103    pub realm_id: [u8; 32],
104    pub member_node: [u8; 32],
105    pub procedure: String,
106    pub param_name: String,
107    pub station_id: [u8; 32],
108    pub mcid: Vec<u8>,
109    pub org_name: String,
110    pub advertiser: [u8; 32],
111    pub subject: Vec<u8>,
112}
113
114/// Reads a tombstone's payload.
115pub fn read_tombstone(r: &Record) -> Result<Tombstone, RecordError> {
116    if r.record_type != RecordType::TOMBSTONE {
117        return Err(malformed("not a tombstone"));
118    }
119    let p = &r.payload;
120    let withdrawn = match p.get("withdrawn_type") {
121        Some(Value::Int(n)) if (1..=255).contains(n) => RecordType(*n as u8),
122        _ => {
123            return Err(malformed(
124                "a withdrawn_type that is not an integer from 1 to 255",
125            ))
126        }
127    };
128    let bytes = |name: &str| match p.get(name) {
129        Some(Value::Bytes(b)) => b.clone(),
130        _ => Vec::new(),
131    };
132    Ok(Tombstone {
133        withdrawn_type: withdrawn,
134        withdrawn_version: bytes("withdrawn_version").try_into().unwrap_or([0; 16]),
135        reason: text_field(p, "reason"),
136        detail: text_field(p, "detail"),
137        realm_id: id_field(p, "realm_id"),
138        member_node: id_field(p, "member_node"),
139        procedure: text_field(p, "procedure"),
140        param_name: text_field(p, "param_name"),
141        station_id: id_field(p, "station_id"),
142        mcid: bytes("mcid"),
143        org_name: text_field(p, "org_name"),
144        advertiser: id_field(p, "advertiser"),
145        subject: bytes("subject"),
146    })
147}