draft
optional
Publications like comments, votes, etc. can be published peer to peer over libp2p Gossipsub. The Gossipsub topic name is defined in subplebbit.pubsubTopic
and must be a libp2p peer ID (hash of an Ed25519 public key). Gossipsub messages are CBOR encoded.
An author first publishes a CHALLENGEREQUEST
which contains his publication, like a comment or vote, encrypted using the scheme defined in subplebbit.encryption
. The subplebbit replies with a CHALLENGE
, such as a base64 image captcha, a plain text question, etc. The author replies with a CHALLENGEANSWER
. The subplebbit finally replies with a CHALLENGEVERIFICATION
with challengeVerification.challengeSuccess
true
or false
.
NOTE: The CHALLENGEREQUEST
can include challenge answers before receiving the CHALLENGE
if some challenges are defined in subplebbit.challenges
.
NOTE 2: The subplebbit can omit CHALLENGE
and directly reply with a successful or unsuccessful CHALLENGEVERIFICATION
, for example if the author is whitelisted, or banned.
The first message in a challenge exchange.
For example (CBOR encoded):
{
"type": "CHALLENGEREQUEST",
"challengeRequestId": <CBOR byte string>,
"timestamp": 1504321021,
"encrypted": {
"ciphertext": <CBOR byte string>,
"iv": <CBOR byte string>,
"tag": <CBOR byte string>,
"type": "ed25519-aes-gcm"
},
"protocolVersion": "1.0.0",
"userAgent": "/plebbit-js:0.0.1/",
"signature": {
"signature": <cbor byte string>,
"publicKey": <cbor byte string>,
"type": "ed25519",
"signedPropertyNames": ["type", "challengeRequestId", "timestamp", "encrypted", "protocolVersion", "userAgent"]
}
}
Required fields are:
type
challengeRequestId
timestamp
encrypted
protocolVersion
userAgent
signature
challengeRequest.encrypted.ciphertext
decrypts to JSON:
{
"comment": {
"title": "Why did the banana go to the doctor?",
"content": "It wasn't peeling well.",
"subplebbitAddress": "jokes.eth",
"author": {"address": "john.eth"},
"timestamp": 1728174027,
"signature": {
"signature": <base64>,
"publicKey": <base64>,
"type": "ed25519",
"signedPropertyNames": ["title", "content", "subplebbitAddress", "author", "timestamp"]
}
}
}
Optional encrypted fields are:
comment
vote
The second message in a challenge exchange. A reply by the subplebbit to a CHALLENGEREQUEST
.
For example (CBOR encoded):
{
"type": "CHALLENGE",
"challengeRequestId": <CBOR byte string>,
"timestamp": 1504321021,
"encrypted": {
"ciphertext": <CBOR byte string>,
"iv": <CBOR byte string>,
"tag": <CBOR byte string>,
"type": "ed25519-aes-gcm"
},
"protocolVersion": "1.0.0",
"userAgent": "/plebbit-js:0.0.1/",
"signature": {
"signature": <cbor byte string>,
"publicKey": <cbor byte string>,
"type": "ed25519",
"signedPropertyNames": ["type", "challengeRequestId", "timestamp", "encrypted", "protocolVersion", "userAgent"]
}
}
Required fields are:
type
challengeRequestId
timestamp
encrypted
protocolVersion
userAgent
signature
challenge.encrypted.ciphertext
decrypts to JSON:
{
"challenges": [
{
"type": "image/png",
"challenge": <base64 image>,
"caseInsensitive": true
},
{
"type": "text/plain",
"challenge": "What is the password?"
}
]
}
Required encrypted fields are:
challenges
The third message in a challenge exchange. A reply by the author to a CHALLENGE
.
For example (CBOR encoded):
{
"type": "CHALLENGEANSWER",
"challengeRequestId": <CBOR byte string>,
"timestamp": 1504321021,
"encrypted": {
"ciphertext": <CBOR byte string>,
"iv": <CBOR byte string>,
"tag": <CBOR byte string>,
"type": "ed25519-aes-gcm"
},
"protocolVersion": "1.0.0",
"userAgent": "/plebbit-js:0.0.1/",
"signature": {
"signature": <cbor byte string>,
"publicKey": <cbor byte string>,
"type": "ed25519",
"signedPropertyNames": ["type", "challengeRequestId", "timestamp", "encrypted", "protocolVersion", "userAgent"]
}
}
Required fields are:
type
challengeRequestId
timestamp
encrypted
protocolVersion
userAgent
signature
challengeAnswer.encrypted.ciphertext
decrypts to JSON:
{
"challengeAnswers": [
"DK4K3A",
"password"
]
}
Required encrypted fields are:
challengeAnswers
The fourth and final message in a challenge exchange. A reply by the subplebbit to a CHALLENGEANSWER
.
For example (CBOR encoded):
{
"type": "CHALLENGEVERIFICATION",
"challengeRequestId": <CBOR byte string>,
"timestamp": 1504321021,
"challengeSuccess": true,
"encrypted": {
"ciphertext": <CBOR byte string>,
"iv": <CBOR byte string>,
"tag": <CBOR byte string>,
"type": "ed25519-aes-gcm"
},
"protocolVersion": "1.0.0",
"userAgent": "/plebbit-js:0.0.1/",
"signature": {
"signature": <cbor byte string>,
"publicKey": <cbor byte string>,
"type": "ed25519",
"signedPropertyNames": ["type", "challengeRequestId", "challengeSuccess", "timestamp", "encrypted", "protocolVersion", "userAgent"]
}
}
Required fields are:
type
challengeRequestId
challengeSuccess
timestamp
encrypted
protocolVersion
userAgent
signature
challengeVerification.encrypted.ciphertext
decrypts to JSON:
{
"comment": {
"title": "Why did the banana go to the doctor?",
"content": "It wasn't peeling well.",
"subplebbitAddress": "jokes.eth",
"author": {"address": "john.eth"},
"timestamp": 1728174027,
"signature": {
"signature": <base64>,
"publicKey": <base64>,
"type": "ed25519",
"signedPropertyNames": ["title", "content", "subplebbitAddress", "author", "timestamp"]
},
"depth": 0,
"previousCid": "QmeQSmkNfDzfuqkKVFrRjuA9VBxF1ubpB24xxtxs1Go67E"
},
"commentUpdate": {
"cid": "QmXnEICVkZBHKgjtj7Vt63HWq3ZfPjcGTSPs79oXtfEZxc",
"signature": {
"signature": <base64>,
"publicKey": <base64>,
"type": "ed25519",
"signedPropertyNames": ["cid"]
}
}
}
Required encrypted fields are:
comment
commentUpdate
Same as defined in PLIP-1.
The gossipsubMessage.encrypted.type
"ed25519-aes-gcm"
encrypt algorithm is:
const plaintext = JSON.stringify({comment})
const plaintextWithRandomPadding = plaintext + ' ' // add random amount of empty space to plaintext JSON
const iv = randomBytes(12)
const aesGcmKey = ed25519.getSharedSecret(
base64.decode(gossipsubMessageSignaturePrivateKey), // private key the author used to sign the gossipsub message
base64.decode(subplebbit.encryption.publicKey) // subplebbit public key
)
const aesGcmKey16Bytes = aesGcmKey.slice(0, 16)
const {ciphertext, tag} = encryptAesGcm(plaintextWithRandomPadding, iv, aesGcmKey16Bytes)
gossipsubMessage.encrypted = {ciphertext, iv, tag, type: 'ed25519-aes-gcm'}
The gossipsubMessage.encrypted.type
"ed25519-aes-gcm"
decrypt algorithm is:
const aesGcmKey = ed25519.getSharedSecret(
base64.decode(subplebbitPrivateKey), // subplebbit private key
base64.decode(gossipsubMessage.signature.publicKey) // public key the author used to sign the gossipsub message
)
const aesGcmKey16Bytes = aesGcmKey.slice(0, 16)
const plaintext = decryptAesGcm(
gossipsubMessage.encrypted.ciphertext,
aesGcmKey16Bytes,
gossipsubMessage.encrypted.iv,
gossipsubMessage.encrypted.tag
)
const {comment} = JSON.parse(plaintext)
Publishing peer to peer via Gossipsub is more censorship resistant as it's more difficult to DDOS, doesn't require an SSL certificate from a provider, a publicly accessible HTTP endpoint, etc.