A Bitswap-inspired peer reputation and rate limiting module for Swift. Tracks bytes exchanged, latency, success rate, and proof-of-work challenges to compute a composite reputation score per peer. Under load, only high-reputation peers get served.
let tally = Tally()
let peer = PeerID(publicKey: "a1f2e3d...")
tally.recordReceived(peer: peer, bytes: 4096)
tally.recordSent(peer: peer, bytes: 2048)
tally.recordLatency(peer: peer, microseconds: 5000)
tally.recordSuccess(peer: peer)
tally.reputation(for: peer) // 0.72
tally.shouldAllow(peer: peer) // true — good reputation, under rate limitEach peer's reputation is a weighted composite of four factors (0.0 to 1.0):
| Factor | Weight | Measures |
|---|---|---|
| Reciprocity | 0.4 | 1 / (1 + debtRatio) — peers who give back score higher |
| Latency | 0.2 | baseline / (meanLatency + 1) — fast peers score higher |
| Success rate | 0.2 | successes / (successes + failures) — reliable peers score higher |
| Challenges | 0.2 | Proof-of-work completions — bootstrapped peers can earn credit |
Instead of a fixed allow/deny threshold, Tally adapts based on current load:
ratePressure = currentRate / rateLimitBytesPerSecond
pressure < 0.5 → allow everyone (plenty of capacity)
pressure 0.5–1.0 → increasingly selective (reputation must exceed threshold)
pressure >= 1.0 → only reputation >= 0.8 gets through
When you're well under your rate limit, even unknown peers get served. As load climbs, Tally progressively gates to high-reputation peers only.
New peers with no exchange history can bootstrap reputation by solving SHA256 proof-of-work challenges:
let challenge = tally.issueChallenge()
// peer solves: find `solution` where SHA256(nonce || solution) has N leading zero bits
let verified = tally.verifyChallenge(challenge, solution: peerSolution, peer: peer)
// verified == true → peer.challengeHardness += difficulty → reputation improvesThis provides Sybil resistance — creating many identities is cheap, but earning reputation for each requires real computation.
- Swift 6.0+
- macOS 13+ / iOS 16+
.package(url: "https://github.com/treehauslabs/Tally.git", from: "1.0.0"),Then add to your target:
.target(name: "YourTarget", dependencies: ["Tally"])import Tally
let tally = Tally()
let peer = PeerID(publicKey: "a1f2e3d4...")
tally.recordSent(peer: peer, bytes: responseData.count)
tally.recordReceived(peer: peer, bytes: incomingData.count)
tally.recordLatency(peer: peer, microseconds: elapsed)
tally.recordSuccess(peer: peer)
tally.recordFailure(peer: peer)
tally.recordRequest(peer: peer)if tally.shouldAllow(peer: peer) {
let data = fetchData(for: cid)
tally.recordSent(peer: peer, bytes: data.count)
send(data, to: peer)
} else {
sendDenial(to: peer)
}// Server side: issue challenge
let challenge = tally.issueChallenge()
send(challenge, to: peer)
// Client side: solve
let solver = ChallengeSolver()
let solution = solver.solve(challenge)
send(solution, to: server)
// Server side: verify and credit
tally.verifyChallenge(challenge, solution: solution, peer: peer)let tally = Tally(config: TallyConfig(
weights: ReputationWeights(
reciprocity: 0.4,
latency: 0.2,
successRate: 0.2,
challenges: 0.2
),
latencyBaseline: 100_000, // microseconds
challengeDifficulty: 16, // leading zero bits
rateLimitBytesPerSecond: 10_000_000,
rateWindow: 1.0, // seconds
maxPeers: 10_000
))tally.reputation(for: peer)
tally.debtRatio(for: peer)
tally.peerLedger(for: peer)
tally.ratePressure()
let m = tally.metrics
// m.allowed, m.denied, m.totalBytesSent, m.totalBytesReceived
// m.challengesIssued, m.challengesVerified| Method | Description |
|---|---|
recordSent(peer:bytes:) |
Record bytes sent to a peer (increases debt). |
recordReceived(peer:bytes:) |
Record bytes received from a peer (credits them). |
recordLatency(peer:microseconds:) |
Record response latency for a peer. |
recordSuccess(peer:) |
Record a successful interaction. |
recordFailure(peer:) |
Record a failed interaction. |
recordRequest(peer:) |
Increment request count. |
shouldAllow(peer:) -> Bool |
Rate-aware + reputation-based allow/deny. |
reputation(for:) -> Double |
Composite reputation score (0.0–1.0). |
debtRatio(for:) -> Double |
Raw debt ratio for a peer. |
ratePressure() -> Double |
Current rate pressure (0.0 = idle, 1.0 = at limit). |
issueChallenge() -> Challenge |
Create a proof-of-work challenge. |
verifyChallenge(_:solution:peer:) -> Bool |
Verify and credit a solved challenge. |
peerLedger(for:) -> PeerLedger? |
Full ledger for a peer. |
allPeers() -> [PeerID] |
All tracked peer IDs. |
peerCount -> Int |
Number of tracked peers. |
resetPeer(_:) |
Remove a peer's ledger. |
metrics -> TallyMetrics |
Aggregate stats. |
- Lock-based, no actor — all state behind
OSAllocatedUnfairLockfor nanosecond-scale operations. - Bitswap debt ratio —
r = bytes_sent / (bytes_received + 1), same formula as IPFS. - Composite reputation — weighted blend of reciprocity, latency, success rate, and proof-of-work.
- Rate-aware gating — permissive when idle, selective under load.
- SHA256 proof of work — Sybil-resistant reputation bootstrapping for new peers.
- Zero external dependencies — uses only Foundation and CryptoKit.
Benchmarked on Apple Silicon (M-series), release mode:
| Operation | Time | Notes |
|---|---|---|
| shouldAllow (fresh peer) | 48ns | Lock + dictionary miss + rate check |
| shouldAllow (known peer) | 69ns | Lock + lookup + reputation + rate check |
| reputation lookup | 33ns | Lock + lookup + weighted score |
| recordSent | 89ns | Lock + lookup + update + rate window |
| recordLatency | 54ns | Lock + lookup + running stats |
| mixed (80% check / 20% record) | 81ns | Realistic workload |
swift test33 tests across 3 suites: PeerLedger (debt ratio, reciprocity, success rate, latency scoring, reputation composition, custom weights), Tally (recording, gating, metrics, rate pressure, peer management), Challenge (solving, verification, accumulation, reputation bootstrapping).