Summary
The DAG carries two conflicting conventions for a genesis (root) delta's parents, and the disagreement leaves a small correctness/robustness hole that could not be closed in #3116.
- The DAG's own model treats the explicit zero hash
[0; 32] as the genesis parent — see the Genesis (zero hash) is always considered applied branch in DagStore::can_apply (crates/dag/src/lib.rs).
- The unified-op layer creates genesis ops with an empty parent list (
parents: vec![]) and relies on all() over an empty iterator being vacuously true so the op applies immediately as a root. Deltas mirror the op's parents verbatim (CausalDelta::new(op.id(), op.parents.clone(), …) in crates/context/src/unified_applier.rs and crates/context/src/unified_op_store.rs; genesis-parent derivation flows through crates/context/src/governance_dag.rs::make_delta).
Why it matters
Because empty parents are a legitimate genesis signal in the unified-op layer, can_apply cannot distinguish a genuine genesis from a malformed/hostile empty-parent delta. That is exactly the reported finding #3116 could not fix:
[M] can_apply is vacuously true for empty parents — a non-genesis delta with parents: [] applies immediately as a disconnected head, bypassing the missing-parent machinery and polluting head computation.
In #3116 the obvious guard (reject empty-parent deltas) was implemented and then reverted, because it stranded legitimate genesis ops and broke the calimero-context suite. The current code documents the conflict inline in can_apply (see the NOTE: an empty parent list is NOT rejected here comment) rather than fixing it.
Net effect today: a corrupt or hostile peer can send an empty-parent, non-genesis delta and it applies as a disconnected head. Convergence still holds (it's deterministic), but the missing-parent/backfill path is bypassed and head computation is polluted.
Proposed resolution
Standardize genesis ops on the DAG's existing sentinel: have the unified-op / governance write path stamp a genesis op with parents: vec![[0; 32]] instead of parents: vec![]. Then:
can_apply can treat an empty parent list as malformed and pend/reject it, closing the disconnected-head hole.
- The
[0; 32] genesis branch already in can_apply handles real roots, so genesis ops still apply immediately.
- One convention across the DAG and the op layers.
This is pre-1.0, so the wire/format change is acceptable (no back-compat shim needed), but it touches op-id hashing for genesis ops (parents are part of the content hash), so it must be rolled out atomically and the projection/ancestry walks re-checked.
Scope / acceptance criteria
References
Summary
The DAG carries two conflicting conventions for a genesis (root) delta's parents, and the disagreement leaves a small correctness/robustness hole that could not be closed in #3116.
[0; 32]as the genesis parent — see theGenesis (zero hash) is always considered appliedbranch inDagStore::can_apply(crates/dag/src/lib.rs).parents: vec![]) and relies onall()over an empty iterator being vacuously true so the op applies immediately as a root. Deltas mirror the op's parents verbatim (CausalDelta::new(op.id(), op.parents.clone(), …)incrates/context/src/unified_applier.rsandcrates/context/src/unified_op_store.rs; genesis-parent derivation flows throughcrates/context/src/governance_dag.rs::make_delta).Why it matters
Because empty parents are a legitimate genesis signal in the unified-op layer,
can_applycannot distinguish a genuine genesis from a malformed/hostile empty-parent delta. That is exactly the reported finding #3116 could not fix:In #3116 the obvious guard (reject empty-parent deltas) was implemented and then reverted, because it stranded legitimate genesis ops and broke the
calimero-contextsuite. The current code documents the conflict inline incan_apply(see theNOTE: an empty parent list is NOT rejected herecomment) rather than fixing it.Net effect today: a corrupt or hostile peer can send an empty-parent, non-genesis delta and it applies as a disconnected head. Convergence still holds (it's deterministic), but the missing-parent/backfill path is bypassed and head computation is polluted.
Proposed resolution
Standardize genesis ops on the DAG's existing sentinel: have the unified-op / governance write path stamp a genesis op with
parents: vec![[0; 32]]instead ofparents: vec![]. Then:can_applycan treat an empty parent list as malformed and pend/reject it, closing the disconnected-head hole.[0; 32]genesis branch already incan_applyhandles real roots, so genesis ops still apply immediately.This is pre-1.0, so the wire/format change is acceptable (no back-compat shim needed), but it touches op-id hashing for genesis ops (parents are part of the content hash), so it must be rolled out atomically and the projection/ancestry walks re-checked.
Scope / acceptance criteria
parents = [[0; 32]]in the production write path (governance + any other op producers).DagStore::can_applyrejects (pends) a delta with a truly empty parent list; add a regression test.calimero-context(unified_applier/unified_op_store) and projection suites updated and green.acl_view_at/cut_ancestry_completehandle the[0; 32]genesis parent correctly (they already skip genesis in the DAG; verify the op-layer equivalents).NOTE: an empty parent list is NOT rejected hereworkaround comment incan_applyonce the guard is in place.References
can_applyinline NOTE).crates/dag/src/lib.rs(can_apply),crates/context/src/unified_applier.rs,crates/context/src/unified_op_store.rs,crates/context/src/governance_dag.rs.