Step 1 of 6
Unrecommended
This NIP has been implemented only once and needs review, so it is marked unrecommended. It names no replacement. Online, use ordinary relays and NIP-77 sync.
NIP-BE
UnrecommendedBehaviour
Unrecommended. Two nearby devices sync Nostr events over Bluetooth Low Energy without internet: one acts as a relay, the other as a client, using NIP-77 to work out what is missing.
Not recommended: only implemented once and unclear whether it works, requires review
Edit the form or the JSON: both stay in sync, the badge says whether the result is valid, and tapping any field explains it.
Step 1 of 6
This NIP has been implemented only once and needs review, so it is marked unrecommended. It names no replacement. Online, use ordinary relays and NIP-77 sync.
Ready. Press play or step forward to send the first message.
| # | From | To | Message |
|---|---|---|---|
| 1 | Device B (GATT server, acts as relay) | Device B (GATT server, acts as relay) | Device B broadcasts the service UUID and its device UUID. |
| 2 | Device A (GATT client) | Device A (GATT client) | Device A reads B's UUID. B's is higher, so B is the server (relay) and A connects as client. |
| 3 | Device A (GATT client) | Device B (GATT server, acts as relay) | A writes a NIP-77 NEG-OPEN with a filter and its first negentropy message to the write characteristic. |
| 4 | Device B (GATT server, acts as relay) | Device A (GATT client) | B confirms the write. Only one message moves at a time. |
| 5 | Device A (GATT client) | Device B (GATT server, acts as relay) | A asks to read B's answer from the read characteristic. |
| 6 | Device B (GATT server, acts as relay) | Device A (GATT client) | B answers with its negentropy message: now both know which events differ. |
| 7 | Device A (GATT client) | Device B (GATT server, acts as relay) | A sends one event that B is missing (or EOSE when it has none left), then reads again. |
| 8 | Device B (GATT server, acts as relay) | Device A (GATT client) | B returns one event A is missing, or EOSE when it has none. The turns repeat until both are in sync. |
| 9 | Device B (GATT server, acts as relay) | Device A (GATT client) | Later, when B gets a new event from another peer, it sends an empty notification. A reads and receives the EVENT. |
No course chapter covers this NIP yet. The editor above is the quickest way in.
Sync uses NIP-77 negentropy messages (NEG-OPEN, NEG-MSG), run half-duplex.
Every message on the wire is a normal NIP-01 message, compressed and chunked.
Specification text is quoted in English from the nostr-protocol/nips repository. Our explanations and the interface are translated. Source on GitHub at commit 0046368
This NIP specifies how Nostr apps can use BLE to communicate and synchronize with each other. The BLE protocol follows a client-server pattern, so this NIP emulates the WS structure in a similar way, but with some adaptations to its limitations.
A device advertises itself with:
0000180f-0000-1000-8000-00805f9b34fbThe device exposes a Nordic UART Service with the following characteristics:
Write Characteristic
87654321-0000-1000-8000-00805f9b34fbRead Characteristic
12345678-0000-1000-8000-00805f9b34fbWhen one device initially finds another advertising the service, it will read the service's data to get the device UUID and compare it with its own advertised device UUID. For this communication, the device with the highest ID will take the role of GATT Server (Relay), the other will be considered the GATT Client (Client) and will proceed to establish the connection.
For devices whose purpose will require a single role, its device UUID will always be:
FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF00000000-0000-0000-0000-000000000000All messages will follow NIP-01 message structure. For a given message, a compression stream (DEFLATE) is applied to the message to generate a byte array. Depending on the BLE version, the byte array can be too large for a single message (20-23 bytes in BLE 4.2, 256 bytes in BLE > 4.2). In that case, this byte array is split into any number of batches following the structure:
[batch index (first 2 bytes)][batch n][is last batch (last byte)]
After reception of all batches, the other device can then join them and decompress. To ensure reliability, only 1 message will be read/written at a time. MTU can be negotiated in advance. The maximum size for a message is 64KB; bigger messages will be rejected.
This example implements a function to split and compress a byte array into chunks, as well as another function to join and decompress them in order to obtain the initial result:
fun splitInChunks(message: ByteArray): Array<ByteArray> {
val chunkSize = 500 // define the chunk size
var byteArray = compressByteArray(message)
val numChunks = (byteArray.size + chunkSize - 1) / chunkSize // calculate the number of chunks
var chunkIndex = 0
val chunks = Array(numChunks) { ByteArray(0) }
for (i in 0 until numChunks) {
val start = i * chunkSize
val end = minOf((i + 1) * chunkSize, byteArray.size)
val chunk = byteArray.copyOfRange(start, end)
// add chunk index to the first 2 bytes and last chunk flag to the last byte
val chunkWithIndex = ByteArray(chunk.size + 2)
chunkWithIndex[0] = chunkIndex.toByte() // chunk index
chunk.copyInto(chunkWithIndex, 1)
chunkWithIndex[chunkWithIndex.size - 1] = numChunks.toByte()
// store the chunk in the array
chunks[i] = chunkWithIndex
chunkIndex++
}
return chunks
}
fun joinChunks(chunks: Array<ByteArray>): ByteArray {
val sortedChunks = chunks.sortedBy { it[0] }
var reassembledByteArray = ByteArray(0)
for (chunk in sortedChunks) {
val chunkData = chunk.copyOfRange(1, chunk.size - 1)
reassembledByteArray = reassembledByteArray.copyOf(reassembledByteArray.size + chunkData.size)
chunkData.copyInto(reassembledByteArray, reassembledByteArray.size - chunkData.size)
}
return decompressByteArray(reassembledByteArray)
}
The relay should notify the client about any new event matching subscription's filters by using the Notify action of the Read Characteristic. After that, the client can proceed to read messages from the relay.
Given the nature of BLE, it is expected that the direct connection between two devices might be extremely intermittent, with gaps of hours or even days. That's why it's crucial to define a synchronization process by following NIP-77 but with an adaptation to the limitations of the technology.
After two devices have successfully connected and established the Client-Server roles, the devices will use half-duplex communication to intermittently send and receive messages.
Right after the 2 devices connect, the Client starts the workflow by sending the first message.
write-success.read-message.EVENT.EOSE. In this case, subsequent messages to the Server will be empty while the Server claims to have more notes for the Client.write-success.read-message.EVENT.EOSE. In this case, subsequent responses to the Client will be empty.While two devices are connected and synchronized, it might happen that one of them receives a new message from another connected peer. Devices MUST keep track of which notes have been sent to its peers while they are connected. If the newly received event is detected as missing in one of the connected and synchronized peers:
EVENT.write-success.read-message.EVENT.