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
-
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.
-
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).
Summary
The three governance metadata apply handlers stamp
MetadataRecord.updated_atwithnow_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.rscrates/governance-store/src/ops/group/member_metadata_set.rscrates/governance-store/src/ops/group/context_metadata_set.rsEach does roughly:
Why it's harmless today
updated_atis not a consensus value. The invariant is documented incrates/governance-store/src/metadata.rs: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
Make the timestamp op-supplied and signature-covered (full fix).
Add a timestamp field to the three metadata
GroupOpvariants (GroupMetadataSet/MemberMetadataSet/ContextMetadataSet) incalimero-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 ofSignableGroupOp). Every node then writes byte-identicalupdated_at. This is a wire-format change (pre-1.0, so acceptable) touching the op enum + builders + apply handlers.SignedGroupOp/SignableGroupOpcurrently carry no clock at all (only a per-signernonce), so there is no existing HLC to reuse — hence the new field.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_byare excluded fromcompute_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).