Boundary 1 (Identity & Hashing Spine) is the foundational leaf of the
boundary-layer architecture. It lives in crates/gossip-contracts/src/identity/
across 11 source files. No other boundary module may be referenced from here --
all four sibling boundaries depend on identity, but identity depends on none
of them.
The module provides four core capabilities:
- Content-addressed identity types -- a hierarchy of strongly-typed 32-byte identifiers (plus a few variable-width types) that represent items, secrets, findings, and occurrences.
- Canonical encoding -- the
CanonicalBytestrait and its primitive implementations, providing deterministic, collision-free binary serialization for hash-input construction. - Domain-separated hashing -- a registry of 18 domain constants and two hashing modes (keyed and derive-key) that prevent cross-derivation and cross-tenant collisions.
- Rule fingerprint derivation --
derive_rule_fingerprintcomputes a position-independentRuleFingerprintfrom a rule's name via BLAKE3 derive-key withRULE_FINGERPRINT_V1, so the same rule always produces the same fingerprint regardless of compilation order.
Every piece of data that flows through the detection pipeline receives a cryptographic identity derived through this spine. The module enforces correctness at compile time through Rust's type system: restricted constructors, selective trait implementations, and nominally distinct newtypes generated by macros.
| File | Role |
|---|---|
mod.rs |
Module root, public re-exports |
types.rs |
TenantId, PolicyHash, TenantSecretKey |
canonical.rs |
CanonicalBytes trait + primitive impls |
hashing.rs |
domain_hasher, finalize_32, finalize_64, derive_from_cached |
domain.rs |
18 domain-separation constants + ALL registry |
item.rs |
ConnectorTag, ConnectorInstanceIdHash, ItemIdentityKey, StableItemId, ObjectVersionId, IdentityInputError |
finding.rs |
NormHash, SecretHash, RuleFingerprint, FindingId, OccurrenceId, ObservationId + derivation fns (derive_rule_fingerprint, derive_finding_id, derive_occurrence_id, derive_observation_id) |
policy.rs |
IdHashMode, PolicyHashInputs, compute_policy_hash |
macros.rs |
define_id_32!, define_id_32_restricted!, smoke-test macros |
coordination.rs |
RunId, ShardId, WorkerId, OpId, JobId, FenceEpoch, LogicalTime (64-bit types) plus ShardKey (16-byte compound key) |
golden.rs |
Golden vector tests (9 derivations) |
NormHash ──────────┐
├─ key_secret_hash() ──> SecretHash ──┐
TenantSecretKey ───┘ │
│
TenantId ────────────────────────┤
├─ derive_finding_id() ──> FindingId ──┐
ConnectorInstanceIdHash ─┐ │ │
ConnectorTag ────────────┼─ ItemIdentityKey │ │
locator ─────────────────┘ │ │ │
StableItemId ──────────────────┤ │
│ │
rule_name ──── derive_rule_fingerprint() ──> RuleFingerprint ───┘ │
│
├─ derive_occurrence_id() ──> OccurrenceId ──┐
ObjectVersionId ─────────────────────────────┤ │
byte_offset (u64) ───────────────────────────┤ │
byte_length (u64) ───────────────────────────┘ │
│
TenantId ────────────────────────┐ │
├─ derive_observation_id() ──> ObservationId
PolicyHash ──────────────────────┤
OccurrenceId ────────────────────┘
| Type | Role |
|---|---|
ConnectorInstanceIdHash |
Fixed-width (32 B) hash of connector-instance identifier (e.g., repo path), scopes items per instance |
ItemIdentityKey |
Human-meaningful item identity: ConnectorTag (8 B) + ConnectorInstanceIdHash (32 B) + locator |
StableItemId |
Fixed-width (32 B) content-addressed identity derived from ItemIdentityKey via domain::ITEM_ID_V1 |
NormHash |
Normalized secret digest from the detection engine (tenant-agnostic) |
SecretHash |
Tenant-scoped secret identity, derived by keying NormHash with TenantSecretKey |
RuleFingerprint |
Position-independent identity of the detection rule, derived from the rule name via derive_rule_fingerprint() using BLAKE3 derive-key with RULE_FINGERPRINT_V1 |
FindingId |
Version-stable finding identity: (TenantId, StableItemId, RuleFingerprint, SecretHash) |
ObjectVersionId |
Version-specific content identity (Git blob OID, S3 ETag, etc.) |
OccurrenceId |
Version-specific occurrence: (FindingId, ObjectVersionId, byte_offset, byte_length) |
ObservationId |
Policy-scoped detection event: (TenantId, PolicyHash, OccurrenceId) |
FindingId captures the statement "rule R found secret S in item I for
tenant T." This statement is true regardless of which version of the item
contained the match. By excluding ObjectVersionId, the same finding persists
across object versions, enabling stable triage state. Version-specific location
information is captured separately in OccurrenceId.
FindingIdInputs is a 128-byte struct (4 x 32 B, all fixed-width):
pub struct FindingIdInputs {
pub tenant: TenantId, // 32 bytes
pub item: StableItemId, // 32 bytes
pub rule: RuleFingerprint, // 32 bytes
pub secret: SecretHash, // 32 bytes (NOT NormHash)
}The secret field takes SecretHash (post-keying), not raw NormHash. This
ensures tenant isolation is baked into the finding identity.
Boundary 1 intentionally keeps per-derivation input structs (FindingIdInputs,
OccurrenceIdInputs, ObservationIdInputs) instead of introducing one
cross-cutting context bag. The scoping axes do not actually line up into a
single reusable bundle:
TenantIdscopes finding and observation identitiesConnectorInstanceIdHashis consumed earlier when derivingStableItemIdPolicyHashscopes observations only
A shared DerivationContext would therefore carry unused fields through most
call sites and obscure which derivation owns which scope. The code keeps those
boundaries explicit and local.
The IdHashMode enum selects between two hashing strategies:
#[repr(u8)]
pub enum IdHashMode {
Unkeyed = 0, // Single global hash domain
KeyedV1 = 1, // Per-tenant keyed hashing
}All derivation functions except key_secret_hash use BLAKE3's derive-key mode
via domain_hasher:
let mut h = Hasher::new_derive_key("gossip/finding/v1");
inputs.write_canonical(&mut h);
let id = finalize_32(&h);The domain string is consumed as a key-derivation context, producing a context-dependent key schedule. Two hashers with different domain tags behave as independent hash functions.
For simple fixed-field input structs, the crate now generates
CanonicalBytes from the struct declaration itself via a declarative helper in
identity/macros.rs, so declaration order remains the single source of truth
for hash order.
key_secret_hash is the sole derivation that uses BLAKE3 keyed mode
(Hasher::new_keyed):
pub fn key_secret_hash(key: &TenantSecretKey, norm: &NormHash) -> SecretHash {
let mut h = Hasher::new_keyed(key.as_bytes());
h.update(domain::SECRET_HASH_V1.as_bytes()); // domain tag as data
h.update(norm.as_bytes());
SecretHash::from_bytes_internal(finalize_32(&h))
}The domain tag SECRET_HASH_V1 is fed as data inside the keyed hasher, not
as a derive-key context. This is because the hasher is already in keyed mode --
the tenant's secret key is the cryptographic context. The domain tag acts as a
versioning prefix that separates this derivation from any other potential use of
the same tenant key.
Because SecretHash = BLAKE3_keyed(tenant_key, domain_tag || norm_hash):
- Same
NormHash+ different keys --> differentSecretHashvalues - An attacker with access to one tenant's
SecretHashvalues cannot determine whether another tenant found the same secret - Precomputed rainbow tables are useless without the tenant's key
| Type | Width | Construction | Domain Constant | Key Traits | Restricted? |
|---|---|---|---|---|---|
TenantId |
32 B | from_bytes (pub) |
-- | Clone Copy Eq Ord Hash CanonicalBytes | No |
PolicyHash |
32 B | from_bytes (pub) |
POLICY_HASH_V2 (via compute_...) |
Clone Copy Eq Ord Hash CanonicalBytes | No |
TenantSecretKey |
32 B | from_bytes (pub) |
-- | Clone Copy Eq (constant-time) | Yes: no Ord, Hash, CanonicalBytes; redacted Debug |
ConnectorTag |
8 B | from_ascii / from_bytes |
-- | Clone Copy Eq Ord Hash CanonicalBytes | No |
ConnectorInstanceIdHash |
32 B | from_instance_id_bytes |
CONNECTOR_INSTANCE_ID_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
ItemIdentityKey |
variable | new(connector, instance, locator) |
-- | Clone Eq Hash CanonicalBytes | No |
StableItemId |
32 B | derived via ItemIdentityKey::stable_id() |
ITEM_ID_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
ObjectVersionId |
32 B | from_version_bytes / from_bytes |
OBJECT_VERSION_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
NormHash |
32 B | from_digest (pub) / from_bytes_internal (pub(crate)) |
-- | Clone Copy Eq Ord Hash CanonicalBytes | Yes: redacted Debug |
SecretHash |
32 B | from_bytes_internal (pub(crate)) |
SECRET_HASH_V1 (keyed mode) |
Clone Copy Eq Ord Hash CanonicalBytes | Yes: redacted Debug |
RuleFingerprint |
32 B | derive_rule_fingerprint(rule_name) / from_bytes (pub) |
RULE_FINGERPRINT_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
FindingId |
32 B | from_bytes (pub) |
FINDING_ID_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
OccurrenceId |
32 B | from_bytes (pub) |
OCCURRENCE_ID_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
ObservationId |
32 B | from_bytes (pub) |
OBSERVATION_ID_V1 |
Clone Copy Eq Ord Hash CanonicalBytes | No |
IdHashMode |
1 B (repr(u8)) |
from_u8 / variant literal |
-- | Clone Copy Debug Eq Hash CanonicalBytes | No |
FindingIdInputs |
128 B | struct literal | -- | Clone Copy Debug Eq CanonicalBytes | No |
OccurrenceIdInputs |
80 B | struct literal | -- | Clone Copy Debug Eq CanonicalBytes | No |
ObservationIdInputs |
96 B | struct literal | -- | Clone Copy Debug Eq CanonicalBytes | No |
PolicyHashInputs |
41 B | struct literal | -- | Clone Copy Debug Eq CanonicalBytes | No |
Persistence code reaches NormHash through
gossip_contracts::persistence::PersistenceFinding. Source-family finding
types may store the digest differently, but they are normalized to this
restricted newtype before key_secret_hash() derives the tenant-scoped
SecretHash.
All 18 domain constants live in domain.rs and follow the naming convention
"gossip/<subsystem>/v<N>[/<operation>]".
| Constant | Value | Subsystem | Hash Mode | Purpose |
|---|---|---|---|---|
SPLIT_ID_V1 |
gossip/coord/v1/split-id |
Coordination | derive-key | Shard-ID derivation during split operations |
OP_PAYLOAD_V1 |
gossip/coord/v1/op-payload |
Coordination | derive-key | Op-log payload hashing for idempotency |
FINDING_ID_V1 |
gossip/finding/v1 |
Identity | derive-key | FindingId derivation |
OCCURRENCE_ID_V1 |
gossip/occurrence/v1 |
Identity | derive-key | OccurrenceId derivation |
OBSERVATION_ID_V1 |
gossip/observation/v1 |
Identity | derive-key | ObservationId derivation |
SECRET_HASH_V1 |
gossip/secret-hash/v1 |
Identity | keyed | SecretHash keying (sole keyed-mode constant) |
ITEM_ID_V1 |
gossip/item-id/v1 |
Identity | derive-key | StableItemId derivation |
CONNECTOR_INSTANCE_ID_V1 |
gossip/connector-instance-id/v1 |
Identity | derive-key | ConnectorInstanceIdHash derivation |
OBJECT_VERSION_V1 |
gossip/object-version/v1 |
Identity | derive-key | ObjectVersionId derivation |
RULE_FINGERPRINT_V1 |
gossip/rule/v1 |
Identity | derive-key | RuleFingerprint derivation from rule name via derive_rule_fingerprint() |
POLICY_HASH_V2 |
gossip/policy-hash/v2 |
Policy | derive-key | PolicyHash derivation (v2: redesigned after spec) |
RULES_DIGEST_V1 |
gossip/rules-digest/v1 |
Policy | derive-key | Content-addressed hash of the full rule set |
OVID_V1 |
gossip/persistence/v1/ovid |
Persistence | derive-key | OVID (Object-Version Identity) hash |
DONE_LEDGER_KEY_V1 |
gossip/persistence/v1/done-key |
Persistence | derive-key | Done-ledger key derivation (reserved) |
TRIAGE_GROUP_KEY_V1 |
gossip/persistence/v1/triage-group |
Persistence | derive-key | TriageGroupKey derivation |
COORDINATION_TELEMETRY_V1 |
gossip/worker/v1/coordination-telemetry |
Worker | derive-key | Coordination telemetry redaction digest |
GIT_REPO_ID_V1 |
gossip/git/v1/repo-id |
Git | derive-key | Stable 64-bit repo-namespace derivation for repo-native Git scans |
GIT_MIRROR_PATH_V1 |
gossip/git/v1/mirror-path |
Git | derive-key | Stable 256-bit mirror-cache path derivation for repo-native Git scans |
- Compile-time array length -- The
ALLarray is declared as[&str; 18]. Adding a constant without updatingALLis a compile error. no_duplicate_valuestest -- IteratesALLthrough aHashSetand panics on collision.no_duplicate_namestest -- Checks the(name, value)fixture for name uniqueness.fixture_covers_all_constantstest -- Cross-checks the named fixture againstALLfor sync.- Format tests -- Validate printable-ASCII charset, naming convention, and length bounds (11..64 bytes).
| # | Invariant | Enforcement | Test Name(s) | Source File |
|---|---|---|---|---|
| 1 | CanonicalBytes collision-freedom (primitives) | proptest | u8_collision_free, u32_collision_free, u64_collision_free, slice_collision_free, fixed_32_collision_free |
canonical.rs |
| 2 | CanonicalBytes determinism (primitives) | proptest | u8_stable, u32_stable, u64_stable, slice_stable, fixed_32_stable |
canonical.rs |
| 3 | Variable-length prefix correctness | unit test | slice_length_prefixed, concatenation_unambiguous |
canonical.rs |
| 4 | Fixed-width encoding format | unit test | u8_writes_single_byte, u32_is_little_endian, u64_is_little_endian, fixed_32_no_length_prefix |
canonical.rs |
| 5 | Domain constant uniqueness | unit test | no_duplicate_values, no_duplicate_names |
domain.rs |
| 6 | Domain constant naming convention | unit test | all_constants_follow_naming_convention, all_constants_are_printable_ascii, all_constants_have_reasonable_length |
domain.rs |
| 7 | Domain constant registry coverage | compile-time (ALL array len) + unit |
fixture_covers_all_constants |
domain.rs |
| 8 | TenantId/PolicyHash trait completeness |
compile-time trait bound | tenant_id_implements_required_traits, policy_hash_implements_required_traits |
types.rs |
| 9 | TenantSecretKey trait restriction (no Ord/Hash) |
compile-time omission | tenant_secret_key_implements_required_traits |
types.rs |
| 10 | TenantSecretKey Debug redaction |
unit test | tenant_secret_key_debug_is_redacted |
types.rs |
| 11 | TenantSecretKey constant-time equality |
subtle::ConstantTimeEq impl |
(manual impl) | types.rs |
| 12 | NormHash/SecretHash Debug redaction |
unit test | restricted_types_debug_is_redacted |
finding.rs |
| 13 | key_secret_hash purity |
proptest | key_secret_hash_is_pure |
finding.rs |
| 14 | key_secret_hash collision-freedom |
proptest | secret_hash_collision_free |
finding.rs |
| 15 | FindingId purity |
proptest | finding_id_is_pure |
finding.rs |
| 16 | FindingId collision-freedom |
proptest | finding_id_collision_free |
finding.rs |
| 17 | FindingId per-field sensitivity |
proptest | finding_id_tenant_field_sensitivity, finding_id_item_field_sensitivity, finding_id_rule_field_sensitivity, finding_id_secret_field_sensitivity |
finding.rs |
| 18 | OccurrenceId purity |
proptest | occurrence_id_is_pure |
finding.rs |
| 19 | OccurrenceId collision-freedom |
proptest | occurrence_id_collision_free |
finding.rs |
| 20 | OccurrenceId per-field sensitivity |
proptest | occurrence_id_finding_field_sensitivity, occurrence_id_version_field_sensitivity, occurrence_id_offset_field_sensitivity, occurrence_id_length_field_sensitivity |
finding.rs |
| 21 | ObservationId purity |
proptest | observation_id_is_pure |
finding.rs |
| 22 | ObservationId collision-freedom |
proptest | observation_id_collision_free |
finding.rs |
| 23 | ObservationId per-field sensitivity |
proptest | observation_id_policy_field_sensitivity, observation_id_tenant_field_sensitivity, observation_id_occurrence_field_sensitivity |
finding.rs |
| 24 | StableItemId purity |
proptest | item_identity_key_stable_id_is_pure |
item.rs |
| 25 | StableItemId collision-freedom |
proptest | item_identity_key_stable_id_collision_free |
item.rs |
| 26 | ObjectVersionId purity |
proptest | object_version_id_is_pure |
item.rs |
| 27 | ObjectVersionId collision-freedom |
proptest | object_version_id_collision_free |
item.rs |
| 28 | ConnectorTag input validation |
unit test + proptest | connector_tag_empty_panics, connector_tag_too_long_panics, from_ascii_rejects_non_graphic, connector_tag_from_ascii_pads_correctly |
item.rs |
| 29 | PolicyHash purity |
proptest | compute_policy_hash_is_pure |
policy.rs |
| 30 | PolicyHash collision-freedom |
proptest | policy_hash_collision_free |
policy.rs |
| 31 | PolicyHash per-field sensitivity |
proptest | policy_hash_version_field_sensitivity, id_hash_mode_field_sensitivity, evidence_hash_version_field_sensitivity, rules_digest_field_sensitivity |
policy.rs |
| 32 | IdHashMode discriminant stability |
unit test | id_hash_mode_discriminants_are_stable, id_hash_mode_roundtrip, id_hash_mode_unknown_returns_none |
policy.rs |
| 33 | Macro-generated types: traits, Debug, Canonical | compile-time + unit + proptest | macro_types_implement_required_traits, pub_debug_shows_hex_prefix, restricted_debug_is_redacted, pub_canonical_bytes_stable (and more) |
macros.rs |
| 34 | Golden vectors (9 derivations) | unit test | connector_instance_id_hash_golden_value, stable_item_id_golden_value, object_version_id_golden_value, key_secret_hash_golden_value, derive_finding_id_golden_value, derive_occurrence_id_golden_value, derive_observation_id_golden_value, compute_policy_hash_golden_value, finalize_64_golden_value |
golden.rs |
| 35 | Golden vector registry completeness | unit test | registry_is_complete |
golden.rs |
| 36 | Full-chain determinism (composed) | proptest | full_chain_item_to_occurrence_is_pure |
golden.rs |
| 37 | Full-chain collision-freedom (composed) | proptest | full_chain_collision_free |
golden.rs |
| 38 | Boundary u64 values in OccurrenceId | unit test | boundary_u64_occurrence_id |
golden.rs |
| 39 | domain_hasher matches blake3::derive_key |
unit test | domain_hasher_matches_blake3_derive_key |
hashing.rs |
| 40 | Domain separation for random payloads | proptest | domain_separation_for_random_payload |
hashing.rs |
If a golden vector test fails, follow this 5-step protocol:
-
Confirm intent -- Determine whether the change was intentional (domain constant bump, encoding change) or an accidental regression.
-
If regression -- Revert the offending change. Golden vectors must not change without a version bump.
-
If intentional -- Bump the version suffix of the affected domain constant in
domain.rs(e.g.,FINDING_ID_V1->FINDING_ID_V2). -
Regenerate vectors -- Run the affected test with the assertion temporarily removed, capture the new output, and update the
const ..._EXPECTEDarray ingolden.rs. -
Update downstream -- Grep for the old domain constant name across all crates and update references. Add a migration note if persisted IDs exist.
| Vector | Domain Constant | Triggers |
|---|---|---|
CONNECTOR_INSTANCE_ID_HASH_EXPECTED |
CONNECTOR_INSTANCE_ID_V1 |
Instance-ID encoding or domain tag changes |
STABLE_ITEM_ID_EXPECTED |
ITEM_ID_V1 |
ItemIdentityKey encoding or domain tag changes |
OBJECT_VERSION_ID_EXPECTED |
OBJECT_VERSION_V1 |
ObjectVersionId encoding changes |
KEY_SECRET_HASH_EXPECTED |
SECRET_HASH_V1 |
Secret keying scheme changes |
FINDING_ID_EXPECTED |
FINDING_ID_V1 |
FindingIdInputs encoding changes |
OCCURRENCE_ID_EXPECTED |
OCCURRENCE_ID_V1 |
OccurrenceIdInputs encoding changes |
OBSERVATION_ID_EXPECTED |
OBSERVATION_ID_V1 |
ObservationIdInputs encoding changes |
POLICY_HASH_EXPECTED |
POLICY_HASH_V2 |
PolicyHashInputs encoding changes |
FINALIZE_64_EXPECTED |
(test-only domain) | finalize_64 truncation or endianness changes |
When a domain constant's version suffix is bumped (e.g., FINDING_ID_V1 →
FINDING_ID_V2), all IDs derived under the old version become stale. This
section documents the migration procedure and its blast radius.
A version bump is required whenever any of these change:
- The domain-separation constant string itself (changes the BLAKE3 key schedule)
- The
CanonicalBytesencoding for an input type (changes the hash input) - The field set or field order of an
*Inputsstruct - The hashing mode (derive-key vs. keyed) for a derivation
Version bumps cascade through the derivation chain. The table below shows which downstream IDs are invalidated when a given constant changes:
| Changed Constant | Directly Invalidated | Transitively Invalidated |
|---|---|---|
RULE_FINGERPRINT_V1 |
RuleFingerprint |
FindingId → OccurrenceId → ObservationId |
ITEM_ID_V1 |
StableItemId |
FindingId → OccurrenceId → ObservationId |
OBJECT_VERSION_V1 |
ObjectVersionId |
OccurrenceId → ObservationId |
SECRET_HASH_V1 |
SecretHash |
FindingId → OccurrenceId → ObservationId |
FINDING_ID_V1 |
FindingId |
OccurrenceId → ObservationId |
OCCURRENCE_ID_V1 |
OccurrenceId |
ObservationId |
POLICY_HASH_V2 |
PolicyHash |
(independent — triggers rescan via RunId) |
Dual-version coexistence is not supported. The system performs a clean-cut migration: old IDs become orphans and a full rescan is required for affected items. This design avoids the complexity of maintaining two derivation paths simultaneously.
Old IDs stored in persistence become orphans after a version bump:
FindingIdvalues in the done-ledger no longer match new derivations → findings are re-reported as new.OccurrenceIdvalues in occurrence stores no longer match → duplicate occurrences may be created until the old data is purged.PolicyHashchanges trigger newRunIdgeneration → the coordination layer treats all items as requiring rescan.
When a derivation constant changes:
- Bump the constant version suffix in
domain.rs. - Bump
CURRENT_VERSIONinpolicy.rs(forcesPolicyHashto change). - Changed
PolicyHash→ newRunId→ coordination layer marks all items as needing rescan. - Rescan produces new IDs under the updated derivation scheme.
- Bump the domain constant version suffix in
domain.rsand updateALLarray length. - Update
CURRENT_VERSIONinpolicy.rsif the change affects detection output identity. - Regenerate golden vectors following the protocol in section 7.
- Update downstream references — grep for the old constant name across all crates.
- Deploy — the new
PolicyHashautomatically triggers rescan on next run. - Purge stale data (optional) — remove orphaned IDs from persistence stores after rescan completes.
TenantSecretKey is the per-tenant BLAKE3 key used in key_secret_hash.
Rotating (replacing) a tenant's key has a well-defined blast radius.
TenantSecretKey change
└─► SecretHash (all secrets for the tenant are re-keyed)
└─► FindingId (all findings for the tenant change)
└─► OccurrenceId (all occurrences for the tenant change)
PolicyHash is not affected — it depends on the derivation scheme
(version numbers, hash mode, rules digest), not on any tenant key.
After key rotation:
- All
SecretHashvalues for the affected tenant become stale — the sameNormHashproduces a differentSecretHashunder the new key. - All
FindingIdvalues for the tenant change — findings that were previously triaged as "resolved" will reappear as new findings. - All
OccurrenceIdvalues for the tenant change — duplicate occurrences may be created until old data is purged.
Key rotation requires a full rescan of all items for the affected tenant. There is no dual-key coexistence mechanism — the rotation is a clean cut.
- Provision new key — replace the tenant's
TenantSecretKeyin the key store. - Trigger rescan — the coordination layer must re-derive all secrets, findings, and occurrences for the tenant.
- Purge stale data (optional) — remove orphaned IDs from persistence stores after rescan completes.
TenantSecretKey implements Copy instead of Zeroize-on-drop. See the
type-level documentation in types.rs for the full rationale. In short:
Rust forbids Drop on Copy types, and the current threat model does not
justify the complexity of a !Copy key wrapper with borrow-lifetime
entanglement throughout the derivation API.
The coordination.rs module defines protocol-critical newtypes for time
and fencing. These types enforce invariants that prevent split-brain
behaviour and immortal leases.
Monotonically increasing epoch for leader fencing. Prevents stale leaders from mutating shard state after a new leader has been elected. The epoch is incremented on every leader transition; operations bearing an outdated epoch are rejected.
| Constant / Method | Description |
|---|---|
ZERO |
Sentinel: "no epoch assigned" (pre-registration state) |
INITIAL |
First valid epoch (value 1); shard records start here after creation |
from_raw(u64) |
Construct from a raw integer |
as_raw(&self) -> u64 |
Return the inner value |
is_assigned(&self) -> bool |
true if not ZERO |
increment(&self) -> Self |
Advance by one. Panics at u64::MAX — saturating_add would silently produce duplicate epochs, breaking mutual exclusion |
FenceEpoch implements CanonicalBytes (8-byte LE, no length prefix).
Compile-time assertions verify ZERO.as_raw() == 0 and
INITIAL.as_raw() == 1.
Design reference: Kleppmann, "How to do distributed locking" (2016); Hochstein, "Fencing Tokens" FizzBee formal model (2025).
Monotonic logical clock for lease expiration and ordering. This is not wall-clock time — it is an abstract counter used for lease expiration, Lamport timestamps, and deterministic simulation clocks.
| Constant / Method | Description |
|---|---|
ZERO |
Origin of time; operations require now > ZERO |
from_raw(u64) |
Construct from a raw integer |
as_raw(&self) -> u64 |
Return the inner value |
checked_add(&self, duration: u64) -> Option<Self> |
Advance time by duration. Returns None on overflow to prevent immortal leases |
LogicalTime implements CanonicalBytes (8-byte LE, no length prefix).
Design reference: Gray & Cheriton, "Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency" (SOSP 1989).
ShardId::is_derived() returns true when bit 63 is set, marking the
shard as having been produced by derive_split_shard_id (in
gossip-coordination). Root shards (externally assigned) have bit 63
clear. This single-bit tag lets any layer distinguish root from split
shards without querying the coordinator.
TenantSecretKey::is_valid() returns true if the key is not all-zeros.
An all-zero key provides no tenant isolation and should be rejected during
provisioning. This is a necessary but not sufficient entropy check;
production provisioning should use cryptographically random key material.
Several identity types provide both panicking constructors (for trusted internal
code) and fallible try_* constructors (for external/untrusted input). The
fallible constructors return IdentityInputError (defined in item.rs).
pub enum IdentityInputError {
/// Connector tag is empty.
EmptyTag,
/// Connector tag exceeds 8 bytes.
TagTooLong(usize),
/// Connector tag contains a non-ASCII-graphic byte at the given index.
NonGraphicByte { index: usize, byte: u8 },
/// Connector instance ID bytes are empty.
EmptyConnectorInstanceId,
/// Item locator is empty.
EmptyLocator,
/// Version bytes are empty.
EmptyVersionBytes,
}All variants are self-describing leaf errors with no inner source. The type
implements Display, Debug, Clone, PartialEq, Eq, and
std::error::Error.
| Type | Method | Returns |
|---|---|---|
ConnectorTag |
try_from_ascii(&[u8]) |
Result<Self, IdentityInputError> |
ConnectorInstanceIdHash |
try_from_instance_id_bytes(&[u8]) |
Result<Self, IdentityInputError> |
ItemIdentityKey |
try_new(connector, connector_instance, locator) |
Result<Self, IdentityInputError> |
ObjectVersionId |
try_from_version_bytes(&[u8]) |
Result<Self, IdentityInputError> |
The panicking equivalents (from_ascii, new, from_version_bytes) remain
available for trusted internal code where input validation is already guaranteed
by the caller.
Truncates the BLAKE3 output to 64 bits (first 8 bytes, little-endian u64).
Used for op-log payload hashes and split shard ID derivation where 64-bit
collision resistance is sufficient. Birthday collision bound is approximately
2^32 (~4.3 billion) values.
pub fn finalize_64(hasher: &Hasher) -> u64Hot-path helper that clones a pre-initialized LazyLock<Hasher> static,
feeds CanonicalBytes inputs, and returns a 32-byte digest. Avoids
re-running the BLAKE3 key-schedule setup on every call.
pub fn derive_from_cached<T: CanonicalBytes>(base: &Hasher, inputs: &T) -> [u8; 32]| Constant | Value | Purpose |
|---|---|---|
CURRENT_VERSION |
1 |
Current identity derivation scheme version |
CURRENT_EVIDENCE_VERSION |
1 |
Current evidence encoding version |