Skip to main content

macroonz_compiler/plan/
anchor.rs

1//! What a plan hangs off, what its account therefore watches, and where that account's content stands in the origin graph.
2//!
3//! Three readings of one value, so a plan's anchor, its triggers, and its origin edges cannot disagree about what the request walked in with.
4//!
5//! # Bounds
6//!
7//! An anchor names ONE address, and that is a spelling rule: what the content stands on is written into the material the identity is derived over, so two plans over different dependency sets reach different identities whatever they anchor at.
8//! A watch naming one address out of several would be a claim about the others, which is why the watch reading covers the whole account and the narrow one-trigger reading refuses instead of electing.
9
10use super::{Account, InvalidationTrigger, PlanError, PlanIssue};
11use crate::identity::{self, Anchoring, Identity, Transcript};
12use crate::kind::Kind;
13use crate::origin::{OriginEdge, OriginRelation};
14
15/// The stable material one captured dependency stands at before a kind-specific content binding names it more narrowly.
16const CAPTURED_CONTENT_NODE: &[u8] = b"content";
17
18/// The origin node one captured dependency stands at.
19fn captured_content_node(
20    commitment: Identity<identity::CapturedDeclaration>,
21) -> Identity<identity::OriginNode> {
22    Identity::derived(Transcript::under_projection(
23        identity::Role::OriginNode,
24        &commitment,
25        CAPTURED_CONTENT_NODE,
26        0,
27    ))
28}
29
30/// The stable material one kind-specific content binding stands at.
31const BOUND_CONTENT_NODE: &[u8] = b"content";
32
33/// The origin node one content binding stands at.
34fn content_node(content: Identity<identity::ProjectionContent>) -> Identity<identity::OriginNode> {
35    Identity::derived(Transcript::under_projection(
36        identity::Role::OriginNode,
37        &content,
38        BOUND_CONTENT_NODE,
39        0,
40    ))
41}
42
43impl<K: Kind> Account<K> {
44    /// What a transcript derived over this account hangs off.
45    ///
46    /// Read off the account's own commitment, so a plan's anchor and a plan's stated cause are one fact rather than two that could disagree.
47    #[must_use]
48    pub fn anchoring(&self) -> Anchoring {
49        Anchoring::UnderProjection(*self.commitment().as_bytes())
50    }
51
52    /// The origin node this account's content stands at.
53    ///
54    /// Derived under the content commitment, so changing its kind, captured declaration, or canonical content moves the node through the binding's stronger identity rather than a second restatement of those facts.
55    #[must_use]
56    pub fn origin_node(&self) -> Identity<identity::OriginNode> {
57        content_node(self.content_commitment())
58    }
59
60    /// The origin edges this account contributes: one per declared dependency, each running from what the content stands on to the content itself.
61    ///
62    /// The relation is [`OriginRelation::ExplicitLink`] because that is what happened — an author supplied this dependency set at the door.
63    /// It is no semantic derivation: nothing here derived meaning from a dependency.
64    ///
65    /// # Ordering
66    ///
67    /// The edges are a FAN-IN and not a walk: every one of them ends at [`Account::origin_node`], so consecutive edges do not join and the set is not a trail.
68    /// A caller draws trails through these edges one at a time rather than handing the set to a trail constructor, which would refuse the discontinuity — correctly.
69    #[must_use]
70    pub fn dependency_edges(&self) -> Vec<OriginEdge> {
71        let to = self.origin_node();
72        self.dependencies()
73            .iter()
74            .map(|dependency| OriginEdge {
75                from: captured_content_node(*dependency),
76                relation: OriginRelation::ExplicitLink,
77                to,
78            })
79            .collect()
80    }
81
82    /// The triggers that watch this account's commitment and every dependency it declares.
83    #[must_use]
84    pub fn cause_triggers(&self) -> Vec<InvalidationTrigger> {
85        let (first, rest) = self.caused_by();
86        core::iter::once(first).chain(rest).collect()
87    }
88
89    /// The single trigger that watches this account's own content.
90    ///
91    /// The deliberately narrow reading, for a caller that can carry one trigger and no more.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`PlanIssue::CauseSetUnwatchable`] where the account also names dependencies: one trigger cannot state that cause set, and a watch covering the commitment alone would read exactly like a complete one.
96    pub fn cause_trigger(&self) -> Result<InvalidationTrigger, PlanError> {
97        let (first, rest) = self.caused_by();
98        if rest.is_empty() {
99            return Ok(first);
100        }
101        Err(PlanError::of(PlanIssue::CauseSetUnwatchable {
102            named: u32::try_from(rest.len().saturating_add(1)).unwrap_or(u32::MAX),
103            watchable: 1,
104        }))
105    }
106
107    /// The content commitment's trigger and one per declared dependency beside it.
108    ///
109    /// The content commitment is already derived under the exact captured declaration, so a second trigger over that declaration would be a weaker restatement rather than another cause.
110    /// This is the one spelling both readings above take, and the one the shared watch derivation opens with.
111    pub(super) fn caused_by(&self) -> (InvalidationTrigger, Vec<InvalidationTrigger>) {
112        (
113            InvalidationTrigger::ProjectionContent {
114                watched: self.content_commitment(),
115            },
116            self.dependencies()
117                .iter()
118                .map(|watched| InvalidationTrigger::CapturedDeclaration { watched: *watched })
119                .collect(),
120        )
121    }
122}