Files
bitchat-android/docs/PEERID_GATT_SERVER_CLIENT_SPEC.md
T
callebtc 6c7483956e feat: implement GATT client identity signaling and connection deduplication
Implements a mechanism for GATT clients to signal their Peer ID to the server immediately after connection, allowing for robust deduplication of redundant connections.

Includes:
- New IDENTITY_CHARACTERISTIC_UUID in AppConstants
- Server-side handling of identity writes and deduplication logic
- Client-side active signaling of Peer ID upon service discovery
- Updated BluetoothConnectionTracker to map devices to Peer IDs
- Comprehensive specification document in docs/PEERID_GATT_SERVER_CLIENT_SPEC.md
2026-01-16 04:14:23 +07:00

9.1 KiB

Bitchat Mesh: Direct Client Identity Signaling & Connection Deduplication Spec

1. Overview

This specification details the mechanism for Bitchat nodes to signal their persistent Peer Identity over Bluetooth Low Energy (BLE) to prevent duplicate connections.

In a privacy-preserving mesh network, devices frequently rotate their MAC addresses. This creates a problem where two nodes might establish multiple redundant connections to each other (thinking they are new devices), wasting limited connection slots and battery power.

This spec defines a dual-strategy approach:

  1. Passive Signaling: Inclusion of Peer ID in BLE Scan Responses (Server-side).
  2. Active Signaling: A dedicated GATT "Identity Characteristic" for clients to write their ID immediately upon connection.

Target Audience: Android & iOS Engineering Teams.


2. Protocol Constants

2.1 UUIDs

All Bitchat BLE operations use the following 128-bit UUIDs.

Name UUID Usage
Service UUID F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C The main Bitchat Mesh Service.
Data Characteristic A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D Main channel for BitchatPacket transfer (Notify/Write).
Identity Characteristic A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5E (NEW) For clients to signal identity (Write/WriteNr).

2.2 Data Formats

  • Peer ID Truncation: For BLE signaling, the full 64-char hex Peer ID is truncated to the first 8 bytes (16 hex characters).
  • Byte Order: Big-Endian (Network Byte Order).

3. Passive Signaling (Advertisements)

Already implemented on Android and iOS.

When acting as a Peripheral (Server), the node MUST include its truncated Peer ID in the Scan Response packet.

  • Data Type: Service Data (0x16)
  • UUID: F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C (16-bit or 128-bit representation depending on space)
  • Payload: First 8 bytes of Peer ID.

Behavior:

  • Scanners (Clients) can map the ephemeral MAC address to this Peer ID before connecting.
  • If the Scanner is already connected to this Peer ID (via a different MAC), it MUST NOT initiate a new connection.

4. Active Signaling (Identity Characteristic)

New implementation requirement.

When acting as a Central (Client), the node cannot advertise its ID to the Server if the Server is not scanning. Therefore, the Client MUST write its identity to the Server immediately after connection establishment.

4.1 Client Implementation Spec

  1. Connect to the Peripheral.
  2. Discover Services.
  3. Check for the existence of Identity Characteristic (...C5E).
  4. Action: If present, write the local truncated Peer ID (8 bytes) to this characteristic.
    • Write Type: WRITE_TYPE_NO_RESPONSE (preferred for speed) or WRITE_TYPE_DEFAULT.
  5. Timing: This MUST be the first action after service discovery, before enabling notifications on the Data Characteristic.

Android Reference (Client)

// In onServicesDiscovered()
val identityChar = service.getCharacteristic(IDENTITY_CHARACTERISTIC_UUID)
if (identityChar != null) {
    // 1. Prepare 8-byte ID
    val idBytes = myPeerID.chunked(2).map { it.toInt(16).toByte() }
        .toByteArray().take(8).toByteArray()
    
    // 2. Write to characteristic
    identityChar.value = idBytes
    identityChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
    gatt.writeCharacteristic(identityChar)
}

4.2 Server Implementation Spec

  1. Add Characteristic: The GATT Server MUST include the Identity Characteristic in its service definition.
    • Properties: Write | Write No Response
    • Permissions: Write
  2. Handle Write Request: When a write request is received on this UUID:
    • Extract the 8-byte Peer ID.
    • Bind the identity to the current connection handle (MAC address).
    • Execute Deduplication Policy (see Section 5).

Android Reference (Server)

