Hashcash Captcha
The Hashcash Captcha provides proof-of-work based challenge-response authentication to prevent automated attacks. Unlike traditional image-based CAPTCHAs, it requires clients to perform computational work, making it accessible and bot-resistant.
Purpose
Hashcash CAPTCHA protects sensitive operations from automated abuse by requiring clients to solve a cryptographic puzzle. The difficulty can be tuned to balance security and user experience - higher difficulty means more computation time required.
How It Works
Proof-of-Work Challenge
The system uses a challenge-response protocol:
- Client requests a challenge - Specifies desired difficulty (19-35) and time-to-live
- Server generates a random 32-byte challenge - Stores it in Redis with the specified TTL
- Client solves the puzzle - Finds a nonce value that when hashed with the challenge produces a hash with the required number of leading zero bits
- Client submits the solution - Sends back the challenge ID and nonce
- Server verifies - Checks the solution and deletes the challenge if valid (one-time use)
The difficulty parameter controls how many leading zero bits are required in the SHA256 hash output. Each increment doubles the expected computation time.
Challenge Lifecycle
Challenges are stateful and stored in Redis:
- Each challenge has a unique 16-byte ID
- TTL ensures challenges expire and don’t accumulate
- Successfully verified challenges are deleted immediately (prevents replay attacks)
- Expired or non-existent challenges return
NotFoundresult
Frontend Integration
Basic Workflow
1. Before submitting sensitive operation (login, registration, etc.)
2. Call RequestCaptcha with difficulty and TTL
3. Receive challenge_id, challenge bytes, and difficulty
4. Solve: Find nonce where SHA256(challenge + nonce) has required leading zero bits
5. Call VerifyCaptcha with challenge_id and nonce
6. Check result: Pass/Fail/NotFound
7. If Pass, proceed with the protected operation
Solving the Challenge
Frontend must implement the proof-of-work algorithm:
- Hash function: SHA256
- Input:
challenge_bytes + nonce_bytes(nonce as 8-byte big-endian u64) - Goal: Find nonce where hash has
difficultyleading zero bits - Method: Brute force increment nonce from 0 until valid hash found
JavaScript/TypeScript developers should use the Web Crypto API or a library like crypto-js for SHA256 hashing. For performance-critical cases, consider using WebAssembly.
Difficulty Guidelines
Difficulty 19-22 (Very Easy):
- Solves in <100ms on modern devices
- Use for non-critical operations or mobile clients
Difficulty 23-26 (Easy to Medium):
- Solves in 100ms - 1 second
- Good balance for most use cases (login, registration)
Difficulty 27-30 (Medium to Hard):
- Solves in 1-10 seconds
- Use for high-value operations (password reset, account deletion)
Difficulty 31-35 (Very Hard):
- Solves in 10+ seconds
- Use sparingly, mainly for administrative or highly sensitive operations
Note: Difficulty is exponential - each +1 doubles the average solve time.
TTL Recommendations
Set TTL based on expected solve time plus user think time:
- Easy challenges: 30-60 seconds
- Medium challenges: 60-120 seconds
- Hard challenges: 120-300 seconds
Too short: Users may timeout while solving Too long: Increases Redis memory usage and replay attack window
Verification Results
Three possible outcomes from VerifyCaptcha:
- Pass: Solution is correct, challenge consumed and deleted
- Fail: Solution is incorrect, challenge remains valid (can retry)
- NotFound: Challenge expired, already used, or never existed
Frontend should handle NotFound by requesting a new challenge, and Fail by continuing to solve or requesting a new one.
Architecture
Redis-Backed Storage
Challenges are stored in Redis with keys: hashcash:{hex(challenge_id)}
Each challenge contains:
- 16-byte unique ID (random)
- 32-byte random challenge data
- Difficulty value (19-35)
- TTL expiration
Redis automatically cleans up expired challenges, and successful verifications delete them immediately to prevent replay attacks.
Stateless and Distributed
The system is stateless from the application perspective - all state lives in Redis. This allows multiple server instances to handle challenge requests and verifications without coordination.
When to Use Hashcash
Good use cases:
- Registration and login endpoints
- Password reset requests
- Anonymous or pre-authentication operations
- Expensive public endpoints (email sending, report generation)
- Rate limiting supplement for unauthenticated users
When NOT to use:
- Authenticated user operations (use rate limiting instead)
- High-frequency read operations
- Mobile-first applications (difficulty must be tuned lower)
- Accessibility-critical features (consider alternatives)
Implementation Notes
- Uses the Processor pattern for challenge creation and verification
- Challenge IDs must be exactly 16 bytes
- Difficulty validation: must be in range [19, 35] inclusive
- TTL validation: must be at least 1 second
- One challenge = one verification (single-use)
- Verification is atomic: check and delete happen together
- All cryptographic operations use SHA256