Skip to content

Commit f3f739b

Browse files
authored
Merge pull request #63 from fiatjaf/fix/wot-atomic
Guard aggregated WoT behind an atomic pointer to avoid torn reads
2 parents c3e4fba + 97cc2da commit f3f739b

5 files changed

Lines changed: 43 additions & 15 deletions

File tree

inbox/core.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func rejectEvent(ctx context.Context, evt nostr.Event) (bool, string) {
183183

184184
for _, pk := range khatru.GetAllAuthed(ctx) {
185185
// at least one authenticated pubkey is in the wot
186-
if wot.Current.Contains(pk) {
186+
if wot.Contains(pk) {
187187
return false, ""
188188
}
189189
}
@@ -256,7 +256,7 @@ func rejectEvent(ctx context.Context, evt nostr.Event) (bool, string) {
256256
}
257257

258258
// ensure this comes from someone in the relay combined extended network
259-
if !wot.Current.Contains(sender) {
259+
if !wot.Contains(sender) {
260260
if evt.Kind == 9735 && sender == evt.PubKey {
261261
// we'll make an exception for zap providers that do not include the "P" temporarily
262262
return false, ""

inbox/inbox.templ

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@ templ inboxPage(loggedUser nostr.PubKey) {
5353
browse inbox →
5454
</a>
5555
<div class="mt-6 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4">
56-
if !wot.Computed {
56+
if !wot.IsComputed() {
5757
<span>calculating aggregated web-of-trust...</span>
5858
} else {
5959
<div class="flex items-center gap-4">
6060
<h3 class="font-semibold text-gray-800 dark:text-gray-200">check who is allowed to post</h3>
6161
<div class="text-sm text-gray-600 dark:text-gray-400">
62-
{ fmt.Sprintf("(total: %d pubkeys)", wot.Current.Items) }
62+
{ fmt.Sprintf("(total: %d pubkeys)", wot.Count()) }
6363
</div>
6464
</div>
6565
<form

inbox/relay.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,5 +237,5 @@ func checkWoTHandler(w http.ResponseWriter, r *http.Request) {
237237
}
238238

239239
w.Header().Set("Content-Type", "application/json")
240-
fmt.Fprintf(w, "%v", wot.Current.Contains(pk))
240+
fmt.Fprintf(w, "%v", wot.Contains(pk))
241241
}

operator/handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,11 @@ func handleRegister(w http.ResponseWriter, r *http.Request) {
211211
return
212212
}
213213
case "wot":
214-
if !wot.Computed {
214+
if !wot.IsComputed() {
215215
http.Error(w, "web-of-trust not yet computed, try again later", http.StatusServiceUnavailable)
216216
return
217217
}
218-
if !wot.Current.Contains(evt.PubKey) {
218+
if !wot.Contains(evt.PubKey) {
219219
http.Error(w, "only web-of-trust members can register", http.StatusForbidden)
220220
return
221221
}

wot/wot.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,46 @@ package wot
22

33
import (
44
"context"
5+
"sync/atomic"
56
"time"
67

8+
"fiatjaf.com/nostr"
79
"github.com/fiatjaf/pyramid/global"
810
)
911

10-
// Current is the latest computed aggregated web-of-trust filter.
11-
// It is safe to read at any time, even before it has been computed
12-
// (it will just return an empty filter that contains nothing).
13-
var Current XorFilter
12+
// current holds the latest computed aggregated web-of-trust filter.
13+
// it is stored behind an atomic pointer so it can be read from request
14+
// goroutines while the background goroutine replaces it, without racing
15+
// on the multi-word XorFilter struct.
16+
var current atomic.Pointer[XorFilter]
1417

15-
// Computed is true after the first successful WoT computation.
16-
var Computed bool
18+
// computed is set to true after the first successful WoT computation.
19+
var computed atomic.Bool
20+
21+
// Contains reports whether the given pubkey is in the latest aggregated WoT.
22+
// It is safe to call at any time, even before the first computation
23+
// (it will just return false).
24+
func Contains(pubkey nostr.PubKey) bool {
25+
f := current.Load()
26+
if f == nil {
27+
return false
28+
}
29+
return f.Contains(pubkey)
30+
}
31+
32+
// IsComputed reports whether the WoT has been computed at least once.
33+
func IsComputed() bool {
34+
return computed.Load()
35+
}
36+
37+
// Count returns the number of pubkeys in the latest aggregated WoT.
38+
func Count() int {
39+
f := current.Load()
40+
if f == nil {
41+
return 0
42+
}
43+
return f.Items
44+
}
1745

1846
// StartBackgroundComputation begins a periodic background loop that
1947
// recomputes the aggregated WoT every 48 hours. It sleeps 2 minutes
@@ -28,8 +56,8 @@ func StartBackgroundComputation() {
2856
global.Log.Error().Err(err).Msg("failed to compute aggregated WoT")
2957
time.Sleep(3 * time.Hour)
3058
} else {
31-
Current = wot
32-
Computed = true
59+
current.Store(&wot)
60+
computed.Store(true)
3361
global.Log.Info().Int("entries", wot.Items).Msg("computed aggregated WoT")
3462
time.Sleep(48 * time.Hour)
3563
}

0 commit comments

Comments
 (0)