Compare commits

..

1 Commits

Author SHA1 Message Date
fiatjaf
aa9e53b61d wip 2024-08-01 15:36:14 -03:00
5 changed files with 36 additions and 521 deletions

View File

@@ -39,21 +39,3 @@ features:
link: https://pkg.go.dev/github.com/fiatjaf/khatru
details: That means it is fast and lightweight, you can learn the language in 5 minutes and it builds your relay into a single binary that's easy to ship and deploy.
---
## A glimpse of `khatru`'s power
It allows you to create a fully-functional relay in 7 lines of code:
```go
func main() {
relay := khatru.NewRelay()
db := badger.BadgerBackend{Path: "/tmp/khatru-badgern-tmp"}
db.Init()
relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
http.ListenAndServe(":3334", relay)
}
```
After that you can customize it in infinite ways. See the links above.

View File

@@ -67,13 +67,12 @@ func (rl *Relay) removeListenerId(ws *WebSocket, id string) {
if specs, ok := rl.clients[ws]; ok {
// swap delete specs that match this id
for s := len(specs) - 1; s >= 0; s-- {
spec := specs[s]
nswaps := 0
for s, spec := range specs {
if spec.id == id {
spec.cancel(ErrSubscriptionClosedByClient)
specs[s] = specs[len(specs)-1]
specs = specs[0 : len(specs)-1]
rl.clients[ws] = specs
specs[s] = specs[len(specs)-1-nswaps]
nswaps++
// swap delete listeners one at a time, as they may be each in a different subrelay
srl := spec.subrelay // == rl in normal cases, but different when this came from a route
@@ -83,11 +82,10 @@ func (rl *Relay) removeListenerId(ws *WebSocket, id string) {
moved := srl.listeners[movedFromIndex] // this wasn't removed, but will be moved
srl.listeners[spec.index] = moved
// now we must update the the listener we just moved
// so its .index reflects its new position on srl.listeners
// update the index of the listener we just moved
movedSpecs := rl.clients[moved.ws]
idx := slices.IndexFunc(movedSpecs, func(ls listenerSpec) bool {
return ls.index == movedFromIndex && ls.subrelay == srl
return ls.index == movedFromIndex
})
movedSpecs[idx].index = spec.index
rl.clients[moved.ws] = movedSpecs
@@ -95,6 +93,7 @@ func (rl *Relay) removeListenerId(ws *WebSocket, id string) {
srl.listeners = srl.listeners[0 : len(srl.listeners)-1] // finally reduce the slice length
}
}
rl.clients[ws] = specs[0 : len(specs)-nswaps]
}
}
@@ -103,7 +102,7 @@ func (rl *Relay) removeClientAndListeners(ws *WebSocket) {
defer rl.clientsMutex.Unlock()
if specs, ok := rl.clients[ws]; ok {
// swap delete listeners and delete client (all specs will be deleted)
for s, spec := range specs {
for _, spec := range specs {
// no need to cancel contexts since they inherit from the main connection context
// just delete the listeners (swap-delete)
srl := spec.subrelay
@@ -113,15 +112,10 @@ func (rl *Relay) removeClientAndListeners(ws *WebSocket) {
moved := srl.listeners[movedFromIndex] // this wasn't removed, but will be moved
srl.listeners[spec.index] = moved
// temporarily update the spec of the listener being removed to have index == -1
// (since it was removed) so it doesn't match in the search below
rl.clients[ws][s].index = -1
// now we must update the the listener we just moved
// so its .index reflects its new position on srl.listeners
// update the index of the listener we just moved
movedSpecs := rl.clients[moved.ws]
idx := slices.IndexFunc(movedSpecs, func(ls listenerSpec) bool {
return ls.index == movedFromIndex && ls.subrelay == srl
return ls.index == movedFromIndex
})
movedSpecs[idx].index = spec.index
rl.clients[moved.ws] = movedSpecs

View File

@@ -1,188 +0,0 @@
package khatru
import (
"math/rand"
"testing"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/require"
)
func FuzzRandomListenerClientRemoving(f *testing.F) {
f.Add(uint(20), uint(20), uint(1))
f.Fuzz(func(t *testing.T, utw uint, ubs uint, ualf uint) {
totalWebsockets := int(utw)
baseSubs := int(ubs)
addListenerFreq := int(ualf) + 1
rl := NewRelay()
f := nostr.Filter{Kinds: []int{1}}
cancel := func(cause error) {}
websockets := make([]*WebSocket, 0, totalWebsockets*baseSubs)
l := 0
for i := 0; i < totalWebsockets; i++ {
ws := &WebSocket{}
websockets = append(websockets, ws)
rl.clients[ws] = nil
}
s := 0
for j := 0; j < baseSubs; j++ {
for i := 0; i < totalWebsockets; i++ {
ws := websockets[i]
w := idFromSeqUpper(i)
if s%addListenerFreq == 0 {
l++
rl.addListener(ws, w+":"+idFromSeqLower(j), rl, f, cancel)
}
s++
}
}
require.Len(t, rl.clients, totalWebsockets)
require.Len(t, rl.listeners, l)
for ws := range rl.clients {
rl.removeClientAndListeners(ws)
}
require.Len(t, rl.clients, 0)
require.Len(t, rl.listeners, 0)
})
}
func FuzzRandomListenerIdRemoving(f *testing.F) {
f.Add(uint(20), uint(20), uint(1), uint(4))
f.Fuzz(func(t *testing.T, utw uint, ubs uint, ualf uint, ualef uint) {
totalWebsockets := int(utw)
baseSubs := int(ubs)
addListenerFreq := int(ualf) + 1
addExtraListenerFreq := int(ualef) + 1
if totalWebsockets > 1024 || baseSubs > 1024 {
return
}
rl := NewRelay()
f := nostr.Filter{Kinds: []int{1}}
cancel := func(cause error) {}
websockets := make([]*WebSocket, 0, totalWebsockets)
type wsid struct {
ws *WebSocket
id string
}
subs := make([]wsid, 0, totalWebsockets*baseSubs)
extra := 0
for i := 0; i < totalWebsockets; i++ {
ws := &WebSocket{}
websockets = append(websockets, ws)
rl.clients[ws] = nil
}
s := 0
for j := 0; j < baseSubs; j++ {
for i := 0; i < totalWebsockets; i++ {
ws := websockets[i]
w := idFromSeqUpper(i)
if s%addListenerFreq == 0 {
id := w + ":" + idFromSeqLower(j)
rl.addListener(ws, id, rl, f, cancel)
subs = append(subs, wsid{ws, id})
if s%addExtraListenerFreq == 0 {
rl.addListener(ws, id, rl, f, cancel)
extra++
}
}
s++
}
}
require.Len(t, rl.clients, totalWebsockets)
require.Len(t, rl.listeners, len(subs)+extra)
rand.Shuffle(len(subs), func(i, j int) {
subs[i], subs[j] = subs[j], subs[i]
})
for _, wsidToRemove := range subs {
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
}
require.Len(t, rl.listeners, 0)
require.Len(t, rl.clients, totalWebsockets)
for _, specs := range rl.clients {
require.Len(t, specs, 0)
}
})
}
func FuzzRouterListenersPabloCrash(f *testing.F) {
f.Add(uint(3), uint(6), uint(2), uint(20))
f.Fuzz(func(t *testing.T, totalRelays uint, totalConns uint, subFreq uint, subIterations uint) {
totalRelays++
totalConns++
subFreq++
subIterations++
rl := NewRelay()
relays := make([]*Relay, int(totalRelays))
for i := 0; i < int(totalRelays); i++ {
relays[i] = NewRelay()
}
conns := make([]*WebSocket, int(totalConns))
for i := 0; i < int(totalConns); i++ {
ws := &WebSocket{}
conns[i] = ws
rl.clients[ws] = make([]listenerSpec, 0, subIterations)
}
f := nostr.Filter{Kinds: []int{1}}
cancel := func(cause error) {}
type wsid struct {
ws *WebSocket
id string
}
s := 0
subs := make([]wsid, 0, subIterations*totalConns*totalRelays)
for i, conn := range conns {
w := idFromSeqUpper(i)
for j := 0; j < int(subIterations); j++ {
id := w + ":" + idFromSeqLower(j)
for _, rlt := range relays {
if s%int(subFreq) == 0 {
rl.addListener(conn, id, rlt, f, cancel)
subs = append(subs, wsid{conn, id})
}
s++
}
}
}
for _, wsid := range subs {
rl.removeListenerId(wsid.ws, wsid.id)
}
for _, wsid := range subs {
require.Len(t, rl.clients[wsid.ws], 0)
}
for _, rlt := range relays {
require.Len(t, rlt.listeners, 0)
}
})
}

View File

@@ -1,28 +1,12 @@
package khatru
import (
"math/rand"
"strings"
"testing"
"github.com/nbd-wtf/go-nostr"
"github.com/stretchr/testify/require"
)
func idFromSeqUpper(seq int) string { return idFromSeq(seq, 65, 90) }
func idFromSeqLower(seq int) string { return idFromSeq(seq, 97, 122) }
func idFromSeq(seq int, min, max int) string {
maxSeq := max - min + 1
nLetters := seq/maxSeq + 1
result := strings.Builder{}
result.Grow(nLetters)
for l := 0; l < nLetters; l++ {
letter := rune(seq%maxSeq + min)
result.WriteRune(letter)
}
return result.String()
}
func TestListenerSetupAndRemoveOnce(t *testing.T) {
rl := NewRelay()
@@ -244,302 +228,48 @@ func TestListenerMoreStuffWithMultipleRelays(t *testing.T) {
ws3: {
{"a", cancel, 0, rlz},
{"a", cancel, 3, rlx},
{"f", cancel, 2, rly},
{"f", cancel, 3, rly},
},
ws4: {
{"d", cancel, 1, rlx},
{"e", cancel, 2, rlx},
{"e", cancel, 1, rly},
{"e", cancel, 2, rly},
},
}, rl.clients)
require.Equal(t, []listener{
{"c", f1, ws1},
{"b", f2, ws2},
{"a", f3, ws3},
{"d", f3, ws4},
{"e", f3, ws4},
{"a", f3, ws3},
}, rlx.listeners)
require.Equal(t, []listener{
{"b", f2, ws2},
{"e", f3, ws4},
{"f", f3, ws3},
}, rly.listeners)
require.Equal(t, []listener{
{"a", f3, ws3},
{"g", f1, ws1},
{"g", f2, ws2},
}, rlz.listeners)
}, rl.listeners)
})
t.Run("removing a subscription id", func(t *testing.T) {
// removing 'd' from ws4
rl.clients[ws4][0].cancel = func(cause error) {} // set since removing will call it
rl.removeListenerId(ws4, "d")
// t.Run("removing a client", func(t *testing.T) {
// rl.removeClientAndListeners(ws2)
require.Equal(t, map[*WebSocket][]listenerSpec{
ws1: {
{"c", cancel, 0, rlx},
{"g", cancel, 1, rlz},
},
ws2: {
{"b", cancel, 0, rly},
{"g", cancel, 2, rlz},
},
ws3: {
{"a", cancel, 0, rlz},
{"a", cancel, 1, rlx},
{"f", cancel, 2, rly},
},
ws4: {
{"e", cancel, 1, rly},
{"e", cancel, 2, rlx},
},
}, rl.clients)
// require.Equal(t, map[*WebSocket][]listenerSpec{
// ws1: {
// {"c", cancel, 0, rl},
// },
// ws3: {
// {"a", cancel, 2, rl},
// },
// ws4: {
// {"d", cancel, 1, rl},
// },
// }, rl.clients)
require.Equal(t, []listener{
{"c", f1, ws1},
{"a", f3, ws3},
{"e", f3, ws4},
}, rlx.listeners)
require.Equal(t, []listener{
{"b", f2, ws2},
{"e", f3, ws4},
{"f", f3, ws3},
}, rly.listeners)
require.Equal(t, []listener{
{"a", f3, ws3},
{"g", f1, ws1},
{"g", f2, ws2},
}, rlz.listeners)
})
t.Run("removing another subscription id", func(t *testing.T) {
// removing 'a' from ws3
rl.clients[ws3][0].cancel = func(cause error) {} // set since removing will call it
rl.clients[ws3][1].cancel = func(cause error) {} // set since removing will call it
rl.removeListenerId(ws3, "a")
require.Equal(t, map[*WebSocket][]listenerSpec{
ws1: {
{"c", cancel, 0, rlx},
{"g", cancel, 1, rlz},
},
ws2: {
{"b", cancel, 0, rly},
{"g", cancel, 0, rlz},
},
ws3: {
{"f", cancel, 2, rly},
},
ws4: {
{"e", cancel, 1, rly},
{"e", cancel, 1, rlx},
},
}, rl.clients)
require.Equal(t, []listener{
{"c", f1, ws1},
{"e", f3, ws4},
}, rlx.listeners)
require.Equal(t, []listener{
{"b", f2, ws2},
{"e", f3, ws4},
{"f", f3, ws3},
}, rly.listeners)
require.Equal(t, []listener{
{"g", f2, ws2},
{"g", f1, ws1},
}, rlz.listeners)
})
t.Run("removing a connection", func(t *testing.T) {
rl.removeClientAndListeners(ws2)
require.Equal(t, map[*WebSocket][]listenerSpec{
ws1: {
{"c", cancel, 0, rlx},
{"g", cancel, 0, rlz},
},
ws3: {
{"f", cancel, 0, rly},
},
ws4: {
{"e", cancel, 1, rly},
{"e", cancel, 1, rlx},
},
}, rl.clients)
require.Equal(t, []listener{
{"c", f1, ws1},
{"e", f3, ws4},
}, rlx.listeners)
require.Equal(t, []listener{
{"f", f3, ws3},
{"e", f3, ws4},
}, rly.listeners)
require.Equal(t, []listener{
{"g", f1, ws1},
}, rlz.listeners)
})
t.Run("removing another subscription id", func(t *testing.T) {
// removing 'e' from ws4
rl.clients[ws4][0].cancel = func(cause error) {} // set since removing will call it
rl.clients[ws4][1].cancel = func(cause error) {} // set since removing will call it
rl.removeListenerId(ws4, "e")
require.Equal(t, map[*WebSocket][]listenerSpec{
ws1: {
{"c", cancel, 0, rlx},
{"g", cancel, 0, rlz},
},
ws3: {
{"f", cancel, 0, rly},
},
ws4: {},
}, rl.clients)
require.Equal(t, []listener{
{"c", f1, ws1},
}, rlx.listeners)
require.Equal(t, []listener{
{"f", f3, ws3},
}, rly.listeners)
require.Equal(t, []listener{
{"g", f1, ws1},
}, rlz.listeners)
})
}
func TestRandomListenerClientRemoving(t *testing.T) {
rl := NewRelay()
f := nostr.Filter{Kinds: []int{1}}
cancel := func(cause error) {}
websockets := make([]*WebSocket, 0, 20)
l := 0
for i := 0; i < 20; i++ {
ws := &WebSocket{}
websockets = append(websockets, ws)
rl.clients[ws] = nil
}
for j := 0; j < 20; j++ {
for i := 0; i < 20; i++ {
ws := websockets[i]
w := idFromSeqUpper(i)
if rand.Intn(2) < 1 {
l++
rl.addListener(ws, w+":"+idFromSeqLower(j), rl, f, cancel)
}
}
}
require.Len(t, rl.clients, 20)
require.Len(t, rl.listeners, l)
for ws := range rl.clients {
rl.removeClientAndListeners(ws)
}
require.Len(t, rl.clients, 0)
require.Len(t, rl.listeners, 0)
}
func TestRandomListenerIdRemoving(t *testing.T) {
rl := NewRelay()
f := nostr.Filter{Kinds: []int{1}}
cancel := func(cause error) {}
websockets := make([]*WebSocket, 0, 20)
type wsid struct {
ws *WebSocket
id string
}
subs := make([]wsid, 0, 20*20)
extra := 0
for i := 0; i < 20; i++ {
ws := &WebSocket{}
websockets = append(websockets, ws)
rl.clients[ws] = nil
}
for j := 0; j < 20; j++ {
for i := 0; i < 20; i++ {
ws := websockets[i]
w := idFromSeqUpper(i)
if rand.Intn(2) < 1 {
id := w + ":" + idFromSeqLower(j)
rl.addListener(ws, id, rl, f, cancel)
subs = append(subs, wsid{ws, id})
if rand.Intn(5) < 1 {
rl.addListener(ws, id, rl, f, cancel)
extra++
}
}
}
}
require.Len(t, rl.clients, 20)
require.Len(t, rl.listeners, len(subs)+extra)
rand.Shuffle(len(subs), func(i, j int) {
subs[i], subs[j] = subs[j], subs[i]
})
for _, wsidToRemove := range subs {
rl.removeListenerId(wsidToRemove.ws, wsidToRemove.id)
}
require.Len(t, rl.listeners, 0)
require.Len(t, rl.clients, 20)
for _, specs := range rl.clients {
require.Len(t, specs, 0)
}
}
func TestRouterListenersPabloCrash(t *testing.T) {
rl := NewRelay()
rla := NewRelay()
rlb := NewRelay()
ws1 := &WebSocket{}
ws2 := &WebSocket{}
ws3 := &WebSocket{}
rl.clients[ws1] = nil
rl.clients[ws2] = nil
rl.clients[ws3] = nil
f := nostr.Filter{Kinds: []int{1}}
cancel := func(cause error) {}
rl.addListener(ws1, ":1", rla, f, cancel)
rl.addListener(ws2, ":1", rlb, f, cancel)
rl.addListener(ws3, "a", rlb, f, cancel)
rl.addListener(ws3, "b", rla, f, cancel)
rl.addListener(ws3, "c", rlb, f, cancel)
rl.removeClientAndListeners(ws1)
rl.removeClientAndListeners(ws3)
// require.Equal(t, []listener{
// {"c", f1, ws1},
// {"d", f3, ws4},
// {"a", f3, ws3},
// }, rl.listeners)
// })
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"slices"
"strings"
"time"
"github.com/nbd-wtf/go-nostr"
)
@@ -80,8 +79,7 @@ func RestrictToSpecifiedKinds(kinds ...uint16) func(context.Context, *nostr.Even
}
}
func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
func PreventTimestampsInThePast(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) {
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
if nostr.Now()-event.CreatedAt > thresholdSeconds {
return true, "event too old"
@@ -90,8 +88,7 @@ func PreventTimestampsInThePast(threshold time.Duration) func(context.Context, *
}
}
func PreventTimestampsInTheFuture(threshold time.Duration) func(context.Context, *nostr.Event) (bool, string) {
thresholdSeconds := nostr.Timestamp(threshold.Seconds())
func PreventTimestampsInTheFuture(thresholdSeconds nostr.Timestamp) func(context.Context, *nostr.Event) (bool, string) {
return func(ctx context.Context, event *nostr.Event) (reject bool, msg string) {
if event.CreatedAt-nostr.Now() > thresholdSeconds {
return true, "event too much in the future"