mirror of
https://github.com/nostr-protocol/nips.git
synced 2025-03-25 17:21:52 +01:00
Merge 81f7deffaf27d379c3f457f98cf63b2f179f7a83 into 0619f370bca3485bb9c5870bc2defa03c7c3d10e
This commit is contained in:
commit
feeae45a77
316
404.md
Normal file
316
404.md
Normal file
@ -0,0 +1,316 @@
|
||||
# NIP-404: Ghost Events
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
This NIP introduces **Ghost Events**—a protocol for creating events that are **plausibly deniable** with a **ephemeral**
|
||||
nature, providing a weak binding to the author's identity. It leverages ring signatures, elliptic curve point-hashing, and a distance-based proof-of-work that references Bitcoin block hashes for chronological anchoring.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation & Overview
|
||||
|
||||
**Maintain Main Identity**
|
||||
You keep your main key for official posts, but can also create Ghost Events that won’t tarnish your identity if they
|
||||
become embarrassing or sensitive later.
|
||||
|
||||
**Plausible Deniability**
|
||||
Nostr events are permanently linked to the signing public key. Ghost Events only *weakly* link to a user’s main key,
|
||||
so the user can later deny authorship.
|
||||
|
||||
**Ephemerality**
|
||||
Each Ghost Event references a Bitcoin block, providing a **time anchor**. As time passes, more “mined keys”
|
||||
satisfying the same proof-of-work can appear, so the event’s authorship becomes questionable.
|
||||
|
||||
**Based on public information**
|
||||
All components of a Ghost Event are public, so anyone could potentially forge a signature. The protocol’s security
|
||||
relies on the difficulty of finding a matching key.
|
||||
|
||||
**OTS "Resistance"**
|
||||
You always can move the event’s creation window further back by referencing older blocks, and, at the same time,
|
||||
increasing the total PoW difficulty. Thus, an OTS proof can confirm the event *existed*, but not that *only you*
|
||||
could have created it.
|
||||
|
||||
|
||||
### High-Level Mechanics
|
||||
|
||||
- **Ring Signature** over exactly two keys:
|
||||
1) Your real key;
|
||||
2) A “mined” key—found by solving a proof-of-work puzzle (attacker) or randomly (by the real signer).
|
||||
- **Distance-based PoW**: The mined key’s public key must be within distance $\delta$ of a “challenge point” derived from your public key and a Bitcoin block hash.
|
||||
- **Deniability**: Verifiers can only tell that *one* of the two keys signed the event, not which one. Because all components are public, anyone can forge a Ghost Event referencing your key.
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Protocol
|
||||
|
||||
There is a demo here: [Ghost Events Demo](https://github.com/gu1p/nip404_demo/blob/master/main.py)
|
||||
|
||||
### 2.1. Reference a Bitcoin Block
|
||||
|
||||
Pick a Bitcoin block $B$ with hash $H_B$ and timestamp $t_B$. This block anchors the event in time.
|
||||
|
||||
### 2.2. Derive a Challenge Point
|
||||
|
||||
1. Take your main public key $P_A$ and concatenate it with $H_B$.
|
||||
2. Compute $S = \mathrm{SHA256}(P_A \,\|\, H_B)$.
|
||||
3. Map $S$ to secp256k1 ([RFC 9380 “Hashing to Elliptic Curves”](https://www.rfc-editor.org/rfc/rfc9380)): $\mathrm{challenge_PK} = \mathrm{HashToCurve}(S).$
|
||||
|
||||
Anyone can verify this point by performing the same steps.
|
||||
|
||||
### 2.3. Pick a “Mined Key” $P_{\mathrm{mined}}$
|
||||
|
||||
- Let $x_c$ be the $x$-coordinate of $\mathrm{challenge_PK}$.
|
||||
- Find $x_m$ such that $| x_m - x_c | \le \delta$ and $(x_m,y_m)$ is a valid secp256k1 point.
|
||||
- $\delta$ reflects the “difficulty” of finding such a key.
|
||||
|
||||
### 2.4. Create a Ring Signature
|
||||
|
||||
Form a ring of two public keys: $\{P_A, P_{\mathrm{mined}}\}$.
|
||||
Use *your real private key* to sign, producing a ring signature that proves **one** of the private keys (either $\mathrm{sk}_A$ or $\mathrm{sk_mined}$) signed—but not which one.
|
||||
|
||||
### 2.5. Publish the Ghost Event
|
||||
|
||||
Publish a standard Nostr event (`kind`, `content`, etc.) plus tags:
|
||||
|
||||
- `["ghost", "block-hash", "<H_B>"]`
|
||||
- `["ghost", "block-hash-timestamp", "<t_B>"]`
|
||||
|
||||
The event’s `sig` field is the **ring signature**. Verifiers can:
|
||||
|
||||
1. Check validity of the ring signature over $\{P_A, P_{\mathrm{mined}}\}$.
|
||||
2. Recompute $\mathrm{challenge_PK}$ from $\langle P_A, H_B\rangle$.
|
||||
3. Measure how close $P_{\mathrm{mined}}$ is to $\mathrm{challenge_PK}$ (i.e., proof-of-work difficulty).
|
||||
4. Conclude: “Either Alice really signed, or someone else who found a matching $P_{\mathrm{mined}}$ did.”
|
||||
|
||||
---
|
||||
## 3. Client & Relay Behavior
|
||||
|
||||
### 3.1. Clients
|
||||
|
||||
- Display “Deniable” or “Ghost” for events with `ghost` tags.
|
||||
- Show $\delta$ to indicate how hard it is to find a matching mined key.
|
||||
- Emphasize **time**: if the event references an older block, collisions become more likely, thus increasing deniability.
|
||||
|
||||
### 3.2. Relays
|
||||
|
||||
- Treat Ghost Events like any other events.
|
||||
- Optionally index them for specialized queries by `ghost` tags.
|
||||
|
||||
---
|
||||
|
||||
## 4. Security Considerations
|
||||
|
||||
1. **Ring Signature Robustness**
|
||||
Any weaknesses in the ring signature could leak which key was used.
|
||||
|
||||
2. **Choice of $\delta$**
|
||||
A smaller $\delta$ means harder PoW but stronger initial binding to your identity. A larger $\delta$ lowers the barrier for collisions, boosting deniability sooner.
|
||||
|
||||
3. **Block Hash Trust**
|
||||
Must rely on a valid, widely recognized Bitcoin chain tip.
|
||||
|
||||
4. **Long-Term Attacks**
|
||||
As time passes or computational power grows, collisions become easier.
|
||||
|
||||
5. **Anyone Can Forge**
|
||||
Attackers can craft a Ghost Event referencing *your* key without your involvement, ensuring it’s never definitively tied to you.
|
||||
|
||||
6. **Real signer secret key leaks**
|
||||
The Ghost Event’s deniability of all events is completely compromised.
|
||||
---
|
||||
|
||||
## 5. Example Workflow
|
||||
|
||||
1. **Alice Picks a Block**
|
||||
She picks a recent (or somewhat older) Bitcoin block $B$.
|
||||
2. **Compute Challenge**
|
||||
$S = \mathrm{SHA256}(P_A \| H_B)$, then $\mathrm{challenge_PK} = \mathrm{HashToCurve}(S)$.
|
||||
3. **Select Mined Key**
|
||||
Choose $P_{\mathrm{mined}}$ with $| x_m - x_c | \le \delta$.
|
||||
Ideally, clients will offer smart UIs to help pick $\delta$ to match the desired level of deniability.
|
||||
|
||||
We also can combine that with [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) tags.
|
||||
4. **Ring Signature**
|
||||
The ring is $\{P_A, P_{\mathrm{mined}}\}$. Alice signs with $\mathrm{sk}_A $.
|
||||
5. **Publish**
|
||||
Attach `["ghost", "block-hash", <Block Hash>]` and so forth, and post to relays.
|
||||
6. **Verify**
|
||||
Clients confirm ring signature validity and check the PoW distance.
|
||||
|
||||
---
|
||||
|
||||
## 6. OpenTimestamps (OTS) "Resistance"
|
||||
|
||||
Even if the event is OTS-stamped at publication, Ghost Events remain deniable because:
|
||||
|
||||
- They can reference arbitrary *older* blocks and with a smaller $\delta$, moving the event’s creation window further back,
|
||||
but also making it the total PoW harder.
|
||||
- Thus, an OTS proof can confirm the event *existed*, but not that *only you* could have created it, as all components
|
||||
to forge the event are public and were available since the block was mined.
|
||||
|
||||
---
|
||||
|
||||
## 7. Challenge Picking & Verification
|
||||
|
||||
### 7.1 Probability & Difficulty
|
||||
|
||||
Ghost Events rely on a “distance” $\delta$ to measure how “close” a mined key must be to the challenge point. A smaller $\delta$ makes finding that key *harder*; a larger $\delta$ makes it *easier*.
|
||||
|
||||
To quantify this difficulty, we can use a simple probabilistic model:
|
||||
|
||||
Probability of finding at least one valid key in:
|
||||
$$T \text{ tries} = 1 - \bigl(1 - \tfrac{\delta}{p}\bigr)^{T} $$
|
||||
|
||||
where $p$ is the size of the search space (typically the secp256k1 curve order or field modulus).
|
||||
|
||||
### 7.2. Given $T$ tries & desired probability \(P\), solve for $\delta$
|
||||
|
||||
We rearrange:
|
||||
|
||||
$1 - (1 - \tfrac{\delta}{p})^T = P \quad\Longrightarrow\quad \delta = p \,\Bigl[1 - (1 - P)^{1/T}\Bigr].$
|
||||
|
||||
A larger $\delta$ leads to fewer tries needed for success—but also to **weaker** deniability.
|
||||
|
||||
### 7.3. Given $\delta$ & desired probability \(P\), solve for $T$
|
||||
|
||||
We rearrange:
|
||||
|
||||
$$
|
||||
1 - (1 - \tfrac{\delta}{p})^T = P
|
||||
\quad\Longrightarrow\quad
|
||||
T \;\ge\; \frac{\ln(1 - P)}{\ln\bigl(1 - \tfrac{\delta}{p}\bigr)}.
|
||||
$$
|
||||
|
||||
Hence if $\delta$ is already fixed (e.g., from a received Ghost Event), you can compute how many attempts were likely needed to find that key with a certain success probability.
|
||||
|
||||
---
|
||||
|
||||
### Example Functions
|
||||
|
||||
Below are **reference** implementations demonstrating how to:
|
||||
|
||||
1. **Compute $\delta$** given the number of tries and target probability.
|
||||
2. **Compute $T$** (number of tries) given $\delta$ and target probability.
|
||||
|
||||
```python
|
||||
def find_interval_given_tries(tries: int, target_prob: float) -> int:
|
||||
"""
|
||||
Given 'tries' and a 'target_prob' (like 0.5 or 0.95),
|
||||
return the minimal delta (distance) needed to reach that probability.
|
||||
"""
|
||||
if target_prob <= 0:
|
||||
return 0
|
||||
if target_prob >= 1:
|
||||
return int(Decimal(SECP256K1_CURVE_ORDER)) # entire space
|
||||
|
||||
target_prob_dec = Decimal(target_prob)
|
||||
|
||||
# Solve 1 - (1 - delta/p)^T = target_prob for delta
|
||||
one_minus_tp = Decimal(1) - target_prob_dec
|
||||
exponent_part = one_minus_tp ** (Decimal(1) / Decimal(tries))
|
||||
ratio = Decimal(1) - exponent_part
|
||||
delta_dec = Decimal(SECP256K1_CURVE_ORDER) * ratio
|
||||
|
||||
# Round up to ensure >= target_prob
|
||||
return int(delta_dec.to_integral_value(rounding="ROUND_CEILING"))
|
||||
|
||||
|
||||
def find_tries_given_interval(delta: int, target_prob: float) -> int:
|
||||
"""
|
||||
Given a distance 'delta' and 'target_prob',
|
||||
return how many tries (T) are needed to reach that probability.
|
||||
"""
|
||||
if target_prob <= 0:
|
||||
return 0
|
||||
if delta >= SECP256K1_CURVE_ORDER:
|
||||
return 1 # 100% success in one try
|
||||
if target_prob >= 1:
|
||||
raise ValueError("Impossible to achieve probability >= 1 unless delta spans entire space.")
|
||||
|
||||
target_prob_dec = Decimal(target_prob)
|
||||
delta_dec = Decimal(delta)
|
||||
|
||||
# Probability of success in one try = delta / p
|
||||
x = delta_dec / Decimal(SECP256K1_CURVE_ORDER)
|
||||
|
||||
# We solve (1 - x)^T <= 1 - target_prob
|
||||
lhs = Decimal(1) - x
|
||||
rhs = Decimal(1) - target_prob_dec
|
||||
T_float = rhs.ln() / lhs.ln()
|
||||
|
||||
T_int = int(T_float.to_integral_value(rounding="ROUND_CEILING"))
|
||||
return max(T_int, 1)
|
||||
```
|
||||
|
||||
- **Edge Cases**
|
||||
- If `target_prob <= 0`, the probability requirement is trivial (0 tries or 0 distance).
|
||||
- If `target_prob >= 1` and `delta < p`, it is impossible to guarantee 100% success in finite tries.
|
||||
- If `delta >= p`, you already span the entire space, so 1 try suffices.
|
||||
|
||||
---
|
||||
|
||||
### Practical Tips
|
||||
|
||||
- **Picking $\delta$**:
|
||||
- Decide how quickly you want deniability to set in. If you pick a very small $\delta$, it requires more CPU time
|
||||
for an attacker to replicate—but also temporarily ties the event more strongly to you.
|
||||
- If you want faster deniability, pick a larger $\delta$, but accept that forging a second “mined” key becomes easier.
|
||||
- If you want avoid OTS, pick a larger $\delta$ and reference older blocks.
|
||||
|
||||
- **Verifying a Received Ghost Event**:
|
||||
- Check the actual $\delta$ provided.
|
||||
- Estimate the tries $T$ needed for a given success probability.
|
||||
- Check the $Key/s$ rate necessary to produce the event since the block was mined.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 7. Example Ghost Event JSON
|
||||
|
||||
**Alice:** Today is a good day to post wild picture of me on Nostr! Maybe I will regret it later...
|
||||
**Alice:** Let post as a ghost event!
|
||||
**Alice:** I will set a PoW of 86400000000 tries for a 30% of success. It is an easy one!
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "f9d32cace9ead9457d121041c4c17a779ab84cecf90f08d90ccacd9673f1bab4",
|
||||
"pubkey": "npub1r587vuykqhf8k7x4e06380edc7mr7pkz59r44tausmlwq970wkyqv9dh85",
|
||||
"created_at": 1736350383,
|
||||
"kind": 1,
|
||||
"tags": [
|
||||
[
|
||||
"ghost",
|
||||
"block-hash",
|
||||
"00000000000000000000c75dc9d3296751a8bb62b2463fbc49035ee75ab45f39"
|
||||
],
|
||||
[
|
||||
"ghost",
|
||||
"block-hash-timestamp",
|
||||
1736264059
|
||||
]
|
||||
],
|
||||
"content": "Hi! This is Alice... Here is a picture of me drinking a beer!",
|
||||
"sig": "{\"members\": [{\"x\": \"67662445654153687740586373927886816071496898620020884082657466854018749152574\", \"y\": \"36654242403647812025958692266000451967507088411740075397012431742148926139504\"}, {\"x\": \"13145165751859428441328823552188587896062471095368091519302299383344531469704\", \"y\": \"88317938924061092515290554803088470753630337048796286302890952317743305618744\"}], \"e0\": \"39084049882600833852841500746712733990015986042592648801067646082050173502362\", \"s\": [\"31193407423453445855440968148549028682673465359135742084404121637747636355393\", \"58541132732934108006164799904805991239360069925983344774829987032327853747970\"]}"
|
||||
}
|
||||
```
|
||||
|
||||
**Bob:** Wow! There is an event from Alice!
|
||||
**Bob:** Let's check the signature to make sure it is from Alice
|
||||
**Bob:** Alice didn't sign this event!
|
||||
**Bob:** I see some ghost tags here...
|
||||
**Bob:** It is a ghost event!
|
||||
**Bob:** There are 2 possible signers!
|
||||
**Bob:** Alice is one of the possible signers!
|
||||
It was produced, allegedly, after the block a 00000000000000000000c75dc9d3296751a8bb62b2463fbc49035ee75ab45f39
|
||||
**Bob:** It says that the block was mined at 1736264059
|
||||
**Bob:** Let see if this block is real!
|
||||
**Bob:** The block is real!
|
||||
**Bob:** Let's check the PoW
|
||||
**Bob:** The other signer is: npub1jktevjlge6vjp2yfaltrlvuv99th2uxaxy8eglvcsvr7e2r9h5lq2978dz
|
||||
**Bob:** The challenge public key is: npub1jktevjldsr7kr52f90602gfhmc8cac9quq6yy8k9vy94w2qptsasz9cfps
|
||||
**Bob:** Let's figure out how hard it was to mine the private key
|
||||
**Bob:** The distance between the challenge and the alleged mined public key is 494635222290174920048599093528121665305037827746663102318405983997
|
||||
**Bob:** For having 50% of chance of figuring out this key, the signer should have done 83496150448 tries
|
||||
**Bob:** The signer should have mined 967231 keys per second, since the block was mined
|
||||
**Bob:** It is not a hard PoW!
|
Loading…
x
Reference in New Issue
Block a user