BitFoundation module to centralize shared components (#1089)

* Run local packages’ tests as well on CI

* BitFoundation module to centralize shared components
This commit is contained in:
Islam
2026-04-14 14:10:03 -05:00
committed by GitHub
parent 3e137d784c
commit 4cfcefcda6
81 changed files with 290 additions and 141 deletions
+27
View File
@@ -0,0 +1,27 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "BitFoundation",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.library(
name: "BitFoundation",
targets: ["BitFoundation"]
)
],
targets: [
.target(
name: "BitFoundation",
path: "Sources"
),
.testTarget(
name: "BitFoundationTests",
dependencies: ["BitFoundation"],
)
]
)
@@ -0,0 +1,57 @@
//
// Data+Hex.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import struct Foundation.Data
public extension Data {
func hexEncodedString() -> String {
if self.isEmpty {
return ""
}
return self.map { String(format: "%02x", $0) }.joined()
}
/// Initialize Data from a hex string.
/// - Parameter hexString: A hex string, optionally prefixed with "0x" or "0X".
/// Whitespace is trimmed. Must have even length after prefix removal.
/// - Returns: nil if the string has odd length or contains invalid hex characters.
init?(hexString: String) {
var hex = hexString.trimmed
// Remove optional 0x prefix
if hex.hasPrefix("0x") || hex.hasPrefix("0X") {
hex = String(hex.dropFirst(2))
}
// Reject odd-length strings
guard hex.count % 2 == 0 else {
return nil
}
// Reject empty strings
guard !hex.isEmpty else {
self = Data()
return
}
let len = hex.count / 2
var data = Data(capacity: len)
var index = hex.startIndex
for _ in 0..<len {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
return nil
}
data.append(byte)
index = nextIndex
}
self = data
}
}
@@ -0,0 +1,33 @@
//
// Data+SHA256.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import struct Foundation.Data
private import struct CryptoKit.SHA256
public extension Data {
/// Returns the hex representation of SHA256 hash
func sha256Fingerprint() -> String {
// Implementation matches existing fingerprint generation in NoiseEncryptionService
sha256Hash().hexEncodedString()
}
/// Returns the SHA256 hash wrapped in Data
func sha256Hash() -> Data {
Data(sha256Digest)
}
func sha256Hex() -> String {
sha256Digest.map { String(format: "%02x", $0) }.joined()
}
}
private extension Data {
var sha256Digest: SHA256.Digest {
SHA256.hash(data: self)
}
}
@@ -0,0 +1,228 @@
//
// PeerID.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import struct Foundation.Data
import struct Foundation.CharacterSet
public struct PeerID: Equatable, Hashable, Sendable {
enum Constants {
/// 16
static let nostrConvKeyPrefixLength = 16
/// 8
static let nostrShortKeyDisplayLength = 8
/// 64
fileprivate static let maxIDLength = 64
/// 16
fileprivate static let hexIDLength = 16 // 8 bytes = 16 hex chars
}
public enum Prefix: String, CaseIterable, Sendable {
/// When no prefix is provided
case empty = ""
/// `"mesh:"`
case mesh = "mesh:"
/// `"name:"`
case name = "name:"
/// `"noise:"` (+ 64 characters hex)
case noise = "noise:"
/// `"nostr_"` (+ 16 characters hex)
case geoDM = "nostr_"
/// `"nostr:"` (+ 8 characters hex)
case geoChat = "nostr:"
}
public let prefix: Prefix
/// Returns the actual value without any prefix
public let bare: String
/// Returns the full `id` value by combining `(prefix + bare)`
public var id: String { prefix.rawValue + bare }
// Private so the callers have to go through a convenience init
private init(prefix: Prefix, bare: any StringProtocol) {
self.prefix = prefix
self.bare = String(bare).lowercased()
}
}
// MARK: - Convenience Inits
public extension PeerID {
/// Convenience init to create GeoDM PeerID by appending `"nostr_"` to the first 16 characters of `pubKey`
init(nostr_ pubKey: String) {
self.init(prefix: .geoDM, bare: pubKey.prefix(Constants.nostrConvKeyPrefixLength))
}
/// Convenience init to create GeoChat PeerID by appending `"nostr:"` to the first 8 characters of `pubKey`
init(nostr pubKey: String) {
self.init(prefix: .geoChat, bare: pubKey.prefix(Constants.nostrShortKeyDisplayLength))
}
/// Convenience init to create PeerID from String/Substring by splitting it into prefix and bare parts
init(str: any StringProtocol) {
if let prefix = Prefix.allCases.first(where: { $0 != .empty && str.hasPrefix($0.rawValue) }) {
self.init(prefix: prefix, bare: String(str).dropFirst(prefix.rawValue.count))
} else {
self.init(prefix: .empty, bare: str)
}
}
/// Convenience init to handle `Optional<String>`
init?(str: (any StringProtocol)?) {
guard let str else { return nil }
self.init(str: str)
}
/// Convenience init to create PeerID by converting Data to String
init?(data: Data) {
self.init(str: String(data: data, encoding: .utf8))
}
/// Convenience init to "hide" hex-encoding implementation detail
init(hexData: Data) {
self.init(str: hexData.hexEncodedString())
}
/// Convenience init to "hide" hex-encoding implementation detail
init?(hexData: Data?) {
guard let hexData else { return nil }
self.init(hexData: hexData)
}
}
// MARK: - Noise Public Key Helpers
public extension PeerID {
/// Derive the stable 16-hex peer ID from a Noise static public key
init(publicKey: Data) {
self.init(str: publicKey.sha256Fingerprint().prefix(16))
}
/// Returns a 16-hex short peer ID derived from a 64-hex Noise public key if needed
func toShort() -> PeerID {
if let noiseKey {
return PeerID(publicKey: noiseKey)
}
return self
}
}
// MARK: - Codable
extension PeerID: Codable {
public init(from decoder: any Decoder) throws {
self.init(str: try decoder.singleValueContainer().decode(String.self))
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(id)
}
}
// MARK: - Helpers
public extension PeerID {
var isEmpty: Bool {
id.isEmpty
}
/// Returns true if `id` starts with "`nostr:`"
var isGeoChat: Bool {
prefix == .geoChat
}
/// Returns true if `id` starts with "`nostr_`"
var isGeoDM: Bool {
prefix == .geoDM
}
func toPercentEncoded() -> String {
id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
}
}
public extension PeerID {
var routingData: Data? {
if let direct = Data(hexString: id), direct.count == 8 { return direct }
if let bareData = Data(hexString: bare), bareData.count == 8 { return bareData }
let short = toShort()
return Data(hexString: short.id)
}
init?(routingData: Data) {
guard routingData.count == 8 else { return nil }
self.init(hexData: routingData)
}
}
// MARK: - Validation
public extension PeerID {
/// Validates a peer ID from any source (short 16-hex, full 64-hex, or internal alnum/-/_ up to 64)
var isValid: Bool {
if prefix != .empty {
return PeerID(str: bare).isValid
}
// Accept short routing IDs (exact 16-hex) or Full Noise key hex (exact 64-hex)
if isShort || isNoiseKeyHex {
return true
}
// If length equals short or full but isn't valid hex, reject
if id.count == Constants.hexIDLength || id.count == Constants.maxIDLength {
return false
}
// Internal format: alphanumeric + dash/underscore up to 63 (not 16 or 64)
let validCharset = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
return !id.isEmpty &&
id.count < Constants.maxIDLength &&
id.rangeOfCharacter(from: validCharset.inverted) == nil
}
/// Returns true if the `bare` id is all hex
var isHex: Bool {
bare.allSatisfy { $0.isHexDigit }
}
/// Short routing IDs (exact 16-hex)
var isShort: Bool {
bare.count == Constants.hexIDLength && isHex
}
/// Full Noise key hex (exact 64-hex)
var isNoiseKeyHex: Bool {
noiseKey != nil
}
/// Full Noise key (exact 64-hex) as Data
var noiseKey: Data? {
guard bare.count == Constants.maxIDLength else { return nil }
return Data(hexString: bare)
}
}
// MARK: - Comparable
extension PeerID: Comparable {
public static func < (lhs: PeerID, rhs: PeerID) -> Bool {
lhs.id < rhs.id
}
}
// MARK: - CustomStringConvertible
extension PeerID: CustomStringConvertible {
/// So it returns the actual `id` like before even inside another String
public var description: String {
id
}
}
@@ -0,0 +1,22 @@
//
// String+Ext.swift
// BitFoundation
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
public extension StringProtocol {
var trimmed: String {
trimmingCharacters(in: .whitespacesAndNewlines)
}
var trimmedOrNilIfEmpty: String? {
let trimmed = self.trimmed
return trimmed.isEmpty ? nil : trimmed
}
var nilIfEmpty: Self? {
isEmpty ? nil : self
}
}
@@ -0,0 +1,495 @@
//
// PeerIDTests.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 PeerIDTests {
private let hex16 = "0011223344556677"
private let hex64 = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
private let encoder: JSONEncoder = {
var encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
return encoder
}()
// MARK: - Empty prefix
@Test func empty_prefix_with16() {
let peerID = PeerID(str: hex16)
#expect(peerID.id == hex16)
#expect(peerID.bare == hex16)
#expect(peerID.prefix == .empty)
}
@Test func empty_prefix_with64() {
let peerID = PeerID(str: hex64)
#expect(peerID.id == hex64)
#expect(peerID.bare == hex64)
#expect(peerID.prefix == .empty)
}
// MARK: - Mesh prefix
@Test func mesh_prefix_with16() {
let str = "mesh:" + hex16
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex16)
#expect(peerID.prefix == .mesh)
}
@Test func mesh_prefix_with64() {
let str = "mesh:" + hex64
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex64)
#expect(peerID.prefix == .mesh)
}
// MARK: - Name prefix
@Test func name_prefix() {
let str = "name:some_name"
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == "some_name")
#expect(peerID.prefix == .name)
}
// MARK: - Noise prefix
@Test func noise_prefix_with16() {
let str = "noise:" + hex16
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex16)
#expect(peerID.prefix == .noise)
}
@Test func noise_prefix_with64() {
let str = "noise:" + hex64
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex64)
#expect(peerID.prefix == .noise)
}
// MARK: - GeoDM prefix
@Test func geoDM_prefix_with16() {
let str = "nostr_" + hex16
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex16)
#expect(peerID.prefix == .geoDM)
}
@Test func geoDM_prefix_with64() {
let str = "nostr_" + hex64
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex64)
#expect(peerID.prefix == .geoDM)
}
// MARK: - GeoChat prefix
@Test func geoChat_prefix_with16() {
let str = "nostr:" + hex16
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex16)
#expect(peerID.prefix == .geoChat)
}
@Test func geoChat_prefix_with64() {
let str = "nostr:" + hex64
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == hex64)
#expect(peerID.prefix == .geoChat)
}
// MARK: - Edge cases
@Test func with_unknown_prefix() {
let str = "unknown:" + hex16
let peerID = PeerID(str: str)
// Falls back to .empty
#expect(peerID.id == str)
#expect(peerID.bare == str)
#expect(peerID.prefix == .empty)
}
@Test func with_only_prefix_no_bare() {
let str = "mesh:"
let peerID = PeerID(str: str)
#expect(peerID.id == str)
#expect(peerID.bare == "")
#expect(peerID.prefix == .mesh)
}
// MARK: - init?(data:)
@Test func data_valid_utf8() {
let peerID = PeerID(data: Data(hex16.utf8))
#expect(peerID != nil)
#expect(peerID?.bare == hex16)
#expect(peerID?.prefix == .empty)
}
@Test func data_invalid_utf8() {
// Random invalid UTF8
let bytes: [UInt8] = [0xFF, 0xFE, 0xFA]
let peerID = PeerID(data: Data(bytes))
#expect(peerID == nil)
}
// MARK: - init(str: Substring)
@Test func substring() {
let substring = hex64.prefix(16)
let peerID = PeerID(str: substring)
#expect(peerID.id == String(substring))
#expect(peerID.bare == String(substring))
#expect(peerID.prefix == .empty)
}
// MARK: - init(nostr_ pubKey:)
@Test func nostrUnderscore_pubKey() {
let pubKey = hex64
let peerID = PeerID(nostr_: pubKey)
#expect(peerID.id == "nostr_\(pubKey.prefix(PeerID.Constants.nostrConvKeyPrefixLength))")
#expect(peerID.bare == String(pubKey.prefix(PeerID.Constants.nostrConvKeyPrefixLength)))
#expect(peerID.prefix == .geoDM)
}
// MARK: - init(nostr pubKey:)
@Test func nostr_pubKey() {
let pubKey = hex64
let peerID = PeerID(nostr: pubKey)
#expect(peerID.id == "nostr:\(pubKey.prefix(PeerID.Constants.nostrShortKeyDisplayLength))")
#expect(peerID.bare == String(pubKey.prefix(PeerID.Constants.nostrShortKeyDisplayLength)))
#expect(peerID.prefix == .geoChat)
}
// MARK: - init(publicKey:)
@Test func publicKey_derivesFingerprint() {
let publicKey = Data(hex64.utf8)
let expected = publicKey.sha256Fingerprint().prefix(16)
let peerID = PeerID(publicKey: publicKey)
#expect(peerID.bare == String(expected))
#expect(peerID.prefix == .empty)
}
// MARK: - toShort()
@Test func toShort_whenNoiseKeyExists() {
let peerID = PeerID(str: hex64)
let short = peerID.toShort()
let expected = Data(hexString: hex64)!.sha256Fingerprint().prefix(16)
#expect(short.bare == String(expected))
#expect(short.prefix == .empty)
}
@Test func toShort_whenNoiseKeyExists_withNoisePrefix() {
let peerID = PeerID(str: "noise:" + hex64)
let short = peerID.toShort()
let expected = Data(hexString: hex64)!.sha256Fingerprint().prefix(16)
#expect(short.bare == String(expected))
#expect(short.prefix == .empty)
#expect(peerID.prefix == .noise)
}
@Test func toShort_whenNoNoiseKey() {
let peerID = PeerID(str: "some_random_key")
let short = peerID.toShort()
#expect(short == peerID)
}
@Test func routingData_fromShortID() throws {
let peerID = PeerID(str: hex16)
let routing = try #require(peerID.routingData)
#expect(routing.count == 8)
#expect(routing == Data(hexString: hex16))
}
@Test func routingData_fromNoiseKey() throws {
let peerID = PeerID(str: hex64)
let routing = try #require(peerID.routingData)
let expectedShort = peerID.toShort()
#expect(routing == Data(hexString: expectedShort.id))
}
@Test func routingPeerRoundTrip() throws {
let raw = try #require(Data(hexString: hex16))
let peerID = try #require(PeerID(routingData: raw))
#expect(peerID.routingData == raw)
}
// MARK: - Codable
@Test func codable_emptyPrefix() throws {
struct Dummy: Codable, Equatable {
let name: String
let peerID: PeerID
}
let str = "aabbccddeeff0011"
let jsonString = "{\"name\":\"some name\",\"peerID\":\"\(str)\"}"
let decoded = try JSONDecoder().decode(Dummy.self, from: Data(jsonString.utf8))
#expect(decoded.peerID == PeerID(str: str))
let encoded = try encoder.encode(decoded)
#expect(String(data: encoded, encoding: .utf8) == jsonString)
}
@Test func codable_withPrefix() throws {
struct Dummy: Codable, Equatable {
let peerID: PeerID
}
let str = "nostr_\(hex16)"
let jsonString = "{\"peerID\":\"\(str)\"}"
let decoded = try JSONDecoder().decode(Dummy.self, from: Data(jsonString.utf8))
#expect(decoded.peerID == PeerID(str: str))
#expect(decoded.peerID.bare == hex16)
#expect(decoded.peerID.prefix == .geoDM)
let encoded = try encoder.encode(decoded)
#expect(String(data: encoded, encoding: .utf8) == jsonString)
}
@Test func codable_multiplePrefixes() throws {
// Loop across all Prefix cases (except .empty since already tested)
for prefix in PeerID.Prefix.allCases where prefix != .empty {
let bare = hex16
let str = prefix.rawValue + bare
let decoded = try JSONDecoder().decode(PeerID.self, from: Data("\"\(str)\"".utf8))
#expect(decoded.prefix == prefix)
#expect(decoded.bare == bare)
let encoded = try encoder.encode(decoded)
#expect(String(data: encoded, encoding: .utf8) == "\"\(str)\"")
}
}
// MARK: - Comparable
@Test func comparable_sorting_and_equality() {
let p1 = PeerID(str: "aaa")
let p2 = PeerID(str: "bbb")
let p3 = PeerID(str: "BBB")
#expect(p1 < p2)
#expect(p2 >= p1)
#expect(p2 == p3)
let sorted = [p2, p1].sorted()
#expect(sorted == [p1, p2])
}
@Test func equality() {
let peerID = PeerID(str: "aaa")
// Regular PeerID <> PeerID
#expect(peerID == PeerID(str: "AAA"))
#expect(peerID == Optional(PeerID(str: "AAA")))
#expect(PeerID(str: "AAA") == peerID)
#expect(Optional(PeerID(str: "AAA")) == Optional(peerID))
#expect(peerID != PeerID(str: "BBB"))
#expect(peerID != Optional(PeerID(str: "BBB")))
#expect(PeerID(str: "BBB") != peerID)
#expect(Optional(PeerID(str: "BBB")) != Optional(peerID))
}
// MARK: - Computed properties
@Test func isEmpty_true_and_false() {
#expect(PeerID(str: "").isEmpty)
#expect(!PeerID(str: "abc").isEmpty)
}
@Test func isGeoChat() {
#expect(PeerID(str: "nostr:abcdef").isGeoChat)
#expect(!PeerID(str: "nostr_abcdef").isGeoChat)
}
@Test func isGeoDM() {
#expect(PeerID(str: "nostr_abcdef").isGeoDM)
#expect(!PeerID(str: "nostr:abcdef").isGeoDM)
}
@Test func toPercentEncoded() {
let peerID = PeerID(str: "name:some value/with spaces?")
let encoded = peerID.toPercentEncoded()
// spaces and ? should be percent-encoded in urlPathAllowed
#expect(encoded == "name%3Asome%20value/with%20spaces%3F")
}
// MARK: - Validation
@Test func accepts_short_hex_peer_id() {
#expect(PeerID(str: "0011223344556677").isValid)
#expect(PeerID(str: "aabbccddeeff0011").isValid)
}
@Test func accepts_full_noise_key_hex() {
let hex64 = String(repeating: "ab", count: 32) // 64 hex chars
#expect(PeerID(str: hex64).isValid)
}
@Test func accepts_internal_alnum_dash_underscore() {
#expect(PeerID(str: "peer_123-ABC").isValid)
#expect(PeerID(str: "nostr_user_01").isValid)
}
@Test func rejects_invalid_characters() {
#expect(!PeerID(str: "peer!@#").isValid)
#expect(!PeerID(str: "gggggggggggggggg").isValid) // not hex for short form
}
@Test func rejects_too_long() {
let tooLong = String(repeating: "a", count: 65)
#expect(!PeerID(str: tooLong).isValid)
}
@Test func isShort() {
#expect(PeerID(str: hex16).isShort)
#expect(!PeerID(str: "abcd").isShort) // wrong length
}
@Test func isNoiseKeyHex_and_noiseKey() {
let hex64 = String(repeating: "ab", count: 32) // 64 chars valid hex
let peerID = PeerID(str: hex64)
#expect(peerID.isNoiseKeyHex)
#expect(peerID.noiseKey != nil)
let prefixedPeerID = PeerID(str: "noise:" + hex64)
#expect(prefixedPeerID.isNoiseKeyHex)
#expect(prefixedPeerID.noiseKey != nil)
let bad = String(repeating: "z", count: 64) // invalid hex
let badPeerID = PeerID(str: bad)
#expect(!badPeerID.isNoiseKeyHex)
#expect(badPeerID.noiseKey == nil)
}
@Test func prefixes() {
let hex64 = String(repeating: "a", count: 64)
#expect(PeerID(str: "noise:\(hex64)").isValid)
#expect(PeerID(str: "nostr:\(hex64)").isValid)
#expect(PeerID(str: "nostr_\(hex64)").isValid)
let hex63 = String(repeating: "a", count: 63)
#expect(PeerID(str: "noise:\(hex63)").isValid)
#expect(PeerID(str: "nostr:\(hex63)").isValid)
#expect(PeerID(str: "nostr_\(hex63)").isValid)
let hex16 = String(repeating: "a", count: 16)
#expect(PeerID(str: "noise:\(hex16)").isValid)
#expect(PeerID(str: "nostr:\(hex16)").isValid)
#expect(PeerID(str: "nostr_\(hex16)").isValid)
let hex8 = String(repeating: "a", count: 8)
#expect(PeerID(str: "noise:\(hex8)").isValid)
#expect(PeerID(str: "nostr:\(hex8)").isValid)
#expect(PeerID(str: "nostr_\(hex8)").isValid)
let mesh = "mesh:abcdefg"
#expect(PeerID(str: "name:\(mesh)").isValid)
let name = "name:some_name"
#expect(PeerID(str: "name:\(name)").isValid)
let badName = "name:bad:name"
#expect(!PeerID(str: "name:\(badName)").isValid)
// Too long
let hex65 = String(repeating: "a", count: 65)
#expect(!PeerID(str: "noise:\(hex65)").isValid)
#expect(!PeerID(str: "nostr:\(hex65)").isValid)
#expect(!PeerID(str: "nostr_\(hex65)").isValid)
}
// MARK: - File Transfer PeerID Normalization
// These tests verify the fix for asymmetric voice/media delivery (BCH-01-XXX)
// The bug occurred when selectedPrivateChatPeer was migrated to 64-hex stable key
// but the receiver expected SHA256-derived 16-hex format
@Test func fileTransfer_toShortNormalizesNoiseKeyToFingerprint() {
// Given: A 64-hex Noise public key (what selectedPrivateChatPeer becomes after session)
let noiseKey = Data(repeating: 0xAB, count: 32)
let stableKeyPeerID = PeerID(hexData: noiseKey) // 64-hex
// When: Convert to short form (what sendFilePrivate should do)
let shortID = stableKeyPeerID.toShort()
// Then: Should be 16-hex SHA256 fingerprint (matching myPeerID format)
let expected = noiseKey.sha256Fingerprint().prefix(16)
#expect(shortID.id == String(expected))
#expect(shortID.id.count == 16)
}
@Test func fileTransfer_shortIDMatchesMyPeerIDFormat() {
// Given: A receiver's myPeerID is SHA256-derived (from refreshPeerIdentity)
let noiseKey = Data(repeating: 0xCD, count: 32)
let myPeerID = PeerID(publicKey: noiseKey) // SHA256-derived 16-hex
// When: Sender uses toShort() on 64-hex stable key
let senderStableKey = PeerID(hexData: noiseKey) // 64-hex
let recipientData = Data(hexString: senderStableKey.toShort().id)!
let receivedRecipientID = PeerID(hexData: recipientData)
// Then: Should match receiver's myPeerID (file transfer accepted)
#expect(receivedRecipientID == myPeerID)
}
@Test func fileTransfer_truncatedRawKeyDoesNotMatchMyPeerID() {
// This test demonstrates the bug we fixed
// When 64-hex was truncated to first 8 bytes instead of using SHA256 fingerprint
// Given: Receiver's myPeerID is SHA256-derived
let noiseKey = Data(repeating: 0xEF, count: 32)
let myPeerID = PeerID(publicKey: noiseKey) // SHA256-derived 16-hex
// When: Truncate raw key (the OLD buggy behavior)
let truncatedRaw = noiseKey.prefix(8) // First 8 bytes of raw key
let wrongRecipientID = PeerID(hexData: truncatedRaw)
// Then: Should NOT match (demonstrates why fix was needed)
#expect(wrongRecipientID != myPeerID)
}
@Test func fileTransfer_shortIDProducesCorrect8ByteRoutingData() {
// Verify the wire format is correct (8 bytes for BinaryProtocol)
let noiseKey = Data(repeating: 0x12, count: 32)
let stableKeyPeerID = PeerID(hexData: noiseKey)
let shortID = stableKeyPeerID.toShort()
// routingData should be 8 bytes (16 hex chars -> 8 bytes)
let routingData = shortID.routingData
#expect(routingData != nil)
#expect(routingData?.count == 8)
// And it should match SHA256 fingerprint first 8 bytes
let expectedFingerprint = noiseKey.sha256Fingerprint()
let expectedFirst8 = Data(hexString: String(expectedFingerprint.prefix(16)))
#expect(routingData == expectedFirst8)
}
}