Protocol Versioning Strategy
Date: 2026-03-14 Status: Draft
Problem Statement
The protocol currently has no versioning strategy. The protocol_version tag exists on custom event kinds (defined in messages.md), but there is no specification for how versions are negotiated between peers, how deprecated versions are sunset, how security fixes propagate, or how clients coordinate upgrades across a decentralized network with no central authority.
Every successful decentralized protocol has a versioning story: Nostr uses NIPs (Nostr Implementation Possibilities), Matrix uses spec versions with a room version negotiation mechanism, and AT Protocol uses lexicon-versioned schemas. Gozzip needs the same.
Without a versioning strategy, any change to event formats, challenge-response semantics, or key derivation creates a hard fork -- some clients understand the new format, others do not, and pacts between incompatible clients silently fail.
Design Principles
- No flag days. The network must never require all clients to upgrade simultaneously.
- Pacts are the unit of compatibility. Two peers in a pact agree on a protocol version for that pact. Different pacts can run different versions concurrently.
- Forward compatibility by default. Clients SHOULD accept events with unknown protocol versions and process them on a best-effort basis.
- Deprecation is gradual. Old versions are supported for a defined window before clients may refuse them.
- Security overrides all. Critical security fixes can accelerate deprecation timelines.
1. Version Field on All Event Kinds
All Gozzip-specific event kinds (10050-10068) MUST include a protocol_version tag:
["protocol_version", "1"]
Format: Simple integer string, monotonically increasing. Version "1" is the initial protocol version defined in messages.md.
Rules:
- Clients MUST include
protocol_versionon all published Gozzip events (kinds 10050-10068). - Clients SHOULD accept events with unknown (higher) protocol versions and process them on a best-effort basis, ignoring fields they do not understand.
- Clients MUST reject events with
protocol_versionbelow their minimum supported version. - A protocol version bump indicates a change to event semantics, tag formats, or cryptographic operations. Additive changes (new optional tags) do not require a version bump.
What constitutes a version bump:
| Change type | Version bump? | Example |
|---|---|---|
| New optional tag on existing kind | No | Adding ["region", "eu"] to kind 10056 |
| New event kind in 10050-10068 range | No (but requires NIP registration) | Adding kind 10067 |
| Changed semantics of existing tag | Yes | Changing challenge hash algorithm |
| New required tag on existing kind | Yes | Making ["tz", ...] mandatory on kind 10056 |
| Changed key derivation | Yes | New HKDF info string format |
| Removed tag or field | Yes | Dropping merkle_root from kind 10051 |
2. Capability Advertisement in Pact Requests
Kind 10055 (storage pact request) gains a supported_versions tag that advertises which protocol versions the requesting client supports:
{
"kind": 10055,
"pubkey": "<root_pubkey>",
"tags": [
["volume", "<bytes>"],
["min_pacts", "<number_needed>"],
["ttl", "3"],
["request_id", "<unique_id>"],
["supported_versions", "1", "2"],
["protocol_version", "1"]
]
}
Tag format: ["supported_versions", "1", "2", ...] -- a list of all protocol versions the client supports, as individual string values after the tag name.
Rules:
- Clients MUST include
supported_versionsin kind 10055 events. - Clients SHOULD include
supported_versionsin kind 10056 (pact offer) responses so the requester can verify compatibility before accepting. - If a kind 10055 omits
supported_versions, peers SHOULD assume it supports only the version in itsprotocol_versiontag. - The
protocol_versiontag on the event itself indicates which version was used to construct the event. Thesupported_versionstag indicates the full range the client can handle.
Pact offers (kind 10056) with version advertisement:
{
"kind": 10056,
"tags": [
["p", "<requester_root_pubkey>"],
["volume", "<bytes>"],
["tz", "UTC+9"],
["supported_versions", "1", "2"],
["protocol_version", "2"]
]
}
3. Version Negotiation
At Pact Formation
When two peers form a pact, they negotiate the protocol version for that pact:
- Requester publishes kind 10055 with
supported_versions. - Offerer publishes kind 10056 with
supported_versions. - Negotiated version = highest mutually supported version.
- Both peers publish kind 10053 (pact event) using the negotiated version as the
protocol_versiontag value.
{
"kind": 10053,
"pubkey": "<root_pubkey>",
"tags": [
["d", "<partner_root_pubkey>"],
["type", "standard"],
["status", "active"],
["since_checkpoint", "<checkpoint_event_id>"],
["volume", "<bytes>"],
["expires", "<unix_timestamp>"],
["negotiated_version", "2"],
["protocol_version", "2"]
]
}
The negotiated_version tag records the agreed-upon version for the pact. All pact-related events between these two peers (challenges, gossip forwarding, checkpoint references) use this version's semantics.
Minimum Version for Pact Formation
Clients enforce a minimum version for new pact formation:
- A client running protocol version N MUST support pact formation with peers running version N-1 (backward compatibility window of 1).
- A client MAY refuse pact formation with peers whose highest supported version is below N-2.
- During the bootstrap phase (network < 500 users), clients SHOULD accept any version to avoid partitioning a small network.
Mid-Pact Upgrades
If a peer upgrades to a new protocol version during an active pact:
- The upgrading peer begins publishing events with the new
protocol_version. - The other peer continues processing these events on a best-effort basis (forward compatibility).
- At the next checkpoint (approximately monthly), both peers re-evaluate version compatibility.
- If both peers now support a higher version, the pact naturally upgrades: the next kind 10053 refresh uses the new
negotiated_version. - No explicit renegotiation event is needed. The checkpoint cycle provides a natural renegotiation point.
No forced migration: Active pacts continue operating at their negotiated version until natural expiry or renewal. A peer upgrading to version 3 does not break its version-2 pacts. The pact continues using version 2 semantics for challenges and verification until both sides agree to upgrade.
4. Deprecation Timeline
Standard Deprecation
The protocol maintains a rolling support window of N, N-1, and N-2:
Version N — current (fully supported)
Version N-1 — supported (pact formation allowed)
Version N-2 — minimum supported (existing pacts continue, new pact formation discouraged)
Version N-3 — sunset (90-day countdown begins when version N is released)
Timeline for a version release:
| Day | Event |
|---|---|
| 0 | Version N released. Version N-3 enters 90-day sunset. |
| 1-90 | Clients log warnings for pacts running version N-3. |
| 90 | Version N-3 sunset complete. Clients MAY refuse new pacts with peers below version N-2. |
| 90+ | Existing version N-3 pacts continue until natural expiry but are not renewed. |
Client behavior during sunset:
- SHOULD display a notification to the user that a pact partner is running a deprecated version.
- SHOULD prioritize replacement of deprecated-version pacts when seeking new partners.
- MUST NOT forcibly terminate active pacts solely due to version deprecation.
- MAY refuse new pact formation with peers whose highest supported version is in sunset.
Example: Version 3 Release
Before release:
Supported: v1, v2
Minimum for new pacts: v1
After v3 release:
Supported: v1, v2, v3
v1 enters 90-day sunset
After 90-day sunset:
Supported: v2, v3
Minimum for new pacts: v2 (clients MAY refuse v1)
Existing v1 pacts: continue until natural expiry, then not renewed
5. Security Update Handling
Security vulnerabilities may require accelerated deprecation. Three categories:
Hash Algorithm Weakening
If the hash algorithm used in challenge-response (currently SHA-256) is found to be weakened:
- A new protocol version introduces the replacement algorithm.
- A mandatory upgrade window of 30 days (instead of the standard 90-day sunset) is announced.
- After the window, clients MUST reject challenge responses using the weakened algorithm.
- Existing pacts using the old algorithm transition to Degraded state and are replaced.
The mandatory upgrade window is enforced by client implementations, not by the protocol itself. Client developers coordinate via the NIP amendment process (see section 7).
Key Derivation Changes
If the HKDF derivation scheme (currently HKDF-SHA256 with salt gozzip-v1) needs updating:
- A new protocol version introduces the new derivation with a new salt (e.g.,
gozzip-v2). - Old keys derived under the previous scheme remain valid for their rotation period.
- New keys are derived under the new scheme.
- Kind 10050 events published under the new version use new-scheme derived keys.
- Clients supporting both versions can verify keys under either scheme during the transition.
Challenge-Response Changes
Challenge-response semantics are negotiated per-pact via the negotiated_version tag:
- A version 2 pact uses version 2 challenge serialization and verification.
- A version 3 pact uses version 3 challenge serialization and verification.
- Mixed-version networks work because each pact independently uses its negotiated version.
- No global coordination needed -- peers upgrade their challenge-response behavior pact by pact.
Emergency Minimum Version Bump
For critical vulnerabilities (e.g., a flaw that allows forging challenge responses):
- Client developers publish an advisory with a new minimum version.
- Clients update their minimum supported version immediately (no 90-day sunset).
- Pacts running below the emergency minimum enter Degraded state.
- Partners are notified via NIP-46 message with reason
security_upgrade_required. - If the partner upgrades within 7 days, the pact resumes at the new version.
- If not, the pact transitions to Failed and is replaced.
This is the only scenario where an active pact can be forcibly disrupted by a version change.
6. Database Schema Migration
Schema migration is a client-local concern, not a protocol-level specification. However, clients implementing Gozzip SHOULD follow these guidelines to ensure smooth upgrades and rollbacks.
Migration Principles
- Additive changes only. New fields are added with sensible defaults. Existing fields are never removed in a single version jump.
- Two-version field retention. A field deprecated in version N is retained (but unused) until version N+2, providing a rollback safety net.
- Versioned migration scripts. Each protocol version bump has a corresponding migration script identified by version number.
- Rollback support. Migration scripts MUST be reversible for at least one version back (version N can roll back to N-1).
Recommended Migration Flow
1. Client starts with database at schema version S.
2. Client detects protocol version P > S.
3. Client runs migration scripts S+1, S+2, ..., P in order.
4. Each script:
a. Adds new columns/tables with default values.
b. Backfills data where possible (e.g., computing new indexes).
c. Records the migration version in a schema_version table.
5. If migration fails at step K:
a. Roll back to step K-1.
b. Client continues operating at the older schema version.
c. Log a warning; do not crash.
What Clients Store Per-Pact
Each pact record in the local database SHOULD include:
| Field | Description |
|---|---|
partner_pubkey |
Root pubkey of the pact partner |
negotiated_version |
Protocol version agreed upon for this pact |
partner_supported_versions |
Last known supported versions from the partner |
pact_created_at |
Unix timestamp of pact formation |
last_challenge_version |
Version used in the most recent challenge-response |
This allows the client to track version compatibility per-pact and detect when renegotiation is appropriate.
7. NIP Registry
All Gozzip custom event kinds are registered in the Nostr NIP (Nostr Implementation Possibilities) registry. This provides discoverability, prevents kind number collisions with other Nostr applications, and establishes a formal amendment process for changes.
Registered Event Kinds
| Kind | Name | NIP | Replaceable? | Description |
|---|---|---|---|---|
| 10050 | Device Delegation | NIP-XX1 | Yes (replaceable) | Device key authorization, DM key, governance key |
| 10051 | Checkpoint | NIP-XX1 | Yes (replaceable) | Per-identity checkpoint with Merkle root |
| 10052 | Conversation State | NIP-XX2 | Yes (parameterized) | DM read-state per conversation partner |
| 10053 | Storage Pact | NIP-XX3 | Yes (parameterized) | Reciprocal storage commitment (private) |
| 10054 | Storage Challenge | NIP-XX3 | No (ephemeral) | Challenge-response proof of storage |
| 10055 | Pact Request | NIP-XX3 | No (regular) | Public broadcast requesting storage partners |
| 10056 | Pact Offer | NIP-XX3 | No (regular) | Response to pact request |
| 10057 | Data Request | NIP-XX4 | No (regular) | Pseudonymous data retrieval request |
| 10058 | Data Offer | NIP-XX4 | No (regular) | Private response with connection endpoint |
| 10059 | Storage Endpoint Hint | NIP-XX5 | No (regular) | Encrypted peer endpoints for followers |
| 10060 | Recovery Delegation | NIP-XX6 | Yes (parameterized) | Social recovery contact designation |
| 10061 | Recovery Attestation | NIP-XX6 | No (regular) | Recovery contact attestation for key rotation |
| 10062 | Push Notification Registration | NIP-XX7 | Yes (parameterized) | Push token registration with notification relay |
| 10063 | Deletion Request | NIP-XX8 | No (regular) | GDPR-compatible event deletion request |
| 10064 | Content Report | NIP-XX9 | No (regular) | Content/user report for moderation |
| 10065 | Temporary Suspension | NIP-XX10 | No (regular) | Emergency device suspension via governance key |
| 10066 | Guardianship Completion | NIP-XX11 | No (regular) | Mutual attestation of successful guardianship |
| 10067 | Reserved | -- | -- | Reserved for future protocol use |
| 10068 | Reserved | -- | -- | Reserved for future protocol use |
NIP numbers (XX1-XX11) are placeholders. Actual NIP numbers are assigned during the Nostr NIP submission process.
NIP Grouping Strategy
Related kinds are grouped into single NIPs for coherent specification:
- NIP-XX1 (Identity): kinds 10050, 10051 -- device delegation and checkpoints
- NIP-XX3 (Storage Pacts): kinds 10053-10056 -- the full pact lifecycle
- NIP-XX4 (Data Retrieval): kinds 10057, 10058 -- pseudonymous data request/response
- NIP-XX6 (Social Recovery): kinds 10060, 10061 -- recovery delegation and attestation
Amendment Process
Changes to Gozzip event kind formats follow the Nostr NIP amendment process:
- Proposal: Draft a NIP amendment describing the change, the motivation, and the protocol version that introduces it.
- Review: Community review period (minimum 14 days for non-security changes).
- Implementation: At least two independent client implementations must demonstrate the change before the NIP is merged.
- Version bump: If the change modifies existing semantics (see section 1), the protocol version is incremented.
- Deprecation: Old behavior follows the standard deprecation timeline (section 4) unless the change is security-critical (section 5).
8. Compatibility Matrix
Feature Availability by Protocol Version
| Feature | v1 | v2 (planned) | Notes |
|---|---|---|---|
| Device delegation (10050) | Yes | Yes | |
| Checkpoints with Merkle root (10051) | Yes | Yes | |
| DM conversation state (10052) | Yes | Yes | |
| Storage pacts (10053-10058) | Yes | Yes | |
| Storage endpoint hints (10059) | Yes | Yes | |
| Social recovery (10060-10061) | Yes | Yes | |
| Push notification registration (10062) | Yes | Yes | |
| Deletion requests (10063) | Yes | Yes | |
| Content reports (10064) | Yes | Yes | |
| Emergency suspension (10065) | Yes | Yes | |
| Guardianship completion (10066) | Yes | Yes | |
supported_versions in pact requests |
No | Yes | v1 clients infer version from protocol_version tag |
negotiated_version in pact events |
No | Yes | v1 pacts implicitly use v1 |
| Version renegotiation at checkpoint | No | Yes | v1 pacts do not renegotiate |
Version 2 is a placeholder showing how the matrix will evolve. The specific features in v2 will be determined by the first round of protocol changes that require a version bump.
Client Version Declaration
Client implementations MUST declare their supported version range in a machine-readable format:
{
"client": "gozzip-ios",
"client_version": "1.2.0",
"min_protocol_version": 1,
"max_protocol_version": 2,
"deprecated_versions": []
}
This declaration is for developer documentation and interoperability testing. It is not published as an event.
Test Vectors
Each protocol version MUST include test vectors for interoperability verification. Test vectors cover:
- Canonical event serialization. A reference event for each kind with all fields populated, plus the expected serialized bytes.
- Challenge hash computation. A reference event set, nonce, and expected SHA-256 output (as defined in pact-state-machine.md).
- Merkle root computation. A reference event set and expected Merkle root (as defined in messages.md for kind 10051).
- Key derivation. A reference root key, derivation parameters, and expected derived keys (DM key, governance key).
- Rotating request token. A reference pubkey, date, and expected token hash.
Version 1 test vectors (to be published alongside the first client implementation):
Test vector 1: Challenge hash
Events: [event_a (device_pubkey=0xAA..., seq=0), event_b (device_pubkey=0xAA..., seq=1)]
Nonce: 0x0000...0001 (32 bytes)
Expected hash: (to be computed from reference implementation)
Test vector 2: Merkle root
Events: [event_a, event_b, event_c] (ordered by device_pubkey, seq)
Expected root: (to be computed from reference implementation)
Test vector 3: Key derivation
Root private key: 0x0101...01 (32 bytes, test only)
Salt: "gozzip-v1"
Info: "dm-decryption-0"
Expected DM public key: (to be computed)
Test vector 4: Rotating request token
Target pubkey: 0xAAAA...AA (32 bytes hex)
Date: "2026-01-15"
Expected token: (to be computed)
Concrete values will be filled in from the reference Rust implementation in simulator/ once it produces canonical outputs. The structure above defines what each test vector must cover.
Interaction with Existing Protocol
Backward Compatibility with Version 1
The current protocol (all events described in messages.md) is version 1. This versioning strategy is itself a version-1 specification -- no existing events need to change.
The additions introduced by this document (the supported_versions tag on kind 10055/10056, the negotiated_version tag on kind 10053) are optional tags that version-1 clients will ignore. A version-1 client that does not understand supported_versions will still form pacts normally; it simply will not participate in explicit version negotiation.
Relationship to Pact State Machine
The pact state machine gains one new consideration: version incompatibility as a reason for pact degradation. If a pact partner publishes events with a protocol_version that the client cannot process (below client's minimum or above client's maximum with no backward-compatible interpretation), the pact transitions to Degraded and the client seeks a replacement.
This is not a new state or transition -- it uses the existing Degraded state and replacement-seeking behavior defined in the state machine.
Relationship to Key Derivation
The HKDF salt gozzip-v1 (defined in ADR 007) is a fixed protocol constant, not tied to the protocol version number. If key derivation changes in a future version, a new salt (e.g., gozzip-v2-kdf) would be introduced, but the version in the salt refers to the KDF scheme version, not the protocol version. This avoids coupling key material to protocol versioning.
Open Questions
-
Version discovery without pact formation. Should there be a lightweight version-check mechanism (e.g., a NIP-11 relay information document extension) so clients can discover a peer's supported versions before initiating pact negotiation?
-
Multi-version event publishing. Should clients publish critical events (kind 10050, kind 10051) in multiple protocol versions simultaneously during transition periods, or is forward compatibility sufficient?
-
Relay-side version filtering. Should relays support filtering by
protocol_versionin subscription filters (e.g.,{"kinds": [10055], "#protocol_version": ["2"]}), or is client-side filtering adequate?