carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movement
2
3import (
4 "crypto/sha256"
5 "math/bits"
6 "strings"
7)
8
9// The proof of work is the price of submitting an address. The client
10// searches for a counter whose hash starts with enough zero bits; the server
11// runs that hash once and compares.
12//
13// The address is part of the preimage, and that is the whole point. An
14// unbound challenge is solved once and replayed against every address in a
15// list, so it costs an attacker one solve in total. Bound, it costs one solve
16// per address — which is exactly the axis a bulk signup attack scales along.
17// It is also why signing up needs JavaScript at all: the work cannot begin
18// until the visitor has typed the address it is bound to.
19//
20// What this does not do: 2^18 SHA-256 is under a millisecond on a commodity
21// GPU. Proof of work prices out casual and scripted abuse and nothing more.
22// The defence against a serious bulk attack is the persisted mail budget,
23// not this.
24
25// DefaultDifficulty is an estimate, not a measurement — roughly 262k
26// expected hashes. It must be re-set against real mid-range hardware before
27// launch, recording p95 and p99 rather than the mean: solve time is
28// geometric, so p99 is about 4.6x the average, and calibrating on the
29// average ships a form that hangs for one visitor in a hundred.
30const DefaultDifficulty = 18
31
32// NormalizeEmail lowercases ASCII and nothing else.
33//
34// Go's strings.ToLower and JavaScript's toLowerCase disagree on parts of
35// Unicode, and the preimage has to be byte-identical on both sides. A
36// disagreement here is a silent pow_short that no visitor can diagnose and
37// no log explains, so the two implementations are kept deliberately dumb and
38// deliberately identical.
39func NormalizeEmail(s string) string {
40 b := []byte(strings.TrimSpace(s))
41 for i, c := range b {
42 if c >= 'A' && c <= 'Z' {
43 b[i] = c + 32
44 }
45 }
46 return string(b)
47}
48
49// PowPreimage is the exact string both sides hash. pow.js builds the same
50// one; the browser test is what proves they agree.
51func PowPreimage(nonce, normalizedEmail, counter string) string {
52 return nonce + ":" + normalizedEmail + ":" + counter
53}
54
55func leadingZeroBits(sum []byte) int {
56 n := 0
57 for _, b := range sum {
58 if b == 0 {
59 n += 8
60 continue
61 }
62 return n + bits.LeadingZeros8(b)
63 }
64 return n
65}
66
67// VerifyPow checks one candidate. O(1) here, ~2^difficulty for whoever had
68// to find it.
69func VerifyPow(nonce, email, counter string, difficulty int) bool {
70 if counter == "" {
71 return false
72 }
73 sum := sha256.Sum256([]byte(PowPreimage(nonce, NormalizeEmail(email), counter)))
74 return leadingZeroBits(sum[:]) >= difficulty
75}
76