Skip to content

Latest commit

 

History

History
614 lines (483 loc) · 47.1 KB

File metadata and controls

614 lines (483 loc) · 47.1 KB

Boundary 1 -- Identity & Hashing Spine

1. Overview

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 CanonicalBytes trait 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_fingerprint computes a position-independent RuleFingerprint from a rule's name via BLAKE3 derive-key with RULE_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.

Source files

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)

2. ID Hierarchy and Derivation Chain

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 ────────────────────┘

Role of each type

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)

Why ObjectVersionId is excluded from FindingId

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 structure

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.

Why there is no umbrella DerivationContext

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:

  • TenantId scopes finding and observation identities
  • ConnectorInstanceIdHash is consumed earlier when deriving StableItemId
  • PolicyHash scopes 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.


3. Keyed / Unkeyed Boundary

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
}

Derive-key mode (most derivations)

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.

Keyed mode (SecretHash only)

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.

Cross-tenant isolation guarantee

Because SecretHash = BLAKE3_keyed(tenant_key, domain_tag || norm_hash):

  • Same NormHash + different keys --> different SecretHash values
  • An attacker with access to one tenant's SecretHash values cannot determine whether another tenant found the same secret
  • Precomputed rainbow tables are useless without the tenant's key

4. Type Reference Table

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.


5. Domain Separation Registry

All 18 domain constants live in domain.rs and follow the naming convention "gossip/<subsystem>/v<N>[/<operation>]".

Constants

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

Uniqueness enforcement

  1. Compile-time array length -- The ALL array is declared as [&str; 18]. Adding a constant without updating ALL is a compile error.
  2. no_duplicate_values test -- Iterates ALL through a HashSet and panics on collision.
  3. no_duplicate_names test -- Checks the (name, value) fixture for name uniqueness.
  4. fixture_covers_all_constants test -- Cross-checks the named fixture against ALL for sync.
  5. Format tests -- Validate printable-ASCII charset, naming convention, and length bounds (11..64 bytes).

6. Invariant-to-Test Matrix

# 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

7. Golden Vector Regeneration Protocol

If a golden vector test fails, follow this 5-step protocol:

  1. Confirm intent -- Determine whether the change was intentional (domain constant bump, encoding change) or an accidental regression.

  2. If regression -- Revert the offending change. Golden vectors must not change without a version bump.

  3. If intentional -- Bump the version suffix of the affected domain constant in domain.rs (e.g., FINDING_ID_V1 -> FINDING_ID_V2).

  4. Regenerate vectors -- Run the affected test with the assertion temporarily removed, capture the new output, and update the const ..._EXPECTED array in golden.rs.

  5. Update downstream -- Grep for the old domain constant name across all crates and update references. Add a migration note if persisted IDs exist.

Version-bump trigger conditions

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

8. Version Migration Protocol

When a domain constant's version suffix is bumped (e.g., FINDING_ID_V1FINDING_ID_V2), all IDs derived under the old version become stale. This section documents the migration procedure and its blast radius.

What triggers a version bump

A version bump is required whenever any of these change:

  • The domain-separation constant string itself (changes the BLAKE3 key schedule)
  • The CanonicalBytes encoding for an input type (changes the hash input)
  • The field set or field order of an *Inputs struct
  • The hashing mode (derive-key vs. keyed) for a derivation

Blast radius

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 FindingIdOccurrenceIdObservationId
ITEM_ID_V1 StableItemId FindingIdOccurrenceIdObservationId
OBJECT_VERSION_V1 ObjectVersionId OccurrenceIdObservationId
SECRET_HASH_V1 SecretHash FindingIdOccurrenceIdObservationId
FINDING_ID_V1 FindingId OccurrenceIdObservationId
OCCURRENCE_ID_V1 OccurrenceId ObservationId
POLICY_HASH_V2 PolicyHash (independent — triggers rescan via RunId)

Dual-version coexistence

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.

Persistence impact

Old IDs stored in persistence become orphans after a version bump:

  • FindingId values in the done-ledger no longer match new derivations → findings are re-reported as new.
  • OccurrenceId values in occurrence stores no longer match → duplicate occurrences may be created until the old data is purged.
  • PolicyHash changes trigger new RunId generation → the coordination layer treats all items as requiring rescan.

PolicyHash interaction

When a derivation constant changes:

  1. Bump the constant version suffix in domain.rs.
  2. Bump CURRENT_VERSION in policy.rs (forces PolicyHash to change).
  3. Changed PolicyHash → new RunId → coordination layer marks all items as needing rescan.
  4. Rescan produces new IDs under the updated derivation scheme.

Step-by-step migration procedure

  1. Bump the domain constant version suffix in domain.rs and update ALL array length.
  2. Update CURRENT_VERSION in policy.rs if the change affects detection output identity.
  3. Regenerate golden vectors following the protocol in section 7.
  4. Update downstream references — grep for the old constant name across all crates.
  5. Deploy — the new PolicyHash automatically triggers rescan on next run.
  6. Purge stale data (optional) — remove orphaned IDs from persistence stores after rescan completes.

9. Key Rotation Strategy

TenantSecretKey is the per-tenant BLAKE3 key used in key_secret_hash. Rotating (replacing) a tenant's key has a well-defined blast radius.

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.

Persistence impact

After key rotation:

  • All SecretHash values for the affected tenant become stale — the same NormHash produces a different SecretHash under the new key.
  • All FindingId values for the tenant change — findings that were previously triaged as "resolved" will reappear as new findings.
  • All OccurrenceId values for the tenant change — duplicate occurrences may be created until old data is purged.

Required action: full rescan

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.

  1. Provision new key — replace the tenant's TenantSecretKey in the key store.
  2. Trigger rescan — the coordination layer must re-derive all secrets, findings, and occurrences for the tenant.
  3. Purge stale data (optional) — remove orphaned IDs from persistence stores after rescan completes.

Copy-vs-Zeroize design rationale

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.


10. Coordination Time and Fencing Types

The coordination.rs module defines protocol-critical newtypes for time and fencing. These types enforce invariants that prevent split-brain behaviour and immortal leases.

FenceEpoch

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::MAXsaturating_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).

LogicalTime

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

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

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.


11. Fallible Constructors and IdentityInputError

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).

IdentityInputError

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.

Fallible constructors

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.


12. Additional Hashing Helpers

finalize_64 (hashing.rs)

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) -> u64

derive_from_cached (hashing.rs)

Hot-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]

Policy version constants (policy.rs)

Constant Value Purpose
CURRENT_VERSION 1 Current identity derivation scheme version
CURRENT_EVIDENCE_VERSION 1 Current evidence encoding version