Skip to content

governance metadata: applier-stamped updated_at is non-deterministic (latent divergence if metadata ever becomes consensus-relevant) #3101

Description

@chefsale

Summary

The three governance metadata apply handlers stamp MetadataRecord.updated_at with now_millis() at apply time, so two nodes applying the same replicated op write different bytes for that field.

  • crates/governance-store/src/ops/group/group_metadata_set.rs
  • crates/governance-store/src/ops/group/member_metadata_set.rs
  • crates/governance-store/src/ops/group/context_metadata_set.rs

Each does roughly:

MetadataRepository::new(store).set_group(
    group_id,
    &MetadataRecord {
        name: name.clone(),
        data: data.clone(),
        updated_at: now_millis(),   // <- wall clock, per-node, at apply time
        updated_by: *signer,
    },
)?;

Why it's harmless today

updated_at is not a consensus value. The invariant is documented in crates/governance-store/src/metadata.rs:

metadata is not consensus-relevant: a MetadataSet op doesn't change the group state hash, and updated_at is applier-stamped.

Nothing hashes updated_at, and no peer compares it, so the differing bytes never cause divergence or op rejection. It behaves as a node-local "last modified" observation, which is legitimately per-node.

Why it's worth tracking anyway

The "metadata is non-consensus" invariant is load-bearing but enforced only by docs/convention, not by the type system or any test. The moment someone folds metadata into a state hash, an anchor snapshot, or any cross-peer hash comparison, applier-stamping silently becomes a real fleet-wide divergence bug — and it will be non-obvious because the op itself looks deterministic.

Possible solutions

  1. Make the timestamp op-supplied and signature-covered (full fix).
    Add a timestamp field to the three metadata GroupOp variants (GroupMetadataSet / MemberMetadataSet / ContextMetadataSet) in calimero-governance-types, populate it once at the op-construction site (the CLI/handler that builds the op), and cover it under the existing signature (it's part of SignableGroupOp). Every node then writes byte-identical updated_at. This is a wire-format change (pre-1.0, so acceptable) touching the op enum + builders + apply handlers.

    • Note: SignedGroupOp/SignableGroupOp currently carry no clock at all (only a per-signer nonce), so there is no existing HLC to reuse — hence the new field.
  2. Guard the invariant instead of the value (cheap, defensive).
    Keep applier-stamping, but make the "non-consensus" contract explicit and testable: e.g. a comment + assertion at the metadata write sites and/or a unit test asserting that updated_at/updated_by are excluded from compute_group_state_hash / snapshot_context_state_hashes. This makes a future "metadata is now hashed" change trip a red test instead of shipping a silent divergence.

Recommendation

Given metadata is non-consensus by design, option 2 (invariant guard) is the low-cost win; option 1 only becomes necessary if/when metadata is promoted into a consensus hash.

Context: surfaced during the determinism review that produced #3096 (deliberately left out of that PR as by-design, non-consensus).

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