13 KiB
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:
- Your real key;
- 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
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
- Take your main public key
P_A
and concatenate it withH_B
. - Compute
S = \mathrm{SHA256}(P_A \,\|\, H_B)
. - Map
S
to secp256k1 (RFC 9380 “Hashing to Elliptic Curves”):\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:
- Check validity of the ring signature over
\{P_A, P_{\mathrm{mined}}\}
. - Recompute
\mathrm{challenge_PK}
from\langle P_A, H_B\rangle
. - Measure how close
P_{\mathrm{mined}}
is to\mathrm{challenge_PK}
(i.e., proof-of-work difficulty). - 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
-
Ring Signature Robustness
Any weaknesses in the ring signature could leak which key was used. -
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. -
Block Hash Trust
Must rely on a valid, widely recognized Bitcoin chain tip. -
Long-Term Attacks
As time passes or computational power grows, collisions become easier. -
Anyone Can Forge
Attackers can craft a Ghost Event referencing your key without your involvement, ensuring it’s never definitively tied to you. -
Real signer secret key leaks
The Ghost Event’s deniability of all events is completely compromised.
5. Example Workflow
-
Alice Picks a Block
She picks a recent (or somewhat older) Bitcoin blockB
. -
Compute Challenge
S = \mathrm{SHA256}(P_A \| H_B)
, then\mathrm{challenge_PK} = \mathrm{HashToCurve}(S)
. -
Select Mined Key
ChooseP_{\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 tags.
-
Ring Signature
The ring is\{P_A, P_{\mathrm{mined}}\}
. Alice signs with\mathrm{sk}_A
. -
Publish
Attach["ghost", "block-hash", <Block Hash>]
and so forth, and post to relays. -
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:
- Compute $\delta$ given the number of tries and target probability.
- Compute $T$ (number of tries) given
\delta
and target probability.
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
anddelta < p
, it is impossible to guarantee 100% success in finite tries. - If
delta >= p
, you already span the entire space, so 1 try suffices.
- If
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.
- Decide how quickly you want deniability to set in. If you pick a very small
-
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.
- Check the actual
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!
{
"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!