diff --git a/events/events.go b/events/events.go index e4e74b9..7980fe5 100644 --- a/events/events.go +++ b/events/events.go @@ -2,59 +2,165 @@ package events import ( "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" "fmt" + "log" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/net/websocket" ) type Event struct { - ID string `json:"id"` - PubKey string `json:"pubkey"` - CreatedAt int64 `json:"created_at"` - Kind int `json:"kind"` - Tags []string `json:"tags"` - Content string `json:"content"` - Sig string `json:"sig"` + ID string `json:"id"` + PubKey string `json:"pubkey"` + CreatedAt int64 `json:"created_at"` + Kind int `json:"kind"` + Tags [][]string `json:"tags"` + Content string `json:"content"` + Sig string `json:"sig"` } -var eventKind0Collection *mongo.Collection -var eventKind1Collection *mongo.Collection +var collections = make(map[int]*mongo.Collection) -func InitCollections(client *mongo.Client, eventKind0, eventKind1 string) { - eventKind0Collection = client.Database("grain").Collection(eventKind0) - eventKind1Collection = client.Database("grain").Collection(eventKind1) +func InitCollections(client *mongo.Client, kinds ...int) { + for _, kind := range kinds { + collectionName := fmt.Sprintf("event-kind%d", kind) + collections[kind] = client.Database("grain").Collection(collectionName) + indexModel := mongo.IndexModel{ + Keys: bson.D{{Key: "id", Value: 1}}, + Options: options.Index().SetUnique(true), + } + _, err := collections[kind].Indexes().CreateOne(context.TODO(), indexModel) + if err != nil { + fmt.Printf("Failed to create index on %s: %v\n", collectionName, err) + } + } +} +func GetCollection(kind int, client *mongo.Client) *mongo.Collection { + if collection, exists := collections[kind]; exists { + return collection + } + collectionName := fmt.Sprintf("event-kind%d", kind) + collection := client.Database("grain").Collection(collectionName) + collections[kind] = collection indexModel := mongo.IndexModel{ Keys: bson.D{{Key: "id", Value: 1}}, Options: options.Index().SetUnique(true), } - _, err := eventKind0Collection.Indexes().CreateOne(context.TODO(), indexModel) + _, err := collection.Indexes().CreateOne(context.TODO(), indexModel) if err != nil { - fmt.Println("Failed to create index on event-kind0: ", err) - } - _, err = eventKind1Collection.Indexes().CreateOne(context.TODO(), indexModel) - if err != nil { - fmt.Println("Failed to create index on event-kind1: ", err) + fmt.Printf("Failed to create index on %s: %v\n", collectionName, err) } + return collection } -func HandleEvent(ctx context.Context, evt Event) error { +func HandleEvent(ctx context.Context, evt Event, client *mongo.Client, ws *websocket.Conn) { + if !ValidateEvent(evt) { + sendOKResponse(ws, evt.ID, false, "invalid: signature verification failed") + return + } + + collection := GetCollection(evt.Kind, client) + + var err error switch evt.Kind { case 0: - return HandleEventKind0(ctx, evt, eventKind0Collection) + err = HandleEventKind0(ctx, evt, collection) case 1: - return HandleEventKind1(ctx, evt, eventKind1Collection) + err = HandleEventKind1(ctx, evt, collection) default: - fmt.Println("Unknown event kind:", evt.Kind) - return fmt.Errorf("unknown event kind: %d", evt.Kind) + err = HandleDefaultEvent(ctx, evt, collection) } + + if err != nil { + sendOKResponse(ws, evt.ID, false, fmt.Sprintf("error: %v", err)) + return + } + + sendOKResponse(ws, evt.ID, true, "") } -func GetCollections() map[string]*mongo.Collection { - return map[string]*mongo.Collection{ - "eventKind0": eventKind0Collection, - "eventKind1": eventKind1Collection, - } +func sendOKResponse(ws *websocket.Conn, eventID string, status bool, message string) { + response := []interface{}{"OK", eventID, status, message} + responseBytes, _ := json.Marshal(response) + websocket.Message.Send(ws, string(responseBytes)) +} + +func SerializeEvent(evt Event) []byte { + eventData := []interface{}{ + 0, + evt.PubKey, + evt.CreatedAt, + evt.Kind, + evt.Tags, + evt.Content, + } + serializedEvent, _ := json.Marshal(eventData) + return serializedEvent +} + + +func ValidateEvent(evt Event) bool { + serializedEvent := SerializeEvent(evt) + hash := sha256.Sum256(serializedEvent) + eventID := hex.EncodeToString(hash[:]) + if eventID != evt.ID { + log.Printf("Invalid ID: expected %s, got %s\n", eventID, evt.ID) + return false + } + + sigBytes, err := hex.DecodeString(evt.Sig) + if err != nil { + log.Printf("Error decoding signature: %v\n", err) + return false + } + + sig, err := schnorr.ParseSignature(sigBytes) + if err != nil { + log.Printf("Error parsing signature: %v\n", err) + return false + } + + pubKeyBytes, err := hex.DecodeString(evt.PubKey) + if err != nil { + log.Printf("Error decoding public key: %v\n", err) + return false + } + + var pubKey *btcec.PublicKey + if len(pubKeyBytes) == 32 { + // Handle 32-byte public key (x-coordinate only) + pubKey, err = btcec.ParsePubKey(append([]byte{0x02}, pubKeyBytes...)) + } else { + // Handle standard compressed or uncompressed public key + pubKey, err = btcec.ParsePubKey(pubKeyBytes) + } + if err != nil { + log.Printf("Error parsing public key: %v\n", err) + return false + } + + verified := sig.Verify(hash[:], pubKey) + if !verified { + log.Printf("Signature verification failed for event ID: %s\n", evt.ID) + } + + return verified +} + +func HandleDefaultEvent(ctx context.Context, evt Event, collection *mongo.Collection) error { + _, err := collection.InsertOne(ctx, evt) + if err != nil { + return fmt.Errorf("Error inserting default event into MongoDB: %v", err) + } + + fmt.Println("Inserted default event into MongoDB:", evt.ID) + return nil } diff --git a/events/kind0.go b/events/kind0.go index 62f5e59..6db5e3e 100644 --- a/events/kind0.go +++ b/events/kind0.go @@ -10,11 +10,6 @@ import ( ) func HandleEventKind0(ctx context.Context, evt Event, collection *mongo.Collection) error { - // Perform specific validation for event kind 0 - if !isValidEventKind0(evt) { - return fmt.Errorf("validation failed for event kind 0: %s", evt.ID) - } - // Replace the existing event if it has the same pubkey filter := bson.M{"pubkey": evt.PubKey} update := bson.M{ @@ -28,21 +23,12 @@ func HandleEventKind0(ctx context.Context, evt Event, collection *mongo.Collecti }, } - options := options.Update().SetUpsert(true) // Insert if not found - _, err := collection.UpdateOne(ctx, filter, update, options) + opts := options.Update().SetUpsert(true) // Insert if not found + _, err := collection.UpdateOne(ctx, filter, update, opts) if err != nil { - fmt.Println("Error updating/inserting event kind 0 into MongoDB:", err) - return err + return fmt.Errorf("Error updating/inserting event kind 0 into MongoDB: %v", err) } fmt.Println("Upserted event kind 0 into MongoDB:", evt.ID) return nil } - -func isValidEventKind0(evt Event) bool { - // Placeholder for actual validation logic - if evt.Content == "" { - return false - } - return true -} diff --git a/events/kind1.go b/events/kind1.go index 331f209..d7bd206 100644 --- a/events/kind1.go +++ b/events/kind1.go @@ -8,26 +8,12 @@ import ( ) func HandleEventKind1(ctx context.Context, evt Event, collection *mongo.Collection) error { - // Perform specific validation for event kind 1 - if !isValidEventKind1(evt) { - return fmt.Errorf("validation failed for event kind 1: %s", evt.ID) - } - // Insert event into MongoDB _, err := collection.InsertOne(ctx, evt) if err != nil { - fmt.Println("Error inserting event into MongoDB:", err) - return err + return fmt.Errorf("Error inserting event into MongoDB: %v", err) } fmt.Println("Inserted event kind 1 into MongoDB:", evt.ID) return nil } - -func isValidEventKind1(evt Event) bool { - // Placeholder for actual validation logic - if evt.Content == "" { - return false - } - return true -} diff --git a/go.mod b/go.mod index d7d33d7..c24361a 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,16 @@ module grain go 1.22.2 require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.4 go.mongodb.org/mongo-driver v1.16.0 golang.org/x/net v0.27.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/montanaflynn/stats v0.7.1 // indirect @@ -18,5 +23,4 @@ require ( golang.org/x/crypto v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/text v0.16.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 9ecfdb3..a852686 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 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/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -50,6 +58,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go index e8f74fa..43c06be 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,7 @@ func main() { defer db.DisconnectDB(client) // Initialize collections - events.InitCollections(client, config.Collections.EventKind0, config.Collections.EventKind1) + events.InitCollections(client, 0, 1) // Initialize known kinds server.SetClient(client) diff --git a/readme.md b/readme.md index 8d3a883..5f03a52 100644 --- a/readme.md +++ b/readme.md @@ -1,63 +1,25 @@ -# GRAIN 🌾 WIP - - -TODO: -- Handling kind 0 and kind 1 EVENTS - - Validation Checking - - updating (replacing replacable notes) -- Handle REQs (requests) -- Handle CLOSE +# GRAIN 🌾 **Go Relay Archetecture for Implementing Nostr** -GRAIN is an open-source Nostr relay implementation written in Go. This project aims to provide a robust and efficient Nostr relay that supports the NIP-01 protocol, focusing on processing user metadata and text notes. +GRAIN is an open-source Nostr relay implementation written in Go. This project aims to provide a robust and efficient Nostr relay that currently supports the NIP-01 of the nostr protocol. ## Features -- **NIP-01 Protocol Support**: GRAIN fully supports the NIP-01 protocol for WebSocket communication. +- **NIP-01 Protocol Support**: GRAIN fully supports the NIP-01 for WebSocket communication. - **Event Processing**: Handles events of kind 0 (user metadata) and kind 1 (text note). - **MongoDB 🍃**: Utilizes MongoDB to store and manage events efficiently. - **Scalability**: Built with Go, ensuring high performance and scalability. - **Open Source**: Licensed under the MIT License, making it free to use and modify. -## Installation +## Configuration 🍃 -1. **Clone the repository**: +Configuration options can be set through environment variables or a configuration file. - ```sh - git clone https://github.com/oceanslim/grain.git - cd grain - ``` +There is an example config in this repo. Copy the example config to config.yml to get started -2. **Build the executable**: - - ```sh - go build -o grain.exe - ``` - - The `grain.exe` will be placed in a temporary directory within `...\appdata\local\temp\go-build` and subdirectories. - -## Usage - -To run the GRAIN relay: - -```sh -./grain.exe -``` - -### Configuration 🍃 - -Configuration options can be set through environment variables or a configuration file. Example configuration: - -```yml -server: - port: 8080 -database: - type: mongodb - connection_string: mongodb://localhost:27017 - database_name: grain -logging: - level: info +```bash +cp config.example.yml config.yml ``` ### WebSocket Endpoints @@ -65,33 +27,37 @@ logging: - Connect: / - Clients can connect to this endpoint to start a WebSocket session. - Publish Event: Send events of kind 0 (user metadata) or kind 1 (text note) to the relay. +### TODO + +- Handle more kinds +- create whitelist/blacklist functionality + for: + - valid nip05 domain + - pubkey + - npub + - kind int + - kind 1 wordlist +- Rate limit Events + ### Development To contribute to GRAIN, follow these steps: 1. Fork the repository. -2. Create a new branch: - -```sh -git checkout -b feature-branch -``` - -3. Make your changes. -4. Commit your changes: +2. Make your changes. +3. Commit your changes: ```sh git commit -m "Description of changes" ``` -5. Push to the branch: +4. Push to the repo: ```sh -git push origin feature-branch +git push ``` -6. Create a Pull Request. - -### Contributing +5. Create a Pull Request. ### License diff --git a/server/server.go b/server/server.go index 340a213..8bb6860 100644 --- a/server/server.go +++ b/server/server.go @@ -7,6 +7,8 @@ import ( "grain/events" "time" + "grain/utils" + "go.mongodb.org/mongo-driver/mongo" "golang.org/x/net/websocket" ) @@ -101,11 +103,7 @@ func handleEvent(ws *websocket.Conn, message []interface{}) { return } - err = events.HandleEvent(context.TODO(), evt) - if err != nil { - fmt.Println("Error handling event:", err) - return - } + events.HandleEvent(context.TODO(), evt, client, ws) fmt.Println("Event processed:", evt.ID) } @@ -131,13 +129,13 @@ func handleReq(ws *websocket.Conn, message []interface{}) { } var f Filter - f.IDs = toStringArray(filterData["ids"]) - f.Authors = toStringArray(filterData["authors"]) - f.Kinds = toIntArray(filterData["kinds"]) - f.Tags = toTagsMap(filterData["tags"]) - f.Since = toTime(filterData["since"]) - f.Until = toTime(filterData["until"]) - f.Limit = toInt(filterData["limit"]) + f.IDs = utils.ToStringArray(filterData["ids"]) + f.Authors = utils.ToStringArray(filterData["authors"]) + f.Kinds = utils.ToIntArray(filterData["kinds"]) + f.Tags = utils.ToTagsMap(filterData["tags"]) + f.Since = utils.ToTime(filterData["since"]) + f.Until = utils.ToTime(filterData["until"]) + f.Limit = utils.ToInt(filterData["limit"]) filters[i] = f } @@ -194,93 +192,4 @@ func handleClose(ws *websocket.Conn, message []interface{}) { fmt.Println("Error sending CLOSE message:", err) return } -} - -func toStringArray(i interface{}) []string { - if i == nil { - return nil - } - arr, ok := i.([]interface{}) - if !ok { - return nil - } - var result []string - for _, v := range arr { - str, ok := v.(string) - if ok { - result = append(result, str) - } - } - return result -} - -func toIntArray(i interface{}) []int { - if i == nil { - return nil - } - arr, ok := i.([]interface{}) - if !ok { - return nil - } - var result []int - for _, v := range arr { - num, ok := v.(float64) - if ok { - result = append(result, int(num)) - } - } - return result -} - -func toTagsMap(i interface{}) map[string][]string { - if i == nil { - return nil - } - tags, ok := i.(map[string]interface{}) - if !ok { - return nil - } - result := make(map[string][]string) - for k, v := range tags { - result[k] = toStringArray(v) - } - return result -} - -func toInt64(i interface{}) *int64 { - if i == nil { - return nil - } - num, ok := i.(float64) - if !ok { - return nil - } - val := int64(num) - return &val -} - -func toInt(i interface{}) *int { - if i == nil { - return nil - } - num, ok := i.(float64) - if !ok { - return nil - } - val := int(num) - return &val -} - -func toTime(data interface{}) *time.Time { - if data == nil { - return nil - } - // Ensure data is a float64 which MongoDB uses for numbers - timestamp, ok := data.(float64) - if !ok { - fmt.Println("Invalid timestamp format") - return nil - } - t := time.Unix(int64(timestamp), 0).UTC() - return &t -} +} \ No newline at end of file diff --git a/utils/decode.go b/utils/decode.go new file mode 100644 index 0000000..fb45f16 --- /dev/null +++ b/utils/decode.go @@ -0,0 +1,95 @@ +package utils + +import ( + "fmt" + "time" +) + +func ToStringArray(i interface{}) []string { + if i == nil { + return nil + } + arr, ok := i.([]interface{}) + if !ok { + return nil + } + var result []string + for _, v := range arr { + str, ok := v.(string) + if ok { + result = append(result, str) + } + } + return result +} + +func ToIntArray(i interface{}) []int { + if i == nil { + return nil + } + arr, ok := i.([]interface{}) + if !ok { + return nil + } + var result []int + for _, v := range arr { + num, ok := v.(float64) + if ok { + result = append(result, int(num)) + } + } + return result +} + +func ToTagsMap(i interface{}) map[string][]string { + if i == nil { + return nil + } + tags, ok := i.(map[string]interface{}) + if !ok { + return nil + } + result := make(map[string][]string) + for k, v := range tags { + result[k] = ToStringArray(v) + } + return result +} + +func ToInt64(i interface{}) *int64 { + if i == nil { + return nil + } + num, ok := i.(float64) + if !ok { + return nil + } + val := int64(num) + return &val +} + +func ToInt(i interface{}) *int { + if i == nil { + return nil + } + num, ok := i.(float64) + if !ok { + return nil + } + val := int(num) + return &val +} + +func ToTime(data interface{}) *time.Time { + if data == nil { + return nil + } + // Ensure data is a float64 which MongoDB uses for numbers + timestamp, ok := data.(float64) + if !ok { + fmt.Println("Invalid timestamp format") + return nil + } + t := time.Unix(int64(timestamp), 0).UTC() + return &t +}