feat(chain): multi-device registry (v2.2.0-alpha1)
PR #1 of the multi-device roadmap. Adds per-device X25519 keys registered on-chain so senders can fan out envelopes across all of a recipient's physical devices — fixes the single-device limitation where a second phone / desktop loses messages as soon as the first one reads them. Chain (blockchain/): - New event types LINK_DEVICE / UNLINK_DEVICE, signed by the identity's master Ed25519. - LinkDevicePayload {x25519_pub_key, device_name} + UnlinkDevicePayload {x25519_pub_key} on the wire. - State: prefixDevice + x25519_pub → DeviceRecord{owner, name, added_at, revoked_at?}; reverse index prefixDevicesByOwner for O(k) listing. Revoke is a soft-delete — the row stays as a visible tombstone so offline clients can detect their own revocation and wipe local state. - MaxDevicesPerOwner = 10 slot cap; MaxDeviceNameLen = 64. - Strict lowercase-hex validation on x25519_pub so clients can't desync on letter case. - Same-owner re-link is a rename/refresh (recreates reverse index too — needed after a revoke). - Chain.DevicesOf(master_pub) returns the active records; empty slice for legacy identities so senders can fall back to IdentityInfo.X25519Pub. HTTP (node/): - GET /api/devices/{master_pub_or_addr} — returns {master_pub, count, devices[]}. Revoked records filtered out. - /api/identity/{pub} gains `device_count` so senders can decide upfront whether to fan out or take the legacy path. Tests (blockchain/devices_test.go): - Happy paths (1, 3 devices), foreign-owner rejection, same-owner refresh after revoke, unlink removes from active set, foreign-signer unlink rejection, idempotent double-unlink, malformed pub/name rejection, MaxDevices cap + recovery after unlink frees a slot, empty list for unknown master. Also in this commit: - deploy/single/join.sh — convenience script operators have been iterating on in this session (joiner-node bring-up + firewall port patching + Caddy opt-out). - client-app/app.json — `usesCleartextTraffic: true` on Android so installed APKs can talk to http:// dev nodes without TLS. See docs/ROADMAP.md for PRs #2..#4 (client fan-out, pairing flow, desktop Electron shell).
This commit is contained in:
@@ -46,6 +46,14 @@ const (
|
||||
EventUnfollow EventType = "UNFOLLOW" // unfollow an author
|
||||
EventLikePost EventType = "LIKE_POST" // like a post
|
||||
EventUnlikePost EventType = "UNLIKE_POST" // remove a previous like
|
||||
|
||||
// Multi-device support (v2.2.0): each physical device of an identity gets
|
||||
// its own X25519 keypair for the messenger mailbox. LINK/UNLINK_DEVICE
|
||||
// publish/revoke those pubs on-chain so senders can fan-out envelopes
|
||||
// across the recipient's active devices. Signed by the identity's
|
||||
// master Ed25519 key.
|
||||
EventLinkDevice EventType = "LINK_DEVICE"
|
||||
EventUnlinkDevice EventType = "UNLINK_DEVICE"
|
||||
)
|
||||
|
||||
// Token amounts are stored in micro-tokens (µT).
|
||||
@@ -554,7 +562,71 @@ type IdentityInfo struct {
|
||||
Address string `json:"address"`
|
||||
X25519Pub string `json:"x25519_pub"` // hex Curve25519 key; empty if not published
|
||||
Nickname string `json:"nickname"`
|
||||
Registered bool `json:"registered"` // true if REGISTER_KEY tx was committed
|
||||
Registered bool `json:"registered"` // true if REGISTER_KEY tx was committed
|
||||
// DeviceCount is the number of currently-linked (non-revoked) devices in
|
||||
// this identity's multi-device registry. `0` for legacy identities that
|
||||
// only published a single X25519 via REGISTER_KEY — senders fall back
|
||||
// to IdentityInfo.X25519Pub in that case.
|
||||
DeviceCount int `json:"device_count"`
|
||||
}
|
||||
|
||||
// ── Multi-device registry (v2.2.0) ───────────────────────────────────────
|
||||
|
||||
// MaxDevicesPerOwner caps how many devices a single identity can have
|
||||
// linked concurrently. A sender must encrypt + relay one envelope per
|
||||
// device, so the multiplier bounds the per-message cost. Ten covers
|
||||
// typical users (phone + tablet + laptop + desktop + work phone + …)
|
||||
// without letting an abuse case blow up mailbox traffic. Revoked devices
|
||||
// don't count.
|
||||
const MaxDevicesPerOwner = 10
|
||||
|
||||
// MaxDeviceNameLen caps the length of a human-friendly device label
|
||||
// ("Alice's iPhone 14", "Work MacBook Pro"). Longer names get rejected
|
||||
// at validation. Kept short to discourage using this field as a free-form
|
||||
// comment/profile channel.
|
||||
const MaxDeviceNameLen = 64
|
||||
|
||||
// LinkDevicePayload is embedded in EventLinkDevice transactions. Master
|
||||
// Ed25519 (= tx.From) asserts that the given X25519 pub belongs to one
|
||||
// of its physical devices, publishing it so senders can fan out envelopes.
|
||||
type LinkDevicePayload struct {
|
||||
// X25519PubKey is the hex-encoded Curve25519 pub the device generated
|
||||
// locally. Must be unique in the device registry — senders index
|
||||
// envelopes by this key in the relay mailbox.
|
||||
X25519PubKey string `json:"x25519_pub_key"`
|
||||
// DeviceName is a short human label shown in Settings → Devices.
|
||||
// Purely informational; not used for routing.
|
||||
DeviceName string `json:"device_name"`
|
||||
}
|
||||
|
||||
// UnlinkDevicePayload is embedded in EventUnlinkDevice transactions. Signed
|
||||
// by master Ed25519; marks the referenced device as revoked so senders
|
||||
// stop shipping envelopes to its X25519 pub. The revoked device itself,
|
||||
// once it sees its pub in the revoked list, is expected to wipe its
|
||||
// local state (master Ed25519 priv + chat cache).
|
||||
type UnlinkDevicePayload struct {
|
||||
X25519PubKey string `json:"x25519_pub_key"`
|
||||
}
|
||||
|
||||
// DeviceRecord is the on-chain persisted state for one device link.
|
||||
// Stored at key `prefixDevice + x25519_pub`; the reverse index
|
||||
// `prefixDevicesByOwner + master_pub` keeps a slice of x25519 pubs per
|
||||
// owner for efficient listing.
|
||||
type DeviceRecord struct {
|
||||
Owner string `json:"owner"` // master Ed25519 pub hex
|
||||
X25519PubKey string `json:"x25519_pub_key"` // device X25519 pub hex
|
||||
DeviceName string `json:"device_name"`
|
||||
AddedAt int64 `json:"added_at"` // unix seconds, from tx timestamp
|
||||
RevokedAt int64 `json:"revoked_at,omitempty"` // 0 = active; >0 = revoked
|
||||
}
|
||||
|
||||
// DeviceInfo is the public view of a DeviceRecord served by
|
||||
// GET /api/devices/{master_pub}. Revoked records are not included
|
||||
// in the response (intentionally — sender only needs the active set).
|
||||
type DeviceInfo struct {
|
||||
X25519PubKey string `json:"x25519_pub_key"`
|
||||
DeviceName string `json:"device_name"`
|
||||
AddedAt int64 `json:"added_at"`
|
||||
}
|
||||
|
||||
// ConsensusMessage types used by the PBFT engine over the P2P layer.
|
||||
|
||||
Reference in New Issue
Block a user