// In onCharacteristicWriteRequest()
if (characteristic.uuid == IDENTITY_CHARACTERISTIC_UUID) {
    val peerID = value.joinToString("") { "%02x".format(it) }
    
    // 1. Update internal mapping (MAC -> PeerID)
    connectionTracker.setDevicePeerID(device.address, peerID)
    
    // 2. Deduplication Check
    val duplicate = connectionTracker.getConnectedDevices().values.firstOrNull { 
        it.peerID == peerID && it.device.address != device.address 
    }
    
    if (duplicate != null) {
        // Peer is already connected via a different MAC
        Log.w(TAG, "Deduplication: Rejecting new connection from $peerID")
        
        // Respond success to complete the transaction gracefully
        if (responseNeeded) {
             gattServer.sendResponse(device, requestId, GATT_SUCCESS, 0, null)
        }
        
        // Terminate the NEW connection
        gattServer.cancelConnection(device)
        return
    }
}

5. Deduplication Policy

The goal is to allow only one logical connection between any two peers, regardless of direction (Client/Server) or MAC address rotation.

5.1 Rules

  1. Peer ID Primacy: The persistent Peer ID is the source of truth, not the MAC address.
  2. First-Come-First-Served: The existing stable connection is preferred. A new incoming connection attempting to claim the same Peer ID will be rejected.
  3. Cross-Role Deduplication:
    • If Node A is connected to Node B (A=Client, B=Server).
    • And Node B tries to connect to Node A (B=Client, A=Server).
    • If both sides identify correctly, one link is redundant.
    • Current Policy: Bitchat currently treats dual-direction links as acceptable but suboptimal. However, strict deduplication via Active Signaling will treat the second link as a duplicate if the application layer maps them to the same ID.
    • Strict Mode (Recommended): If PeerID_X is connected as a client, do NOT accept PeerID_X as a server.

5.2 Conflict Resolution Flow

  1. New Connection (Incoming): Device AA:BB:CC connects.
  2. Identity Signal: Device writes ID 12345678.
  3. Lookup: Server checks list of other active connections.
    • Is 12345678 connected via DD:EE:FF?
  4. Decision:
    • Yes: Disconnect AA:BB:CC immediately. (Keep the old, stable link).
    • No: Accept AA:BB:CC and update the map.

6. iOS Implementation Notes

Swift Implementation Guidance

The iOS CoreBluetooth implementation should mirror the logic above.

CBPeripheralManagerDelegate: In didReceiveWrite requests:

func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
    for request in requests {
        if request.characteristic.uuid == CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5E") {
            let peerData = request.value
            // 1. Parse PeerID
            // 2. Check Connection Manager for existing PeerID
            // 3. If duplicate, cancel connection:
            //    peripheralManager.respond(to: request, withResult: .success)
            //    // Wait briefly or disconnect immediately?
            //    // CBCentralManager usually handles disconnects, but as a Peripheral, 
            //    // you simply stop processing. To force disconnect, you might need 
            //    // to let the Central handle the rejection or implement app-layer timeout.
            //    // NOTE: iOS Peripheral cannot force-disconnect a Central easily.
            //    // Alternative: Send an app-layer "Disconnect" packet or error response.
        }
    }
}

Note on iOS Peripheral Disconnects: Unlike Android, iOS CBPeripheralManager does not have a direct cancelConnection for a specific central.

  • Strategy: If a duplicate is detected, the iOS Peripheral should respond with a custom GATT Error (if possible) or simply ignore further data requests, causing the Central (if Bitchat) to timeout.
  • Preferred Strategy: The Android/iOS Client (Central) is the one that typically manages the connection lifecycle. If the Server detects a duplicate, it could write a specific "Go Away" code to the characteristic or simply refuse to process further packets.
  • Protocol Refinement: If the Server responds with GATT_SUCCESS, the Client assumes it's accepted. If the Server responds with an error (e.g., CBATTError.insufficientResources or a custom code), the Client should disconnect itself.

Refined Server Logic (Cross-Platform): If the Server detects a duplicate:

  1. Respond to the Write Request with GATT_SUCCESS (to satisfy the protocol).
  2. Internally flag this CBCentral as "Rejected".
  3. Ignore any subsequent data writes from this Central.
  4. (Optional) If the Central subscribes to the Data Characteristic, send a single "Disconnect Command" packet and then stop notifying.

Android Compliance

The Android implementation provided in the Reference section uses gattServer.cancelConnection(device), which effectively terminates the link. iOS devices acting as Clients will see this as a disconnection and should handle it gracefully (auto-retry logic should respect the fact that it might be a purposeful disconnect, but standard backoff applies).