holochain_types 0.8.0-dev.4

Holochain common types
Documentation
//! Signals which can be emitted from within Holochain, out across an interface.
//! There are two main kinds of Signal: system-defined, and app-defined:
//! - App-defined signals are produced via the `emit_signal` host function.
//! - System-defined signals are produced in various places in the system

use crate::app::{InstalledAppId, UnrecoverableCellReason};
use crate::impl_from;
use holochain_serialized_bytes::prelude::*;
use holochain_zome_types::prelude::*;

/// A Signal is some information emitted from within Holochain out through
/// an Interface
#[derive(Clone, Debug, Serialize, Deserialize, SerializedBytes, PartialEq, Eq)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/app/types.ts"))]
pub enum Signal {
    /// Signal from a Cell, generated by `emit_signal`
    App {
        /// The Cell from which the signal was emitted
        cell_id: CellId,
        /// The Zome from which the signal was emitted
        zome_name: ZomeName,
        /// The actual signal that was emitted
        signal: AppSignal,
    },
    /// A direct signal, sent with an app call to `SendDirectSignal`.
    AppDirect {
        /// The receiving `CellId`.
        cell_id: CellId,

        /// The agent who sent the signal.
        from_agent: AgentPubKey,

        /// The payload sent by the remote agent.
        signal: Vec<u8>,
    },
    /// System-defined signals
    System(SystemSignal),
}

impl Signal {
    /// Parse from vec.
    pub fn try_from_vec(v: Vec<u8>) -> Result<Self, SerializedBytesError> {
        Self::try_from(SerializedBytes::from(UnsafeBytes::from(v)))
    }
}

/// A Signal which originates from within the Holochain system, as opposed to
/// from within a Cell
#[derive(Clone, Debug, Serialize, Deserialize, SerializedBytes, PartialEq, Eq)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/app/types.ts"))]
pub enum SystemSignal {
    /// A countersigning session has successfully completed.
    SuccessfulCountersigning(EntryHash),
    /// A countersigning session has been abandoned.
    AbandonedCountersigning(EntryHash),
    /// A single cell within an app has finished restoring its source chain from the DHT.
    RestoreComplete {
        /// The cell that finished restoring.
        cell_id: CellId,
    },
    /// All cells of an app have finished restoring; the app is now Disabled(NeverStarted).
    AppRestoreComplete {
        /// The app that finished restoring.
        installed_app_id: InstalledAppId,
    },
    /// A cell's restore hit a permanent failure (e.g. validated ChainIntegrityWarrant). The whole
    /// app transitions to [`crate::app::AppStatus::Unrecoverable`] at the same moment.
    RestoreFailed {
        /// The cell that failed to restore.
        cell_id: CellId,
        /// Why the restore is unrecoverable.
        reason: UnrecoverableCellReason,
    },
}

impl_from! {
    SystemSignal => Signal, |s| { Self::System(s) },
}

/// The maximum size that Holochain will permit sending or receiving in a single direct signal.
pub const DIRECT_SIGNAL_MAX_SIZE: usize = 1024 * 1024;

/// Wrapper type for transmitting a signed direct signal over the network.
///
/// The payload is encoded as msgpack `bin` (one byte per byte) and the sender
/// signs the SHA2-512 hash of the encoding, never the raw bytes: lair signing
/// frames are capped at 8 KiB. The `bin` encoding starts with a msgpack bin
/// header, so the signed hash can never collide with a signed zome call,
/// whose parameters encode as a msgpack map.
#[derive(Debug, Clone, Serialize, Deserialize, SerializedBytes)]
pub struct DirectSignal(#[serde(with = "serde_bytes")] pub Vec<u8>);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::{UnrecoverableCellReason, WarrantSummary};
    use ::fixt::prelude::*;
    use holo_hash::fixt::*;

    #[test]
    fn restore_system_signals_serde_round_trip() {
        let cell_id = CellId::new(fixt!(DnaHash), fixt!(AgentPubKey));
        let summary = WarrantSummary {
            author: fixt!(AgentPubKey),
            warrantee: fixt!(AgentPubKey),
            timestamp: Timestamp::from_micros(1_000_000),
        };
        let reason = UnrecoverableCellReason::ChainForkWarrant(Box::new(summary));

        let signals: Vec<SystemSignal> = vec![
            SystemSignal::RestoreComplete {
                cell_id: cell_id.clone(),
            },
            SystemSignal::AppRestoreComplete {
                installed_app_id: "my_app".to_string(),
            },
            SystemSignal::RestoreFailed {
                cell_id: cell_id.clone(),
                reason: reason.clone(),
            },
        ];

        for signal in &signals {
            let json = serde_json::to_string(signal).unwrap();
            let recovered: SystemSignal = serde_json::from_str(&json).unwrap();
            assert_eq!(signal, &recovered);
        }
    }

    /// A max-size payload must encode as msgpack `bin` (1 byte per payload
    /// byte plus a 5-byte bin32 header), never as an array of ints, or the
    /// receiver's pre-decode size bound would reject legitimate signals.
    #[test]
    fn direct_signal_max_payload_encodes_as_bin() {
        // 0xFF forces the worst case: an array-of-ints encoding would need
        // two bytes per element and blow past the bound.
        let signal = DirectSignal(vec![0xFF; DIRECT_SIGNAL_MAX_SIZE]);

        let bytes = holochain_serialized_bytes::encode(&signal).unwrap();

        assert!(bytes.len() >= DIRECT_SIGNAL_MAX_SIZE);
        assert!(
            bytes.len() <= DIRECT_SIGNAL_MAX_SIZE + 5,
            "encoded direct signal is {} bytes, bound is {}",
            bytes.len(),
            DIRECT_SIGNAL_MAX_SIZE + 5
        );

        let decoded: DirectSignal = holochain_serialized_bytes::decode(&bytes).unwrap();
        assert_eq!(decoded.0, signal.0);
    }
}