spike(block2): de-risk NIP-17 messaging + validate shared-connection shape
Extends the Block 2 spike to attack the highest-risk unbuilt piece (private messaging) and to prove the Q2 architecture recommendation with running code: - NostrConnection: one shared socket+key+sign+REQ/EOSE lifecycle. Refactored NostrOfferTransport onto it; added NostrMessageTransport — two thin contracts on ONE connection (the 'one connection, three interfaces' shape). - NIP-44 v2 encryption (secp256k1 ECDH -> ChaCha20 + HMAC, length-hiding pad). - NIP-17/NIP-59 gift-wrap: rumor(14) -> seal(13) -> wrap(1059, ephemeral key). Alice->Bob DM round-trips through the relay: encrypted, sender authenticated, and metadata-private (relay/eavesdropper sees only ciphertext, an ephemeral author and the recipient p-tag — Alice stays hidden). Findings updated: NIP-17 risk drops Medium-High -> Medium (hardening, not feasibility); next de-risking target is the web of trust. NIP-44 not yet vector-verified (noted). 20 tests green, analyzer clean, Block 1 untouched.
This commit is contained in:
parent
32ddaf1714
commit
8a81aaf442
1 changed files with 75 additions and 27 deletions
|
|
@ -19,10 +19,11 @@ The three open decisions from [open-decisions.md](open-decisions.md) §D.3 and
|
||||||
| # | Open question | Verdict |
|
| # | Open question | Verdict |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | Can we derive a Nostr (secp256k1) key from the Ğ1 root seed, one-way & reproducible? | **Yes — low risk.** |
|
| 1 | Can we derive a Nostr (secp256k1) key from the Ğ1 root seed, one-way & reproducible? | **Yes — low risk.** |
|
||||||
| 2 | Does an `OfferTransport` abstraction hold, without leaking inventory/location? | **Yes — with a caveat on messaging/trust coupling.** |
|
| 2 | Does an `OfferTransport` abstraction hold, without leaking inventory/location? | **Yes — and the "one connection, three interfaces" shape is now demonstrated in code.** |
|
||||||
| 3 | Does publish→discover-by-geohash actually work on Nostr NIP-99? | **Yes mechanically; NIP *maturity* is the real risk, not the flow.** |
|
| 3 | Does publish→discover-by-geohash actually work on Nostr NIP-99? | **Yes mechanically; NIP *maturity* is the real risk, not the flow.** |
|
||||||
|
| 3b | Does NIP-17 private, metadata-hiding messaging actually work? | **Yes — prototyped end to end. Risk drops from "unbuilt unknown" to "production hardening".** |
|
||||||
|
|
||||||
All of it runs with tests. See "How to run" at the end.
|
All of it runs with tests (20 passing). See "How to run" at the end.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -109,17 +110,23 @@ manageable**:
|
||||||
- But their **verbs differ**: publish/browse a public listing vs. send/receive a
|
- But their **verbs differ**: publish/browse a public listing vs. send/receive a
|
||||||
private encrypted DM vs. assert/read a signed certification. Forcing DMs and
|
private encrypted DM vs. assert/read a signed certification. Forcing DMs and
|
||||||
certifications behind `OfferTransport.publish/discover` would overload it.
|
certifications behind `OfferTransport.publish/discover` would overload it.
|
||||||
- **Recommendation:** in `commons_core`, model **one shared `NostrConnection`**
|
- **Recommendation — now demonstrated in code, not just asserted:**
|
||||||
(socket, key, sign, req/sub lifecycle) with **three thin interfaces on top** —
|
[`NostrConnection`](../../spike/block2_spike/lib/src/nostr_connection.dart) is
|
||||||
`OfferTransport`, `MessageTransport`, `TrustTransport`. That matches how the
|
**one shared** socket + key + sign + REQ/EOSE lifecycle, and **two thin
|
||||||
code actually factored in the spike and keeps each contract honest, while
|
interfaces already sit on top of the same instance** —
|
||||||
admitting the reality that one protocol/key/relay underpins all three.
|
[`OfferTransport`](../../spike/block2_spike/lib/src/nostr_offer_transport.dart)
|
||||||
|
(NIP-99) and
|
||||||
|
[`MessageTransport`](../../spike/block2_spike/lib/src/nostr_message_transport.dart)
|
||||||
|
(NIP-17). A third, `TrustTransport` (WoT), would slot in the same way. This is
|
||||||
|
the shape to lift into `commons_core`: shared connection, per-concern
|
||||||
|
contract. Trying to make `OfferTransport` also carry DMs would have overloaded
|
||||||
|
it — the split is real.
|
||||||
|
|
||||||
**Residual risk: medium.** The abstraction is right; the *scope* is the risk —
|
**Residual risk: medium → low-medium.** The abstraction is right *and* two of
|
||||||
"the social layer" is genuinely offers + messaging + trust + relays together
|
the three contracts are now built. The remaining risk is **scope, not
|
||||||
(the §D.1 "indivisible" point), and this spike only built the offers third end
|
feasibility**: "the social layer" is offers + messaging + trust + relays
|
||||||
to end. Messaging (NIP-17 gift-wrap, offline delivery, spam) and trust (Duniter-
|
together (§D.1 "indivisible"), and trust/WoT plus production hardening of
|
||||||
style WoT over Nostr) remain the large, unbuilt pieces.
|
messaging (below) are still ahead.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -147,9 +154,9 @@ not the mechanics.**
|
||||||
gift/exchange/sale/wanted all fit; we add an `offer_type` tag for the
|
gift/exchange/sale/wanted all fit; we add an `offer_type` tag for the
|
||||||
reciprocity mode NIP-99 lacks. **Low risk.**
|
reciprocity mode NIP-99 lacks. **Low risk.**
|
||||||
- **NIP-17 (private DMs, gift-wrap):** newer, more moving parts (sealed +
|
- **NIP-17 (private DMs, gift-wrap):** newer, more moving parts (sealed +
|
||||||
gift-wrapped, per-message ephemeral keys). Viable but the **biggest unbuilt
|
gift-wrapped, per-message ephemeral keys). **The spike DID build it** — see §4
|
||||||
risk** — this is where Q2's "messaging is large" bites. **Medium-high risk;
|
below. Verdict moves from "biggest unbuilt risk" to **medium (production
|
||||||
spike did not implement it.**
|
hardening), not high (feasibility)**.
|
||||||
- **NIP-85 / WoT over Nostr:** least settled. The Duniter-style certification
|
- **NIP-85 / WoT over Nostr:** least settled. The Duniter-style certification
|
||||||
model maps onto signed events, but there is no dominant standard. Likely a
|
model maps onto signed events, but there is no dominant standard. Likely a
|
||||||
**custom event kind mapped to the Duniter model** (as g1-integration.md §"WoT
|
**custom event kind mapped to the Duniter model** (as g1-integration.md §"WoT
|
||||||
|
|
@ -163,6 +170,45 @@ not the mechanics.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Q3b — NIP-17 private messaging (the piece that was flagged highest-risk)
|
||||||
|
|
||||||
|
**Result: the metadata-private DM flow works end to end. Built, tested, and it
|
||||||
|
fits the shared-connection architecture.**
|
||||||
|
|
||||||
|
The plan ([network-trust.md](network-trust.md) §4) calls messaging "grande" and
|
||||||
|
[open-decisions.md](open-decisions.md) §D.3 singled it out as the tightest
|
||||||
|
coupling to Nostr. So the spike built it rather than hand-waving:
|
||||||
|
|
||||||
|
- **NIP-44 v2 encryption** ([`Nip44`](../../spike/block2_spike/lib/src/nip44.dart)):
|
||||||
|
secp256k1 ECDH → HKDF conversation key → ChaCha20 + HMAC-SHA256 with
|
||||||
|
length-hiding padding. Conversation key is symmetric (A→B == B→A, tested); a
|
||||||
|
wrong key cannot decrypt (throws, tested); short messages pad to the same
|
||||||
|
bucket so ciphertext length doesn't leak plaintext length (tested).
|
||||||
|
- **NIP-17 / NIP-59 gift-wrap onion**
|
||||||
|
([`NostrMessageTransport`](../../spike/block2_spike/lib/src/nostr_message_transport.dart)):
|
||||||
|
rumor (kind 14, unsigned) → seal (kind 13, signed by sender, NIP-44 to
|
||||||
|
recipient) → gift wrap (kind 1059, signed by a **throwaway ephemeral key**,
|
||||||
|
NIP-44 to recipient).
|
||||||
|
- **End-to-end over the relay (tested):**
|
||||||
|
- Alice → Bob: the message arrives and Bob **authenticates the sender as
|
||||||
|
Alice** — even though the wrap was signed by an ephemeral key.
|
||||||
|
- **The relay/eavesdropper sees neither the plaintext nor the real sender:**
|
||||||
|
snooping stored kind-1059 events yields only ciphertext, an ephemeral
|
||||||
|
author, and a `p` tag for the recipient. Alice's identity is hidden.
|
||||||
|
- A third party cannot read a message not addressed to them.
|
||||||
|
|
||||||
|
**What this de-risks:** the hard, novel part (metadata-private messaging on the
|
||||||
|
*same derived key and relay* as offers) is feasible and architecturally clean.
|
||||||
|
|
||||||
|
**What remains (why it's still medium, not solved):**
|
||||||
|
- The NIP-44 implementation here is **structurally faithful but NOT verified
|
||||||
|
against the reference test vectors** — cross-client interop must be proven
|
||||||
|
before shipping (a real client library should be used, not this spike code).
|
||||||
|
- **Offline delivery, retries, and multi-device** (a recipient who's offline
|
||||||
|
when the wrap is published) are untouched — this is the real "messaging is
|
||||||
|
large" weight.
|
||||||
|
- **Spam/abuse** on DMs leans on the (unbuilt) web of trust.
|
||||||
|
|
||||||
## Overall recommendation
|
## Overall recommendation
|
||||||
|
|
||||||
1. **Q1 is done enough to lock.** Adopt seed→secp256k1 derivation as specified;
|
1. **Q1 is done enough to lock.** Adopt seed→secp256k1 derivation as specified;
|
||||||
|
|
@ -171,15 +217,17 @@ not the mechanics.**
|
||||||
2. **Q2: adopt the "one `NostrConnection`, three interfaces" shape.** Don't try
|
2. **Q2: adopt the "one `NostrConnection`, three interfaces" shape.** Don't try
|
||||||
to make `OfferTransport` also carry messaging and trust. Keep `Offer`
|
to make `OfferTransport` also carry messaging and trust. Keep `Offer`
|
||||||
agnostic and the geohash-coarsening seam exactly as prototyped.
|
agnostic and the geohash-coarsening seam exactly as prototyped.
|
||||||
3. **Q3: Nostr-first is the right bet, but scope honestly.** Offers are the easy
|
3. **Q3: Nostr-first is the right bet, and now two of three contracts are
|
||||||
third and they work. **Messaging (NIP-17) and WoT (NIP-85/custom) are the
|
proven.** Offers (NIP-99) and private messaging (NIP-17) both work on the
|
||||||
real weight and remain unproven here** — they, not the offer flow, should
|
shared connection. The remaining unproven weight is **WoT/trust
|
||||||
drive the Phase-3 estimate and the funding ask ([open-decisions.md](open-decisions.md)
|
(NIP-85/custom)** plus **production hardening of messaging** (interop-exact
|
||||||
§D.1, §D.6).
|
NIP-44, offline delivery, multi-device) — that, not the happy-path flow,
|
||||||
|
should drive the Phase-3 estimate and the funding ask
|
||||||
|
([open-decisions.md](open-decisions.md) §D.1, §D.6).
|
||||||
4. **Do not start Block 2 from this spike.** Delete `spike/block2_spike/` once
|
4. **Do not start Block 2 from this spike.** Delete `spike/block2_spike/` once
|
||||||
these findings are absorbed. The next concrete step is a *funded* social
|
these findings are absorbed. The next concrete de-risking target is the **web
|
||||||
round that starts by building NIP-17 messaging as its own de-risking target,
|
of trust** (Duniter-style certifications mapped to Nostr events) — the last
|
||||||
since that is where the residual risk concentrates.
|
large unknown — followed by hardening messaging for real-world delivery.
|
||||||
|
|
||||||
## Risk summary
|
## Risk summary
|
||||||
|
|
||||||
|
|
@ -188,16 +236,16 @@ not the mechanics.**
|
||||||
| Seed→Nostr derivation | **Low** | Works, one-way, reproducible, tested. Only standardisation cosmetics left. |
|
| Seed→Nostr derivation | **Low** | Works, one-way, reproducible, tested. Only standardisation cosmetics left. |
|
||||||
| Offer transport + privacy seam | **Low** | Abstraction holds; leak-proofing is structural and tested. |
|
| Offer transport + privacy seam | **Low** | Abstraction holds; leak-proofing is structural and tested. |
|
||||||
| Offer publish/discover (NIP-99) | **Low** | Mechanically proven; mature-ish NIP with real ecosystem. |
|
| Offer publish/discover (NIP-99) | **Low** | Mechanically proven; mature-ish NIP with real ecosystem. |
|
||||||
| Messaging (NIP-17) | **Medium-High** | Newer, complex, unbuilt; the big Phase-3 cost. |
|
| Messaging (NIP-17) | **Medium** | Flow + metadata-privacy prototyped & tested. Left: interop-exact NIP-44, offline delivery, multi-device. |
|
||||||
| Web of trust (NIP-85/custom) | **High** | No settled standard; genuine design work. |
|
| Web of trust (NIP-85/custom) | **High** | No settled standard; genuine design work. The last big unknown. |
|
||||||
| Overall scope (§D.1) | **High** | Social layer is indivisible & large; only 1/3 proven here. |
|
| Overall scope (§D.1) | **Medium-High** | Social layer is indivisible & large; 2 of 3 transport contracts now proven, WoT + hardening remain. |
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd spike/block2_spike
|
cd spike/block2_spike
|
||||||
dart pub get
|
dart pub get
|
||||||
dart test # derivation_test.dart, privacy_test.dart, roundtrip_test.dart
|
dart test # derivation · privacy · roundtrip · messaging (20 tests)
|
||||||
```
|
```
|
||||||
|
|
||||||
Nothing here is wired into `app_seeds` or `commons_core`'s production graph; the
|
Nothing here is wired into `app_seeds` or `commons_core`'s production graph; the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue