Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@

### Changed

- `ContentAddressV1` now lives in one module, `warp_core::content_address_v1`,
with content node ids, content and slot edge ids, and attachment digests.
Effect admission and read evidence use it, replacing the inline hashing in
derivation and `edict_effects::value_digest`. A duplicate outgoing
(edge type, target) is refused rather than hashed. Identities are unchanged:
a regression test admits ids captured before the move, and independent
vectors, frozen hex and mutation postcards check the law without calling
the module. A two-effect proposal, a content node with an edge from it, is
admitted under independently computed ids, and admission refuses a content
edge id computed for another target, a slot id from a content source, and a
node id that omits an edge the same proposal adds. Edges already in
ascending order are hashed without a copy, and admission sorts its own.

- An authored effect package no longer carries a single `obstruction_coordinate`.
Its embedded exports declare every obstruction the program can return, and
admission checks each arm against them. The effect-package shape exists only
Expand Down
162 changes: 162 additions & 0 deletions crates/warp-core/src/content_address_v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: Apache-2.0
// © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots>
//! `ContentAddressV1`: the identities Echo admits for authored graph effects.
//!
//! This module is the single implementation of identity law `1`. Effect
//! admission recomputes every id a proposal creates with it, and authored
//! programs will compute the same ids through Echo's pure identity intrinsics.
//! Every value below is part of the law: changing any of them is a new law.
//!
//! Every hash is BLAKE3 over the listed fields, concatenated with no separators.
//! Ids, warps and type ids are their 32 raw bytes; lengths and counts are
//! unsigned 64-bit little-endian.
//!
//! - **Attachment digest** of an atom: [`ATTACHMENT_VALUE_DOMAIN`] (with its NUL
//! terminator), the tag byte [`ATOM_ATTACHMENT_TAG`], the atom type id, the
//! atom's byte length, then the atom bytes.
//! - **Content node id:** [`NODE_ID_DOMAIN`], the warp id, the node type id, the
//! attachment digest of its atom, the number of outgoing edges, then for each
//! outgoing edge in ascending byte order of (edge type id, target node id)
//! the edge type id followed by the target node id.
//! - **Content edge id** (an edge whose source is a content node):
//! [`CONTENT_EDGE_ID_DOMAIN`], the warp id, the source node id, the edge type
//! id, the target node id.
//! - **Slot edge id** (an edge whose source is a mutable node):
//! [`SLOT_EDGE_ID_DOMAIN`], the warp id, the source node id, the edge type id.
//! The target is not part of it, so retargeting the slot keeps its id.
//!
//! **Duplicate outgoing edges.** A content node's outgoing edges are a set:
//! two edges with the same (edge type, target) from one content node would
//! have the same content edge id, which admission refuses as a duplicate
//! effect target. [`content_node_id`] therefore refuses a duplicate tuple
//! instead of hashing or silently dropping it, so it never names an edge set
//! admission could not accept. One edge type to several targets, or several
//! edge types to one target, are distinct edges.
//!
//! The law has no edge-count ceiling. Authoring interfaces may impose their
//! own; this module does not.
use crate::{EdgeId, Hash, NodeId, TypeId, WarpId};

/// Domain of an attachment value digest.
pub const ATTACHMENT_VALUE_DOMAIN: &[u8] = b"echo.bridge.attachment-value.v1\0";
/// Attachment tag of an atom value.
pub const ATOM_ATTACHMENT_TAG: u8 = 0;
/// Attachment tag of a descended-warp value, used by read evidence.
pub const DESCEND_ATTACHMENT_TAG: u8 = 1;
/// Domain of a content node id.
pub const NODE_ID_DOMAIN: &[u8] = b"echo.bridge.node-id.v1\0";
/// Domain of an edge id whose source is a content node.
pub const CONTENT_EDGE_ID_DOMAIN: &[u8] = b"echo.bridge.edge-id.v1\0";
/// Domain of an edge id whose source is a mutable node.
pub const SLOT_EDGE_ID_DOMAIN: &[u8] = b"echo.bridge.slot-edge-id.v1\0";

/// One outgoing edge of a content node, as it enters the node's identity.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OutgoingEdge {
/// The edge's type id.
pub edge_type: TypeId,
/// The edge's target node id.
pub to: NodeId,
}

/// Inputs that name no content node admission could accept.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ContentAddressError {
/// The same (edge type, target) appears more than once.
DuplicateOutgoingEdge {
/// The repeated edge type id.
edge_type: TypeId,
/// The repeated target node id.
to: NodeId,
},
}

