diff --git a/config/parameters.go b/config/parameters.go index a3bd557..a8dac6d 100644 --- a/config/parameters.go +++ b/config/parameters.go @@ -89,18 +89,18 @@ func (c *Config) LoadParameters(db *database.Database) error { Icon: "https://raw.githubusercontent.com/dezh-tech/immortal/refs/heads/main/assets/images/immortal.png", WebsocketServer: &websocket.Config{ Limitation: &websocket.Limitation{ - MaxMessageLength: 8192, // Maximum length of a single message (in bytes or characters) - MaxSubscriptions: 20, // Maximum number of concurrent subscriptions a client can create - MaxFilters: 20, // Maximum number of filters a client can apply in a subscription - MaxSubidLength: 256, // Maximum length of a subscription identifier - MinPowDifficulty: 0, // Minimum proof-of-work difficulty for publishing events - AuthRequired: false, // Whether authentication is required for writes - PaymentRequired: false, // Whether payment is required to interact with the relay - RestrictedWrites: false, // Whether writes are restricted to authenticated or paying users - MaxEventTags: 1000, // Maximum number of tags allowed in a single event - MaxContentLength: 4096, // Maximum content length of an event (in bytes) - CreatedAtLowerLimit: 0, // Earliest timestamp allowed for event creation - CreatedAtUpperLimit: 0, // Latest timestamp allowed for event creation (0 for no limit) + MaxMessageLength: 8192, // Maximum length of a single message (in bytes or characters) + MaxSubscriptions: 20, // Maximum number of concurrent subscriptions a client can create + MaxFilters: 20, // Maximum number of filters a client can apply in a subscription + MaxSubidLength: 256, // Maximum length of a subscription identifier + MinPowDifficulty: 0, // Minimum proof-of-work difficulty for publishing events + AuthRequired: false, // Whether authentication is required for writes + PaymentRequired: false, // Whether payment is required to interact with the relay + RestrictedWrites: false, // Whether writes are restricted to authenticated or paying users + MaxEventTags: 1000, // Maximum number of tags allowed in a single event + MaxContentLength: 4096, // Maximum content length of an event (in bytes) + CreatedAtLowerLimit: 0, // Earliest timestamp allowed for event creation + CreatedAtUpperLimit: 2_147_483_647, // Latest timestamp allowed for event creation }, }, Handler: &handler.Config{ diff --git a/documents/NIPs.md b/documents/NIPs.md index 3e17350..8f6300d 100644 --- a/documents/NIPs.md +++ b/documents/NIPs.md @@ -5,7 +5,7 @@ The Immortal follows [NIPs](https://github.com/nostr-protocol/nips) and tries to - [X] **NIP-01**: Basic Protocol Flow Description - [ ] **NIP-09**: Event Deletion Request - [X] **NIP-11**: Relay Information Document -- [ ] **NIP-13**: Proof of Work +- [X] **NIP-13**: Proof of Work - [ ] **NIP-40**: Expiration Timestamp - [ ] **NIP-42**: Authentication of Clients to Relays - [ ] **NIP-50**: Search Capability diff --git a/go.mod b/go.mod index 7d515d3..9cb0790 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/mailru/easyjson v0.7.7 github.com/prometheus/client_golang v1.20.4 - github.com/redis/go-redis/v9 v9.6.2 + github.com/redis/go-redis/v9 v9.7.0 github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.18.0 go.mongodb.org/mongo-driver v1.17.1 diff --git a/go.sum b/go.sum index a16db85..cd02ded 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJN github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk= -github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/server/websocket/config.go b/server/websocket/config.go index 2022394..a461518 100644 --- a/server/websocket/config.go +++ b/server/websocket/config.go @@ -1,18 +1,18 @@ package websocket type Limitation struct { - MaxMessageLength int `bson:"max_message_length" json:"max_message_length"` // todo. - MaxSubscriptions int `bson:"max_subscriptions" json:"max_subscriptions"` - MaxFilters int `bson:"max_filters" json:"max_filters"` - MaxSubidLength int `bson:"max_subid_length" json:"max_subid_length"` - MinPowDifficulty int `bson:"min_pow_difficulty" json:"min_pow_difficulty"` // todo. - AuthRequired bool `bson:"auth_required" json:"auth_required"` // todo. - PaymentRequired bool `bson:"payment_required" json:"payment_required"` // todo. - RestrictedWrites bool `bson:"restricted_writes" json:"restricted_writes"` // todo. - MaxEventTags int `bson:"max_event_tags" json:"max_event_tags"` // todo. - MaxContentLength int `bson:"max_content_length" json:"max_content_length"` - CreatedAtLowerLimit int `bson:"created_at_lower_limit" json:"created_at_lower_limit"` // todo. - CreatedAtUpperLimit int `bson:"created_at_upper_limit" json:"created_at_upper_limit"` // todo. + MaxMessageLength int `bson:"max_message_length" json:"max_message_length"` // todo?. + MaxSubscriptions int `bson:"max_subscriptions" json:"max_subscriptions"` + MaxFilters int `bson:"max_filters" json:"max_filters"` + MaxSubidLength int `bson:"max_subid_length" json:"max_subid_length"` + MinPowDifficulty int `bson:"min_pow_difficulty" json:"min_pow_difficulty"` + AuthRequired bool `bson:"auth_required" json:"auth_required"` // todo. + PaymentRequired bool `bson:"payment_required" json:"payment_required"` // todo. + RestrictedWrites bool `bson:"restricted_writes" json:"restricted_writes"` // todo. + MaxEventTags int `bson:"max_event_tags" json:"max_event_tags"` + MaxContentLength int `bson:"max_content_length" json:"max_content_length"` + CreatedAtLowerLimit int64 `bson:"created_at_lower_limit" json:"created_at_lower_limit"` + CreatedAtUpperLimit int64 `bson:"created_at_upper_limit" json:"created_at_upper_limit"` } type Config struct { diff --git a/server/websocket/server.go b/server/websocket/server.go index 3b99ac4..6836f5e 100644 --- a/server/websocket/server.go +++ b/server/websocket/server.go @@ -266,19 +266,6 @@ func (s *Server) handleEvent(conn *websocket.Conn, m message.Message) { return } - if len(msg.Event.Content) > s.config.Limitation.MaxContentLength { - okm := message.MakeOK(false, - "", - fmt.Sprintf("error: max limit of message length is %d", s.config.Limitation.MaxContentLength), - ) - - _ = conn.WriteMessage(1, okm) - - status = limitsFail - - return - } - eID := msg.Event.GetRawID() qCtx, cancel := context.WithTimeout(context.Background(), s.redis.QueryTimeout) @@ -309,6 +296,60 @@ func (s *Server) handleEvent(conn *websocket.Conn, m message.Message) { return } + if len(msg.Event.Content) > s.config.Limitation.MaxContentLength { + okm := message.MakeOK(false, + "", + fmt.Sprintf("error: max limit of content length is %d", s.config.Limitation.MaxContentLength), + ) + + _ = conn.WriteMessage(1, okm) + + status = limitsFail + + return + } + + if msg.Event.Difficulty() < s.config.Limitation.MinPowDifficulty { + okm := message.MakeOK(false, + "", + fmt.Sprintf("error: min pow required is %d", s.config.Limitation.MinPowDifficulty), + ) + + _ = conn.WriteMessage(1, okm) + + status = limitsFail + + return + } + + if len(msg.Event.Tags) < s.config.Limitation.MaxEventTags { + okm := message.MakeOK(false, + "", + fmt.Sprintf("error: max limit of tags count is %d", s.config.Limitation.MaxEventTags), + ) + + _ = conn.WriteMessage(1, okm) + + status = limitsFail + + return + } + + if msg.Event.CreatedAt < s.config.Limitation.CreatedAtLowerLimit || + msg.Event.CreatedAt > s.config.Limitation.CreatedAtUpperLimit { + okm := message.MakeOK(false, + "", + fmt.Sprintf("error: created at must be as least %d and at most %d", + s.config.Limitation.CreatedAtLowerLimit, s.config.Limitation.CreatedAtUpperLimit), + ) + + _ = conn.WriteMessage(1, okm) + + status = limitsFail + + return + } + if !msg.Event.Kind.IsEphemeral() { err := s.handlers.HandleEvent(msg.Event) if err != nil { diff --git a/types/event/nip13.go b/types/event/nip13.go new file mode 100644 index 0000000..3e76635 --- /dev/null +++ b/types/event/nip13.go @@ -0,0 +1,30 @@ +package event + +import ( + "encoding/hex" + "math/bits" +) + +// Difficulty returns the leading zeros of event id in base-2. +func (e *Event) Difficulty() int { + var zeros int + var b [1]byte + + for i := 0; i < 64; i += 2 { + if e.ID[i:i+2] == "00" { + zeros += 8 + + continue + } + + if _, err := hex.Decode(b[:], []byte{e.ID[i], e.ID[i+1]}); err != nil { + return -1 + } + + zeros += bits.LeadingZeros8(b[0]) + + break + } + + return zeros +} diff --git a/types/event/nip13_test.go b/types/event/nip13_test.go new file mode 100644 index 0000000..5b12273 --- /dev/null +++ b/types/event/nip13_test.go @@ -0,0 +1,27 @@ +package event_test + +import ( + "testing" + + "github.com/dezh-tech/immortal/types/event" + "github.com/stretchr/testify/assert" +) + +func TestDifficulty(t *testing.T) { + testCases := []struct { + result int + id string + }{ + {36, "000000000e9d97a1ab09fc381030b346cdd7a142ad57e6df0b46dc9bef6c7e2d"}, + {22, "0000024d38993bae75a61e82710842305fac9cda280f541476c31426c42ca81a"}, + {0, "f2775f4eeaa0aa45f66440490b45d6aede8d1ceb7ac443e6328763db5ce8d6e3"}, + {7, "010b807e82a1417588be0bcd7606b2aae4163a365afe1f6c97404b17fc56d30b"}, + {21, "000004f20d022b65dd961cbbdc157347dbd37ca375899fe38b40d174cccd8ee3"}, + {18, "00003db72b8385511ef2c1dd5fb3a43988269c9d7f51986f03c1aecd675dc506"}, + } + + for _, tc := range testCases { + e := event.Event{ID: tc.id} + assert.Equal(t, tc.result, e.Difficulty()) + } +}