4 Commits
v0.0.8 ... main

Author SHA1 Message Date
4862dcf898 revision of protocol to be more concise and simple
more work to increase the use of RESTful path queries in general is required

simplicity of event format is definitely better now without unnecessary newlines (that are fine, but not great for reading) and the elimination of a bracket-like syntax for tags (they are just a list terminated by the content)
2025-02-22 12:08:00 -01:06
64bd999cdd ackchually generates events 2025-02-20 19:13:48 -01:06
ec19703727 improve structure for auth and capability messaging 2025-02-12 14:06:11 -01:06
91447950c1 revised structuring a little 2025-02-12 11:49:23 -01:06
25 changed files with 535 additions and 149 deletions

View File

@@ -1,3 +1,3 @@
= directories
repos are relays that store data about users, including access privilege lists for other relays
directories are relays that store data about users, including access privilege lists for other relays

5
go.mod
View File

@@ -8,11 +8,14 @@ require (
go.uber.org/atomic v1.11.0
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3
lukechampine.com/frand v1.5.1
realy.lol v1.7.13
)
require (
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
golang.org/x/sys v0.30.0 // indirect
)

13
go.sum
View File

@@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -10,18 +12,21 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3Wb1+pWBaWv/BlHK0ZYIu/KaL6eHg=
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3 h1:qNgPs5exUA+G0C96DrPwNrvLSj7GT/9D+3WMWUcUg34=
golang.org/x/exp v0.0.0-20250207012021-f9890c6ad9f3/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w=
lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q=
realy.lol v1.7.13 h1:+7kIa+RFmvdP23DRjj1GEe7+F7cmyl/xuII8QMwe7nM=
realy.lol v1.7.13/go.mod h1:qtk9aklmo7dpX+uSj20ol4utmh3ldWXQOpyzH4dcRG8=

View File

@@ -20,7 +20,7 @@ func (c *C) Marshal(d []byte) (r []byte, err error) {
r = append(r, '\n')
// log.I.S(r)
r = append(r, c.Content...)
r = append(r, '\n')
// r = append(r, '\n')
// log.I.S(r)
return
}
@@ -63,4 +63,4 @@ func (c *C) Unmarshal(d []byte) (r []byte, err error) {
}
r = r[1:]
return
}
}

View File

@@ -5,6 +5,8 @@ import (
"crypto/rand"
mrand "math/rand"
"testing"
"protocol.realy.lol/pkg/separator"
)
func TestC_Marshal_Unmarshal(t *testing.T) {
@@ -19,6 +21,7 @@ func TestC_Marshal_Unmarshal(t *testing.T) {
if res, err = c1.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
res = separator.Add(res)
c2 := new(C)
var rem []byte
if rem, err = c2.Unmarshal(res); chk.E(err) {
@@ -32,4 +35,4 @@ func TestC_Marshal_Unmarshal(t *testing.T) {
log.I.S(rem)
t.Fatalf("unexpected remaining bytes: '%0x'", rem)
}
}
}

View File

@@ -70,6 +70,7 @@ func (n *T) Marshal(d []byte) (r []byte, err error) {
r = append(r, bb...)
n.N = n.N - q*powers[k].N
}
// r = append(r, '\n')
n.N = nn
return
}
@@ -111,4 +112,4 @@ func (n *T) Unmarshal(d []byte) (r []byte, err error) {
n.N = n.N*10 + uint64(ch)
}
return
}
}

View File

