Secret
The Secret is the central protocol-level object that an Owner identity protects. It carries the user-facing data along with everything the protocol needs to operate: the roster of paired Helpers, the roster of paired Replicas, and the keys those rosters depend on. Every Secret is self-contained — a device that holds the current Secret holds the full Owner identity.
This chapter is the canonical reference for the Secret. It covers the shape, the lifecycle, and the design choice that makes the Secret self-contained.
Two views
At the primitives level, a Secret is an opaque bag of bytes. The threshold secret-sharing scheme treats it as a single value to be split into shares; the math doesn't care what's inside.
At the protocol level, a Secret is a self-contained structured object:
{
"secret_id": "...",
"version": "...",
"secret": {
"helpers": [...],
"replicas": {
"shared_key": [...],
"replicas": [...],
},
"secrets": [...],
"owner_replica_id": "..."
}
}The outer level — secret_id, version, secret — is the addressable container. The inner secret field is where the substance lives: the rosters of paired participants, the user-facing data, and the attribution stamp of whoever produced this version. Helpers store opaque shares of the bag-of-bytes view; the structured view exists only on devices that hold the Owner identity.
The fields
secret_id
secret_idThe protocol-level identifier of this Secret. Fixed for the lifetime of the Owner identity — it is chosen when the identity is created and never changes. The secret_id partitions every other piece of state in the protocol: paired channels, stored shares, version history. An application may manage multiple Owner identities, each with its own secret_id, but a single Owner identity always has exactly one.
version
versionA monotonic counter that increments every time the inner secret field changes. The (secret_id, version) pair uniquely identifies a specific snapshot of the Secret.
version is always incremented on any change to secret — there is no batching, no debounce. Every observable change is a new version.
secret.helpers
secret.helpersThe roster of paired Helpers. Each entry carries:
The Helper's
channel_id— the protocol-level handle for the channel.The Helper's transport endpoint — where messages should be sent to reach this Helper.
The per-Helper Shared Key — the symmetric key that authenticates and encrypts every message on this channel.
Because the per-Helper Shared Key lives inside the Secret, every device in the Owner identity that holds the current Secret can talk to this Helper without further pairing. This is what makes recovery transitivity work — see Recovery transitivity.
secret.replicas
secret.replicasThe roster of paired Replicas under the same Owner identity. Each entry carries:
The Replica's
replica_id— the per-device stable identifier.The Replica's transport endpoint.
Shared Key material for the replica group.
The replica-group Shared Key that authenticates and encrypts every message between Replicas lives in this section. Unlike the per-Helper Shared Key (one per Helper), the replica-group Shared Key is one key shared across the whole group. A new Replica joining adopts it during its initial sync — see The shared replica-group key.
secret.secrets
secret.secretsThe user-facing data the application is protecting. Each entry is one logical secret — a wallet seed phrase, a private key, a credential, encrypted backup material, etc. The protocol does not impose a structure on what an individual entry looks like; applications choose the shape they need.
Some applications might opt to use onse UserSecret per secret to be protected (e.g. one UserSecret for Wallet Seed Phrase, one for TOPT Recovery Code, etc) while others might use a single UserSecret to put them all in a custom format.
secret.owner_replica_id
secret.owner_replica_idThe replica_id of the device that created or last updated this version of the Secret. Every increment of version stamps this field with the writer's id, so a device receiving a Secret sync can see who produced it without consulting any other source.
This is the protocol's internal attribution channel. It complements the replica_id that gets stamped on each share write that reaches the Helpers — the two values should agree for any given (secret_id, version), and disagreement between them is one of the surfaces that drives Conflict resolution.
How the Secret evolves
Every observable change to the inner secret field is a new version. The triggers, in practice:
User secrets change. An entry in
secret.secretsis added, removed, or rotated.The Helper roster changes. A Helper is paired or unpaired, or an existing Helper's addressing changes.
The Replica roster changes. A Replica is paired or unpaired.
Every increment is a full new snapshot, not a diff. The protocol does not transmit per-field changes — when version increments, the new state of every field is published in full to every paired Replica, and a fresh share goes to every paired Helper. The new version is also stamped with owner_replica_id = the replica_id of the device that drove the change. See Secret synchronization for the wire mechanics.
Worked example
The table below traces a single Secret from "just created" through pairing two Replicas, three Helpers, and a third Replica. Threshold t = 3.
State notation per device: v=N (h=H, s=S, r=R) — version N of the Secret, with H paired Helpers, S user secrets, and R paired Replicas (the originating device counts itself in r). The annotation shares list means the share map in the snapshot is populated; absence means it isn't yet.
Step
Action
latest_version after
Replica A
Replica B
Replica C
Helpers
0
new()
None ("v0")
—
—
—
—
1
pair replica A → verify_fingerprint
1
✓ v=1 (h=0, s=0, r=1)
—
—
—
2
ProtectSecret([s1])
2
✓ v=2 (h=0, s=1, r=1)
—
—
—
3
pair replica B → verify_fingerprint
3
✓ v=3 (h=0, s=1, r=2)
✓ v=3 — bootstrap
—
—
4
pair helper #1 → auto-publish
4
✓ v=4 (h=1, s=1, r=2)
✓ v=4
—
— (1<3)
5
pair helper #2 → auto-publish
5
✓ v=5 (h=2, s=1, r=2)
✓ v=5
—
— (2<3)
6
ProtectSecret([s1, s2])
6
✓ v=6 (h=2, s=2, r=2)
✓ v=6
—
— (2<3)
7
pair helper #3 → auto-publish
7
✓ v=7 (h=3, s=2, r=2, shares list)
✓ v=7 (same)
—
✓ all 3 Helpers get VSS shares
8
pair replica C → verify_fingerprint
8
✓ v=8 (h=3, s=2, r=3, shares list)
✓ v=8 (same)
✓ v=8 — full bootstrap (h=3, s=2, r=3, shares list)
✓ all 3 Helpers get fresh VSS shares at v=8
Worth highlighting:
Every step bumps
versionby exactly one. It does not matter whether the trigger was a user-secret change, a roster change, or a Helper join — the version increments once per change and the new snapshot is what gets distributed.The Replica roster includes the device itself. Step 1 starts at
r=1, notr=0— the originating device is insecret.replicasfrom the beginning.Joining as a Replica is a bootstrap. Steps 3 and 8 show the new device receiving the full Secret at the current version directly. There is no per-version replay to catch up.
Below the threshold, no Helper shares are distributed. Steps 4–6 add Helpers and a user secret without producing any wire traffic to Helpers (annotated
1<3/2<3). The Helpers are paired but holding nothing.The first time the threshold is met, every paired Helper receives a share. Step 7 is the inflection point — pairing the third Helper takes the roster from 2 to 3, the threshold is reached, and all three Helpers receive a VSS share at v=7 in one round (not just the newly-paired one).
Subsequent changes refresh every Helper. Step 8 adds a Replica; all three Helpers get fresh shares stamped with the new version. The threshold-recovery path stays consistent with the current Secret without any extra application-level coordination.
Self-containment
The Secret is self-contained: it carries every key, every roster entry, every configuration value that the Owner identity needs in order to drive the protocol. There is no extra state held outside the Secret that a device would also need in order to act as the Owner identity.
This is the design choice that makes the replica feature work. A device that holds the current Secret holds everything:
The plaintext user data, in
secret.secrets.The per-Helper Shared Keys, in
secret.helpers. Any of them is enough to talk to the corresponding Helper.The replica-group Shared Key, in
secret.replicas. Enough to talk to the rest of the replica group.
The cost of this design is documented in Recovery transitivity: a compromised device that holds the Secret is equivalent to a compromised Owner. There is no privilege separation between devices in the Owner identity — they all hold the same thing.
What's not in the Secret
The protocol leaves a few things to the implementation, deliberately:
Where the Secret is stored locally. Applications decide between in-memory, on-disk, encrypted-at-rest with a device key, etc.
How the Secret is serialised at rest. The wire format is part of the protocol; the at-rest format is the application's choice.
Cached state about in-flight protocol flows. Pending sharing rounds, outstanding recovery sessions, accumulators — these are runtime state owned by the protocol implementation and are not part of the Secret.
These boundaries keep the Secret as the durable, replicable snapshot of "what the Owner identity is", while the runtime layer manages the volatile state.
Last updated