Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4862dcf898 | |||
| 64bd999cdd | |||
| ec19703727 | |||
| 91447950c1 |
@@ -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
5
go.mod
@@ -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
13
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
15
pkg/id/id.go
15
pkg/id/id.go
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
pkg/separator/separator.go
Normal file
10
pkg/separator/separator.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
17
pkg/tag/common.go
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
120
readme.adoc
120
readme.adoc
@@ -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.
|
||||
|====
|
||||
|====
|
||||
Reference in New Issue
Block a user