Skip to content

DAG: unify the genesis-op parent convention (empty parents vs [0; 32]) #3126

Description

@chefsale

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:

  1. can_apply can treat an empty parent list as malformed and pend/reject it, closing the disconnected-head hole.
  2. The [0; 32] genesis branch already in can_apply handles real roots, so genesis ops still apply immediately.
  3. 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

  • Genesis ops are created with parents = [[0; 32]] in the production write path (governance + any other op producers).
  • DagStore::can_apply rejects (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.
  • Confirm ancestry walks / acl_view_at / cut_ancestry_complete handle the [0; 32] genesis parent correctly (they already skip genesis in the DAG; verify the op-layer equivalents).
  • Remove the NOTE: an empty parent list is NOT rejected here workaround comment in can_apply once the guard is in place.

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions