Attack Vector: Generating random 32-byte strings (fake nullifiers) is computationally free, but storing them in RAM (seenNullifiers) costs resources. An attacker could flood the gossip network with invalid nullifiers to exhaust validator memory and CPU resources, causing denial of service.
Impact:
- Memory exhaustion (unbounded
seenNullifiersmap growth) - CPU exhaustion (expensive Witness proof verification for spam)
- Network degradation (bandwidth consumed by invalid messages)
Scarcity implements a three-layer defense against nullifier spam attacks, moving from the network layer up to the economic layer:
Peer Reputation & Throttling - First line of defense at the P2P connection layer.
Located in: src/gossip.ts
Each peer is assigned a reputation score that tracks their behavior:
- Starting score: 0
- Invalid Witness proof: -10 points (heavy penalty)
- Duplicate spam: -1 point per duplicate
- Expired nullifiers: -2 points
- Future timestamps: -5 points
- Valid messages: +1 point (capped at 100)
When a peer's score drops below the threshold (default: -50), they are automatically disconnected.
// Configuration
const gossip = new NullifierGossip({
witness,
peerScoreThreshold: -50, // Disconnect threshold
// ... other options
});
// Monitor peer reputation
const peerStats = gossip.getPeerStats(peerId);
console.log(peerStats); // { score, invalidProofs, duplicates, validMessages }Benefits:
- Prevents malicious peers from continuously flooding the network
- Low-cost rejection of known bad actors
- Incentivizes honest behavior
Located in: src/vendor/hypertoken/E2EEncryption.ts
All peer communication is encrypted using ECDH key exchange + AES-256-GCM:
- Key Exchange: ECDH with P-256 curve establishes shared secret
- Encryption: AES-256-GCM for authenticated encryption
- Mandatory: Messages cannot be sent without an established E2E session
// E2E encryption is automatic and mandatory
// Messages are encrypted before sending, decrypted on receipt
// No configuration needed - enforced by HyperTokenAdapterBenefits:
- Relay servers cannot read message content
- Protects against man-in-the-middle attacks
- Prevents traffic analysis of message payloads
Located in: src/integrations/hypertoken.ts
Each peer connection has an independent leaky bucket rate limiter:
- Default rate: 10 messages/second per peer
- Burst capacity: 20 messages
- Behavior: Silently drops messages exceeding the limit
const adapter = new HyperTokenAdapter({
relayUrl: 'ws://localhost:8080',
rateLimitPerSecond: 10, // Refill rate
rateLimitBurst: 20 // Bucket capacity
});
// Monitor rate limiting
const peer = adapter.createPeer(peerId);
const stats = peer.getRateLimitStats();
console.log(stats); // { droppedMessages, currentTokens }Benefits:
- Prevents single malicious node from overwhelming CPU
- Graceful degradation under load
- No configuration required for honest peers
How it works:
- Each peer starts with a full bucket (20 tokens)
- Each message consumes 1 token
- Tokens refill at 10/second
- Messages received when bucket is empty are dropped
Proof-of-Work & Timestamp Validation - Computational "tolls" and strict timestamp windows.
Located in: src/integrations/witness.ts, src/crypto.ts
Before the Witness timestamps a nullifier, the client must solve a computational puzzle:
- Find a nonce such that
Hash(nullifier + nonce)has N leading zero bits - Default difficulty: 0 (disabled, for backward compatibility)
- Recommended: 16 bits = ~65,000 attempts (~50-200ms on modern hardware)
- High security: 20 bits = ~1,000,000 attempts (~5-10 seconds)
const witness = new WitnessAdapter({
gatewayUrl: 'https://witness.example.com',
powDifficulty: 16 // 16 leading zero bits
});
// When you call timestamp(), it automatically solves the PoW
const proof = await witness.timestamp(nullifierHash);
// Output: [Witness] PoW solved in 87ms (difficulty: 16, nonce: 54321)Benefits:
- Imposes computation cost on spammer (generating 1M fake nullifiers becomes expensive)
- Legitimate users barely notice the delay (< 100ms typical)
- Tunable difficulty based on threat level
Implementation: Uses SHA-256 mining algorithm similar to Bitcoin, but with much lower difficulty.
Located in: src/gossip.ts
The gossip layer enforces strict acceptance windows for Witness timestamps:
- Future timestamps: Reject if > 5 seconds in the future (prevents pre-mining spam)
- Old timestamps: Reject if older than
maxNullifierAge(default: ~1.5 years) - Early rejection: Invalid timestamps are rejected before verifying the expensive Witness signature
const gossip = new NullifierGossip({
witness,
maxTimestampFuture: 5, // Max 5 seconds in future
maxNullifierAge: 86400000 // Max age: 24 hours
});Benefits:
- Saves CPU by rejecting obviously invalid nullifiers early
- Prevents attackers from pre-generating spam with future timestamps
- Prevents replay attacks with old nullifiers
Sybil Resistance - The root solution that makes spam economically infeasible.
A valid nullifier is derived from a valid Token ID and Secret:
nullifier = Hash(secret || tokenId || timestamp)
To generate a valid nullifier that passes Witness verification, an attacker needs:
- A valid token from Freebird (requires passing Sybil resistance)
- Knowledge of the token's secret (obtained via VOPRF)
Benefits:
- Attacker cannot easily get 1 million tokens from Freebird
- Rate limiting or invitation-only issuance on Freebird server prevents mass token acquisition
- Economic cost scales linearly with spam volume
Located in: src/gossip.ts
For maximum spam resistance, you can require every nullifier message to include a Freebird Ownership Proof:
const gossip = new NullifierGossip({
witness,
requireOwnershipProof: true // Enforce ownership proofs
});How it works:
- Each gossip message must include
ownershipProof(cryptographic proof of token ownership) - Attacker must perform expensive VOPRF operation for each spam message
- Significantly slower than just generating random hashes
Benefits:
- Forces attacker to use valid Freebird tokens for spam
- Makes spam as expensive as legitimate transfers
- Reduces spam to economic denial-of-service (which is rate-limited by Freebird)
Trade-off: Increases bandwidth and verification overhead for all messages. Recommended only for high-security deployments.
Freebird supports multiple Sybil resistance mechanisms that can be combined for defense-in-depth.
Recommended configuration:
# Docker environment variables for Freebird issuer
SYBIL_RESISTANCE=combined
SYBIL_COMBINED_MODE=and
SYBIL_COMBINED_MECHANISMS=progressive_trust,proof_of_diversityMechanisms:
| Mechanism | Description |
|---|---|
progressive_trust |
New users start with limited issuance, limits increase over time |
proof_of_diversity |
Requires proof of unique identity signals (device, network, behavior) |
Combined mode and: User must satisfy ALL mechanisms to receive tokens. This provides layered protection:
- Progressive trust prevents burst attacks from new identities
- Proof of diversity prevents Sybil attacks from single actors with many identities
Why this matters for Scarcity:
- Token issuance is the economic root of the system
- Unlimited issuance would enable spam attacks and inflation
- Combined resistance makes mass token acquisition prohibitively difficult
Alternative modes:
or: User satisfies ANY mechanism (more permissive)and: User satisfies ALL mechanisms (more restrictive, recommended)
// Basic rate limiting only
const gossip = new NullifierGossip({
witness: new WitnessAdapter({ gatewayUrl: '...' }),
peerScoreThreshold: -50
});
const adapter = new HyperTokenAdapter({
rateLimitPerSecond: 10,
rateLimitBurst: 20
});// All layers except ownership proofs
const gossip = new NullifierGossip({
witness: new WitnessAdapter({
gatewayUrl: '...',
powDifficulty: 16 // ~65k attempts
}),
peerScoreThreshold: -50,
maxTimestampFuture: 5,
maxNullifierAge: 86400000 // 24 hours
});
const adapter = new HyperTokenAdapter({
rateLimitPerSecond: 10,
rateLimitBurst: 20
});// All layers including ownership proofs
const gossip = new NullifierGossip({
witness: new WitnessAdapter({
gatewayUrl: '...',
powDifficulty: 20 // ~1M attempts, ~5-10 seconds
}),
peerScoreThreshold: -30, // Stricter threshold
maxTimestampFuture: 2, // Tighter window
maxNullifierAge: 3600000, // 1 hour max age
requireOwnershipProof: true // Mandatory ownership proofs
});
const adapter = new HyperTokenAdapter({
rateLimitPerSecond: 5, // Lower rate limit
rateLimitBurst: 10 // Lower burst
});- Cost to generate 1M fake nullifiers: ~0 (just random bytes)
- Validator impact: Memory exhaustion, crash
- Cost: Attacker needs to create multiple peer identities
- Limit: 10 msg/sec/peer × N peers
- Result: Significantly slows attack, but still possible with Sybil network
- Cost: 1M nullifiers × 50ms PoW = ~14 hours of computation
- Limit: Rate limiting + computational cost
- Result: Attack becomes expensive and slow
- Cost: Attacker needs 1M valid Freebird tokens + PoW computation
- Limit: Freebird Sybil resistance limits token acquisition
- Result: Attack becomes economically infeasible
const allScores = gossip.getAllPeerScores();
for (const [peerId, score] of allScores) {
console.log(`${peerId}: ${score.score} (invalid: ${score.invalidProofs}, dupes: ${score.duplicates})`);
}for (const peer of adapter.getPeers()) {
const stats = peer.getRateLimitStats();
console.log(`${peer.id}: dropped ${stats.droppedMessages}, tokens: ${stats.currentTokens}`);
}const stats = gossip.getStats();
console.log(`Nullifiers: ${stats.nullifierCount}, Peers: ${stats.peerCount}, Active: ${stats.activePeers}`);- Adaptive PoW Difficulty: Automatically increase difficulty during attack
- Peer Reputation Persistence: Save scores across restarts
- Distributed Banlist: Share malicious peer IDs across network
- Freebird Rate Limiting Integration: Query Freebird for token issuance rate
- Machine Learning: Detect spam patterns using ML
- Leaky Bucket Algorithm: https://en.wikipedia.org/wiki/Leaky_bucket
- Proof-of-Work: https://en.wikipedia.org/wiki/Proof_of_work
- Sybil Resistance: Freebird whitepaper (see
README.md) - P2P Reputation Systems: https://dl.acm.org/doi/10.1145/1030194.1015504
While the spam mitigation measures above protect against Availability attacks (DoS/crashes), the following features address Integrity and Trust vulnerabilities that could enable theft, double-spending, or undetected inflation.
Vulnerability: Single gateway operator could perform targeted censorship or split-view attacks by returning false 404 responses for nullifiers that exist.
Solution: Query multiple independent gateways with quorum voting.
// Backward compatible: single gateway
const witness = new WitnessAdapter({
gatewayUrl: 'https://witness1.example.com'
});
// Multi-gateway with 2-of-3 quorum (recommended)
const witness = new WitnessAdapter({
gatewayUrls: [
'https://witness1.example.com',
'https://witness2.example.com',
'https://witness3.example.com'
],
quorumThreshold: 2 // Require 2 gateways to agree
});
// Custom quorum (e.g., 3-of-5)
const witness = new WitnessAdapter({
gatewayUrls: [
'https://witness1.example.com',
'https://witness2.example.com',
'https://witness3.example.com',
'https://witness4.example.com',
'https://witness5.example.com'
],
quorumThreshold: 3
});When checking if a nullifier has been seen:
- Query All Gateways: Queries all configured gateways in parallel
- Quorum Voting:
- If ≥ quorum say "seen" → Return 1.0 (double-spend detected)
- If ≥ quorum say "not seen" → Return 0.0 (safe to accept)
- If split vote or insufficient responses → Return 0.5 (suspicious, possible censorship attack)
- Censorship Resistance: Single malicious gateway cannot hide double-spends
- Availability: Continues working if some gateways are down
- Split-View Detection: Warns when gateways disagree (possible attack)
[Witness] Configured with 3 gateway(s), quorum threshold: 2
[Witness] Nullifier check: 0/3 gateways report seen (quorum: 2)
✅ Safe: Quorum agrees nullifier has NOT been seen
[Witness] Nullifier check: 3/3 gateways report seen (quorum: 2)
❌ Double-spend: Quorum agrees nullifier HAS been seen
[Witness] Nullifier check: 1/3 gateways report seen (quorum: 2)
⚠️ Suspicious: Split vote on nullifier check - possible censorship attack
Vulnerability: Attackers can easily connect TO you (inbound connections) and surround your node with malicious peers, creating a "reality bubble" where they lie about nullifier states.
Solution: Weight outbound peers (connections YOU initiated) 3x higher in confidence scoring.
Peers now support optional direction and remoteAddress tracking:
// When adding peers, specify connection direction
const outboundPeer: PeerConnection = {
id: 'trusted-peer-1',
direction: 'outbound', // YOU initiated this connection
remoteAddress: '203.0.113.1',
send: async (data) => { /* ... */ },
isConnected: () => true
};
const inboundPeer: PeerConnection = {
id: 'unknown-peer-1',
direction: 'inbound', // They connected to YOU
remoteAddress: '198.51.100.1',
send: async (data) => { /* ... */ },
isConnected: () => true
};
gossip.addPeer(outboundPeer);
gossip.addPeer(inboundPeer);The validator's confidence scoring now uses effective peer count:
Effective Peers = (Outbound × 3) + (Inbound × 1) + (Unknown × 1)
Example:
- 2 outbound peers + 3 inbound peers = (2×3) + 3 = 9 effective peers
- 0 outbound peers + 9 inbound peers = (0×3) + 9 = 9 effective peers
But the first scenario is more secure because outbound peers are more trustworthy.
- Eclipse Resistance: Attackers cannot easily surround your node
- Trust Asymmetry: Recognizes that outbound connections are inherently more trustworthy
- Backward Compatible: Works with peers that don't specify direction (treated as inbound for safety)
const validator = new TransferValidator({ gossip, witness });
// Scenario 1: Mixed peers (3 outbound, 2 inbound)
// Effective = (3×3) + 2 = 11
// peerScore = min(11/10, 0.5) = 0.5 ✅
// Scenario 2: All inbound (0 outbound, 11 inbound)
// Effective = (0×3) + 11 = 11
// peerScore = min(11/10, 0.5) = 0.5 ⚠️ (but less trustworthy)
// Scenario 3: All outbound (4 outbound, 0 inbound)
// Effective = (4×3) + 0 = 12
// peerScore = min(12/10, 0.5) = 0.5 ✅✅ (high trust)Vulnerability: Attacker controls 20 nodes on same /24 subnet (e.g., rented VPS instances). All report "never seen it" for a double-spent nullifier.
Solution: Track and warn about peers from the same IP subnet.
const gossip = new NullifierGossip({ witness });
// Add peers from diverse subnets (good)
gossip.addPeer({ id: 'peer-1', remoteAddress: '203.0.113.1', ... });
gossip.addPeer({ id: 'peer-2', remoteAddress: '198.51.100.1', ... });
gossip.addPeer({ id: 'peer-3', remoteAddress: '192.0.2.1', ... });
// Add 4th peer from same subnet as peer-1 (triggers warning)
gossip.addPeer({ id: 'peer-4', remoteAddress: '203.0.113.5', ... });
// Output:
// ⚠️ [Gossip] Warning: 4 peers from subnet 203.0.113.
// Possible Sybil attack. Consider limiting connections from same subnet.
// Check subnet diversity
const subnetStats = gossip.getSubnetStats();
console.log(subnetStats);
// Map { '203.0.113' => 2, '198.51.100' => 1, '192.0.2' => 1 }The maximum peers per subnet is currently hardcoded to 3, but can be adjusted in src/gossip.ts:
// In addPeer method
const MAX_PEERS_PER_SUBNET = 3; // Adjust as needed- IPv4: First 3 octets (e.g.,
192.168.1.x→ subnet192.168.1) - IPv6: First 3 groups (e.g.,
2001:db8:85a3::x→ subnet2001:db8:85a3)
- Sybil Detection: Warns when too many peers come from same network
- Operational Visibility: Helps operators identify potential attacks
- Non-Breaking: Still accepts the peer, just logs a warning
// Get subnet diversity statistics
const stats = gossip.getSubnetStats();
for (const [subnet, count] of stats) {
if (count > 3) {
console.warn(`⚠️ ${count} peers from subnet ${subnet} - investigate`);
}
}
// Example output:
// ⚠️ 5 peers from subnet 192.168.1 - investigate
// ✅ 2 peers from subnet 10.0.0
// ✅ 1 peer from subnet 203.0.113Vulnerability: Without a public ledger, there's no way to audit total supply. A compromised Freebird issuer key could mint unlimited tokens, causing "invisible inflation" undetectable by users.
Solution: Multi-Party Computation (MPC) threshold issuance splits the issuer key across multiple servers. Clients aggregate partial signatures locally using Lagrange interpolation.
// Single issuer (backward compatible, no MPC)
const freebird = new FreebirdAdapter({
issuerEndpoints: ['https://issuer.example.com'],
verifierUrl: 'https://verifier.example.com'
});
// Multi-issuer MPC with 3 servers (recommended)
const freebird = new FreebirdAdapter({
issuerEndpoints: [
'https://issuer1.example.com',
'https://issuer2.example.com',
'https://issuer3.example.com'
],
verifierUrl: 'https://verifier.example.com'
});
// High-security: 5-of-5 threshold
const freebird = new FreebirdAdapter({
issuerEndpoints: [
'https://issuer1.example.com',
'https://issuer2.example.com',
'https://issuer3.example.com',
'https://issuer4.example.com',
'https://issuer5.example.com'
],
verifierUrl: 'https://verifier.example.com'
});Trusted Dealer Setup (Phase 1 implementation):
- Key dealer splits issuer secret key k into shares: k₁, k₂, k₃
- Each server receives one share
- Shares satisfy: k = λ₁·k₁ + λ₂·k₂ + λ₃·k₃ (where λᵢ are Lagrange coefficients)
Token Issuance Flow:
- Client: Blinds recipient public key → A = H(pubkey) · r
- Broadcast: Sends blinded element to all issuers in parallel
- Issuers: Each server i computes partial signature Bᵢ = A · kᵢ
- Verify: Client verifies DLEQ proof for each partial signature
- Threshold: Client waits for majority (⌈n/2⌉) valid responses
- Aggregate: Client computes final signature using Lagrange interpolation:
B = Σ(λᵢ · Bᵢ) where λᵢ = ∏(j≠i) (xⱼ / (xⱼ - xᵢ)) mod N - Finalize: Client uses aggregated B as the final token
P-256 VOPRF with Threshold Cryptography:
- Each issuer has key share kᵢ, public key Qᵢ = G · kᵢ
- Partial evaluation: Bᵢ = A · kᵢ (where A is blinded element)
- DLEQ proof proves: log_G(Qᵢ) = log_A(Bᵢ)
- Aggregation uses Lagrange interpolation in scalar field mod N
- Final point: B = A · k (where k = Σλᵢ·kᵢ)
Security Guarantees:
- Byzantine Fault Tolerance: Tolerates up to ⌊n/2⌋ malicious servers
- No Single Point of Failure: No single server can inflate supply alone
- Immediate Verification: Each partial signature verified with DLEQ proof
- Privacy Preserved: Client aggregates locally without revealing input
- Backward Compatible: Single issuer works as before (no aggregation)
✅ Inflation Resistance: Requires collusion of multiple servers to inflate supply ✅ Fault Tolerance: Continues working if some servers are offline ✅ Proof Verification: Invalid partial signatures detected immediately ✅ Local Aggregation: Client controls the final token construction ✅ Graceful Degradation: Warns when below threshold but proceeds with available responses
[Freebird] MPC threshold mode: 3 issuers
[Freebird] Connected to 3/3 issuers
[Freebird] Nullifier check: Broadcasting to 3 issuers...
[Freebird] ✅ VOPRF token issued and aggregated (3/3 issuers)
Warning output when below threshold:
[Freebird] MPC threshold mode: 3 issuers
[Freebird] Connected to 2/3 issuers
[Freebird] ⚠️ Only 2/3 valid responses, threshold is 2. Proceeding with available responses.
[Freebird] ✅ VOPRF token issued and aggregated (2/3 issuers)
Each issuer returns:
{
"token": "base64url_encoded_token",
"index": 1 // Server's key share index (1-based)
}The token format is: [ A (33 bytes) | Bᵢ (33 bytes) | DLEQ Proof (64 bytes) ]
Combine all Phase 1 hardening features:
// 1. Multi-gateway Witness with quorum
const witness = new WitnessAdapter({
gatewayUrls: [
'https://witness1.example.com',
'https://witness2.example.com',
'https://witness3.example.com'
],
quorumThreshold: 2,
powDifficulty: 16 // Also use PoW from spam mitigation
});
// 2. Gossip with peer diversity tracking
const gossip = new NullifierGossip({
witness,
peerScoreThreshold: -50,
maxTimestampFuture: 5,
maxNullifierAge: 86400000
});
// 3. Add diverse outbound peers
const outboundPeers = [
{ id: 'peer-1', direction: 'outbound', remoteAddress: '203.0.113.1' },
{ id: 'peer-2', direction: 'outbound', remoteAddress: '198.51.100.1' },
{ id: 'peer-3', direction: 'outbound', remoteAddress: '192.0.2.1' }
];
for (const peer of outboundPeers) {
gossip.addPeer(createPeerConnection(peer));
}
// 4. Validator with high confidence threshold
const validator = new TransferValidator({
gossip,
witness,
waitTime: 5000,
minConfidence: 0.8 // High threshold for security
});
// Result: System resistant to:
// ✅ Gateway censorship (multi-gateway quorum)
// ✅ Eclipse attacks (outbound peer preference)
// ✅ Sybil attacks (subnet diversity + peer scoring)
// ✅ Spam attacks (PoW + rate limiting from previous layers)
// ✅ Invisible inflation (MPC threshold issuance)// Monitor all security dimensions
setInterval(() => {
// 1. Peer diversity
const subnetStats = gossip.getSubnetStats();
console.log('Subnet diversity:', subnetStats.size);
// 2. Peer direction mix
const peers = gossip.peers;
const outbound = peers.filter(p => p.direction === 'outbound').length;
const inbound = peers.filter(p => p.direction === 'inbound').length;
console.log(`Peers: ${outbound} outbound, ${inbound} inbound`);
// 3. Peer reputation
const scores = gossip.getAllPeerScores();
const lowScorePeers = Array.from(scores.entries())
.filter(([_, score]) => score.score < -20);
if (lowScorePeers.length > 0) {
console.warn('Peers with low reputation:', lowScorePeers);
}
// 4. Gateway availability (conceptual - depends on monitoring implementation)
// Check if all gateways are reachable
}, 60000); // Every minute✅ Single Point of Failure (Gateway): Solved with multi-gateway quorum ✅ Eclipse Attack (Gossip Layer): Mitigated with outbound peer preference ✅ Sybil Attack (Gossip Layer): Partially mitigated with subnet diversity checks ✅ Invisible Inflation (Economic Layer): Solved with MPC threshold issuance
These require more complex solutions and are documented for future development:
Vulnerability: While Tor hides IPs and VOPRF hides identity, transaction graph analysis could correlate transfers.
Future Solutions:
- Fixed Denominations: Force all tokens to standard values (1, 10, 100) like Monero
- Dandelion++ Routing: Privacy-preserving routing phase before epidemic broadcast
- Decoy Traffic: Send fake transactions to obfuscate real patterns
Current mitigation (outbound peer preference) is not foolproof. An attacker with network-level access could still intercept outbound connections.
Future Solutions:
- Trusted Anchor Peers: Hardcoded set of known-good peers
- Kademlia DHT: Distributed hash table to enforce topology
- Proof-of-Stake Peer Selection: Weight peer trust by economic stake
To report security vulnerabilities in Scarcity, please open an issue at: https://github.com/flammafex/scarcity/issues
Do not disclose security vulnerabilities publicly until they have been addressed.