Authoritative state snapshots¶
This page is the normative PETRA 1.0 contract for recovering declared durable read models after the configured offline horizon. It extends the Zenoh and DDIL data-plane contract; it does not turn Web history into endpoint-authored traffic.
Contract target, not current implementation
The shared Common schema, verifier, and vectors are the first dependent slice, but purpose 3 remains unusable end to end until the remaining components adopt it. Directory issues no snapshot authority, Web has no cross-family deterministic cut, and consumers have no snapshot applier. The protocol below is the interoperability gate for the dependent Common, Directory, Web, Android, Node, Gateway, and Server work. Existing realtime “snapshot” APIs and replay exports are not this protocol.
Authority and purpose separation¶
Common appends one purpose without renumbering the existing values:
enum EnvelopePurpose {
ENVELOPE_PURPOSE_LIVE = 0;
ENVELOPE_PURPOSE_DURABLE_GRANT = 1;
ENVELOPE_PURPOSE_DURABLE_CONTENT = 2;
ENVELOPE_PURPOSE_AUTHORITATIVE_STATE_SNAPSHOT = 3;
}
Only an authenticated Web machine identity with a matching Directory-signed
snapshot_authority capability may sign purpose 3. Identity and snapshot authority are
separate proofs:
- machine identity is a Directory-signed
IdentityTokenplus a verified signature under that token'sprincipal_sign_key; and - snapshot authority is the separate Directory-signed grant below whose principal and signing key exactly equal the authenticated token.
The grant alone is not identity proof and never substitutes for the token or machine
signature. Directory issues it only while provisioning an active
platform_type=web machine principal, only for that machine's current token principal and
signing key, and only for the approved scopes, audience, and classification ceiling.
platform_type remains a Directory-side issuance constraint rather than a new
IdentityToken claim: Common verifies the token and exact grant/token binding, while
Directory is responsible for never issuing this grant to a human, browser session,
ordinary endpoint, Server, or non-Web machine. It is not a human role, cannot be delegated
or re-issued by Web, and is never minted by Relay + Storage.
Issuance requires the current
Web core-application relationship.
Directory carries the canonical grant in field 5 of every matching purpose-3
CapabilitySetEntryV1, alongside the exact snapshot publication capability. There is no
standalone snapshot-authority refresh. Complete-set verification installs the pair
atomically; relationship, key, family, scope, or audience removal omits the entry and
therefore removes both before snapshot publication, shared operation, or outbox drainage
resumes. A missing grant, orphan grant, or capability/grant mismatch invalidates the whole
refresh and preserves the prior cache only in degraded mode until its original absolute
deadlines.
The outer AuthEnvelope is signed by Web's device key and uses purpose 3. Verification
relaxes the short-lived identity-token expiry in the same way as other durable purposes,
but still requires:
- a valid Directory signature on the embedded machine
IdentityToken, a valid machine signature, and a matching grant valid at the outerAuthEnvelope.issued_at_ms; - the current revocation gate for the Web principal and signing key;
- exact snapshot key, family, scope, audience, classification, and capability binding;
- a valid Web device signature over the PETRA 1.0
AuthEnvelopesigning input; and - the fresh capability-use proof for publication or query.
PETRA 1.0 adds bytes snapshot_authority_grant = 9 to AuthEnvelope. It is empty for
purposes 0–2 and contains the deterministic protobuf bytes below for purpose 3. The
canonical signing input for every 1.0 envelope appends
u32be(len(snapshot_authority_grant)) || snapshot_authority_grant after the existing
purpose field, followed by the fixed 32-byte storage_key_digest field 10 defined by
#124, then
u32be(len(publication_capability)) || publication_capability for the canonical
Directory-signed field 11. This is an intentional flag-day wire break: pre-1.0 envelopes
and clients do not interoperate, and implementations carry no legacy signing-input
verifier. Relay + Storage persists the complete envelope, so the grant, durable key
binding, and publication authority remain available when a snapshot is queried later;
the ephemeral storage-use proof is not a substitute for them.
message SnapshotAuthorityScopeV1 {
SnapshotFamily family = 1;
string scope_segment = 2; // 32 lowercase hex; closed aggregate exception below
}
message SnapshotAuthorityGrantV1 {
uint32 version = 1;
string web_principal_id = 2;
bytes web_signing_key = 3; // Ed25519, exactly 32 bytes
repeated SnapshotAuthorityScopeV1 scopes = 4;
bytes audience_digest = 5; // exactly 32 bytes
Clearance classification_ceiling = 6;
uint64 issued_at_ms = 7;
uint64 expires_at_ms = 8;
bytes grant_id = 9; // exactly 16 bytes
bytes directory_key_id = 10; // SHA-256(Directory public key), 32 bytes
bytes directory_signature = 11; // Ed25519, exactly 64 bytes
string audience_segment = 12; // signed opaque key segment, 32 lowercase hex
}
Directory signs the exact snapshot_authority_grant_signing_input defined below.
Verification requires the embedded IdentityToken principal and signing key to equal fields
2–3, the requested family/scope pair to be contained by field 4, the snapshot audience
digest to equal field 5, and the parsed key's audience segment to equal signed field 12.
Common's normative CMBAC decide(snapshot.classification, classification_ceiling) must return
Permit. That decision validates the policy and classification rank, requires every
restrictive label value, requires at least one held value for every permissive tag group,
and ignores informative categories. Missing/malformed clearance, cross-policy denial, or
any other CMBAC denial fails closed. It also requires grant.issued_at_ms <=
AuthEnvelope.issued_at_ms < grant.expires_at_ms, a trusted Directory key whose
SHA-256(public_key) equals field 10, a valid signature, and current Web revocation state.
Directory computes field 12 as
opaque("snapshot-audience", lowercase_hex(audience_digest)) using the deployment opaque
salt and rejects a supplied value that does not match. A salt-holding Web publisher or
endpoint repeats that derivation when building or applying a snapshot. Relay + Storage
never receives the salt and therefore performs only the checks available at its trust
boundary: field 12 is canonical 32-character lowercase hex, its bytes exactly equal the
audience segment in the publication key, the family/scope pair is present in the signed
grant, and both the grant and exact-key StorageCapabilityV1 verify for the same
authenticated principal and signing key. Relay cannot prove the opaque preimage, audience
membership, or domain derivation and must not claim that it has done so.
All strings below are UTF-8. str32(s) = u32be(len(utf8(s))) || utf8(s) and
bytes32(b) = u32be(len(b)) || b. A list is u32be(count) followed by its elements.
Authority scopes are ascending unique (u32 family, UTF-8 scope_segment) pairs.
canonical_clearance(c) is:
str32(policy_id) || str32(max_classification)
|| u32be(category_count)
|| concat(
u32be(category_type) || str32(tag)
|| u32be(value_count) || concat(str32(value))
)
Clearance categories are sorted by (category_type, UTF-8 tag) with no duplicate type/tag;
their values are unique and sorted by UTF-8 bytes. The Directory rejects non-canonical
inputs rather than silently normalizing them. The exact grant input is:
ascii("petra-snapshot-authority-v1\0")
|| u32be(version)
|| str32(web_principal_id)
|| web_signing_key[32]
|| u32be(scope_count) || concat(u32be(family) || str32(scope_segment))
|| audience_digest[32]
|| str32(audience_segment)
|| bytes32(canonical_clearance(classification_ceiling))
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| grant_id[16]
|| directory_key_id[32]
An endpoint or ordinary Web browser identity signing purpose 3 fails closed. A Web
signature under LIVE, DURABLE_GRANT, or DURABLE_CONTENT is not a snapshot. A
purpose-3 value is never a command, event, receipt, acknowledgement, proof that an
endpoint was online, or replacement for the original endpoint signature.
Audience and encryption¶
Each snapshot is homogeneous for one recovery family, opaque scope, audience, classification label, and current encryption-key epoch. For PETRA 1.0, snapshot content uses the current deployment group-key epoch distributed by Directory. Relay + Storage is denied that key and retains only opaque sealed bytes.
The audience is enforced by the Directory-issued query capability, CMBAC label, current
ORBAT/channel policy at the endpoint, and the signed audience_digest. The deployment
group key remains a deployment-wide confidentiality boundary; it is not per-channel or
per-unit encryption. This preserves the existing product model rather than introducing a
new key hierarchy. A member that obtains snapshot ciphertext outside its authorized query
path retains the same broad group-key residual documented in the
security model.
Web and Directory use this closed audience schema:
enum SnapshotAudienceMemberKindV1 {
SNAPSHOT_AUDIENCE_MEMBER_KIND_UNSPECIFIED = 0;
SNAPSHOT_AUDIENCE_MEMBER_KIND_PRINCIPAL = 1;
SNAPSHOT_AUDIENCE_MEMBER_KIND_UNIT = 2;
SNAPSHOT_AUDIENCE_MEMBER_KIND_CHANNEL = 3;
}
message SnapshotAudienceMemberV1 {
SnapshotAudienceMemberKindV1 kind = 1;
string opaque_segment = 2; // registered #124 domain, 32 lowercase hex
}
message SnapshotAudienceDescriptorV1 {
uint32 version = 1; // exactly 1
string deployment_segment = 2; // audience-deployment, 32 lowercase hex
repeated SnapshotAudienceMemberV1 members = 3;
}
Directory adds one deployment-static deployment_id, minted as a canonical lowercase
RFC 4122 UUID during the 1.0 reset. The deployment segment is
opaque("audience-deployment", deployment_id). The member segment domain and canonical
identifier are fixed by kind: auth-principal over the exact Directory-issued
IdentityToken.principal_id for PRINCIPAL, record-orbat-unit over the exact signed
OrbatRecord.unit_id for UNIT, and channel-id over the exact signed channel id for
CHANNEL. Identifiers are case-sensitive UTF-8 and are never trimmed or normalized by a
consumer; their owning producer must emit the canonical form. members is a set sorted by
(u32 kind, UTF-8 opaque_segment); duplicates,
UNSPECIFIED, empty deployment, and non-canonical segments fail closed. An empty set means
the whole named deployment subject to CMBAC and current ORBAT/channel policy. Canonical
audience bytes are independent of protobuf encoding:
u32be(version) || str32(deployment_segment) || u32be(member_count)
|| concat(u32be(kind) || str32(opaque_segment))
audience_digest = SHA-256(canonical_audience_bytes). The snapshot key's audience segment
uses the #124 snapshot-audience domain over the lowercase hexadecimal audience digest,
not over cleartext identities. Web seals the snapshot under the current epoch; a snapshot
sealed only under an old, unavailable epoch cannot complete post-horizon recovery.
The descriptor's domain-to-kind rule is enforced where the deployment salt and canonical
source identifiers exist: Directory when issuing the matching grant, and a salt-holding
endpoint when matching its current audience. Relay + Storage can validate only the enum,
ordering, uniqueness, and opaque-segment syntax. Outputs from different opaque domains are
intentionally indistinguishable without the salt and preimage, so Relay must not infer or
claim that a PRINCIPAL, UNIT, or CHANNEL segment used the right derivation domain.
Key expression¶
Snapshots use a separate opaque namespace:
waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/<snapshot_seq>/manifest
waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/<snapshot_seq>/chunk/<index>
<family>is the exact literal mapped fromSnapshotFamily:drawing,target,channel,invite,plan,report-requirement,sitrep-current, ororbat.<scope_segment>is normally the #124 opaque derivation using domainsnapshot-scopeover the canonical family scope. The literal*is reserved for a Directory-issued automatic Web aggregate in theDRAWING,CHANNEL, orINVITEfamily. It contains only concrete 32-lowercase-hex scopes in that same family.<audience_segment>uses domainsnapshot-audienceover the lowercase hexadecimalaudience_digestand exactly equals signedSnapshotAuthorityGrantV1.audience_segment.<snapshot_seq>is canonical positiveuint64decimal with no leading zero; it is monotonic per(family, scope_segment, audience_digest). Classification is deliberately absent from the key, so every classification partition sharing that opaque namespace draws from this one sequence stream and can never publish the same key/sequence.<index>is canonicaluint32decimal starting at zero, with no leading zero.
Directory pre-authorizes future Web-allocated sequences with exactly two bounded PUBLISH selectors per signed family/scope/audience grant:
waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/*/manifest
waypoint/global/snapshot/<family>/<scope_segment>/<audience_segment>/*/chunk/*
Common recognizes * in the sequence and chunk-index selector positions. It also
recognizes the closed aggregate scope above only for DRAWING, CHANNEL, and INVITE;
the requested concrete key must retain the same family and audience and supply a valid
opaque scope. Every other scope remains byte-identical. It rejects **, wildcard family
or audience, aggregate scope in any other family, aggregate-to-aggregate use where a
concrete key is required, zero sequence, overflow, leading zero, manifest/chunk crossing,
and every missing or extra segment.
Action remains exact: a QUERY selector never publishes, and a PUBLISH selector never
queries. Only PUBLISH entries carry field-5 snapshot authority. This does not authorize
another family, scope, audience, action, or purpose.
Manifest and chunk keys are cross-bound to the decoded sealed payload. A value copied to
another family, scope, audience, sequence, or chunk index fails before application.
Zenoh delete is never used to retract a snapshot; supersession is semantic and retained
values expire under the deployment horizon.
The canonical family scopes and atomic replacement boundaries are closed in version 1:
| Family | Canonical family scope | Snapshot replacement boundary |
|---|---|---|
DRAWING |
cell:<geohash5> |
Every current drawing and drawing tombstone in that exact cell |
TARGET |
operation:<operation_segment> |
The current target board and target tombstones for that operation |
CHANNEL |
channel:<channel_segment> |
One channel definition, membership projection, ownership state, and tombstone |
INVITE |
principal:<principal_segment> |
Every effective invite/grant and revocation for that principal |
PLAN |
operation:<operation_segment> |
Every latest plan and cancellation in that operation |
REPORT_REQUIREMENT |
operation:<operation_segment> |
Every latest report requirement and cancellation in that operation |
SITREP_CURRENT |
operation:<operation_segment> |
The whole current SitRep/compliance projection for that operation |
ORBAT |
operation:<operation_segment> |
The whole effective ORBAT and retirement set for that operation |
Each table boundary is further partitioned by the exact signed audience_digest and
canonical classification label. Applying a snapshot replaces absence only inside that
(family, scope, audience, classification) projection; it never deletes a differently
classified or differently addressed projection. The shared sequence allocator above is
wider than the replacement partition solely to keep classification-free storage keys
collision-free. A gap in one classification's observed sequence is therefore normal and
does not imply missing state for that partition.
geohash5 is exactly the lowercase five-character alphabet defined by the wire protocol.
operation_segment, channel_segment, and principal_segment are #124 derivations over
the exact signed Common/Web operation id, channel id, and Directory principal id under
operation-id, channel-id, and auth-principal respectively. No whitespace, alternate
prefix, percent encoding, cleartext identifier, or nested scope is canonical. A grant
contains exact (family, scope_segment) pairs, so authority for an ORBAT scope and a
drawing scope cannot be recombined into a Cartesian-product target scope.
Logical schema¶
The logical snapshot is deterministic protobuf. Common owns the actual .proto and
generated cross-language types; field numbers below are reserved by this contract.
enum SnapshotFamily {
SNAPSHOT_FAMILY_UNSPECIFIED = 0;
SNAPSHOT_FAMILY_DRAWING = 1;
SNAPSHOT_FAMILY_TARGET = 2;
SNAPSHOT_FAMILY_CHANNEL = 3;
SNAPSHOT_FAMILY_INVITE = 4;
SNAPSHOT_FAMILY_PLAN = 5;
SNAPSHOT_FAMILY_REPORT_REQUIREMENT = 6;
SNAPSHOT_FAMILY_SITREP_CURRENT = 7;
SNAPSHOT_FAMILY_ORBAT = 8;
}
message AuthoritativeStateSnapshotV1 {
uint32 version = 1; // exactly 1
SnapshotFamily family = 2;
string scope_segment = 3; // 32 lowercase hex
uint64 snapshot_seq = 4;
uint64 source_cut = 5; // Web durable-ingest high-water
uint64 snapshot_created_at_ms = 6;
ConfidentialityLabel classification = 7;
bytes audience_digest = 8; // exactly 32 bytes
uint32 key_epoch = 9;
bool complete = 10; // must be true for replacement
repeated SnapshotEntryV1 entries = 11; // canonical key order
SnapshotProvenanceV1 provenance = 12;
bytes state_digest = 13; // exactly 32 bytes
}
message SnapshotEntryV1 {
string canonical_opaque_key = 1;
SnapshotEntryKind kind = 2; // VALUE = 1, TOMBSTONE = 2
uint64 source_version = 3; // family semantic seq/revision/timestamp
bytes canonical_value = 4; // current decoded application value
bytes source_envelope_sha256 = 5; // original envelope, when one exists
bytes source_author_digest = 6; // attribution, not snapshot signer
uint64 source_issued_at_ms = 7;
uint64 source_observed_at_ms = 8; // Web durable-ingest time
bytes source_audit_sha256 = 9; // Web audit row when no envelope exists
}
message SnapshotProvenanceV1 {
uint64 source_cut = 1;
uint64 first_observed_at_ms = 2;
uint64 last_observed_at_ms = 3;
uint64 value_count = 4;
uint64 tombstone_count = 5;
bytes ordered_source_digest = 6;
}
canonical_value is the exact Common-defined deterministic protobuf encoding of the
family value. The encoder orders map entries by UTF-8 key bytes and repeated set-like
fields by their family rule. Consumers hash the received bytes before decoding and never
derive the digest by re-encoding a platform object. Unknown fields are preserved only
when the Common family contract says so; otherwise the snapshot builder rejects them
rather than producing platform-dependent bytes.
The family-to-value mapping is closed for version 1:
| Family | VALUE encoding | TOMBSTONE encoding and version |
|---|---|---|
DRAWING |
DrawingShape; source_version = revision >= 1 |
DrawingDelete with the same shape id and a strictly higher revision |
TARGET |
TargetCop; source_version = timestamp_ms > 0 |
TargetDelete; delete timestamp advances the same target version |
CHANNEL |
SnapshotChannelStateV1; source_version = revision >= 1 |
Same aggregate with tombstoned=true; revision strictly advances |
INVITE |
SnapshotInviteGrantV1; source_version = revision >= 1 |
Same aggregate with revoked=true; revision strictly advances |
PLAN |
PlanRecord; source_version = seq >= 1 |
PlanRecord.cancelled=true at a higher seq |
REPORT_REQUIREMENT |
ReportRequirement; source_version = seq >= 1 |
cancelled=true at a higher seq |
SITREP_CURRENT |
SnapshotSitRepCurrentV1; source_version = revision >= 1 |
A period removed from the current projection uses status=RETIRED at a higher revision; no ReportRecord is synthesized |
ORBAT |
OrbatRecord; source_version = seq >= 1 |
OrbatRecord.retired=true at a higher seq |
The three snapshot-only aggregates are:
message SnapshotChannelMemberV1 {
string member_segment = 1;
bool joined = 2;
uint64 revision = 3;
}
message SnapshotChannelStateV1 {
uint32 channel_kind = 1; // 1 = chat, 2 = voice
bytes canonical_definition = 2; // deterministic Chat or Voice
repeated SnapshotChannelMemberV1 members = 3; // member_segment order
string owner_segment = 4;
uint64 revision = 5;
bool tombstoned = 6;
}
message SnapshotInviteGrantV1 {
string invitee_segment = 1;
string channel_segment = 2;
bytes canonical_grant = 3;
uint64 revision = 4;
bool revoked = 5;
}
enum SnapshotSitRepStatusV1 {
SNAPSHOT_SITREP_STATUS_UNSPECIFIED = 0;
SNAPSHOT_SITREP_STATUS_REPORTED = 1;
SNAPSHOT_SITREP_STATUS_LATE = 2;
SNAPSHOT_SITREP_STATUS_SILENT = 3;
SNAPSHOT_SITREP_STATUS_RETIRED = 4;
}
message SnapshotSitRepCurrentV1 {
string requirement_segment = 1;
string unit_segment = 2;
uint64 period_start_ms = 3;
SnapshotSitRepStatusV1 status = 4;
bytes latest_report_digest = 5;
uint64 revision = 6;
string operation_id = 7;
}
Drawing values and tombstones share one signed semantic revision stream per
shape_id. Every accepted create, edit, or delete carries revision >= 1; a new
mutation must strictly advance the prior value/tombstone revision. The direct
canonical payload timestamp_ms remains signed event and expiry metadata, but
neither timestamp nor receipt order can order the drawing projection or become
its snapshot source_version. During the unpublished Android transition, its
existing wrapped live and replay handlers continue requiring wrapper/body
timestamp equality; removing that check ships only with a behavior-equivalent
direct decoder. A producer allocates
the revision atomically with its durable mutation/outbox write. Equal revision plus the
same canonical value is an idempotent duplicate; equal revision plus different canonical
bytes is a fork and quarantines the affected snapshot partition until a strictly higher
authoritative mutation resolves it.
Channel and invite aggregates use the already signed
StorageCapabilityV1.authority_ledger_version; Common does not add a second revision
field to their control payloads. Directory maintains one strictly increasing ledger
stream per channel channel_segment and one per invite principal_segment, matching the
two snapshot replacement boundaries above. Every committed definition, membership,
ownership-transfer, grant, revocation, or tombstone mutation in that aggregate advances
the same stream and carries that value in its COMMITTED_AUTHORITY_MUTATION publish
capability. Splitting mutation kinds into independent version streams is invalid because
it would make their effective aggregate unordered.
Web verifies the committed capability before ingest, persists its
authority_ledger_version with the mutation, and uses that exact value as the affected
snapshot member's revision. SnapshotChannelStateV1.revision and the enclosing
SnapshotEntryV1.source_version are the maximum effective ledger version included in the
channel aggregate; each SnapshotChannelMemberV1.revision is the ledger version of that
member's latest effective membership mutation. SnapshotInviteGrantV1.revision is the
grant/revocation's latest effective ledger version, and its enclosing invite entry's
source_version is that exact same value. Web observation order, row ids, local counters,
and HLC cannot create, replace, or break a tie in this authority version stream.
ORBAT has one atomic projection per deployment-global unit_id, not independent
definition, membership, appointment, or lifecycle wire projections. Every change emits
the complete OrbatRecord on its one canonical opaque unit key with seq + 1, and its
verified COMMITTED_AUTHORITY_MUTATION capability carries
authority_ledger_version == OrbatRecord.seq. Snapshot ORBAT entries use that same full
record and exact sequence as source_version; live ingest, retained catch-up, and
snapshot reconciliation therefore share one merge rule. Lower sequence is rollback,
equal sequence plus identical bytes is idempotent, equal sequence plus different bytes is
equivocation, and a higher complete record replaces the lower one. A retired=true
record remains the full unit tombstone and cannot be undone by a delayed earlier record.
One Directory transaction may update several units atomically, but every affected unit
advances its own sequence independently and exactly once.
SITREP_CURRENT is one durable derived cell per
(operation_id, requirement_id, expected_unit_id, current_period_start_ms). A current
one-shot or cadence requirement contributes cells only for live units in its ORBAT
subtree that have a current commander, or a provisional owner while forming. The first
policy-equivalent report in the period determines REPORTED (within the fixed 15-minute
grace) versus LATE; absence is SILENT, while latest_report_digest identifies the
latest equivalent report without embedding or re-authoring that event. Policy
equivalence merges repeated same-tag categories and ignores only label creation metadata;
the emitted partition label itself is canonical and exact. Cadence rollover,
cancellation, command loss, audience removal and relabelling advance the durable cell
revision and tombstone every prior exact partition before replacement. Web persists the
previous/current audience identities and canonical audit evidence in the same transaction
as its class-2 ledger mutation. The snapshot carries that audit digest, an empty original
envelope digest, and operation_id so Common and every endpoint can bind the decoded cell
back to the signed operation scope. It is evidence of the Web read model, never proof that
an endpoint authored a report or was online at snapshot time.
source_author_digest, source_envelope_sha256, and source_audit_sha256 preserve attribution/provenance where
Web retained it. They do not make the entry endpoint-signed and must never be exposed as
proof that the endpoint authored the snapshot or was online at snapshot time. For a
Web-derived read model with no original envelope, the source-envelope digest is empty and
the required audit-row digest is carried in field 9 and contributes to
ordered_source_digest.
Attachments such as KML remain content-addressed references. Snapshot entries carry the existing SHA-256 manifest reference, never blob bytes; clients fetch authorized blobs over the artifact path after state recovery.
Canonical ordering and digest¶
Entries are sorted by the UTF-8 bytes of canonical_opaque_key. Duplicate keys,
non-canonical keys, unknown family/kind values, zero source versions for a versioned
family, and equal key/version entries with different value digests are rejected.
source_version is family-defined and always greater than zero:
- plan, report-requirement, and ORBAT entries use their signed semantic
seq; - drawing entries use the signed
DrawingShape.revisionorDrawingDelete.revision; - target entries use their signed payload timestamp;
- effective channel and invite projections use the highest verified
StorageCapabilityV1.authority_ledger_versionfrom their single aggregate stream; and - the Web-derived current SitRep/compliance projection uses its durable materialization revision, not a submitted report's event time.
Tombstones use the same version domain and must advance it. Arrival time, Web observation order, database row id, and Zenoh HLC never break a tie. Equal version plus identical digest is a duplicate; equal version plus different bytes quarantines that family/scope and prevents a new complete head assertion until the application authority resolves it with a strictly higher semantic version. Web must not pick a winner by receipt order.
The cross-language digest deliberately does not depend on protobuf field order. For each entry, encode:
u32be(key_len) || key_utf8
|| u8(kind)
|| u64be(source_version)
|| u32be(value_len) || canonical_value
|| source_envelope_sha256[32-or-empty-as-32-zero-bytes]
|| source_audit_sha256[32-or-empty-as-32-zero-bytes]
|| source_author_digest[32-or-empty-as-32-zero-bytes]
|| u64be(source_issued_at_ms)
|| u64be(source_observed_at_ms)
state_digest = SHA-256(concat(u32be(entry_len) || entry_bytes)) in sorted order.
ordered_source_digest uses the same framing over each entry's source-envelope or audit
digest. The shared digest, ordering, tombstone, rollback, and confusion cases live in
authoritative-snapshot-v1.json.
Executable cross-language corpus¶
The docs JSON is the normative digest/precedence seed. The dependent Common change must
publish one executable common/testdata/authoritative_snapshot_vectors.json consumed by
Rust, TypeScript, and Kotlin/JNI. With fixed test keys, nonce, current group-key epoch,
classification, audience, and entries, it includes exact hex for:
- the authority-grant signing input including its signed audience segment, Directory signature, and encoded grant;
- logical snapshot protobuf, per-entry/state/whole-snapshot digests;
- manifest and every chunk protobuf, key expression, and chunk commitment;
- sealed manifest/chunk payloads, purpose-3
AuthEnvelopesigning inputs/signatures, and complete encoded envelopes; - nonce-bound head assertion signing input including the exact machine identity token, Web signature, and encoded assertion; and
- exact shared-verifier bytes for the authority, audience, classification, chunk, and digest mutations materialized by Common, plus a structured manifest of the remaining rollback, fork, epoch, tombstone, and component-integration cases listed below.
Negative rows are structured hostile_cases with an exact target, mutation operation,
value where applicable, and stable expected error. Rust and TypeScript execute the exact
materialized shared-verifier bases and gate-specific cases across every shared category;
the complete row list is also the required manifest for the dependent consumer suites.
This avoids pretending Common can execute UI, database, outbox, or transport behavior it
does not own. Idempotent behaviors such as
an identical duplicate chunk live under positive_cases; they are never mislabeled as
rejections. Directory/audience construction rows carry their test-only clear source
identifiers and domains so salt-holding implementations can reproduce the expected opaque
segments, while Relay drivers consume only the opaque signed outputs.
The protocol remains “target” in the status roadmap until all three language drivers run that byte corpus. A prose example or independently regenerated platform fixture is not a substitute.
Bounded manifest and chunks¶
The logical message may exceed one retained-value limit. PETRA 1.0 fixes these hard cross-language bounds; a deployment may configure lower limits but never higher ones:
| Bound | PETRA 1.0 maximum |
|---|---|
| Deterministic logical snapshot bytes | 16 MiB (16,777,216 bytes) |
| Chunk data | 49,152 bytes |
| Chunks per snapshot | 342 |
| Entries per logical snapshot | 65,536 |
One SnapshotEntryV1.canonical_value |
1 MiB (1,048,576 bytes) |
Web deterministically encodes the logical snapshot, rejects it if any entry or the whole message exceeds those bounds, splits the bytes into 49,152-byte chunks (the final chunk may be shorter), and publishes the following inner transport value:
message SnapshotTransportValueV1 {
oneof value {
SnapshotManifestV1 manifest = 1;
SnapshotChunkV1 chunk = 2;
}
}
message SnapshotManifestV1 {
uint32 version = 1;
SnapshotFamily family = 2;
string scope_segment = 3;
uint64 snapshot_seq = 4;
ConfidentialityLabel classification = 5;
bytes audience_digest = 6;
uint32 key_epoch = 7;
uint64 logical_length = 8;
uint32 chunk_size = 9;
repeated bytes chunk_sha256 = 10; // index order, each exactly 32 bytes
bytes logical_sha256 = 11; // exact logical protobuf bytes
bytes state_digest = 12;
uint64 snapshot_created_at_ms = 13;
uint64 source_cut = 14;
}
message SnapshotChunkV1 {
uint32 version = 1;
SnapshotFamily family = 2;
string scope_segment = 3;
uint64 snapshot_seq = 4;
bytes logical_sha256 = 5;
bytes state_digest = 6;
uint32 index = 7;
bytes data = 8;
}
It emits:
- a signed/sealed manifest containing the header, logical byte length, chunk count,
ordered chunk SHA-256 values, and
state_digest; and - signed/sealed chunks containing snapshot sequence,
logical_sha256,state_digest, zero-based index, and exact byte slice.
logical_sha256 covers the exact encoded AuthoritativeStateSnapshotV1, including
family, scope, source cut, creation time, classification, audience, key epoch,
provenance, entries, and state_digest. It is the canonical whole-snapshot identity used
by manifests, chunks, head assertions, retries, fork detection, and persisted high-water.
Before allocating an assembly buffer, a consumer requires a fresh verified head and a
manifest that exactly matches that head. It then validates 1 <= logical_length <=
16,777,216, chunk_size == 49,152, and
chunk_sha256.len == ceil(logical_length / 49,152) <= 342; every listed digest is exactly
32 bytes. It may then preallocate exactly logical_length, never a size derived from a
chunk index, encoded protobuf length, compressed length, or unverified manifest. Every
non-final chunk is exactly 49,152 bytes and the final chunk is exactly the remaining
length. The sum of accepted unique chunk bytes must equal logical_length without integer
overflow. After reassembly, deterministic decoding rejects more than 65,536 entries or
any canonical_value longer than 1,048,576 bytes even though the outer 16 MiB bound also
holds.
The snapshot schema is uncompressed in version 1: manifest lengths and hashes describe the post-decryption logical protobuf bytes. A transport stack that performs compression outside this schema must enforce the same 16 MiB limit while streaming decompression and abort as soon as output would exceed it. It must not preallocate from an unauthenticated compressed or advertised length, and decompression never relaxes the per-entry or entry count limits.
Every manifest and chunk uses purpose 3, the same classification/audience/key epoch, and
its exact cross-bound key. Every outer AuthEnvelope.issued_at_ms equals the manifest's
snapshot_created_at_ms; after opening and reassembly the consumer also requires the
logical snapshot timestamp to match. This lets opaque Relay + Storage enforce grant
validity using signed-cleartext time without opening content. A consumer applies nothing
until it has one valid manifest and all chunks. Missing, conflicting, oversized, or
out-of-range chunks fail the assembly. It verifies each chunk digest, the reassembled byte
length, deterministic decode, header equality, entry ordering, and whole-state digest
before opening a database transaction.
An assembly exists only under one still-fresh verified head identity
(family, scope, audience, classification, snapshot_seq, logical_sha256, source_cut).
There is at most one in-flight assembly per (family, scope, audience, classification).
An identical head and manifest reuse it; a verified newer head cancels and zeroes the
older partial assembly before starting its replacement; an older or conflicting equal
head never creates parallel state. Byte-identical duplicate chunks from multiple
storages are idempotent and consume neither additional byte budget nor a new slot.
Different bytes or commitments for an already accepted index fail and discard the whole
assembly. Unrequested chunks and chunks received before the matching verified head and
manifest are dropped without allocation.
The assembly expires no later than its head assertion. Receiving duplicate or later chunks does not extend that deadline. If the head, machine identity or revocation state, embedded grant, publication capability, or current encryption epoch ceases to be valid before atomic commit, the client discards the partial state, obtains a new head, and starts again. Implementations may impose a shorter local timeout or a lower global memory/concurrency budget; exhaustion fails closed and never evicts a verified complete projection in favour of partial data.
Deterministic Web cut¶
Web adds one transactional, gap-free durable-ingest counter covering every class-2 source row and tombstone. Each class-2 ingest transaction locks that counter row, increments it, stamps the mutation and audit row with the new value, and commits them together; rollback also rolls back the counter. A database sequence that can leave allocation gaps is not sufficient. To create a snapshot Web:
- starts one repeatable-read database transaction, locks the shared class-2 ingest
counter (blocking new class-2 commits), then locks the snapshot counter for
(family, scope, audience_digest); - increments
snapshot_seqand captures the committed ingest counter assource_cut; - reads the complete latest projection and retained tombstones whose ingest sequence is at or below that cut;
- sorts, encodes, digests, seals with the current group-key epoch, and persists the exact manifest/chunk bytes plus digest before commit; and
- publishes those persisted bytes. A retry republishes byte-identical values for the same sequence; it never rebuilds the cut from newer rows.
The shared counter lock remains held through the snapshot transaction commit. Therefore
every committed class-2 mutation at or below source_cut is visible and included, while
every later mutation commits strictly above it; concurrent allocation cannot create a
hole inside a snapshot marked complete.
This is a complete replacement snapshot, not a delta. Absence removes a local value only inside the declared family, scope, audience, and classification partition. Tombstones remain explicit so a later stale value cannot recreate a deleted object. Web history/audit tables are not replaced or projected back into the mesh.
Web now has the transactional gap-free class-2 ingest high-water and retains original author/event time separately from Web observation time. Each family remains ineligible for authoritative publication until all of that family's producers and recorder paths write this ledger atomically; drawing, target, and registered plan mutations currently do so, while the remaining class-2 families stay explicit runout work.
Web creates a new cut after a coalesced class-2 change, immediately after group-key epoch rotation, and on a refresh interval no longer than half the configured offline horizon. Even unchanged state is republished at a new sequence/current epoch so at least one head and complete manifest/chunk set remains inside horizon-based Storage retention. If Web is unavailable for a full horizon, recovery remains blocked until Web creates and asserts a new current cut; caches do not extend authority by refreshing old bytes themselves.
Partition discovery¶
A cold or wiped client cannot construct an exact head request from a snapshot query
selector alone. The selector carries (family, scope_segment, audience_segment), while
the replacement partition also requires the signed audience_digest and one exact
canonical classification label. The audience segment is deliberately non-invertible
without the deployment salt, and a clearance is a ceiling rather than an inventory of
labels that actually exist. Storage presence is neither authority nor evidence of a
complete set.
After installing a fresh complete Directory capability set, the client asks Common to
enumerate its verified purpose-3 manifest QUERY selectors. For each selector it obtains
one complete partition catalog directly from Web before asking for any exact heads.
Directory remains the sole issuer of query authority; Web reports only which current
class-2 partitions exist within that authority.
message SnapshotPartitionCatalogRequestV1 {
uint32 version = 1; // exactly 1
SnapshotFamily family = 2;
string scope_segment = 3; // 32 lowercase hex
string audience_segment = 4; // 32 lowercase hex
bytes request_nonce = 5; // fresh, exactly 12 bytes
bytes query_authorization = 6; // canonical StorageAuthorizationV1
}
enum SnapshotPartitionAvailabilityV1 {
SNAPSHOT_PARTITION_AVAILABILITY_UNSPECIFIED = 0;
SNAPSHOT_PARTITION_AVAILABILITY_READY = 1;
SNAPSHOT_PARTITION_AVAILABILITY_PUBLICATION_PENDING = 2;
SNAPSHOT_PARTITION_AVAILABILITY_KEY_EPOCH_PENDING = 3;
SNAPSHOT_PARTITION_AVAILABILITY_HORIZON_EXPIRED = 4;
}
message SnapshotRecoveryPartitionV1 {
ConfidentialityLabel classification = 1;
SnapshotPartitionAvailabilityV1 availability = 2;
}
message SnapshotPartitionCatalogAssertionV1 {
uint32 version = 1;
SnapshotFamily family = 2;
string scope_segment = 3;
string audience_segment = 4;
bytes audience_digest = 5; // exactly 32 bytes
repeated SnapshotRecoveryPartitionV1 partitions = 6;
uint64 catalog_generation = 7;
uint64 catalog_source_cut = 8;
uint64 issued_at_ms = 9;
uint64 expires_at_ms = 10;
bytes request_nonce = 11; // exact caller nonce, 12 bytes
bytes query_capability_sha256 = 12; // exact Directory capability, 32 bytes
bytes snapshot_authority_grant = 13;
bytes web_identity_token = 14;
bytes web_signature = 15;
}
The client sends SnapshotPartitionCatalogRequestV1 to
POST /api/snapshots/catalog with the same HTTPS, bearer-token, protobuf,
no-compression, 64 KiB request, and Cache-Control: no-store rules as the head endpoint.
The query authorization contains a current Directory-issued QUERY,
REUSABLE_CONTENT, purpose-3 manifest capability. Its proof nonce exactly equals
request_nonce, has an empty value digest, and names this exact selector:
Web verifies the bearer and proof exactly as for a head request. It additionally requires
the explicit family, scope, and audience segment to equal the parsed selector. It resolves
that tuple through exactly one current Directory-signed Web
SnapshotAuthorityGrantV1; ambiguity or absence fails the whole request. This obtains the
Directory-approved audience_digest without exposing an audience descriptor or treating
Web as an authority issuer.
In one repeatable-read transaction, Web reads the complete union of published partition
rows and dirty/unpublished partition rows for the exact family, scope, and audience. It
returns only classifications for which Common CMBAC permits both the bearer clearance and
the query-capability ceiling. A current exact partition is never omitted merely because
its newest cut is not yet usable: Web marks it PUBLICATION_PENDING,
KEY_EPOCH_PENDING, or HORIZON_EXPIRED. Missing current revocation data, publisher
identity/grant state, a complete inventory read, or a canonical classification fails the
whole request rather than producing an empty or partial catalog.
catalog_source_cut is the class-2 durable-ingest counter observed by that transaction.
catalog_generation is a transactional, monotonically increasing Web counter that
advances whenever partition membership, dirty/readiness state, fully published head,
current key epoch, or publisher-grant mapping changes. The signed partition list remains
the source of truth; generation is a race detector, not authority and not a replacement
for an exact head.
partitions is sorted by the unsigned bytes of
canonical_label(classification) and contains no duplicate label. It is bounded to 4,096
entries and the complete canonical response is bounded to 4 MiB. PETRA 1.0 has no catalog
pagination: either Web returns the complete set or recovery is visibly blocked. A
successful signed zero-entry catalog means that no authorized partition exists at that
catalog cut. It does not delete a warm client's existing projection; session replacement
or wipe handles removed authority.
The assertion is deterministic protobuf. It has the same Web identity/grant binding,
revocation checks, issue interval, and maximum five-minute lifetime as a head assertion.
query_capability_sha256 is SHA-256 of the exact Directory-signed capability embedded in
the request authorization. The client verifies that digest, exact request coordinates and
nonce, sorted unique bounded partitions, every classification through Common, and the
following Web signature:
ascii("petra-snapshot-partition-catalog-v1\0")
|| u32be(version) || u32be(family)
|| str32(scope_segment) || str32(audience_segment)
|| audience_digest[32]
|| u32be(partition_count)
|| concat(bytes32(canonical_label(classification)) || u32be(availability))
|| u64be(catalog_generation) || u64be(catalog_source_cut)
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| request_nonce[12] || query_capability_sha256[32]
|| bytes32(snapshot_authority_grant) || bytes32(web_identity_token)
For each READY partition the client uses a new nonce and fresh proof for the unchanged
exact head flow below. A non-ready partition leaves recovery visibly blocked. A head
lookup that races the catalog causes rediscovery rather than fallback to an older head.
After applying the batch, the client obtains the catalogs again and declares recovery
complete only when every authorized manifest selector still exists, every catalog has the
same generation and exact partition list as its first pass, and every listed partition is
ready and applied or idempotent. Capability-set generation, expiry, revocation, Web grant,
or key-epoch change restarts the run. Missing a matching purpose-3 chunk-query selector
also blocks recovery; manifest authority is never widened into chunk authority.
Catalog assertions cannot enter an application projection, authorize a head or Storage
read, substitute for an AuthEnvelope, or clear incomplete transient-history status.
Exact classification and audience metadata is active-session data and is erased on Android
session wipe. Relay + Storage never sees the catalog and remains classification-blind.
Head assertion and rollback¶
A client persists the highest accepted (snapshot_seq, logical_sha256, source_cut) per
family/scope/audience/classification replacement partition. A lower sequence is rollback.
Sequence gaps caused by snapshots for another classification are valid. Any incoming source_cut below the
persisted cut is stale-cut rollback even when snapshot_seq is higher. An equal cut is
valid only for an unchanged-state refresh: state_digest, ordered_source_digest, entry
count, value count, and tombstone count must equal the persisted snapshot. Any difference
at an equal cut is Web equivocation because every class-2 state mutation must advance the
gap-free counter. An equal sequence with the same whole-snapshot digest is idempotent; an
equal sequence with a different digest is Web/storage
equivocation and fails closed. A cold client has no persisted cut, so its fresh Web head
supplies the required sequence and cut; the snapshot must match both exactly.
A cold or wiped client has no trustworthy local high-water, so a compromised cache could
withhold the newest valid snapshot. For every READY catalog entry it must obtain a fresh,
purpose-separated
SnapshotHeadAssertionV1 directly from Web's authenticated HTTPS endpoint, using a fresh
12-byte request nonce and a fresh proof for its current Directory-issued snapshot
QUERY capability. The assertion contains the exact
family/scope/audience/classification, head sequence, whole-snapshot digest, source cut,
issued time, and short expiry. A Zenoh storage/query path is not a head-assertion source in
1.0. Until HTTPS Web is available, the client remains visibly recovery-blocked and does
not treat an arbitrary cached snapshot as current.
The request and assertion are deterministic protobuf returned outside retained Storage:
message SnapshotHeadRequestV1 {
uint32 version = 1; // exactly 1
SnapshotFamily family = 2;
string scope_segment = 3; // 32 lowercase hex
bytes audience_digest = 4; // exactly 32 bytes
ConfidentialityLabel classification = 5;
bytes request_nonce = 6; // fresh, exactly 12 bytes
bytes query_authorization = 7; // canonical StorageAuthorizationV1
}
message SnapshotHeadAssertionV1 {
uint32 version = 1;
SnapshotFamily family = 2;
string scope_segment = 3;
bytes audience_digest = 4;
ConfidentialityLabel classification = 5;
uint64 snapshot_seq = 6;
bytes logical_sha256 = 7;
uint64 source_cut = 8;
uint64 issued_at_ms = 9;
uint64 expires_at_ms = 10;
bytes request_nonce = 11; // exact caller nonce, 12 bytes
bytes snapshot_authority_grant = 12;
bytes web_signature = 13;
bytes web_identity_token = 14; // exact Directory-signed machine IdentityToken
}
The client sends the request with POST /api/snapshots/head over HTTPS, an
Authorization: Bearer <base64 canonical IdentityToken bytes> header, and
Content-Type: application/x-protobuf. Compression is not accepted. The request is at
most 64 KiB, and success is application/x-protobuf with Cache-Control: no-store.
Malformed or non-canonical requests fail before any partition lookup.
query_authorization contains the caller's current Directory-signed QUERY,
REUSABLE_CONTENT, purpose-3 capability and a fresh StorageUseProofV1. The proof's
nonce exactly equals request_nonce, its value digest is empty, and its selector is the
exact manifest capability expression:
Web verifies the bearer token's Directory signature, canonical bytes, exactly one
recognized kind:operator or kind:device role, current expiry, principal revocation,
and signing-key revocation. Identity kind classifies the principal; it does not grant
snapshot authority. Web then verifies the fresh query proof through Common, requires the
capability principal and signing key to equal the bearer, requires purpose 3 and the exact
family/scope selector, derives the audience segment from the requested digest under its
current signed snapshot grant, and requires CMBAC Permit against both the bearer
clearance and capability ceiling. A capability without its possession proof is not a
bearer credential. The head request proof is consumed only by Web and is never reused for
Storage; every later manifest or chunk query carries another fresh proof. This makes
Directory remain the sole issuer of opaque-audience query authority without making Web a
second authority source.
Web selects only the latest fully published exact replacement partition. It returns no older cut while a newer cut is incomplete, no old-key cut after rotation, and no cut outside the configured retention horizon. Missing current revocation state, current Web identity/grant, current key epoch, or signing authority fails recovery closed.
web_identity_token is the exact deterministic protobuf bytes for the Web machine's
Directory-signed IdentityToken. web_signature uses the token's
principal_sign_key; the embedded grant must name that same principal and key. The grant
does not replace the identity token on this non-envelope path. A
ConfidentialityLabel is canonicalized as:
str32(policy_id) || str32(classification)
|| u32be(category_count)
|| concat(
u32be(category_type) || str32(tag)
|| u32be(value_count) || concat(str32(value))
)
|| u64be(creation_dtg_ms) || str32(originator_id)
Categories and values use the same canonical sorting, uniqueness, and rejection rules as the grant clearance. The exact head input is:
ascii("petra-snapshot-head-v1\0")
|| u32be(version) || u32be(family) || str32(scope_segment)
|| audience_digest[32] || bytes32(canonical_label(classification))
|| u64be(snapshot_seq) || logical_sha256[32] || u64be(source_cut)
|| u64be(issued_at_ms) || u64be(expires_at_ms)
|| request_nonce[12] || bytes32(snapshot_authority_grant)
|| bytes32(web_identity_token)
The client verifies exact request-nonce equality; the identity token's canonical bytes,
Directory signature, kind:device machine kind, and current expiry; the Web signature;
exact token/grant principal and signing-key equality; current principal and signing-key
revocation; grant.issued_at_ms <= head.issued_at_ms < grant.expires_at_ms; exact requested
family/scope/audience/classification; issued_at_ms <= now < expires_at_ms; and
expires_at_ms - issued_at_ms <= 300_000 (five minutes). This object is snapshot-head
authority only; it cannot enter an application projection or substitute for an
AuthEnvelope.
head.issued_at_ms is the time Web answers this request, not the retained snapshot's
creation time. head.expires_at_ms is no later than five minutes after that time and no
later than the current Web identity or embedded grant expiry. The manifest retains its
original snapshot_created_at_ms; those timestamps need not be equal. The head's exact
snapshot_seq, logical_sha256, source_cut, family, scope, audience, and classification
bind it to the retained manifest, while the manifest and logical snapshot continue to
bind their own creation timestamp. Requiring head issue time to equal snapshot creation
would make every retained cut unrecoverable five minutes after publication and is
therefore invalid.
An existing client may reject values below its persisted high-water while disconnected, but it cannot prove that a cache is not withholding a newer head. It reports the age of its last Web head assertion. Withholding remains an availability/staleness attack, not a way to roll state backward or forge a snapshot.
Apply and precedence¶
For each family/scope/audience/classification replacement partition, recovery is one atomic state transition:
- Mark transient history incomplete when the offline horizon or available key backfill is exceeded. This status is not cleared by a snapshot.
- Obtain and stabilize the complete Web partition catalogs, then obtain a fresh matching Web head assertion for every ready entry and query all applicable storages for that exact sequence's manifest and chunks, with HLC consolidation disabled.
- Verify purpose, Web identity/grant, revocation, capability/key binding, classification, audience, current encryption epoch, chunks, canonical state, digest, sequence, source cut, and head assertion.
- Treat ordinary values returned by that recovery query as historical candidates only. Run their normal verification/merge for diagnostics, but do not apply them after the complete snapshot.
- In one database transaction, replace only the declared durable projection, install its values and tombstones, persist the snapshot high-water and per-entry source-version floors, then commit.
- Only newly received live samples that pass
FreshOnly, current token/revocation, authority, classification, key binding, and the family's normal semantic merge may advance the projection. Stored/AllowStalereplay from the completed recovery cannot.
Zenoh HLC and arrival order never confer authority. A lower/equal family source version is stale; equal version with different content is a conflict and fails closed. A tombstone floor outranks an older value. Purpose 3 itself does not grant authority to modify another family or scope.
Consumer behavior¶
| Consumer | Required behavior |
|---|---|
| Android | Atomically replace only declared operational projections; retain tombstones and high-water; never emit notifications, commands, acknowledgements, outbox items, or endpoint events merely because a snapshot was applied. Surface recovery progress, head age, and incomplete transient history. |
| Web | Build/persist deterministic cuts, self-verify before publish, and keep snapshots separate from endpoint ingest and audit history. Never record them as proof of endpoint activity. |
| Node | It owns no class-2 application projection in 1.0. Recognize purpose 3, verify or explicitly no-op at the dispatch boundary, and never execute it as a command. |
| Gateway | Apply no class-2 snapshot in 1.0. Recognize purpose 3 and never translate/export it as partner traffic or endpoint-authored provenance. |
| Relay + Storage | Verify the outer snapshot/storage contract and retain/return opaque bytes. Never open chunks, build application projections, mint authority, select winners, or rewrite provenance. |
Authoritative recovery classes¶
Every current family has exactly one tactical recovery class. Web can retain class-3 history for a class-1/2 family without republishing that history.
Class 1 means there is no post-horizon tactical reconstruction. It does not itself grant
mesh retention: a closed retained-family row may provide bounded replay (for example,
chat), while a LIVE_PUBLICATION family resumes live and reports the gap. A verified Web
recipient may keep separate class-3 application history without turning that archive into
Zenoh Storage or endpoint-authored replay.
| Family / read model | Class | PETRA 1.0 recovery behavior |
|---|---|---|
| router liveliness/token and Directory revocation/group-key refresh | 1 — transient/control refresh | Refresh from Directory; cached signed state remains bounded by its own validity |
| position, heartbeat, presence, channel-member liveliness | 1 — live only | No mesh retention/query/replay; resume live and report the gap. Verified Web position history is class 3 |
| chat messages | 1 — bounded transient replay | Bounded mesh replay under retained REUSABLE_CONTENT; full Web chat history is class 3 |
| voice frames | 1 — live only | No mesh retention or tactical reconstruction; verified Web audio archive is class 3 |
| Node live camera-advertisement metadata | 1 — live only | Resume live; PETRA 1.0 has no standalone sensor namespace, and sensor map objects use the drawing class-2 model |
device commands and live CommandAck responses |
1 — live only | Never retain, reconstruct, or re-execute expired commands |
PlanAcknowledgementV1 records |
1 — bounded transient replay | Relay + Storage retains the original durable envelope only within the offline horizon; verified Web exact-version history is class 3 and is not synthesized by a snapshot |
append-only submitted SitRep record/report events |
1 — bounded transient replay | Do not republish historical reports as endpoint events; Web audit is class 3 |
unregistered generic state classes |
1 — unregistered transient | No retention or latest-state promise until a distinct registered model exists |
| drawings/annotations and tombstones | 2 — durable latest state | Snapshot current shapes and delete floors by cell/scope |
| COP targets and tombstones | 2 — durable latest state | Snapshot current target projection and delete floors |
| channel definitions, membership, transfer, and tombstones | 2 — durable latest state | Snapshot one effective channel projection with its authority/tombstone floors |
| effective invite/grant state | 2 — durable latest state | Snapshot current grants only; activity history remains Web-only |
| plans and KML/content-addressed attachment references | 2 — durable latest state | Snapshot latest seq including cancelled plans; resolve authorized blobs separately |
| SitRep/report requirements | 2 — durable latest state | Snapshot latest seq including cancelled requirements |
| derived current SitRep/compliance projection | 2 — durable latest state | Snapshot current status as a distinct Web-derived model, never as reconstructed ReportRecord authorship |
| ORBAT | 2 — durable latest state | Snapshot latest unit seq, memberships, appointments, and retired tombstones |
| Web replay/audit, full lifecycle histories, provenance, and blob contents | 3 — Web-only audit/history | Retain in Core; never reconstruct as endpoint-authored Zenoh traffic |
Required hostile tests¶
Common and every consumer run the same vectors and add integration coverage for:
- forged Web identity/grant, a grant presented without identity proof, token/grant principal or signing-key mismatch, revoked Web, endpoint-signature confusion, wrong purpose, family, scope, key, audience, classification, or encryption epoch;
- malformed or unequal signed audience segments at Relay, and wrong opaque-domain derivation at Directory and salt-holding endpoints without pretending Relay can perform that derivation check;
- stale/lower snapshot, equal-sequence different-digest fork, stale head assertion, and a cold client offered a storage-only snapshot;
- missing, stale, unsigned, non-canonical, truncated, oversized, differently nonce-bound, or wrong-capability partition catalogs; duplicate/unsorted classifications; a selector omitted from the complete capability set; unmatched manifest/chunk selector pairs; catalog/head and first-pass/second-pass generation races; any non-ready partition being silently omitted or treated as complete; and any zero-entry error path represented as a successful empty catalog;
- non-canonical ordering, duplicate/conflicting entries, bad state/source digest, every exact and one-over assembly bound, preallocation/decompression overflow, missing/conflicting/out-of-range chunks, idempotent byte-identical duplicate chunks, superseding heads, assembly expiry, concurrency-budget exhaustion, and partial assembly;
- split or reused channel/invite authority ledger versions; drawing values/tombstones
that omit, reuse, or disagree on their signed revision; and any attempt to derive those
families' snapshot
source_versionfrom observation order, HLC, event time, or an unsigned field; - ORBAT membership updates that lose definition or appointment fields, appointment updates that lose definition or membership fields, retirement followed by a delayed stale full record, lower or equal-conflicting unit sequences, capability/record sequence mismatch, concurrent mutation lost update, partial multi-unit commit, and any difference between live, retained, and snapshot per-unit reconciliation;
- tombstone-before-value, value-before-tombstone, snapshot followed by older retained values, newer live value after snapshot, and duplicate replies from multiple storages;
- application into the wrong projection and any snapshot-triggered command, ack, notification, outbox, interop export, or audit impersonation; and
- recovery when old group-key epochs are unavailable, followed by a visible incomplete transient-history state.
Non-goals¶
- Reconstructing complete chat, voice, presence, Node camera-advertisement, command, live command-acknowledgement, plan-ack mesh history, or submitted-SitRep event history beyond the horizon. Web may retain separately verified exact-version plan acknowledgements as class-3 audit/replay history.
- Letting Relay + Storage or an endpoint mint snapshot authority.
- Re-signing historical endpoint envelopes or presenting Web attribution as endpoint authorship.
- Per-channel/per-unit encryption keys in PETRA 1.0.
- Making HLC, cache arrival order, or snapshot presence evidence that an endpoint was online.