pub struct StateSnapshot {
pub version: u8,
pub entity_id: EntityId,
pub through_seq: u64,
pub chain_link: CausalLink,
pub state: Bytes,
pub horizon: ObservedHorizon,
pub created_at: u64,
pub bindings_bytes: Vec<u8>,
pub identity_envelope: Option<IdentityEnvelope>,
pub head_payload: Option<Bytes>,
}Expand description
A serializable state snapshot at a point in the causal chain.
Fields§
§version: u8Wire version this snapshot was produced under. Writers
always stamp SNAPSHOT_VERSION; readers accept v0 bytes
by surfacing them with the v1 defaults populated.
entity_id: EntityIdEntity this snapshot belongs to.
through_seq: u64Sequence number this snapshot is valid through.
chain_link: CausalLinkCausalLink at the snapshot point (for chain verification).
state: BytesSerialized daemon state (opaque bytes).
horizon: ObservedHorizonThe entity’s observed horizon at snapshot time.
created_at: u64Timestamp when snapshot was taken (unix nanos).
bindings_bytes: Vec<u8>Opaque wire slot for channel-re-bind metadata populated by
DAEMON_CHANNEL_REBIND_PLAN.md.
Stage 1 of the identity-migration plan lands this as an
always-empty Vec so the wire format is forward-compatible
with the channel-re-bind work even though the typed
DaemonBindings decoder isn’t yet present. Plan #1 will
decode these bytes into its own struct at restore time.
identity_envelope: Option<IdentityEnvelope>Encrypted ed25519 seed + attestation for cross-node identity
transport. Populated by
DAEMON_IDENTITY_MIGRATION_PLAN.md
Stage 3; Stage 1 always emits None. A None envelope on
restore means “public-identity migration” — the target gets
a read-only keypair that can still serve entity_id /
origin_hash queries but refuses to sign anything new.
head_payload: Option<Bytes>Runtime-only: payload bytes of the event at
chain_link.sequence. Required by
super::log::EntityLog::from_snapshot to validate the
next event’s parent_hash after restore (the chain validator
computes xxh3(prev_link_bytes ++ prev_payload)).
Not serialized — to_bytes / from_bytes skip this
field, so the wire format is unchanged. Callers reconstructing
a snapshot from the log have the head event in hand and
populate this via with_head_payload before passing the
snapshot to restore. Cross-node migration carries the head
event through the migration message itself, paired with the
snapshot bytes.
Option<Bytes> so the “no head_payload context attached”
case is structurally distinct from “head event genuinely
had an empty payload.” An empty-Bytes sentinel would
conflate both: assess_continuity would reject legitimate
non-genesis snapshots whose head event happens to carry an
empty payload as if they were missing-context. With the
Option, Some(Bytes::new()) is “head event payload is
empty” (legitimate) and None is “caller hasn’t populated
this field” (verification can’t proceed).
Default for snapshots deserialized from wire bytes is
None; callers populate it from the head event via
with_head_payload before EntityLog::from_snapshot can
validate subsequent events.
Implementations§
Source§impl StateSnapshot
impl StateSnapshot
Sourcepub const HEADER_SIZE: usize
pub const HEADER_SIZE: usize
Compact header size (excluding state payload). Historical constant from the v0 compat path — kept for any external caller that may still reference it for sizing math.
Sourcepub fn new(
entity_id: EntityId,
chain_link: CausalLink,
state: Bytes,
horizon: ObservedHorizon,
) -> StateSnapshot
pub fn new( entity_id: EntityId, chain_link: CausalLink, state: Bytes, horizon: ObservedHorizon, ) -> StateSnapshot
Create a new snapshot stamped with the current wire version and empty v1 extension fields.
Sourcepub fn with_head_payload(self, head_payload: Bytes) -> StateSnapshot
pub fn with_head_payload(self, head_payload: Bytes) -> StateSnapshot
Attach the head event’s payload bytes — needed by
EntityLog::from_snapshot to validate the next event’s
chain link after restore. Genesis snapshots
(chain_link.sequence == 0) carry empty bytes; subsequent
snapshots carry the payload of the event at
chain_link.sequence.
Sourcepub fn with_identity_envelope(
self,
source_kp: &EntityKeypair,
target_static_pub: [u8; 32],
) -> Result<StateSnapshot, EnvelopeError>
pub fn with_identity_envelope( self, source_kp: &EntityKeypair, target_static_pub: [u8; 32], ) -> Result<StateSnapshot, EnvelopeError>
Attach an identity envelope sealed to target_static_pub,
returning self by value so the call chains cleanly off
Self::new / the source’s snapshot-build path.
Fails with
EnvelopeError::SourceReadOnly
when source_kp is public-only — a public-only caller can’t
produce the attestation signature the target needs to verify.
The attestation transcript binds to self.chain_link, so the
resulting envelope is non-replayable at a different migration
point.
Sourcepub fn open_identity_envelope(
&self,
target_x25519_priv: &StaticSecret,
) -> Result<Option<EntityKeypair>, EnvelopeError>
pub fn open_identity_envelope( &self, target_x25519_priv: &StaticSecret, ) -> Result<Option<EntityKeypair>, EnvelopeError>
Open the attached identity envelope (if any) using the
target’s X25519 static private key. Returns the daemon’s
fully-keyed EntityKeypair, which the target-side
restore path uses instead of the caller-supplied fallback.
Returns Ok(None) when the snapshot has no envelope —
callers interpret this as “public-identity migration, target
gets a read-only keypair.” Returns Err if the envelope is
present but fails to verify / unseal; callers must treat
that as a terminal error, not a fallback trigger, or an
attacker could downgrade identity transport by tampering.
Sourcepub fn to_bytes(&self) -> Vec<u8> ⓘ
pub fn to_bytes(&self) -> Vec<u8> ⓘ
Serialize to bytes for transfer.
§v1 wire format
magic: 4 bytes (b"CDS1")
version: 1 byte (SNAPSHOT_VERSION)
entity_id: 32 bytes
through_seq: 8 bytes
chain_link: CAUSAL_LINK_SIZE bytes (28)
created_at: 8 bytes
state_len: 4 bytes (u32)
state: state_len bytes
bindings_len: 4 bytes (u32)
bindings: bindings_len bytes (opaque; see `bindings_bytes`)
envelope_flag: 1 byte (0 = none, 1 = present)
[envelope: 208 bytes] (if envelope_flag == 1)Horizon and head_payload are not serialized in the compact
format — head_payload is a runtime-only field populated by
the caller from the head event before invoking restore (see
the field’s doc).
Sourcepub fn try_to_bytes(&self) -> Result<Vec<u8>, SnapshotError>
pub fn try_to_bytes(&self) -> Result<Vec<u8>, SnapshotError>
Fallible serialization to bytes.
Returns SnapshotError::ExceedsWireFormat { .. } when
state.len() or bindings_bytes.len() exceeds the
u32::MAX wire-format cap (4 GiB). The wire format encodes
each as a u32 length prefix; a payload that overflows
would be permanently un-decodable.
to_bytes is on the migration / snapshot-send path, where
a panic on u32 length-prefix overflow would crash the
dispatch task without releasing locks. state is opaque
caller-supplied bytes (compute orchestrator, FFI clients)
and bindings_bytes is opaque externally-controlled
migration metadata, so the >4 GiB case is reachable from
outside-controlled inputs. Production callers should use
try_to_bytes and surface the error; the legacy to_bytes
wrapper is kept for well-known-bounded test callers.
Sourcepub fn from_bytes(data: &[u8]) -> Option<StateSnapshot>
pub fn from_bytes(data: &[u8]) -> Option<StateSnapshot>
Deserialize from bytes.
Accepts only v2 (post-audit-#102 wire bump) layouts. v1
and pre-magic v0 bytes are rejected — see the project
release notes for the migration cliff. The audit-#102 bump
changed IDENTITY_ENVELOPE_SIZE (208 → 209), shifting
every offset in the v1 trailer; a v1 reader cannot
consume v2 bytes correctly, and conversely. v1 bytes
reach this function with the current magic but a stale
version byte; the version-byte check inside from_bytes_v2
rejects them.
head_payload is runtime-only and always defaults to empty
after deserialize; callers must populate it from the head
event before passing the snapshot to
super::log::EntityLog::from_snapshot.
Trait Implementations§
Source§impl Clone for StateSnapshot
impl Clone for StateSnapshot
Source§fn clone(&self) -> StateSnapshot
fn clone(&self) -> StateSnapshot
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more