eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// Hashcash at space creation (#561): the client burns a few seconds of CPU
4// per space, making the Sybil loop (mint invites → fresh accounts → another
5// free space each) cost compute instead of nothing. The burn runs during the
6// fader's "Hold on…" build phase — the cost IS the animation.
7//
8// Classic hashcash: the server hands out a sealed, expiring, single-use
9// challenge bound to the requesting account; the client finds a nonce where
10// SHA-256(challenge ":" nonce) has at least `bits` leading zero bits;
11// verification is one hash. Asymmetric by construction — the server never
12// works. Off by default (LCHAT_POW_BITS unset/0): self-hosters and dev setups
13// never see it.
14
15import (
16 "crypto/rand"
17 "crypto/sha256"
18 "encoding/binary"
19 "fmt"
20 "strconv"
21 "sync"
22 "time"
23)
24
25// powChallengeWindow bounds how long a minted challenge stays solvable. Long
26// enough for slow hardware at production difficulty, short enough that a
27// stockpile of pre-mined challenges goes stale.
28const powChallengeWindow = 5 * time.Minute
29
30// powGate is the creation gate. nil = disabled.
31type powGate struct {
32 bits int
33 keys *stateKeys
34 mu sync.Mutex
35 used map[string]int64 // consumed challenge -> expiry (replay guard)
36}
37
38func newPowGate(bits int, keys *stateKeys) *powGate {
39 if bits <= 0 {
40 return nil
41 }
42 return &powGate{bits: bits, keys: keys, used: map[string]int64{}}
43}
44
45// mint issues a challenge for one account: seal(expiry || random || uid).
46// Stateless until solved — only CONSUMED challenges take memory, and those
47// expire out of the map.
48func (g *powGate) mint(uid string) (string, error) {
49 return g.mintAt(uid, time.Now().Add(powChallengeWindow))
50}
51
52// mintAt is the expiry-injectable body of mint, so tests can mint stale
53// challenges without a clock.
54func (g *powGate) mintAt(uid string, expiry time.Time) (string, error) {
55 payload := make([]byte, 8+16+len(uid))
56 binary.BigEndian.PutUint64(payload, uint64(expiry.Unix()))
57 if _, err := rand.Read(payload[8:24]); err != nil {
58 return "", err
59 }
60 copy(payload[24:], uid)
61 return g.keys.seal(payload)
62}
63
64// verify checks one solution: the challenge opens, is bound to this account,
65// hasn't expired or been used, and the nonce clears the difficulty. A passing
66// challenge is consumed — one challenge, one space.
67func (g *powGate) verify(uid, challenge, nonce string) error {
68 payload, ok := g.keys.open(challenge)
69 if !ok || len(payload) < 24 {
70 return fmt.Errorf("bad challenge")
71 }
72 expiry := int64(binary.BigEndian.Uint64(payload))
73 if time.Now().Unix() > expiry {
74 return fmt.Errorf("challenge expired")
75 }
76 if string(payload[24:]) != uid {
77 return fmt.Errorf("challenge is not yours")
78 }
79 if !powSolves(challenge, nonce, g.bits) {
80 return fmt.Errorf("proof of work does not check out")
81 }
82 g.mu.Lock()
83 defer g.mu.Unlock()
84 now := time.Now().Unix()
85 for c, exp := range g.used { // expire the replay guard as we go
86 if exp < now {
87 delete(g.used, c)
88 }
89 }
90 if _, replayed := g.used[challenge]; replayed {
91 return fmt.Errorf("challenge already used")
92 }
93 g.used[challenge] = expiry
94 return nil
95}
96
97// powSolves reports whether SHA-256(challenge ":" nonce) clears `bits`
98// leading zero bits — the one-hash verification (and the test suite's solver
99// building block).
100func powSolves(challenge, nonce string, bits int) bool {
101 h := sha256.Sum256([]byte(challenge + ":" + nonce))
102 return leadingZeroBits(h[:]) >= bits
103}
104
105func leadingZeroBits(h []byte) int {
106 bits := 0
107 for _, b := range h {
108 if b == 0 {
109 bits += 8
110 continue
111 }
112 for mask := byte(0x80); mask != 0; mask >>= 1 {
113 if b&mask != 0 {
114 return bits
115 }
116 bits++
117 }
118 }
119 return bits
120}
121
122// solvePow grinds nonces until one clears the difficulty — used by tests (at
123// low bits) and by any Go client; the web client has its own solver loop.
124func solvePow(challenge string, bits int) string {
125 for n := 0; ; n++ {
126 nonce := strconv.Itoa(n)
127 if powSolves(challenge, nonce, bits) {
128 return nonce
129 }
130 }
131}
132