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>;