A clean three-state machine: fresh, warned, locked. Each PIN attempt advances the state for that source IP.
| State | Counter range | What happens on next attempt |
|---|---|---|
| Fresh | fail_count = 0 | Verify PIN; on fail → increment to 1 |
| Warned | 1 ≤ fail_count < max_retry | Verify PIN; on fail → increment; on success → reset to 0 |
| Locked | fail_count ≥ max_retry | Reject before bcrypt; add IP to blacklist; admin must release |
The "reset on success" trade
Resetting the counter on a successful login is friendly: a few legitimate typos before getting it right do not lock you out. It is also safe: the attacker doesn't know the PIN, so they can't reset the counter by guessing right — they'd have to guess right to do that, in which case the auth has already succeeded and they don't need to brute force anymore.