holochain_chc/lib.rs
1//! Defines the Chain Head Coordination API and an HTTP client for talking to a remote CHC server.
2//!
3//! A Chain Head Coordinator (CHC) is an external service which Holochain can communicate with.
4//! A CHC is used in situations where it is desirable to run the same Holochain Cell (source chain)
5//! on multiple different conductors. The CHC ensures that these multiple devices don't inadvertently
6//! create a fork of the source chain by doing simultaneous uncoordinated writes.
7//!
8//! This crate introduces a [`ChainHeadCoordinator`] trait which defines the interface that Holochain
9//! weaves into its logic to make use of a CHC service. Currently, the only Holochain-supported implementation
10//! of this trait is [`ChcHttp`][chc_http::ChcHttp], which makes HTTP requests to a remote server and returns the responses.
11//! There is also a [`ChcLocal`][chc_local::ChcLocal] reference implementation which is used for testing.
12//! Other implementations can be written as needed, including alternate implementations of a HTTP client,
13//! or also other kinds of clients using other protocols.
14//!
15//! Holochain specifies an optional `chc_url` field in its configuration which can point to an HTTP
16//! server that implements the CHC interface.
17//! See the [`chc_http`] module docs for specs on how to set up a remote CHC HTTP server that
18//! Holochain can talk to using the provided [chc_http::ChcHttp] implementation.
19//!
20//! The CHC trait contains two methods, and Holochain actually only uses one of them:
21//! Every time Holochain is about to commit some records to a source chain, it first calls
22//! [add_records_request][`ChainHeadCoordinator::add_records_request`]
23//! to request that the CHC store those new records.
24//! The CHC must be written so that it recognizes that the new records being added are a valid
25//! extension of the existing chain of stored records, i.e. that the first record being pushed has a
26//! [`Action::prev_action`] field which matches the hash of the last record already stored,
27//! and additionally that the new records also form a valid hash chain (via `prev_action`).
28//! If the new records are valid, they are added to CHC's chain, and an Ok result is returned.
29//!
30//! If these criteria for valid new records are not met, the CHC must return [`ChcError::InvalidChain`],
31//! which lets the client know that the local chain is out of sync with CHC's version,
32//! due to some other conductor having updated its own chain. In this case, the user (the driver
33//! of the conductor) should call the other CHC method,
34//! [get_record_data_request][`ChainHeadCoordinator::get_record_data_request`],
35//! which will return the diff of records that CHC has that the local conductor does not.
36//! Then, the Holochain admin method `GraftRecords` can be called to "stitch" these records onto the
37//! existing chain, removing any fork if necessary.
38//!
39//! Note that currently, [get_record_data_request][`ChainHeadCoordinator::get_record_data_request`]
40//! is never called directly by Holochain, but it might be in the future.
41//!
42//! Note also that when a CHC is used, the CHC is always considered the authoritative source of truth.
43//! If a local conductor's state for whatever reason contradicts the CHC in any way, whether the local
44//! chain contains different data altogether or is strictly ahead of the CHC, the CHC's version is always
45//! considered correct and the local chain should always be modified to match the CHC's state.
46//!
47
48use std::{collections::HashMap, fmt::Debug, sync::Arc};
49
50use futures::FutureExt;
51use holochain_keystore::{AgentPubKeyExt, MetaLairClient};
52use holochain_nonce::Nonce256Bits;
53use holochain_serialized_bytes::SerializedBytesError;
54use holochain_types::prelude::*;
55use must_future::MustBoxFuture;
56
57use holochain_types::chain::ChainItem;
58
59pub mod chc_local;
60
61#[cfg(feature = "http")]
62pub mod chc_http;
63
64/// The API which a Chain Head Coordinator service must implement.
65#[async_trait::async_trait]
66pub trait ChainHeadCoordinator {
67 /// The item which the chain is made of.
68 type Item: ChainItem;
69
70 /// Request that the CHC append these records to its chain.
71 ///
72 /// Whenever Holochain is about to commit something, this function will first be called.
73 /// The CHC will do some integrity checks, which may fail.
74 /// All signatures and hashes need to line up properly.
75 /// If the records added would result in a fork, then a [`ChcError::InvalidChain`] will be returned
76 /// along with the current chain top.
77 // If there is an out-of-sync error, it will return a hash, designating the point of fork.
78 async fn add_records_request(&self, request: AddRecordsRequest) -> ChcResult<()>;
79
80 /// Get actions after (not including) the given hash.
81 async fn get_record_data_request(
82 &self,
83 request: GetRecordsRequest,
84 ) -> ChcResult<Vec<(SignedActionHashed, Option<(Arc<EncryptedEntry>, Signature)>)>>;
85}
86
87/// Add some convenience methods to the CHC trait
88pub trait ChainHeadCoordinatorExt:
89 'static + Send + Sync + ChainHeadCoordinator<Item = SignedActionHashed>
90{
91 /// Get info necessary for signing
92 fn signing_info(&self) -> (MetaLairClient, AgentPubKey);
93
94 /// More convenient way to call `add_records_request`
95 fn add_records(self: Arc<Self>, records: Vec<Record>) -> MustBoxFuture<'static, ChcResult<()>> {
96 let (keystore, agent) = self.signing_info();
97 async move {
98 let payload = AddRecordPayload::from_records(keystore, agent, records).await?;
99 self.add_records_request(payload).await
100 }
101 .boxed()
102 .into()
103 }
104
105 /// More convenient way to call the low-level CHC API method `get_record_data_request`.
106 /// This method actually decodes and assembles those results into a list of `Record`s.
107 fn get_record_data(
108 self: Arc<Self>,
109 since_hash: Option<ActionHash>,
110 ) -> MustBoxFuture<'static, ChcResult<Vec<Record>>> {
111 let (keystore, agent) = self.signing_info();
112 async move {
113 let mut bytes = [0; 32];
114 getrandom::getrandom(&mut bytes).map_err(|e| ChcError::Other(e.to_string()))?;
115 let nonce = Nonce256Bits::from(bytes);
116 let payload = GetRecordsPayload { since_hash, nonce };
117 let signature = agent.sign(&keystore, &payload).await?;
118 self.get_record_data_request(GetRecordsRequest { payload, signature })
119 .await?
120 .into_iter()
121 .map(|(a, me)| {
122 Ok(Record::new(
123 a,
124 me.map(|(e, _s)| holochain_serialized_bytes::decode(&e.0))
125 .transpose()?,
126 ))
127 })
128 .collect()
129 }
130 .boxed()
131 .into()
132 }
133
134 /// Just a convenience for testing. Should not be used otherwise.
135 #[cfg(feature = "test_utils")]
136 fn head(self: Arc<Self>) -> MustBoxFuture<'static, ChcResult<Option<ActionHash>>> {
137 async move {
138 Ok(self
139 .get_record_data(None)
140 .await?
141 .pop()
142 .map(|r| r.action_address().clone()))
143 }
144 .boxed()
145 .into()
146 }
147}
148
149/// A CHC implementation
150pub type ChcImpl = Arc<dyn 'static + Send + Sync + ChainHeadCoordinatorExt>;
151
152/// A Record to be added to the CHC.
153///
154/// The SignedActionHashed is constructed as usual.
155/// The Entry data is encrypted (TODO: by which key?), and the encrypted data
156/// is signed by the agent. This ensures that only the correct agent is adding
157/// records to its CHC. This EncryptedEntry signature is not used anywhere
158/// outside the context of the CHC.
159#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
160pub struct AddRecordPayload {
161 /// The msgpack-encoded SignedActionHashed for the Record. This is encoded as such because the CHC
162 /// needs to verify the signature, and these are the exact bytes which are signed, so
163 /// this removes the need to deserialize and then re-serialize.
164 ///
165 /// This must be deserialized as `SignedActionHashed`.
166 #[serde(with = "serde_bytes")]
167 pub signed_action_msgpack: Vec<u8>,
168
169 /// The signature of the SignedActionHashed
170 /// (NOTE: usually signatures are of just the Action, but in this case we want to
171 /// include the entire struct in the signature so we don't have to recalculate that on the CHC)
172 pub signed_action_signature: Signature,
173
174 /// The entry, encrypted (TODO: by which key?), with the signature of
175 /// of the encrypted bytes
176 pub encrypted_entry: Option<(Arc<EncryptedEntry>, Signature)>,
177}
178
179impl AddRecordPayload {
180 /// Create a payload from a list of records.
181 /// This performs the necessary signing and encryption the CHC requires.
182 #[cfg_attr(feature = "instrument", tracing::instrument(skip(keystore, records)))]
183 pub async fn from_records(
184 keystore: MetaLairClient,
185 agent_pubkey: AgentPubKey,
186 records: Vec<Record>,
187 ) -> ChcResult<Vec<Self>> {
188 futures::future::join_all(records.into_iter().map(
189 |Record {
190 signed_action,
191 entry,
192 }| {
193 let keystore = keystore.clone();
194 let agent_pubkey = agent_pubkey.clone();
195
196 async move {
197 let encrypted_entry_bytes = entry
198 .into_option()
199 .map(|entry| {
200 let entry = holochain_serialized_bytes::encode(&entry)?;
201 tracing::warn!(
202 "CHC is using unencrypted entry data. TODO: add encryption"
203 );
204
205 ChcResult::Ok(entry)
206 })
207 .transpose()?;
208 let encrypted_entry = if let Some(bytes) = encrypted_entry_bytes {
209 let signature = keystore
210 .sign(agent_pubkey.clone(), bytes.clone().into())
211 .await?;
212 Some((Arc::new(bytes.into()), signature))
213 } else {
214 None
215 };
216 let signed_action_msgpack = holochain_serialized_bytes::encode(&signed_action)?;
217 let author = signed_action.action().author();
218
219 let signed_action_signature = author
220 .sign_raw(&keystore, signed_action_msgpack.clone().into())
221 .await?;
222
223 assert!(author
224 .verify_signature_raw(
225 &signed_action_signature,
226 signed_action_msgpack.clone().into()
227 )
228 .await
229 .unwrap());
230 ChcResult::Ok(AddRecordPayload {
231 signed_action_msgpack,
232 signed_action_signature,
233 encrypted_entry,
234 })
235 }
236 },
237 ))
238 .await
239 .into_iter()
240 .collect::<Result<Vec<_>, _>>()
241 }
242}
243
244/// The request type for `add_records`
245pub type AddRecordsRequest = Vec<AddRecordPayload>;
246
247/// The request to retrieve records from the CHC.
248///
249/// If a `since_hash` is specified, all records with sequence numbers at and
250/// above the one at the given hash will be returned. If no `since_hash` is
251/// given, then all records will be returned.
252///
253/// Since this payload is signed, including a unique nonce helps prevent replay
254/// attacks.
255#[derive(Debug, serde::Serialize, serde::Deserialize)]
256pub struct GetRecordsPayload {
257 /// Only records beyond and including this hash are returned
258 pub since_hash: Option<ActionHash>,
259 /// Randomly selected nonce to prevent replay attacks
260 pub nonce: Nonce256Bits,
261}
262
263/// The full request for get_record_data
264#[derive(Debug, serde::Serialize, serde::Deserialize)]
265pub struct GetRecordsRequest {
266 /// The payload
267 pub payload: GetRecordsPayload,
268 /// The signature of the payload
269 pub signature: Signature,
270}
271
272/// Encrypted bytes of an Entry
273#[derive(Debug, serde::Serialize, serde::Deserialize, derive_more::From)]
274pub struct EncryptedEntry(#[serde(with = "serde_bytes")] pub Vec<u8>);
275
276/// Assemble records from a list of Actions and a map of Entries
277pub fn records_from_actions_and_entries(
278 actions: Vec<SignedActionHashed>,
279 mut entries: HashMap<EntryHash, Entry>,
280) -> ChcResult<Vec<Record>> {
281 let mut records = vec![];
282 for action in actions {
283 let entry = if let Some(hash) = action.hashed.entry_hash() {
284 Some(
285 entries
286 .remove(hash)
287 .ok_or_else(|| ChcError::MissingEntryForAction(action.as_hash().clone()))?,
288 )
289 } else {
290 None
291 };
292 let record = Record::new(action, entry);
293 records.push(record);
294 }
295 Ok(records)
296}
297
298#[allow(missing_docs)]
299#[derive(Debug, thiserror::Error)]
300pub enum ChcError {
301 #[error(transparent)]
302 SerializationError(#[from] SerializedBytesError),
303
304 #[error(transparent)]
305 JsonSerializationError(#[from] serde_json::Error),
306
307 #[error(transparent)]
308 LairError(#[from] one_err::OneErr),
309
310 /// The out of sync error only happens when you attempt to add actions
311 /// that would cause a fork with respect to the CHC. This can be remedied
312 /// by syncing.
313 #[error("Local chain is out of sync with the CHC. The CHC head has advanced beyond the first action provided in the `add_records` request. Try calling `get_record_data` from hash {1} (sequence #{0}).")]
314 InvalidChain(u32, ActionHash),
315
316 /// All other errors are due to an invalid request, which is a mistake
317 /// that can't be remedied other than by fixing the programming mistake
318 /// (which would be on the Holochain side)
319 /// Examples include:
320 /// - `Vec<AddRecordPayload>` must be sorted by `seq_number`
321 /// - There is a gap between the first action and the current CHC head
322 /// - The `Vec<AddRecordPayload>` does not constitute a valid chain (prev_action must be correct)
323 #[error("Invalid `add_records` payload. Seq number: {0}")]
324 NoRecordsAdded(u32),
325
326 /// An Action which has an entry was returned without the Entry
327 #[error("Missing Entry for ActionHash: {0}")]
328 MissingEntryForAction(ActionHash),
329
330 #[error("The CHC service is unreachable: {0}")]
331 ServiceUnreachable(String),
332
333 /// Unexpected error
334 #[error("Unexpected error: {0}")]
335 Other(String),
336}
337
338#[allow(missing_docs)]
339pub type ChcResult<T> = Result<T, ChcError>;