/// The attachment value digest of an atom.
#[must_use]
pub fn attachment_digest(atom_type: TypeId, atom_bytes: &[u8]) -> Hash {
let mut hash = blake3::Hasher::new();
hash.update(ATTACHMENT_VALUE_DOMAIN);
hash.update(&[ATOM_ATTACHMENT_TAG]);
hash.update(&atom_type.0);
hash.update(&(atom_bytes.len() as u64).to_le_bytes());
hash.update(atom_bytes);
hash.finalize().into()
}

/// The attachment value digest of a descended warp, used by read evidence.
#[must_use]
pub fn descend_attachment_digest(warp: WarpId) -> Hash {
let mut hash = blake3::Hasher::new();
hash.update(ATTACHMENT_VALUE_DOMAIN);
hash.update(&[DESCEND_ATTACHMENT_TAG]);
hash.update(&warp.0);
hash.finalize().into()
}

/// The content address of a node with this atom and complete outgoing edge
/// set, in any order.
///
/// # Errors
///
/// [`ContentAddressError::DuplicateOutgoingEdge`] when two edges share an
/// (edge type, target).
pub fn content_node_id(
warp: WarpId,
node_type: TypeId,
atom_type: TypeId,
atom_bytes: &[u8],
outgoing: &[OutgoingEdge],
) -> Result<NodeId, ContentAddressError> {
// A strictly ascending input is already canonical and duplicate-free, so
// it is hashed in place; only an unordered input is copied and sorted.
let sorted;
let edges: &[OutgoingEdge] = if outgoing.windows(2).all(|pair| pair[0] < pair[1]) {
outgoing
} else {
let mut copy = outgoing.to_vec();
copy.sort_unstable();
if let Some(pair) = copy.windows(2).find(|pair| pair[0] == pair[1]) {
return Err(ContentAddressError::DuplicateOutgoingEdge {
edge_type: pair[0].edge_type,
to: pair[0].to,
});
}
sorted = copy;
&sorted
};
let mut hash = blake3::Hasher::new();
hash.update(NODE_ID_DOMAIN);
hash.update(&warp.0);
hash.update(&node_type.0);
hash.update(&attachment_digest(atom_type, atom_bytes));
hash.update(&(edges.len() as u64).to_le_bytes());
for edge in edges {
hash.update(&edge.edge_type.0);
hash.update(&edge.to.0);
}
Ok(NodeId(hash.finalize().into()))
}

/// The id of an edge whose source is a content node.
#[must_use]
pub fn content_edge_id(warp: WarpId, from: NodeId, edge_type: TypeId, to: NodeId) -> EdgeId {
let mut hash = blake3::Hasher::new();
hash.update(CONTENT_EDGE_ID_DOMAIN);
hash.update(&warp.0);
hash.update(&from.0);
hash.update(&edge_type.0);
hash.update(&to.0);
EdgeId(hash.finalize().into())
}