@@ -1,21 +1,27 @@
package event
import (
"realy.lol/sha256"
"realy.lol/signer"
"protocol.realy.lol/pkg/content"
"protocol.realy.lol/pkg/decimal"
"protocol.realy.lol/pkg/event/types"
"protocol.realy.lol/pkg/pubkey"
"protocol.realy.lol/pkg/separator"
"protocol.realy.lol/pkg/signature"
"protocol.realy.lol/pkg/tags"
"protocol.realy.lol/pkg/types"
)
type E struct {
id []byte
Type *types.T
Pubkey *pubkey.P
Timestamp *decimal.T
Tags *tags.T
Content *content.C
Signature *signature.S
encoded []byte
}
// New creates a new event with some typical data already filled. This should be
@@ -44,26 +50,87 @@ func New(pk []byte, typ string) (ev *E, err error) {
return
}
func (e *E) Marshal(d []byte) (r []byte, err error) {
// Invalidate empties the existing encoded cache of the event. This needs to be
// called in case of mutating its fields. It also nils the signature.
func (e *E) Invalidate() { e.encoded = e.encoded[:0]; e.Signature = nil; e.id = nil }
func (e *E) Sign(s signer.I) (err error) {
var h []byte
if h, err = e.Hash(); chk.E(err) {
return
}
var sig []byte
if sig, err = s.Sign(h); chk.E(err) {
return
}
if e.Signature, err = signature.New(sig); chk.E(err) {
return
}
return
}
func (e *E) Encode(d []byte) (r []byte, err error) {
r = d
if r, err = e.Type.Marshal(d); chk.E(err) {
if e.Type == nil {
err = errorf.E("type is not defined for event")
return
}
if r, err = e.Pubkey.Marshal(d); chk.E(err) {
if r, err = e.Type.Marshal(r); chk.E(err) {
return
}
if r, err = e.Timestamp.Marshal(d); chk.E(err) {
r = separator.Add(r, ':')
if e.Pubkey == nil {
err = errorf.E("pubkey is not defined for event")
return
}
if r, err = e.Tags.Marshal(d); chk.E(err) {
// log.I.S(r)
if r, err = e.Pubkey.Marshal(r); chk.E(err) {
return
}
if r, err = e.Content.Marshal(d); chk.E(err) {
r = separator.Add(r, ';')
if e.Timestamp == nil {
err = errorf.E("timestamp is not defined for event")
return
}
if r, err = e.Signature.Marshal(d); chk.E(err) {
if r, err = e.Timestamp.Marshal(r); chk.E(err) {
return
}
r = separator.Add(r)
if r, err = e.Tags.Marshal(r); chk.E(err) {
return
}
if e.Content != nil {
if r, err = e.Content.Marshal(r); chk.E(err) {
return
}
r = separator.Add(r)
}
e.encoded = r
return
}
func (e *E) Hash() (h []byte, err error) {
var b []byte
if e.encoded == nil {
if e.encoded, err = e.Encode(nil); chk.E(err) {
return
}
b = e.encoded
}
hh := sha256.Sum256(b)
h = hh[:]
e.id = h
return
}
func (e *E) Marshal(d []byte) (r []byte, err error) {
if r, err = e.Encode(d); chk.E(err) {
return
}
if r, err = e.Signature.Marshal(r); chk.E(err) {
return
}
r = separator.Add(r)
return
}

View File

@@ -1,9 +1,205 @@
package event
import (
"encoding/binary"
mrand "math/rand"
"math/rand/v2"
"testing"
"time"
"lukechampine.com/frand"
"realy.lol/signer"
"protocol.realy.lol/pkg/content"
"protocol.realy.lol/pkg/decimal"
"protocol.realy.lol/pkg/id"
"protocol.realy.lol/pkg/pubkey"
"protocol.realy.lol/pkg/tag"
"protocol.realy.lol/pkg/tags"
"protocol.realy.lol/pkg/types"
"realy.lol/p256k"
)
func TestE_Marshal_Unmarshal(t *testing.T) {
const seed = 0
func GenerateFake32Bytes(rng *rand.Rand) (fake []byte) {
fake = make([]byte, 32)
for i := range 4 {
n := rng.Uint64()
binary.LittleEndian.PutUint64(fake[i*8:i*8+8], n)
}
return
}
var Hashtags, _ = tag.New(
"halsey",
"$DIAM",
"Trevor Lawrence",
"#AEWCEO",
"Reuters",
"Linda McMahon",
"Bolton",
"Raining in Houston",
"#SwiftDay",
"Munich",
"NATO",
"#thursdayvibes",
"Good Thursday",
"$SEA",
"#AEWGrandSlam",
"Brian Steele",
"#GalentinesDay",
"Bregman",
"Afghan",
"The Accountant 2",
"Happy Friday Eve",
"TLaw",
"Red Sox",
"Large Scale Social Deception",
"2024 BMW",
"Onew",
"Secretary of Education",
"$HIMS",
"Core PPI",
"Avowed",
"Kemp",
"Angel's Venture",
"YouTube TV",
"Bri Bri",
"Teslas",
"Thirsty Thursday",
"matz",
"Jack the Ripper",
"Paramount",
"Megan Boswell",
"Zeldin",
"Zelensky",
"Censure",
"Sheldon Whitehouse",
"Arenado",
"Parasite Class",
"Kennedy Center",
"I Love Jesus",
"James Cook",
)
func GenerateContent(rng *rand.Rand, l int) (c *content.C) {
c = &content.C{}
return
}
const lorem = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`
func GenerateTags(rng *rand.Rand, n int) (t *tags.T, err error) {
nE, nP, nH := rng.IntN(n)+1, rng.IntN(n)+1, rng.IntN(n)+1
var tt []*tag.T
k := tag.List.GetElementBytes(tag.KeyEvent)
for range nE {
var tg *tag.T
v := GenerateFake32Bytes(rng)
var e *id.T
if e, err = id.New(v); chk.E(err) {
return
}
var b []byte
if b, err = e.Marshal(b); chk.E(err) {
return
}
if tg, err = tag.New(k, b, []byte("root")); chk.E(err) {
return
}
tt = append(tt, tg)
}
k = tag.List.GetElementBytes(tag.KeyPubkey)
for range nP {
var tg *tag.T
v := GenerateFake32Bytes(rng)
var p *pubkey.P
if p, err = pubkey.New(v); chk.E(err) {
return
}
var b []byte
if b, err = p.Marshal(b); chk.E(err) {
return
}
if tg, err = tag.New(k, b); chk.E(err) {
return
}
tt = append(tt, tg)
}
k = tag.List.GetElementBytes(tag.KeyHashtag)
for range nH {
var tg *tag.T
v := Hashtags.GetElementBytes(rng.IntN(Hashtags.Len() - 1))
// v = bytes.ReplaceAll(v, []byte{';'}, []byte{'_'})
// v = bytes.ReplaceAll(v, []byte{':'}, []byte{'-'})
// log.I.S(v)
if tg, err = tag.New(k, v); chk.E(err) {
return
}
tt = append(tt, tg)
}
t = tags.New(tt...)
return
}
func GenerateEvent(sign signer.I) (ev *E, err error) {
s2 := rand.NewPCG(seed, seed)
rng := rand.New(s2)
sign = new(p256k.Signer)
if err = sign.Generate(); chk.E(err) {
return
}
var pk *pubkey.P
if pk, err = pubkey.New(sign.Pub()); chk.E(err) {
return
}
var t *tags.T
if t, err = GenerateTags(rng, 3+1); chk.E(err) {
return
}
cont := make([]byte, mrand.Intn(100)+25)
_, err = frand.Read(cont)
ev = &E{
Type: types.New("note/adoc"),
Pubkey: pk,
Timestamp: decimal.New(time.Now().Unix()),
Tags: t,
Content: &content.C{Content: []byte(lorem)},
}
if err = ev.Sign(sign); chk.E(err) {
return
}
return
}
func TestE_Marshal_Unmarshal(t *testing.T) {
var ev *E
var err error
var b1, b2 []byte
sign := &p256k.Signer{}
if err = sign.Generate(); chk.E(err) {
t.Fatal(err)
}
for range 10 {
if ev, err = GenerateEvent(sign); chk.E(err) {
t.Fatal(err)
}
if b1, err = ev.Marshal(b1); chk.E(err) {
t.Fatal(err)
}
log.I.F("\n```\n%s```\n", b1)
// log.I.S(ev)
b1 = b1[:0]
_ = b2
}
}

View File

@@ -9,19 +9,19 @@ import (
const Len = 43
type P struct{ b []byte }
type T struct{ b []byte }
func New(id []byte) (p *P, err error) {
func New(id []byte) (p *T, err error) {
if len(id) != ed25519.PublicKeySize {
err = errorf.E("invalid public key size: %d; require %d",
len(id), ed25519.PublicKeySize)
return
}
p = &P{id}
p = &T{id}
return
}
func (p *P) Marshal(d []byte) (r []byte, err error) {
func (p *T) Marshal(d []byte) (r []byte, err error) {
r = d
if p == nil || p.b == nil || len(p.b) == 0 {
err = errorf.E("nil/zero length pubkey")
@@ -40,11 +40,12 @@ func (p *P) Marshal(d []byte) (r []byte, err error) {
if err = w.Close(); chk.E(err) {
return
}
r = append(buf.Bytes(), '\n')
r = append(r, buf.Bytes()...)
// r = append(buf.Bytes(), '\n')
return
}
func (p *P) Unmarshal(data []byte) (r []byte, err error) {
func (p *T) Unmarshal(data []byte) (r []byte, err error) {
r = data
if p == nil {
err = errorf.E("can't unmarshal into nil types.T")
@@ -71,4 +72,4 @@ func (p *P) Unmarshal(data []byte) (r []byte, err error) {
}
err = io.EOF
return
}
}

View File

@@ -5,6 +5,8 @@ import (
"crypto/ed25519"
"crypto/rand"
"testing"
"protocol.realy.lol/pkg/separator"
)
func TestT_Marshal_Unmarshal(t *testing.T) {
@@ -14,7 +16,7 @@ func TestT_Marshal_Unmarshal(t *testing.T) {
if _, err = rand.Read(pk); chk.E(err) {
t.Fatal(err)
}
var p *P
var p *T
if p, err = New(pk); chk.E(err) {
t.Fatal(err)
}
@@ -22,7 +24,8 @@ func TestT_Marshal_Unmarshal(t *testing.T) {
if o, err = p.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
p2 := &P{}
o = separator.Add(o)
p2 := &T{}
var rem []byte
if rem, err = p2.Unmarshal(o); chk.E(err) {
t.Fatal(err)
@@ -34,4 +37,4 @@ func TestT_Marshal_Unmarshal(t *testing.T) {
t.Fatal("public key did not encode/decode faithfully")
}
}
}
}

View File

@@ -32,7 +32,7 @@ func (p *P) Marshal(d []byte) (r []byte, err error) {
len(p.PublicKey), ed25519.PublicKeySize, p.PublicKey)
return
}
buf := bytes.NewBuffer(r)
buf := new(bytes.Buffer)
w := base64.NewEncoder(base64.RawURLEncoding, buf)
if _, err = w.Write(p.PublicKey); chk.E(err) {
return
@@ -40,7 +40,9 @@ func (p *P) Marshal(d []byte) (r []byte, err error) {
if err = w.Close(); chk.E(err) {
return
}
r = append(buf.Bytes(), '\n')
// log.I.S(buf.Bytes())
r = append(r, buf.Bytes()...)
// r = append(buf.Bytes(), '\n')
return
}
@@ -71,4 +73,4 @@ func (p *P) Unmarshal(d []byte) (r []byte, err error) {
}
err = io.EOF
return
}
}

View File

@@ -5,6 +5,8 @@ import (
"crypto/ed25519"
"crypto/rand"
"testing"
"protocol.realy.lol/pkg/separator"
)
func TestP_Marshal_Unmarshal(t *testing.T) {
@@ -22,6 +24,8 @@ func TestP_Marshal_Unmarshal(t *testing.T) {
if o, err = p.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
o = separator.Add(o)
log.I.S(o)
p2 := &P{}
var rem []byte
if rem, err = p2.Unmarshal(o); chk.E(err) {
@@ -34,4 +38,4 @@ func TestP_Marshal_Unmarshal(t *testing.T) {
t.Fatal("public key did not encode/decode faithfully")
}
}
}
}

View File

@@ -0,0 +1,10 @@
package separator
func Add(dst []byte, custom ...byte) (r []byte) {
sep := byte('\n')
if len(custom) > 0 {
sep = custom[0]
}
r = append(dst, sep)
return
}

View File

@@ -8,6 +8,7 @@ import (
)
const Len = 86
const Sentinel = "sig:"
type S struct{ Signature []byte }
@@ -40,7 +41,7 @@ func (p *S) Marshal(d []byte) (r []byte, err error) {
len(p.Signature), ed25519.SignatureSize, p.Signature)
return
}
buf := bytes.NewBuffer(r)
buf := new(bytes.Buffer)
w := base64.NewEncoder(base64.RawURLEncoding, buf)
if _, err = w.Write(p.Signature); chk.E(err) {
return
@@ -48,7 +49,9 @@ func (p *S) Marshal(d []byte) (r []byte, err error) {
if err = w.Close(); chk.E(err) {
return
}
r = append(buf.Bytes(), '\n')
r = append(r, Sentinel...)
r = append(r, buf.Bytes()...)
// r = append(buf.Bytes(), '\n')
return
}
@@ -64,16 +67,18 @@ func (p *S) Unmarshal(d []byte) (r []byte, err error) {
}
for i := range r {
if r[i] == '\n' {
if i != Len {
if i != Len+len(Sentinel) {
err = errorf.E("invalid encoded signature length %d; require %d '%0x'",
i, Len, r[:i])
return
}
// discard the sentinel
r = r[len(Sentinel):]
p.Signature = make([]byte, ed25519.SignatureSize)
if _, err = base64.RawURLEncoding.Decode(p.Signature, r[:i]); chk.E(err) {
if _, err = base64.RawURLEncoding.Decode(p.Signature, r[:Len]); chk.E(err) {
return
}
r = r[i+1:]
r = r[Len:]
return
}
}

View File

@@ -5,6 +5,8 @@ import (
"crypto/ed25519"
"crypto/rand"
"testing"
"protocol.realy.lol/pkg/separator"
)
func TestS_Marshal_Unmarshal(t *testing.T) {
@@ -14,23 +16,24 @@ func TestS_Marshal_Unmarshal(t *testing.T) {
if _, err = rand.Read(sig); chk.E(err) {
t.Fatal(err)
}
var s *S
if s, err = New(sig); chk.E(err) {
var s1 *S
if s1, err = New(sig); chk.E(err) {
t.Fatal(err)
}
var o []byte
if o, err = s.Marshal(nil); chk.E(err) {
if o, err = s1.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
p2 := &S{}
o = separator.Add(o)
s2 := &S{}
var rem []byte
if rem, err = p2.Unmarshal(o); chk.E(err) {
if rem, err = s2.Unmarshal(o); chk.E(err) {
t.Fatal(err)
}
if len(rem) > 0 {
log.I.F("%d %s", len(rem), rem)
}
if !bytes.Equal(sig, p2.Signature) {
if !bytes.Equal(sig, s2.Signature) {
t.Fatal("signature did not encode/decode faithfully")
}

17
pkg/tag/common.go Normal file
View File

@@ -0,0 +1,17 @@
package tag
const (
KeyEvent = iota
KeyPubkey
KeyHashtag
)
var List, _ = New(
// event is a reference to an event, the value is an Event Id
"event",
// pubkey is a reference to a public key, the value is a pubkey.P
"pubkey",
// hashtag is a string that can be searched by a hashtag filter tag
"hashtag",
// ... can many more things be in here for purposes
)

View File

@@ -27,6 +27,7 @@ func New[V ~[]byte | ~string](v ...V) (t *T, err error) {
t.fields = append(t.fields, k)
for i, val := range v {
var b []byte
// log.I.S(val)
if b, err = ValidateField(val, i); chk.E(err) {
return
}
@@ -35,6 +36,29 @@ func New[V ~[]byte | ~string](v ...V) (t *T, err error) {
return
}
func (t *T) Len() int { return len(t.fields) }
func (t *T) Less(i, j int) bool { return bytes.Compare(t.fields[i], t.fields[j]) < 0 }
func (t *T) Swap(i, j int) { t.fields[i], t.fields[j] = t.fields[j], t.fields[i] }
func (t *T) GetElementBytes(i int) (s []byte) {
if i >= len(t.fields) {
// return empty string if not found
return
}
return t.fields[i]
}
func (t *T) GetElementString(i int) (s string) {
return string(t.GetElementBytes(i))
}
func (t *T) GetStringSlice() (s []string) {
for _, v := range t.fields {
s = append(s, string(v))
}
return
}
// ValidateKey checks that the key is valid. Keys must be the same most language symbols:
//
// - first character is alphabetic [a-zA-Z]
@@ -132,4 +156,4 @@ func (t *T) Unmarshal(d []byte) (r []byte, err error) {
}
}
return
}
}

View File

@@ -1,56 +1,53 @@
package tags
import (
"bytes"
"fmt"
"protocol.realy.lol/pkg/tag"
)
const Sentinel = "tags:\n"
var SentinelBytes = []byte(Sentinel)
type tags []*tag.T
type T struct{ tags }
func New(v ...*tag.T) *T { return &T{tags: v} }
func (t *T) Marshal(dst []byte) (result []byte, err error) {
result = dst
result = append(result, Sentinel...)
for _, tt := range t.tags {
if result, err = tt.Marshal(result); chk.E(err) {
return
func (t *T) Marshal(dst []byte) (r []byte, err error) {
r = dst
if t != nil {
for _, tt := range t.tags {
if r, err = tt.Marshal(r); chk.E(err) {
return
}
}
}
result = append(result, '\n')
return
}
func (t *T) Unmarshal(data []byte) (rem []byte, err error) {
if len(data) < len(Sentinel) {
err = fmt.Errorf("bytes too short to contain tags")
return
}
var dat []byte
if bytes.Equal(data[:len(Sentinel)], SentinelBytes) {
dat = data[len(Sentinel):]
}
if len(dat) < 1 {
return
}
for len(dat) > 0 {
if len(dat) == 1 && dat[0] == '\n' {
break
}
// log.I.S(dat)
tt := new(tag.T)
if dat, err = tt.Unmarshal(dat); chk.E(err) {
return
}
t.tags = append(t.tags, tt)
}
// todo: update for the lack of start/end markers
// if len(data) < len(Sentinel) {
// err = fmt.Errorf("bytes too short to contain tags")
// return
// }
// var d []byte
// if bytes.Equal(data[:len(Sentinel)], SentinelBytes) {
// d = data[len(Sentinel):]
// }
// l := decimal.New(0)
// if d, err = l.Unmarshal(d); chk.E(err) {
// return
// }
// // and then there must be a newline
// if d[0] != '\n' {
// err = errorf.E("must be newline after content:<length>:\n%n", d)
// return
// }
// d = d[1:]
// for range l.N {
// tt := new(tag.T)
// if d, err = tt.Unmarshal(d); chk.E(err) {
// return
// }
// t.tags = append(t.tags, tt)
// }
return
}

View File

@@ -1,7 +1,6 @@
package tags
import (
"bytes"
"testing"
"protocol.realy.lol/pkg/tag"
@@ -9,9 +8,9 @@ import (
func TestT_Marshal_Unmarshal(t *testing.T) {
var tegs = [][]string{
{"reply", "e:l_T9Of4ru-PLGUxxvw3SfZH0e6XW11VYy8ZSgbcsD9Y", "realy.example.com/repo1"},
{"root", "e:l_T9Of4ru-PLGUxxvw3SfZH0e6XW11VYy8ZSgbcsD9Y", "realy.example.com/repo2"},
{"mention", "p:JMkZVnu9QFplR4F_KrWX-3chQsklXZq_5I6eYcXfz1Q", "realy.example.com/repo3"},
{"reply", "l_T9Of4ru-PLGUxxvw3SfZH0e6XW11VYy8ZSgbcsD9Y", "realy.example.com/repo1"},
{"root", "l_T9Of4ru-PLGUxxvw3SfZH0e6XW11VYy8ZSgbcsD9Y", "realy.example.com/repo2"},
{"mention", "JMkZVnu9QFplR4F_KrWX-3chQsklXZq_5I6eYcXfz1Q", "realy.example.com/repo3"},
}
var err error
var tgs []*tag.T
@@ -27,19 +26,22 @@ func TestT_Marshal_Unmarshal(t *testing.T) {
if m1, err = t1.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
t2 := new(T)
var rem []byte
if rem, err = t2.Unmarshal(m1); chk.E(err) {
t.Fatal(err)
}
if len(rem) > 0 {
t.Fatalf("%s", rem)
}
var m2 []byte
if m2, err = t2.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
if !bytes.Equal(m1, m2) {
t.Fatalf("not equal:\n%s\n%s", m1, m2)
}
_ = m1
// todo: unmarshal not currently working
// t2 := new(T)
// var rem []byte
// if rem, err = t2.Unmarshal(m1); chk.E(err) {
// t.Fatal(err)
// }
// if len(rem) > 0 {
// t.Fatalf("%s", rem)
// }
// var m2 []byte
// if m2, err = t2.Marshal(nil); chk.E(err) {
// t.Fatal(err)
// }
// if !bytes.Equal(m1, m2) {
// log.I.S(m1, m2)
// t.Fatalf("not equal:\n%s\n%s", m1, m2)
// }
}

View File

@@ -18,7 +18,7 @@ func (t *T) Marshal(d []byte) (r []byte, err error) {
if t == nil {
return
}
r = append(append(d, t.t...), '\n')
r = append(d, t.t...)
return
}
@@ -39,11 +39,11 @@ func (t *T) Unmarshal(d []byte) (r []byte, err error) {
// write read data up to the newline and return the remainder after
// the newline.
t.t = r[:i]
r = r[i+1:]
r = r[i:]
return
}
}
// a T must end with a newline or an io.EOF is returned.
err = io.EOF
return
}
}

View File

@@ -2,6 +2,8 @@ package types
import (
"testing"
"protocol.realy.lol/pkg/separator"
)
func TestT_Marshal_Unmarshal(t *testing.T) {
@@ -11,6 +13,8 @@ func TestT_Marshal_Unmarshal(t *testing.T) {
if res, err = typ.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
res = separator.Add(res)
log.I.S(res)
t2 := new(T)
var rem []byte
if rem, err = t2.Unmarshal(res); chk.E(err) {
@@ -22,4 +26,4 @@ func TestT_Marshal_Unmarshal(t *testing.T) {
if !typ.Equal(t2) {
t.Fatal("types.T did not encode/decode faithfully")
}
}
}

View File

@@ -27,7 +27,7 @@ func (u *U) Equal(u2 *U) bool { return bytes.Equal(u.uu, u2.uu) }
// Marshal a URL, use New to ensure it is valid beforehand. Appends a terminal
// newline.
func (u *U) Marshal(dst []byte) (result []byte, err error) {
result = append(append(dst, u.uu...), '\n')
result = append(dst, u.uu...)
return
}
@@ -46,4 +46,4 @@ func (u *U) Unmarshal(data []byte) (rem []byte, err error) {
return
}
return
}
}

View File

@@ -2,6 +2,8 @@ package url
import (
"testing"
"protocol.realy.lol/pkg/separator"
)
func TestU_Marshal_Unmarshal(t *testing.T) {
@@ -15,6 +17,7 @@ func TestU_Marshal_Unmarshal(t *testing.T) {
if m1, err = u1.Marshal(nil); chk.E(err) {
t.Fatal(err)
}
m1 = separator.Add(m1)
u2 := new(U)
var rem []byte
if rem, err = u2.Unmarshal(m1); chk.E(err) {
@@ -26,4 +29,4 @@ func TestU_Marshal_Unmarshal(t *testing.T) {
if !u2.Equal(u1) {
t.Fatalf("u1 should be equal to u2: '%s' != '%s'", u1, u2)
}
}
}

View File

@@ -1,5 +1,11 @@
= REALY Protocol
:toc:
:important-caption: 🔥
:note-caption: 🗩
:tip-caption: 💡
:caution-caption: ⚠
:table-caption: 🔍
:example-caption: 🥚
image:https://img.shields.io/badge/godoc-documentation-blue.svg[Documentation,link=https://pkg.go.dev/protocol.realy.lol]
image:https://img.shields.io/badge/matrix-chat-green.svg[matrix chat,link=https://matrix.to/#/#realy-general:matrix.org]
@@ -77,12 +83,9 @@ To save space and eliminate the need for ugly `=` padding characters, we invoke
In this case, it is used for IDs and pubkeys (32 bytes payload each, 43 characters base64 raw URL encoded) and signatures (64 bytes payload, 86 characters base64 raw URL encoded) - the further benefit here is the exact same string can be used in HTTP GET parameters `?key=value&...` context.
The standard `=` padding would break this usage as well.
For ease of human usage, also, it is recommended when the value is printed in plain text that it be on its own line so triple click catches all of it including the normally word-wise separated `-` hyphen/minus character, as follows:
For those who can't find a "raw" codec for base64, the 32 byte length has 1 `=` pad suffix and the 64 byte length has 2: `==` and this can be trimmed off and added back to conform to this requirement.
CF4I5dXYPZ_lu2pYRjey1QMDmgNJEyT-MM8Vvj6EnZM
For those who can't find a "raw" codec for base64, the 32 byte length has 1`=` pad suffix and the 64 byte length has 2: `==` and this can be trimmed off and added back to conform to this requirement.
Due to the fact that potentially there can be hundreds if not thousands of these in event content and tag fields the benefit can be quite great, as well as the benefit of being able to use these codes also in URL parameter values.
Due to the fact that potentially there can be hundreds if not thousands of these in event content and tag fields the benefit can be quite great, as well as the benefit of being able to use these codes also in URL parameter values - 43 bytes is not so much more than 32 binary bytes and because it is an even number base it is also cheaper to decode.
=== HTTP for Request/Response, Websockets for Push and Subscriptions
@@ -144,6 +147,8 @@ The signing key does not have to be identifying, but it serves as a HMAC for the
Thus access control becomes simple, and privacy also equally simple if the relay is public access to read, the client should default to one-shot keys for each request.
=== Authentication Message Format
Authenticating messages, for simplicity, is a simple message suffix.
.Authenticated Message Encoding
@@ -161,10 +166,18 @@ For simplicity, the signature is on a separate line, just as it is in the event
For reasons of security, a relay should not allow a time skew in the timestamp of more than 15 seconds.
The signature is upon the Blake 2b message hash of everything up to the semicolon preceding it, and only relates to the HTTP POST payload, not including the header.
The signature is upon the Blake 2b message hash of everything up to and including the newline preceding it, and only relates to the HTTP POST payload, not including the header.
Even subscription messages should be signed the same way, to avoid needing a secondary protocol. "open" relays that have no access control (which is retarded, but just to be complete) must still require this authentication message, but simply the client can use one-shot keys to sign with, as it also serves as a HMAC to validate the consistency of the request data, since it is based on the hash.
IMPORTANT: One shot keys for requests and publications is recommended especially for the case of users of Tor accessing relays, as this ensures traffic that emerges from the same exit or comes to the same hidden service looks the same. However, it should be also pointed out that a client is likely to query for one specific pubkey on a fairly regular basis which should be considered with respect to triggering the use of a new path in the tor connection (or other anonymizing protocol).
== RESTful APIs
HTTP conveniently allows for the use of paths, and a list of key/values for parameters where necessary, to enable a query to stay entirely within the context of a HTTP GET request.
As such, most queries can be identified simply by the path they refer to, instead of the messaging needing to additionally conatin this context.
== Capability Messages
Capabilities are an important concept for an open, extensible network protocol.
@@ -175,29 +188,31 @@ One of the biggest mistakes in the design of `nostr` is precisely in the blurrin
The `COUNT` and `AUTH` protocol method types have this property.
Their structure is defined by an implicit data point - the sender of the message, which means parsing the message isn't just identifying it but also reading context.
.Capability Request
[Options="header"]
|====
| Message | Description
| `capability\n` |
|====
Capability *must* be provided at the `/capability` path of the relay's web server path scheme.
=== Capability Response
The message that is sent back from a GET request at `/capability` should be as follows:
.Capability Response
[Options="header"]
|====
| Message | Description
| `capabilities\n` |
| `tags:\`| use the same syntax as in events
| `<protocol name>:vX.X.X;<URL of protocol spec>;<flag,...>\n` | Protocol name and version, the protocol spec URL.
| `<protocol name>:<URL of protocol spec>;vX.X.X;` | Protocol name and version, the protocol spec URL.
_The protocol name must be identical to the message header used in the protocol._
The version number should be a tag on the commit at the URL that matches the version specified.
`flag,...` for relevant flags on the protocol, for example `auth-required`, so for a `filter` this means "authenticate to read".
| `\n` |
| `<flag>[=<value>];...>` | `flag,...` for relevant flags on the protocol, for example `whitelisted`, so for a `filter` this means "authenticate to read as whitelisted user". All messages must be authenticated, but without this flag any user can use this protocol on this relay. The last flag ends with a newline, not a semicolon.
By maintaining a very small, method-based definition of protocols, complex flags are not required, in many cases, unnecessary
| `\n` | Each protocol spec ends with a newline.
|====
NOTE: Because lists of event Ids are relatively small, there should be no need for a limit on a filter with at least one parameter, even if it may yield a > 500kb message this is trivial considering the client can keep this and use it for a long time without needing to do that query again. _This is the reason for separating the filter and fulltext-search from the event retrieval syntax._
Protocol names should be defined in the same sense as a set of API calls - the details of how to write that exactly differs somewhat for different languages (and may involve checks not native to the language) but they should map to something along similar lines as a link:https://go.dev[_Go⌯_] `interface{}`
The protocol name is a shortcut and convenience, but should make automatic decisions by clients regarding a capability set simple.
@@ -212,29 +227,50 @@ As per implementation, each capability should be part of a registered list of me
[options="header,footer"]
|====
| Message | Description
| `<type name>\n` | can be anything, hierarchic names like `note/html` `note/md` are possible, or `type.subtype` or whatever
| `<pubkey>\n` | encoded in URL-base64 with the padding single `=` elided
| `<unix second precision timestamp in decimal ascii>\n` |
| `tags:\n`| Tags are a zero or more length list of lines delimited by this header and a new line after the content
| `<type name>:` | can be anything, hierarchic names like `note/html` `note/md` are possible, or `type.subtype` or whatever
| `<pubkey>;` | encoded in URL-base64 with the padding single `=` elided
| `<unix second precision timestamp in decimal ascii>\n` | this ends the first line of the event format
2+^| tags follow, they end with `\ncontent:<length>`; the end of tags and beginning of content
| `key:value;extra;...\n` | zero or more line separated, fields cannot contain a semicolon, end with newline instead of semicolon, key lowercase alphanumeric, first alpha, no whitespace or symbols, only key and following `:` are mandatory
| `\n` | tags end with a double linebreak
| `content:\n` | literally this word on one line *directly* after the newline of the previous
| `content:<length>\n` | literally this word on one line *directly* after the newline of the previous, the length value refers to *after* the newline and the end of it MUST be a newline and then the signature
| `<content>\n` | any number of further line breaks, last line is signature, everything before signature line is part of the canonical hash
2+^| The canonical form is the above, creating the message hash that is generated with Blake 2b
| `<ed25519 signature encoded in URL-base64>\n` | this field would have two padding chars `==`, these should be elided before generating the encoding.
2+^| The canonical form is the above, creating the message hash that is generated with SHA256
| `sig:<BIP-340 secp256k1 schnorr signature encoded in unpadded URL-base64>\n` | this field would have two padding chars `==`, these should be elided before generating the encoding. The length is always 86 characters/bytes.
|====
==== Example
```
note/adoc:6iiJMRHgRA4SZcc7Jg-k8kD81tJQYpM1saUykC5YCDs;1740226569
event:V6zWuopmz3D7pWZyqTZOZtIHlq8LrLAToWNZ9wBbnLo;root
event:4g6hb5mpNXupjigkdYU_vim9rnmUhR_mibfkpPs5d2A;root
event:jjBUzkXZkD9vwmHqwsCzQP07o-npo-4F-ciA0pWrJr8;root
pubkey:DLAJqN-E2n1OLP1gXDnMk2lgra6qYGTULuIJk4KriCk
pubkey:j5L8SIYV3yQPhHkp4vbFSTUh4kEbeL9SfZM8CGk5lMs
hashtag:Megan Boswell
hashtag:#AEWGrandSlam
hashtag:2024 BMW
hashtag:Censure
content:449
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
sig:OX5r0PdsC6p1dXf1Jr225O5bDupLGA-ZGKKxC59GOtqMPXfW9HZQPhURMe24WdrciWwJ0j7R7WWnuS32xFUyjA
```
=== Use in Data Storage
The encoding is already suitable for encoding to a database, it is optional to use a somewhat more compact binary encoding, especially if the database has good compression like ZST, which will flatten tables of these values quite effectively.
The encoding is already suitable for encoding to a database, it is optional to use a somewhat more compact binary encoding, especially if the database has good compression like ZST, which will flatten tables of these values quite effectively, with little overhead cost for lowered complexity.
=== Tags
Event ID hashes will be encoded in URL-base64 where used in tags or mentioned in content with the prefix `e:`.
Public keys must be prefixed with `p:` Tag keys should be intelligible words and a specification for their structure should be defined by users of them and shared with other REALY devs.
Tags are simply a list of `<key>:<field>[;<field>]...\n` and the terminator is the sentinel `\ncontent:<length>\n`
NOTE: Indexing tag keys should be done with a truncated Blake2b hash cut at 8 bytes in the event store, keys should be short and thus the chances of collisions are practically zero.
Blake2b is required so it is a good choice to use.
Common tags would include `event` and `pubkey` and `hashtag` - these are guidelines, the specifics of tag content and syntax is tied to the type, the first string at the top of the event, as described above.
== Protocols
@@ -242,7 +278,7 @@ Every REALY protocol should be simple and precise, and use HTTP for request/resp
The list of protocols below can be expanded to add new categories. The design should be as general as possible for each to isolate the application features from the relay processing cleanly.
=== Publication
== Publication Protocols
=== store, update and relay
@@ -263,21 +299,21 @@ The use of specific different types of store requests eliminates the complexity
A relay can also only allow one of these, such as a pure relay, which only accepts `relay` requests but neither `store` nor `replace`, or any combination of these.
The available API calls should be listed in the `capability` response
An event is then acknowledged to be stored or rejected with a message `ok:<true/false>;<Event Id>;<reason type>:human readable part` where the reason type is one of a set of common types to indicate the reason for the false
An event is then acknowledged to be stored or rejected with a message `ok:<true/false>;<Event Id>;<reason type>:human readable part` where the reason type is one of a set of common types to indicate the reason for the false.
Events that are returned have the `<subscription Id>:<Event Id>\n` as the first line, and then the event in the format described above afterwards.
There is four basic types of queries in REALY, derived from the `nostr` design, but refined and separated into distinct, small API calls.
=== Queries
== Query Protocols
==== events
=== events
A key concept in REALY protocol is minimising the footprint of each API call.
Thus, a primary query type is the simple request for a list of events by their ID hash:
===== Request
==== Request
.events request
[options="header"]
@@ -294,7 +330,7 @@ Normally clients will gather a potentially longer list of events and then send E
The results are returned as a series as follows, for each item returned:
===== Response
==== Response
.events response
[options="header"]
@@ -304,11 +340,11 @@ The results are returned as a series as follows, for each item returned:
|`<event>\n`| the full event text as described previously
|====
==== filter
=== filter
A filter has one or more of the fields listed below, and headed with `filter`:
===== Request
==== Request
.filter request
[options="header"]
@@ -326,7 +362,7 @@ A filter has one or more of the fields listed below, and headed with `filter`:
The response message is simply a list of the matching events IDs, which are expected to be in reverse chronological order:
===== Response
==== Response
.filter response
[options="header"]
@@ -337,7 +373,7 @@ The response message is simply a list of the matching events IDs, which are expe
|`...` | ...any number of events further.
|====
==== subscribe
=== subscribe
`subscribe` means to request to be sent events that match a filter, from the moment the request is received. Mixing queries and subscriptions is a bad idea because it makes it difficult to specify the expected behaviour from a relay, or client. Thus, a subset of the `filter` is used. The subscription ends when the client sends `unsubscribe` message.
@@ -390,7 +426,7 @@ The `subscribe` query streams back results containing just the event ID hash, in
The client can then send an `events` query to actually fetch the data.
This enables collecting a list and indicating the count without consuming the bandwidth for it until the view is opened.
==== `fulltext` Query
=== fulltext
A fulltext query is just `fulltext:` followed by a series of space separated tokens if the event store has a full text index, terminated with a newline.
@@ -410,4 +446,4 @@ The response message is like as the `filter`, the actual fetching of events is a
|`response:fulltext\n`| each event is marked with his header, so `\nevent:` serves as a section marker
|`<event id>\n`| event id that matches the search terms
|`...` | any number of events further, sorted by relevance.
|====
|====