Context
Companion to #21 (forward-relay opt-in transport) — extends the "relays-as-availability-infrastructure" story with an always-on signed record registry that fills the gap between transient topic-swarm announces and persistent custody intents.
First consumer: anonGPT marketplace (seller publishes signed Offer, buyer lists + verifies). Generic enough that future apps (job boards, file-share directories, signed gossip feeds) can reuse without modification.
Design contract
Trust model: identical to topic-swarm — relay can omit/reorder records but cannot forge them. Records are signed by their author. Verification happens on the buyer side (verifyDirectory()-style adapter); relay's job is transport only.
Trustless properties:
- Signature binds (authorPubkey, payload, timestamp) tuple
- Relay never inspects payload contents beyond size + signature + timestamp envelope
- Buyer's
verifyDirectory() adapter is the authoritative validator
Service shape
Mirrors ForwardRelay exactly — opt-in, default-off, fleet-deploy-on-config-flip.
Wire protocol (hiverelay-signed-directory channel)
publish({ authorPubkey, payload, timestamp, signature }) — open to anonymous peers; relay validates signature + size + timestamp; stores in local Hyperbee keyed by authorPubkey; newest-timestamp-wins overwrite
list({ since? }) — open; returns all non-expired records, optionally filtered by timestamp > since
subscribe() — streaming pushes of new records as they land (replication primitive for cross-relay)
Storage policy (concrete defaults)
| knob |
default |
rationale |
maxEntryBytes |
8192 |
per-record cap |
ttlSeconds |
86_400 (24h) |
balances always-on vs eviction churn |
maxEntriesPerAuthor |
1 |
single live record per authorPubkey; updates overwrite |
publishRatePerMinute |
5 (per-peer) |
mirrors circuit-relay.maxReservesPerMin |
maxTotalEntries |
10_000 |
bounds the bee for storage-constrained operators |
evictionPolicy |
TTL-oldest-first, then LRU |
predictable, simple |
clockSkewToleranceSeconds |
60 |
prevents backdated "always-newest" key squatting |
Critical: clockSkewToleranceSeconds enforces |now - offerTs| < tolerance on BOTH sides. Without it, a malicious author with offerTs = 2099-01-01 permanently outranks legitimate updates.
Config namespace
{
"signedDirectory": {
"enabled": true,
"maxEntryBytes": 8192,
"ttlSeconds": 86400,
"maxEntriesPerAuthor": 1,
"publishRatePerMinute": 5,
"maxTotalEntries": 10000,
"clockSkewToleranceSeconds": 60
}
}
Default-off. Operators opt in deliberately, same posture as forwardRelay.
Replication
Cross-relay replication mechanism — prefer option 2 below:
Federation HTTP poll extension — pull-based, lag scales with poll interval. Don't.
- New Protomux push channel (
hiverelay-signed-directory-push) — symmetric with the existing custody-push design. Source of truth: any relay that received a publish propagates to peer relays. Receivers dedupe by (authorPubkey, timestamp).
Seller multi-publishes (no inter-relay replication) — doesn't fit the "always-on" framing if a buyer hits an empty relay.
Out of scope (intentionally)
- Relay does NOT validate payload semantics (no "is this a real offer?" check)
- Relay does NOT charge for publish (zero-cost open-write; abuse-resistance is in the limits, not in payment)
- Relay does NOT enforce uniqueness beyond
(authorPubkey, timestamp) — multiple authors can publish identical payloads
- Relay does NOT broadcast to non-subscribing clients (no push to random peers)
PR shape
Mirrors the ForwardRelay PR shape:
packages/core/core/services/signed-directory.js — service implementation
test/unit/signed-directory.test.js — brittle: rejects unsigned/oversized/backdated entries, TTL eviction, newest-timestamp-wins, per-peer rate limit, total-entries cap eviction under pressure
- Optional but ideal:
test/integration/signed-directory-replication.test.js — two-relay setup proving cross-relay propagation
config.signedDirectory plumbing in RelayNode
PRODUCTION.md "## SignedDirectory (opt-in registry)" operator enable section
- CHANGELOG entry for v0.10.2
Acceptance
- Brittle suite green
- Manual: a buyer connecting to a
signedDirectory.enabled=true relay can list() records the seller published; relay restart doesn't lose records within TTL; expired records eviction-pass cleans up
- Replication: a record published to relay A appears on relay B within 30s when both are signedDirectory-enabled
Deploy plan (maintainer)
- Land PR → tag v0.10.2 → roll fleet → flip
signedDirectory.enabled=true in each box's /root/.hiverelay/config.json (same atomic-merge pattern used for forwardRelay)
- Three already-fronted endpoints become signed-directory-capable:
wss://relay-us.p2phiverelay.xyz, wss://relay-sg.p2phiverelay.xyz, and wss://relay-eu.p2phiverelay.xyz (once DNS lands)
Context
Companion to #21 (forward-relay opt-in transport) — extends the "relays-as-availability-infrastructure" story with an always-on signed record registry that fills the gap between transient topic-swarm announces and persistent custody intents.
First consumer: anonGPT marketplace (seller publishes signed Offer, buyer lists + verifies). Generic enough that future apps (job boards, file-share directories, signed gossip feeds) can reuse without modification.
Design contract
Trust model: identical to topic-swarm — relay can omit/reorder records but cannot forge them. Records are signed by their author. Verification happens on the buyer side (
verifyDirectory()-style adapter); relay's job is transport only.Trustless properties:
verifyDirectory()adapter is the authoritative validatorService shape
Mirrors
ForwardRelayexactly — opt-in, default-off, fleet-deploy-on-config-flip.Wire protocol (
hiverelay-signed-directorychannel)publish({ authorPubkey, payload, timestamp, signature })— open to anonymous peers; relay validates signature + size + timestamp; stores in local Hyperbee keyed byauthorPubkey; newest-timestamp-wins overwritelist({ since? })— open; returns all non-expired records, optionally filtered bytimestamp > sincesubscribe()— streaming pushes of new records as they land (replication primitive for cross-relay)Storage policy (concrete defaults)
maxEntryBytesttlSecondsmaxEntriesPerAuthorpublishRatePerMinutecircuit-relay.maxReservesPerMinmaxTotalEntriesevictionPolicyclockSkewToleranceSecondsCritical:
clockSkewToleranceSecondsenforces|now - offerTs| < toleranceon BOTH sides. Without it, a malicious author withofferTs = 2099-01-01permanently outranks legitimate updates.Config namespace
{ "signedDirectory": { "enabled": true, "maxEntryBytes": 8192, "ttlSeconds": 86400, "maxEntriesPerAuthor": 1, "publishRatePerMinute": 5, "maxTotalEntries": 10000, "clockSkewToleranceSeconds": 60 } }Default-off. Operators opt in deliberately, same posture as
forwardRelay.Replication
Cross-relay replication mechanism — prefer option 2 below:
Federation HTTP poll extension— pull-based, lag scales with poll interval. Don't.hiverelay-signed-directory-push) — symmetric with the existing custody-push design. Source of truth: any relay that received apublishpropagates to peer relays. Receivers dedupe by(authorPubkey, timestamp).Seller multi-publishes (no inter-relay replication)— doesn't fit the "always-on" framing if a buyer hits an empty relay.Out of scope (intentionally)
(authorPubkey, timestamp)— multiple authors can publish identical payloadsPR shape
Mirrors the ForwardRelay PR shape:
packages/core/core/services/signed-directory.js— service implementationtest/unit/signed-directory.test.js— brittle: rejects unsigned/oversized/backdated entries, TTL eviction, newest-timestamp-wins, per-peer rate limit, total-entries cap eviction under pressuretest/integration/signed-directory-replication.test.js— two-relay setup proving cross-relay propagationconfig.signedDirectoryplumbing inRelayNodePRODUCTION.md"## SignedDirectory (opt-in registry)" operator enable sectionAcceptance
signedDirectory.enabled=truerelay canlist()records the seller published; relay restart doesn't lose records within TTL; expired records eviction-pass cleans upDeploy plan (maintainer)
signedDirectory.enabled=truein each box's/root/.hiverelay/config.json(same atomic-merge pattern used for forwardRelay)wss://relay-us.p2phiverelay.xyz,wss://relay-sg.p2phiverelay.xyz, andwss://relay-eu.p2phiverelay.xyz(once DNS lands)