mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 22:45:19 +00:00
Add capability bits to announce TLV (#1375)
Announces now carry an optional capabilities TLV (0x05): a little-endian bitfield with named bits for upcoming features (prekeys, wifiBulk, gateway, groups, board, vouch, meshDiagnostics). Old clients skip the unknown TLV; peers without it decode as nil so features can distinguish "legacy peer" from "advertises nothing". PeerCapabilities lives in BitFoundation with a minimal-length encoding that preserves unknown bits for forward compatibility. Peer capabilities are stored in the BLE peer registry on verified announce and exposed via BLEService.peerCapabilities(_:). The local advertisement set is empty until each feature ships its bit. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
c2a5668569
commit
5fdc15d5af
@@ -1,3 +1,4 @@
|
||||
import BitFoundation
|
||||
import Foundation
|
||||
|
||||
// MARK: - Protocol TLV Packets
|
||||
@@ -7,12 +8,28 @@ struct AnnouncementPacket {
|
||||
let noisePublicKey: Data // Noise static public key (Curve25519.KeyAgreement)
|
||||
let signingPublicKey: Data // Ed25519 public key for signing
|
||||
let directNeighbors: [Data]? // 8-byte peer IDs
|
||||
let capabilities: PeerCapabilities? // advertised feature bits; nil when absent (old clients)
|
||||
|
||||
init(
|
||||
nickname: String,
|
||||
noisePublicKey: Data,
|
||||
signingPublicKey: Data,
|
||||
directNeighbors: [Data]?,
|
||||
capabilities: PeerCapabilities? = nil
|
||||
) {
|
||||
self.nickname = nickname
|
||||
self.noisePublicKey = noisePublicKey
|
||||
self.signingPublicKey = signingPublicKey
|
||||
self.directNeighbors = directNeighbors
|
||||
self.capabilities = capabilities
|
||||
}
|
||||
|
||||
private enum TLVType: UInt8 {
|
||||
case nickname = 0x01
|
||||
case noisePublicKey = 0x02
|
||||
case signingPublicKey = 0x03
|
||||
case directNeighbors = 0x04
|
||||
case capabilities = 0x05
|
||||
}
|
||||
|
||||
func encode() -> Data? {
|
||||
@@ -48,6 +65,15 @@ struct AnnouncementPacket {
|
||||
}
|
||||
}
|
||||
|
||||
// TLV for capabilities (optional)
|
||||
if let capabilities = capabilities {
|
||||
let capabilityBytes = capabilities.encoded()
|
||||
guard capabilityBytes.count <= 255 else { return nil }
|
||||
data.append(TLVType.capabilities.rawValue)
|
||||
data.append(UInt8(capabilityBytes.count))
|
||||
data.append(capabilityBytes)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -57,6 +83,7 @@ struct AnnouncementPacket {
|
||||
var noisePublicKey: Data?
|
||||
var signingPublicKey: Data?
|
||||
var directNeighbors: [Data]?
|
||||
var capabilities: PeerCapabilities?
|
||||
|
||||
while offset + 2 <= data.count {
|
||||
let typeRaw = data[offset]
|
||||
@@ -87,6 +114,8 @@ struct AnnouncementPacket {
|
||||
}
|
||||
directNeighbors = neighbors
|
||||
}
|
||||
case .capabilities:
|
||||
capabilities = PeerCapabilities(encoded: Data(value))
|
||||
}
|
||||
} else {
|
||||
// Unknown TLV; skip (tolerant decoder for forward compatibility)
|
||||
@@ -99,7 +128,8 @@ struct AnnouncementPacket {
|
||||
nickname: nickname,
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: signingPublicKey,
|
||||
directNeighbors: directNeighbors
|
||||
directNeighbors: directNeighbors,
|
||||
capabilities: capabilities
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import BitFoundation
|
||||
|
||||
extension PeerCapabilities {
|
||||
/// Capabilities this build advertises in its announce packets.
|
||||
/// Each feature adds its bit here when it ships.
|
||||
static let localSupported: PeerCapabilities = []
|
||||
}
|
||||
@@ -9,6 +9,7 @@ struct BLEPeerInfo: Equatable {
|
||||
var signingPublicKey: Data?
|
||||
var isVerifiedNickname: Bool
|
||||
var lastSeen: Date
|
||||
var capabilities: PeerCapabilities = []
|
||||
}
|
||||
|
||||
struct BLEPeerAnnounceUpdate: Equatable {
|
||||
@@ -107,6 +108,10 @@ struct BLEPeerRegistry {
|
||||
peers[peerID]?.noisePublicKey?.sha256Fingerprint()
|
||||
}
|
||||
|
||||
func capabilities(for peerID: PeerID) -> PeerCapabilities {
|
||||
peers[peerID.toShort()]?.capabilities ?? []
|
||||
}
|
||||
|
||||
func displayNicknames(selfNickname: String) -> [PeerID: String] {
|
||||
let connected = peers.filter { $0.value.isConnected }
|
||||
let tuples = connected.map { ($0.key, $0.value.nickname, true) }
|
||||
@@ -157,7 +162,8 @@ struct BLEPeerRegistry {
|
||||
noisePublicKey: Data,
|
||||
signingPublicKey: Data?,
|
||||
isConnected: Bool,
|
||||
now: Date
|
||||
now: Date,
|
||||
capabilities: PeerCapabilities = []
|
||||
) -> BLEPeerAnnounceUpdate {
|
||||
let existing = peers[peerID]
|
||||
let update = BLEPeerAnnounceUpdate(
|
||||
@@ -173,7 +179,8 @@ struct BLEPeerRegistry {
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: signingPublicKey,
|
||||
isVerifiedNickname: true,
|
||||
lastSeen: now
|
||||
lastSeen: now,
|
||||
capabilities: capabilities
|
||||
)
|
||||
|
||||
return update
|
||||
|
||||
@@ -619,6 +619,12 @@ final class BLEService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Capabilities the peer advertised in its last verified announce.
|
||||
/// Empty for peers that predate the capabilities TLV.
|
||||
func peerCapabilities(_ peerID: PeerID) -> PeerCapabilities {
|
||||
collectionsQueue.sync { peerRegistry.capabilities(for: peerID) }
|
||||
}
|
||||
|
||||
func getPeerNicknames() -> [PeerID: String] {
|
||||
return collectionsQueue.sync {
|
||||
peerRegistry.displayNicknames(selfNickname: myNickname)
|
||||
@@ -1261,7 +1267,8 @@ final class BLEService: NSObject {
|
||||
nickname: myNickname,
|
||||
noisePublicKey: noisePub,
|
||||
signingPublicKey: signingPub,
|
||||
directNeighbors: connectedPeerIDs
|
||||
directNeighbors: connectedPeerIDs,
|
||||
capabilities: PeerCapabilities.localSupported
|
||||
)
|
||||
|
||||
guard let payload = announcement.encode() else {
|
||||
@@ -3315,7 +3322,8 @@ extension BLEService {
|
||||
noisePublicKey: announcement.noisePublicKey,
|
||||
signingPublicKey: announcement.signingPublicKey,
|
||||
isConnected: isConnected,
|
||||
now: now
|
||||
now: now,
|
||||
capabilities: announcement.capabilities ?? []
|
||||
) ?? BLEPeerAnnounceUpdate(isNewPeer: false, wasDisconnected: false, previousNickname: nil)
|
||||
},
|
||||
shouldEmitReconnectLog: { [weak self] peerID, now in
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import BitFoundation
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@@ -108,6 +109,42 @@ struct PacketsTests {
|
||||
#expect(decoded.directNeighbors == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func announcementPacketRoundTripsCapabilities() throws {
|
||||
let capabilities: PeerCapabilities = [.prekeys, .board, .meshDiagnostics]
|
||||
let packet = AnnouncementPacket(
|
||||
nickname: "alice",
|
||||
noisePublicKey: Data(repeating: 0x11, count: 32),
|
||||
signingPublicKey: Data(repeating: 0x22, count: 32),
|
||||
directNeighbors: nil,
|
||||
capabilities: capabilities
|
||||
)
|
||||
|
||||
let encoded = try #require(packet.encode())
|
||||
let decoded = try #require(AnnouncementPacket.decode(from: encoded))
|
||||
#expect(decoded.capabilities == capabilities)
|
||||
}
|
||||
|
||||
@Test
|
||||
func announcementPacketWithoutCapabilitiesDecodesNilAndUnknownBitsSurvive() throws {
|
||||
let legacy = try #require(
|
||||
AnnouncementPacket(
|
||||
nickname: "alice",
|
||||
noisePublicKey: Data(repeating: 0x11, count: 32),
|
||||
signingPublicKey: Data(repeating: 0x22, count: 32),
|
||||
directNeighbors: nil
|
||||
).encode()
|
||||
)
|
||||
// The TLV is emitted only when capabilities are set, so legacy peers
|
||||
// (and this packet) decode as nil rather than empty.
|
||||
#expect(try #require(AnnouncementPacket.decode(from: legacy)).capabilities == nil)
|
||||
|
||||
var withFutureBits = legacy
|
||||
withFutureBits.append(makeTLV(type: 0x05, value: Data([0x80, 0x01])))
|
||||
let decoded = try #require(AnnouncementPacket.decode(from: withFutureBits))
|
||||
#expect(decoded.capabilities?.rawValue == 0x0180)
|
||||
}
|
||||
|
||||
@Test
|
||||
func privateMessagePacketRejectsUnknownTypeAndTruncation() {
|
||||
let unknownTLV = Data([0x7F, 0x01, 0x41])
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import Foundation
|
||||
|
||||
/// Feature capabilities a peer advertises in its announce packet.
|
||||
///
|
||||
/// Encoded as a little-endian bitfield with trailing zero bytes dropped, so the
|
||||
/// wire form grows only when high bits are assigned. Decoders keep the low 64
|
||||
/// bits and ignore any longer field, and unknown bits are preserved verbatim —
|
||||
/// old clients skip the TLV entirely, new clients degrade per-feature.
|
||||
public struct PeerCapabilities: OptionSet, Equatable, Hashable, Sendable {
|
||||
public let rawValue: UInt64
|
||||
|
||||
public init(rawValue: UInt64) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let prekeys = PeerCapabilities(rawValue: 1 << 0)
|
||||
public static let wifiBulk = PeerCapabilities(rawValue: 1 << 1)
|
||||
public static let gateway = PeerCapabilities(rawValue: 1 << 2)
|
||||
public static let groups = PeerCapabilities(rawValue: 1 << 3)
|
||||
public static let board = PeerCapabilities(rawValue: 1 << 4)
|
||||
public static let vouch = PeerCapabilities(rawValue: 1 << 5)
|
||||
public static let meshDiagnostics = PeerCapabilities(rawValue: 1 << 6)
|
||||
|
||||
/// Minimal little-endian byte encoding; always at least one byte so an
|
||||
/// empty set is distinguishable from an absent TLV.
|
||||
public func encoded() -> Data {
|
||||
var value = rawValue
|
||||
var bytes = Data()
|
||||
repeat {
|
||||
bytes.append(UInt8(truncatingIfNeeded: value))
|
||||
value >>= 8
|
||||
} while value != 0
|
||||
return bytes
|
||||
}
|
||||
|
||||
/// Accepts any length; bytes beyond the low 64 bits are ignored for
|
||||
/// forward compatibility.
|
||||
public init(encoded data: Data) {
|
||||
var value: UInt64 = 0
|
||||
for (index, byte) in data.prefix(8).enumerated() {
|
||||
value |= UInt64(byte) << (8 * index)
|
||||
}
|
||||
self.init(rawValue: value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// PeerCapabilitiesTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import BitFoundation
|
||||
|
||||
struct PeerCapabilitiesTests {
|
||||
@Test
|
||||
func encodingIsMinimalAndRoundTrips() {
|
||||
#expect(PeerCapabilities([]).encoded() == Data([0x00]))
|
||||
#expect(PeerCapabilities.prekeys.encoded() == Data([0x01]))
|
||||
#expect(PeerCapabilities.meshDiagnostics.encoded() == Data([0x40]))
|
||||
|
||||
let high = PeerCapabilities(rawValue: 1 << 9)
|
||||
#expect(high.encoded() == Data([0x00, 0x02]))
|
||||
|
||||
let all: PeerCapabilities = [.prekeys, .wifiBulk, .gateway, .groups, .board, .vouch, .meshDiagnostics]
|
||||
#expect(PeerCapabilities(encoded: all.encoded()) == all)
|
||||
#expect(PeerCapabilities(encoded: high.encoded()) == high)
|
||||
#expect(PeerCapabilities(encoded: PeerCapabilities([]).encoded()) == [])
|
||||
}
|
||||
|
||||
@Test
|
||||
func decodingToleratesUnknownBitsAndOversizedFields() {
|
||||
// Unknown bits survive a round-trip untouched.
|
||||
let unknown = PeerCapabilities(encoded: Data([0xFF, 0xFF]))
|
||||
#expect(unknown.rawValue == 0xFFFF)
|
||||
#expect(unknown.contains(.gateway))
|
||||
|
||||
// Fields longer than 8 bytes keep the low 64 bits and ignore the rest.
|
||||
let oversized = Data([0x01] + [UInt8](repeating: 0x00, count: 7) + [0xAA, 0xBB])
|
||||
#expect(PeerCapabilities(encoded: oversized) == .prekeys)
|
||||
|
||||
// Empty value decodes to no capabilities.
|
||||
#expect(PeerCapabilities(encoded: Data()) == [])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user