/// The id of an edge whose source is a mutable node; it names the slot, not
/// the target.
#[must_use]
pub fn slot_edge_id(warp: WarpId, from: NodeId, edge_type: TypeId) -> EdgeId {
let mut hash = blake3::Hasher::new();
hash.update(SLOT_EDGE_ID_DOMAIN);
hash.update(&warp.0);
hash.update(&from.0);
hash.update(&edge_type.0);
EdgeId(hash.finalize().into())
}
1 change: 0 additions & 1 deletion crates/warp-core/src/edict_effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ pub use package::{EffectPackageError, EffectProgram};
mod completion;
#[cfg(feature = "native_rule_bootstrap")]
pub(crate) mod derive;
mod value_digest;
#[cfg(feature = "native_rule_bootstrap")]
pub use derive::{DerivedEffectPatch, EffectDerivationError, EffectDerivationKind};
mod frame;
Expand Down
49 changes: 23 additions & 26 deletions crates/warp-core/src/edict_effects/derive/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::{
EffectDerivationKind, Hash, NodeId, NodeKey, NodeRecord, OutgoingEdgeRange, SlotId,
SlotReading, TypeId, WarpId,
};
use crate::content_address_v1;
pub(super) struct Resolved<'a> {
effect: &'a M,
node: Option<NodeRecord>,
Expand Down Expand Up @@ -291,19 +292,24 @@ pub(super) fn identities(
if schema.node_addressing(*ty) != Some(Addressing::Content) {
return Err(bad());
}
let mut edges = edges_for(node, effects);
edges.sort_by_key(|e| (e.ty, e.to));
let mut hash = blake3::Hasher::new();
hash.update(b"echo.bridge.node-id.v1\0");
hash.update(&node.warp_id.0);
hash.update(&ty.0);
hash.update(&super::super::value_digest::atom(atom));
hash.update(&(edges.len() as u64).to_le_bytes());
for edge in edges {
hash.update(&edge.ty.0);
hash.update(&edge.to.0);
}
if hash.finalize().as_bytes() != &node.local_id.0 {
let mut edges = edges_for(node, effects)
.iter()
.map(|e| content_address_v1::OutgoingEdge {
edge_type: e.ty,
to: e.to,
})
.collect::<Vec<_>>();
// Sorted here, the law hashes this vector without copying it.
edges.sort_unstable();
let id = content_address_v1::content_node_id(
node.warp_id,
*ty,
atom.type_id,
&atom.bytes,
&edges,
)
.map_err(|_| bad())?;
if id != node.local_id {
return Err(bad());
}
}
Expand Down Expand Up @@ -358,19 +364,10 @@ pub(super) fn identities(
Ok(())
}
fn edge_id(warp: WarpId, from: NodeId, ty: TypeId, to: NodeId, kind: Addressing) -> Hash {
let mut hash = blake3::Hasher::new();
hash.update(if kind == Addressing::Content {
b"echo.bridge.edge-id.v1\0"
} else {
b"echo.bridge.slot-edge-id.v1\0"
});
hash.update(&warp.0);
hash.update(&from.0);
hash.update(&ty.0);
if kind == Addressing::Content {
hash.update(&to.0);
match kind {
Addressing::Content => content_address_v1::content_edge_id(warp, from, ty, to).0,
Addressing::Mutable => content_address_v1::slot_edge_id(warp, from, ty).0,
}
hash.finalize().into()
}
pub(super) fn writes(resolved: &[Resolved<'_>]) -> BTreeSet<SlotId> {
let mut slots = BTreeSet::new();
Expand Down Expand Up @@ -485,7 +482,7 @@ pub(super) fn preconditions(
{
Some(EffectDerivationKind::ContentIdentityConflict)
}
M::ReplaceAtom { expected, .. } if !matches!(&r.atom,Some(AttachmentValue::Atom(atom)) if super::super::value_digest::atom(atom)==*expected) => {
M::ReplaceAtom { expected, .. } if !matches!(&r.atom,Some(AttachmentValue::Atom(atom)) if content_address_v1::attachment_digest(atom.type_id, &atom.bytes)==*expected) => {
Some(EffectDerivationKind::PreconditionMismatch)
}
M::EnsureEdge { .. } if r.edge.is_some() && !r.noop => {
Expand Down
8 changes: 2 additions & 6 deletions crates/warp-core/src/edict_effects/read_image/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fn prepare<'a>(
(SlotId::Attachment(_), SlotReading::Attachment(Some(AttachmentValue::Atom(atom))))
if atom.type_id == ty =>
{
row.subject = super::super::value_digest::atom(atom);
row.subject = crate::content_address_v1::attachment_digest(atom.type_id, &atom.bytes);
}
(
SlotId::Attachment(key),
Expand All @@ -147,11 +147,7 @@ fn prepare<'a>(
return Err(Error::InconsistentObservation);
}
row.outcome = 3;
let mut hash = blake3::Hasher::new();
hash.update(b"echo.bridge.attachment-value.v1\0");
hash.update(&[1]);
hash.update(&warp.0);
row.subject = hash.finalize().into();
row.subject = crate::content_address_v1::descend_attachment_digest(*warp);
}
(SlotId::OutgoingEdges(range), SlotReading::OutgoingEdges(edges)) if ty.0 == [0; 32] => {
let owner = source.readings.get(&SlotId::Node(range.source));
Expand Down
12 changes: 0 additions & 12 deletions crates/warp-core/src/edict_effects/value_digest.rs

This file was deleted.

2 changes: 2 additions & 0 deletions crates/warp-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub mod math {
/// WSC (Write-Streaming Columnar) snapshot format for deterministic serialization.
pub mod wsc;

pub mod content_address_v1;

mod actual_footprint;
mod admission;
#[cfg(feature = "application_root_shadow")]
Expand Down
Loading
Loading