holochain_types/countersigning.rs
1//! Types related to countersigning sessions.
2
3use holo_hash::{AgentPubKey, EntryHash};
4use holochain_timestamp::Timestamp;
5use holochain_zome_types::{
6 cell::CellId,
7 prelude::PreflightRequest,
8 record::{SignedAction, SignedActionHashed},
9};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13/// State and data of an ongoing countersigning session.
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub enum CountersigningSessionState {
17 /// This is the entry state. Accepting a countersigning session through the HDK will immediately
18 /// register the countersigning session in this state, for management by the countersigning workflow.
19 ///
20 /// The session will stay in this state even when the agent commits their countersigning entry and only
21 /// move to the next state when the first signature bundle is received.
22 Accepted(PreflightRequest),
23 /// This is the state where we have collected one or more signatures for a countersigning session.
24 ///
25 /// This state can be entered from the [CountersigningSessionState::Accepted] state, which happens
26 /// when a witness returns a signature bundle to us. While the session has not timed out, we will
27 /// stay in this state and wait until one of the signatures bundles we have received is valid for
28 /// the session to be completed.
29 ///
30 /// If we entered this state from the [CountersigningSessionState::Accepted] state, we will either
31 /// complete the session successfully or the session will time out. On a timeout we will move
32 /// to the [CountersigningSessionState::Unknown] for a limited number of attempts to recover the session.
33 ///
34 /// This state can also be entered from the [CountersigningSessionState::Unknown] state, which happens when we
35 /// have been able to recover the session from the source chain and have requested signed actions
36 /// from agent authorities to build a signature bundle.
37 ///
38 /// If we entered this state from the [CountersigningSessionState::Unknown] state, we will either
39 /// complete the session successfully, or if the signatures are invalid, we will return to the
40 /// [CountersigningSessionState::Unknown] state.
41 SignaturesCollected {
42 /// The preflight request that has been exchanged among countersigning peers.
43 preflight_request: PreflightRequest,
44 /// Signed actions of the committed countersigned entries of all participating peers.
45 signature_bundles: Vec<Vec<SignedAction>>,
46 /// This field is set when the signature bundle came from querying agent activity authorities
47 /// in the unknown state. If we started from that state, we should return to it if the
48 /// signature bundle is invalid. Otherwise, stay in this state and wait for more signatures.
49 resolution: Option<SessionResolutionSummary>,
50 },
51 /// The session is in an unknown state and needs to be resolved.
52 ///
53 /// This state is used when we have lost track of the countersigning session. This happens if
54 /// we have got far enough to create the countersigning entry but have crashed or restarted
55 /// before we could complete the session. In this case we need to try to discover what the other
56 /// agent or agents involved in the session have done.
57 ///
58 /// This state is also entered temporarily when we have published a signature and then the
59 /// session has timed out. To avoid deadlocking with two parties both waiting for each other to
60 /// proceed, we cannot stay in this state indefinitely. We will make a limited number of attempts
61 /// to recover and if we cannot, we will abandon the session.
62 ///
63 /// The only exception to the attempt limiting is if we are unable to reach agent activity authorities
64 /// to progress resolving the session. In this case, the attempts are not counted towards the
65 /// configured limit. This does not protect us against a network partition where we can only see
66 /// a subset of the network, but it does protect us against Holochain forcing a decision while
67 /// it is unable to reach any peers.
68 ///
69 /// Note that because the [PreflightRequest] is stored here, we only ever enter the unknown state
70 /// if we managed to keep the preflight request in memory, or if we have been able to recover it
71 /// from the source chain as part of the committed countersigning session data. Otherwise, we
72 /// are unable to discover what session we were participating in, and we must abandon the session
73 /// without going through this recovery state.
74 Unknown {
75 /// The preflight request that has been exchanged.
76 preflight_request: PreflightRequest,
77 /// Summary of the attempts to resolve this session.
78 resolution: SessionResolutionSummary,
79 /// Flag if the session is programmed to be force-abandoned on the next countersigning workflow run.
80 force_abandon: bool,
81 /// Flag if the session is programmed to be force-published on the next countersigning workflow run.
82 force_publish: bool,
83 },
84}
85
86impl CountersigningSessionState {
87 /// Get preflight request of the countersigning session.
88 pub fn preflight_request(&self) -> &PreflightRequest {
89 match self {
90 CountersigningSessionState::Accepted(preflight_request) => preflight_request,
91 CountersigningSessionState::SignaturesCollected {
92 preflight_request, ..
93 } => preflight_request,
94 CountersigningSessionState::Unknown {
95 preflight_request, ..
96 } => preflight_request,
97 }
98 }
99
100 /// Get app entry hash from preflight request.
101 pub fn session_app_entry_hash(&self) -> &EntryHash {
102 let request = match self {
103 CountersigningSessionState::Accepted(request) => request,
104 CountersigningSessionState::SignaturesCollected {
105 preflight_request, ..
106 } => preflight_request,
107 CountersigningSessionState::Unknown {
108 preflight_request, ..
109 } => preflight_request,
110 };
111
112 &request.app_entry_hash
113 }
114}
115
116/// Summary of the workflow's attempts to resolve the outcome a failed countersigning session.
117///
118/// This tracks the numbers of attempts and the outcome of the most recent attempt.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct SessionResolutionSummary {
121 /// The reason why session resolution is required.
122 pub required_reason: ResolutionRequiredReason,
123 /// How many attempts have been made to resolve the session.
124 ///
125 /// This count is only correct for the current run of the Holochain conductor. If the conductor
126 /// is restarted then this counter is also reset.
127 pub attempts: usize,
128 /// The time of the last attempt to resolve the session.
129 pub last_attempt_at: Option<Timestamp>,
130 /// The outcome of the most recent attempt to resolve the session.
131 pub outcomes: Vec<SessionResolutionOutcome>,
132}
133
134impl Default for SessionResolutionSummary {
135 fn default() -> Self {
136 Self {
137 required_reason: ResolutionRequiredReason::Unknown,
138 attempts: 0,
139 last_attempt_at: None,
140 outcomes: Vec::with_capacity(0),
141 }
142 }
143}
144
145/// The reason why a countersigning session can not be resolved automatically and requires manual resolution.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub enum ResolutionRequiredReason {
148 /// The session has timed out, so we should try to resolve its state before abandoning.
149 Timeout,
150 /// Something happened, like a conductor restart, and we lost track of the session.
151 Unknown,
152}
153
154/// The outcome for a single agent who participated in a countersigning session.
155///
156/// [NUM_AUTHORITIES_TO_QUERY] authorities are made to agent activity authorities for each agent,
157/// and the decisions are collected into [SessionResolutionOutcome::decisions].
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct SessionResolutionOutcome {
160 /// The agent who participated in the countersigning session and is the subject of this
161 /// resolution outcome.
162 // Unused until the next PR
163 #[allow(dead_code)]
164 pub agent: AgentPubKey,
165 /// The resolved decision for each authority for the subject agent.
166 // Unused until the next PR
167 #[allow(dead_code)]
168 pub decisions: Vec<SessionCompletionDecision>,
169}
170
171/// Number of authorities to be queried for agent activity, in an attempt to resolve a countersigning
172/// session in an unknown state.
173pub const NUM_AUTHORITIES_TO_QUERY: usize = 3;
174
175/// Decision about an incomplete countersigning session.
176#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
177pub enum SessionCompletionDecision {
178 /// Evidence found on the network that this session completed successfully.
179 Complete(Box<SignedActionHashed>),
180 /// Evidence found on the network that this session was abandoned and other agents have
181 /// added to their chain without completing the session.
182 Abandoned,
183 /// No evidence, or inconclusive evidence, was found on the network. Holochain will not make an
184 /// automatic decision until the evidence is conclusive.
185 Indeterminate,
186 /// There were errors encountered while trying to resolve the session. Errors such as network
187 /// errors are treated differently to inconclusive evidence. We don't want to force a decision
188 /// when we're offline, for example. In this case, the resolution must be retried later and this
189 /// attempt should not be counted.
190 Failed,
191}
192
193/// Errors related to countersigning sessions.
194#[derive(Debug, Error)]
195pub enum CountersigningError {
196 /// Countersigning workspace does not exist for cell.
197 #[error("Countersigning workspace does not exist for cell id {0:?}. Probably an invalid cell id was provided.")]
198 WorkspaceDoesNotExist(CellId),
199 /// No countersigning session found for the cell.
200 #[error("No countersigning session found for cell id {0:?}")]
201 SessionNotFound(CellId),
202 /// Countersigning session must be in an unresolved state to be abandoned or published.
203 #[error("Countersigning session for cell id {0:?} is not unresolved. Only unresolved sessions can be abandoned or published.")]
204 SessionNotUnresolved(CellId),
205}