diff --git a/README.md b/README.md index 8ed8f071..70d76454 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ in Go. ## Build & Run -From the root directory run: +From the `impl` directory run: ``` docker build . -t did-dht -f build/Dockerfile diff --git a/impl/README.md b/impl/README.md new file mode 100644 index 00000000..37d9be56 --- /dev/null +++ b/impl/README.md @@ -0,0 +1,41 @@ +# Server Implementation + +- Heavily a work-in-progress +- Designed to be run as a single instance + +## Config + +# TOML Config File + +Config is managed using a [TOML](https://toml.io/en/) [file](../../config/dev.toml). There are sets of configuration values for the server +(e.g. which port to listen on), the services (e.g. which database to use), and each service. + +Each service may define specific configuration, such as which DID methods are enabled for the DID service. + +A full config example is [provided here](../../config/kitchensink.toml). + +## Usage + +How it works: + +1. On startup the service loads default values into the `ServiceConfig` +2. Checks for a TOML config file: + - If exists, load toml file + - If does not exist, it uses a default config defined in the code inline +3. Loads the `config/.env` file and adds the env variables defined in this file to the final `ServiceConfig` + +## Build & Run + +Run: + +``` +docker build . -t did-dht -f build/Dockerfile +``` + +and then + +``` +docker run -p8305:8305 did-dht +``` + + diff --git a/impl/cmd/cli/identity.go b/impl/cmd/cli/identity.go index 081b5c8c..d6ad6811 100644 --- a/impl/cmd/cli/identity.go +++ b/impl/cmd/cli/identity.go @@ -102,7 +102,7 @@ var identityAddCmd = &cobra.Command{ Answer: rrds, } // generate put request - putReq, err := dht.CreatePKARRPublishRequest(pubKey, privKey, msg) + putReq, err := dht.CreatePKARRPublishRequest(privKey, msg) if err != nil { logrus.WithError(err).Error("failed to create put request") return err @@ -133,8 +133,8 @@ var identityAddCmd = &cobra.Command{ var identityGetCmd = &cobra.Command{ Use: "get", - Short: "Get an identity", - Long: `Get an identity by its id.`, + Short: "GetRecord an identity", + Long: `GetRecord an identity by its id.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { id := args[0] diff --git a/impl/config/config.go b/impl/config/config.go index c2c5846e..7ff5f30a 100644 --- a/impl/config/config.go +++ b/impl/config/config.go @@ -36,14 +36,16 @@ func (e EnvironmentVariable) String() string { } type Config struct { - ServerConfig ServiceConfig `toml:"server"` - DHTConfig DHTServiceConfig `toml:"dht"` + ServerConfig ServerConfig `toml:"server"` + DHTConfig DHTServiceConfig `toml:"dht"` + PKARRConfig PKARRServiceConfig `toml:"pkarr"` } -type ServiceConfig struct { +type ServerConfig struct { Environment Environment `toml:"env"` APIHost string `toml:"api_host"` APIPort int `toml:"api_port"` + BaseURL string `toml:"base_url"` LogLocation string `toml:"log_location"` LogLevel string `toml:"log_level"` DBFile string `toml:"db_file"` @@ -53,17 +55,17 @@ type DHTServiceConfig struct { BootstrapPeers []string `toml:"bootstrap_peers"` } -type GossipServiceConfig struct { - // if set, the API will only accept signed messages - EnforceSignedMessages bool `toml:"enforce_signed_messages"` +type PKARRServiceConfig struct { + RepublishCRON string `toml:"republish_cron"` } func GetDefaultConfig() Config { return Config{ - ServerConfig: ServiceConfig{ + ServerConfig: ServerConfig{ Environment: EnvironmentDev, APIHost: "0.0.0.0", APIPort: 8305, + BaseURL: "http://localhost:8305", LogLocation: "log", LogLevel: "debug", DBFile: "diddht.db", @@ -71,6 +73,9 @@ func GetDefaultConfig() Config { DHTConfig: DHTServiceConfig{ BootstrapPeers: GetDefaultBootstrapPeers(), }, + PKARRConfig: PKARRServiceConfig{ + RepublishCRON: "0 */2 * * *", + }, } } diff --git a/impl/config/config.toml b/impl/config/config.toml index 1eab64be..f145c235 100644 --- a/impl/config/config.toml +++ b/impl/config/config.toml @@ -8,4 +8,7 @@ db_file = "diddht.db" [dht] bootstrap_peers = ["router.magnets.im:6881", "router.bittorrent.com:6881", "dht.transmissionbt.com:6881", - "router.utorrent.com:6881", "router.nuh.dev:6881"] \ No newline at end of file + "router.utorrent.com:6881", "router.nuh.dev:6881"] + +[pkarr] +republish_cron = "0 */2 * * *" # every 2 hours \ No newline at end of file diff --git a/impl/docs/docs.go b/impl/docs/docs.go index 948b5618..7b24e6b2 100644 --- a/impl/docs/docs.go +++ b/impl/docs/docs.go @@ -5,7 +5,7 @@ package docs import "github.com/swaggo/swag/v2" const docTemplate = `{ - "schemes": {{ marshal .Schemes }},"swagger":"2.0","info":{"description":"{{escape .Description}}","title":"{{.Title}}","contact":{"name":"TBD","url":"https://github.com/TBD54566975/did-dht-method/issues","email":"tbd-developer@squareup.com"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"{{.Version}}"},"host":"{{.Host}}","basePath":"{{.BasePath}}","paths":{"/health":{"get":{"description":"Health is a simple handler that always responds with a 200 OK","consumes":["application/json"],"produces":["application/json"],"tags":["Health"],"summary":"Health Check","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/pkg_server.GetHealthCheckResponse"}}}}},"/{id}":{"get":{"description":"Get a PKARR from the DHT","consumes":["application/octet-stream"],"produces":["application/octet-stream"],"tags":["Relay"],"summary":"Get a PKARR from the DHT","parameters":[{"type":"string","description":"ID to get","name":"id","in":"path","required":true}],"responses":{"200":{"description":"64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v.","schema":{"type":"array","items":{"type":"integer"}}},"400":{"description":"Bad request","schema":{"type":"string"}},"404":{"description":"Not found","schema":{"type":"string"}},"500":{"description":"Internal server error","schema":{"type":"string"}}}},"put":{"description":"Put a PKARR record into the DHT","consumes":["application/octet-stream"],"tags":["Relay"],"summary":"Put a PKARR record into the DHT","parameters":[{"type":"string","description":"ID to put","name":"id","in":"path","required":true},{"description":"64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v.","name":"request","in":"body","required":true,"schema":{"type":"array","items":{"type":"integer"}}}],"responses":{"200":{"description":"OK"},"400":{"description":"Bad request","schema":{"type":"string"}},"500":{"description":"Internal server error","schema":{"type":"string"}}}}}},"definitions":{"pkg_server.GetHealthCheckResponse":{"type":"object","properties":{"status":{"description":"Status is always equal to ` + "`" + `OK` + "`" + `.","type":"string"}}}}}` + "schemes": {{ marshal .Schemes }},"swagger":"2.0","info":{"description":"{{escape .Description}}","title":"{{.Title}}","contact":{"name":"TBD","url":"https://github.com/TBD54566975/did-dht-method/issues","email":"tbd-developer@squareup.com"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"version":"{{.Version}}"},"host":"{{.Host}}","basePath":"{{.BasePath}}","paths":{"/health":{"get":{"description":"Health is a simple handler that always responds with a 200 OK","consumes":["application/json"],"produces":["application/json"],"tags":["Health"],"summary":"Health Check","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/pkg_server.GetHealthCheckResponse"}}}}},"/{id}":{"get":{"description":"GetRecord a PKARR record from the DHT","consumes":["application/octet-stream"],"produces":["application/octet-stream"],"tags":["Relay"],"summary":"GetRecord a PKARR record from the DHT","parameters":[{"type":"string","description":"ID to get","name":"id","in":"path","required":true}],"responses":{"200":{"description":"64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v.","schema":{"type":"array","items":{"type":"integer"}}},"400":{"description":"Bad request","schema":{"type":"string"}},"404":{"description":"Not found","schema":{"type":"string"}},"500":{"description":"Internal server error","schema":{"type":"string"}}}},"put":{"description":"PutRecord a PKARR record into the DHT","consumes":["application/octet-stream"],"tags":["Relay"],"summary":"PutRecord a PKARR record into the DHT","parameters":[{"type":"string","description":"ID of the record to put","name":"id","in":"path","required":true},{"description":"64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v.","name":"request","in":"body","required":true,"schema":{"type":"array","items":{"type":"integer"}}}],"responses":{"200":{"description":"OK"},"400":{"description":"Bad request","schema":{"type":"string"}},"500":{"description":"Internal server error","schema":{"type":"string"}}}}}},"definitions":{"pkg_server.GetHealthCheckResponse":{"type":"object","properties":{"status":{"description":"Status is always equal to ` + "`" + `OK` + "`" + `.","type":"string"}}}}}` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ diff --git a/impl/docs/swagger.yaml b/impl/docs/swagger.yaml index 11000bca..b8a5db1b 100644 --- a/impl/docs/swagger.yaml +++ b/impl/docs/swagger.yaml @@ -22,7 +22,7 @@ paths: get: consumes: - application/octet-stream - description: Get a PKARR from the DHT + description: GetRecord a PKARR record from the DHT parameters: - description: ID to get in: path @@ -50,15 +50,15 @@ paths: description: Internal server error schema: type: string - summary: Get a PKARR from the DHT + summary: GetRecord a PKARR record from the DHT tags: - Relay put: consumes: - application/octet-stream - description: Put a PKARR record into the DHT + description: PutRecord a PKARR record into the DHT parameters: - - description: ID to put + - description: ID of the record to put in: path name: id required: true @@ -82,7 +82,7 @@ paths: description: Internal server error schema: type: string - summary: Put a PKARR record into the DHT + summary: PutRecord a PKARR record into the DHT tags: - Relay /health: diff --git a/impl/go.mod b/impl/go.mod index 2610d727..039da2e3 100644 --- a/impl/go.mod +++ b/impl/go.mod @@ -54,6 +54,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-co-op/gocron v1.35.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect @@ -98,6 +99,7 @@ require ( github.com/piprate/json-gold v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -112,6 +114,7 @@ require ( github.com/swaggo/swag v1.8.12 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.14.0 // indirect diff --git a/impl/go.sum b/impl/go.sum index 20dd084f..5177ffd1 100644 --- a/impl/go.sum +++ b/impl/go.sum @@ -181,6 +181,8 @@ github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-co-op/gocron v1.35.2 h1:lG3rdA9TqBBC/PtT2ukQqgLm6jEepnAzz3+OQetvPTE= +github.com/go-co-op/gocron v1.35.2/go.mod h1:NLi+bkm4rRSy1F8U7iacZOz0xPseMoIOnvabGoSe/no= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -454,9 +456,12 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= @@ -548,6 +553,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/impl/internal/dht/scheduler.go b/impl/internal/dht/scheduler.go new file mode 100644 index 00000000..10bb9fce --- /dev/null +++ b/impl/internal/dht/scheduler.go @@ -0,0 +1,45 @@ +package dht + +import ( + "time" + + "github.com/go-co-op/gocron" + "github.com/pkg/errors" +) + +// Scheduler runs crob jobs asynchronously, designed to just schedule one job +type Scheduler struct { + scheduler *gocron.Scheduler + job *gocron.Job +} + +// NewScheduler creates a new scheduler +func NewScheduler() Scheduler { + s := gocron.NewScheduler(time.UTC) + s.SingletonModeAll() + return Scheduler{scheduler: s} +} + +// Schedule schedules a job to run and starts it asynchronously +func (s *Scheduler) Schedule(_ string, job func()) error { + if s.job != nil { + return errors.New("job already scheduled") + } + j, err := s.scheduler.Cron("* * * * *").Do(job) + if err != nil { + return err + } + s.job = j + s.Start() + return nil +} + +// Start starts the scheduler +func (s *Scheduler) Start() { + s.scheduler.StartAsync() +} + +// Stop stops the scheduler +func (s *Scheduler) Stop() { + s.scheduler.Stop() +} diff --git a/impl/pkg/dht/dht.go b/impl/pkg/dht/dht.go index db96f2ab..d1f4b98a 100644 --- a/impl/pkg/dht/dht.go +++ b/impl/pkg/dht/dht.go @@ -8,7 +8,6 @@ import ( "github.com/anacrolix/dht/v2/bep44" "github.com/anacrolix/dht/v2/exts/getput" "github.com/anacrolix/torrent/types/infohash" - "github.com/sirupsen/logrus" dhtint "github.com/TBD54566975/did-dht-method/internal/dht" "github.com/TBD54566975/did-dht-method/internal/util" @@ -25,8 +24,7 @@ func NewDHT(bootstrapPeers []string) (*DHT, error) { c.StartingNodes = func() ([]dht.Addr, error) { return dht.ResolveHostPorts(bootstrapPeers) } s, err := dht.NewServer(c) if err != nil { - logrus.WithError(err).Error("failed to create dht server") - return nil, err + return nil, errutil.LoggingErrorMsg(err, "failed to create dht server") } return &DHT{Server: s}, nil } @@ -47,8 +45,7 @@ func (d *DHT) Put(ctx context.Context, request bep44.Put) (string, error) { func (d *DHT) Get(ctx context.Context, key string) (*getput.GetResult, error) { z32Decoded, err := util.Z32Decode(key) if err != nil { - logrus.WithError(err).Error("failed to decode key") - return nil, err + return nil, errutil.LoggingErrorMsg(err, "failed to decode key") } res, t, err := getput.Get(ctx, infohash.HashBytes(z32Decoded), d.Server, nil, nil) if err != nil { @@ -58,13 +55,12 @@ func (d *DHT) Get(ctx context.Context, key string) (*getput.GetResult, error) { } // GetFull returns the full BEP-44 result for the given key from the DHT, using our modified -// implementation of getput.Get. IT should only be used when it's needed to get the signature +// implementation of getput.Get. It should ONLY be used when it's needed to get the signature // data for a record. func (d *DHT) GetFull(ctx context.Context, key string) (*dhtint.FullGetResult, error) { z32Decoded, err := util.Z32Decode(key) if err != nil { - logrus.WithError(err).Error("failed to decode key") - return nil, err + return nil, errutil.LoggingErrorMsg(err, "failed to decode key") } res, t, err := dhtint.Get(ctx, infohash.HashBytes(z32Decoded), d.Server, nil, nil) if err != nil { diff --git a/impl/pkg/dht/pkarr.go b/impl/pkg/dht/pkarr.go index 3820172a..fddbf6c8 100644 --- a/impl/pkg/dht/pkarr.go +++ b/impl/pkg/dht/pkarr.go @@ -33,11 +33,12 @@ import ( // }, // } // } -func CreatePKARRPublishRequest(publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, msg dns.Msg) (*bep44.Put, error) { +func CreatePKARRPublishRequest(privateKey ed25519.PrivateKey, msg dns.Msg) (*bep44.Put, error) { packed, err := msg.Pack() if err != nil { return nil, util.LoggingErrorMsg(err, "failed to pack records") } + publicKey := privateKey.Public().(ed25519.PublicKey) put := &bep44.Put{ V: packed, K: (*[32]byte)(publicKey), diff --git a/impl/pkg/dht/pkarr_test.go b/impl/pkg/dht/pkarr_test.go index f9000648..62dfc9c4 100644 --- a/impl/pkg/dht/pkarr_test.go +++ b/impl/pkg/dht/pkarr_test.go @@ -2,7 +2,6 @@ package dht import ( "context" - "crypto/ed25519" "testing" "github.com/TBD54566975/ssi-sdk/crypto" @@ -22,7 +21,7 @@ func TestGetPutPKARRDHT(t *testing.T) { d, err := NewDHT(config.GetDefaultBootstrapPeers()) require.NoError(t, err) - pubKey, privKey, err := util.GenerateKeypair() + _, privKey, err := util.GenerateKeypair() require.NoError(t, err) txtRecord := dns.TXT{ @@ -44,7 +43,7 @@ func TestGetPutPKARRDHT(t *testing.T) { }, Answer: []dns.RR{&txtRecord}, } - put, err := CreatePKARRPublishRequest(pubKey, privKey, msg) + put, err := CreatePKARRPublishRequest(privKey, msg) require.NoError(t, err) id, err := d.Put(context.Background(), *put) @@ -105,8 +104,7 @@ func TestGetPutDIDDHT(t *testing.T) { didDocPacket, err := didID.ToDNSPacket(*doc) require.NoError(t, err) - key := privKey.Public().(ed25519.PublicKey) - putReq, err := CreatePKARRPublishRequest(key, privKey, *didDocPacket) + putReq, err := CreatePKARRPublishRequest(privKey, *didDocPacket) require.NoError(t, err) gotID, err := dht.Put(context.Background(), *putReq) diff --git a/impl/pkg/server/did.go b/impl/pkg/server/did.go deleted file mode 100644 index 7a574edf..00000000 --- a/impl/pkg/server/did.go +++ /dev/null @@ -1,142 +0,0 @@ -package server - -// const ( -// DIDParam = "id" -// ) -// -// type DIDDHTRouter struct { -// service *service.DIDService -// } -// -// func NewDIDDHTRouter(service *service.DIDService) (*DIDDHTRouter, error) { -// if service == nil { -// return nil, errors.New("service cannot be nil") -// } -// return &DIDDHTRouter{service: service}, nil -// } -// -// type PublishDIDRequest struct { -// } -// -// func (PublishDIDRequest) toServiceRequest() service.PublishDIDRequest { -// return service.PublishDIDRequest{} -// } -// -// // PublishDID godoc -// // -// // @Summary Publish a DID to the DHT -// // @Description Publishes a DID to the DHT -// // @Tags DID -// // @Accept json -// // @Produce json -// // @Param request body PublishDIDRequest true "Publish DID Request" -// // @Success 202 -// // @Failure 400 {string} string "Bad request" -// // @Failure 500 {string} string "Internal server error" -// // @Router /v1/did [put] -// func (r *DIDDHTRouter) PublishDID(c *gin.Context) { -// var request PublishDIDRequest -// if err := Decode(c.Request, &request); err != nil { -// LoggingRespondErrWithMsg(c, err, "invalid add dht record request", http.StatusBadRequest) -// return -// } -// -// if err := r.service.PublishDID(c, request.toServiceRequest()); err != nil { -// LoggingRespondErrWithMsg(c, err, "failed to publish record", http.StatusInternalServerError) -// return -// } -// -// Respond(c, nil, http.StatusAccepted) -// } -// -// type GetDIDResponse struct { -// DIDDocument did.Document `json:"did,omitempty"` -// } -// -// // GetDID godoc -// // -// // @Summary Read a DID record from the DHT -// // @Description Read a DID record from the DHT -// // @Tags DID -// // @Accept json -// // @Produce json -// // @Param id path string true "did to request" -// // @Success 200 {object} GetDIDResponse -// // @Failure 400 {string} string "Bad request" -// // @Failure 500 {string} string "Internal server error" -// // @Router /v1/did/{id} [get] -// func (r *DIDDHTRouter) GetDID(c *gin.Context) { -// did := GetParam(c, DIDParam) -// if did == nil || *did == "" { -// LoggingRespondErrMsg(c, "missing did param", http.StatusBadRequest) -// return -// } -// -// resp, err := r.service.GetDID(c, *did) -// if err != nil { -// LoggingRespondErrWithMsg(c, err, "failed to query", http.StatusInternalServerError) -// return -// } -// -// if resp == nil { -// LoggingRespondErrMsg(c, "did not found", http.StatusNotFound) -// return -// } -// -// Respond(c, GetDIDResponse{DIDDocument: *resp}, http.StatusOK) -// } -// -// type ListDIDsResponse struct { -// DIDDocuments []did.Document `json:"dids,omitempty"` -// } -// -// // ListDIDs godoc -// // -// // @Summary List all DIDs from the service -// // @Description List all DIDs from the service -// // @Tags DID -// // @Accept json -// // @Produce json -// // @Success 200 {array} ListDIDsResponse -// // @Failure 500 {string} string "Internal server error" -// // @Router /v1/did [get] -// func (r *DIDDHTRouter) ListDIDs(c *gin.Context) { -// resp, err := r.service.ListDIDs(c) -// if err != nil { -// LoggingRespondErrWithMsg(c, err, "failed to list DIDs", http.StatusInternalServerError) -// return -// } -// -// Respond(c, ListDIDsResponse{DIDDocuments: resp}, http.StatusOK) -// } -// -// type DeleteDIDRequest struct { -// } -// -// // DeleteDID godoc -// // -// // @Summary Remove a DID from the service -// // @Description Remove a DID from the service, which stops republishing it to the DHT -// // @Tags DID -// // @Accept json -// // @Produce json -// // @Param request body DeleteDIDRequest true "Delete DID Request" -// // @Success 200 -// // @Failure 400 {string} string "Bad request" -// // @Failure 500 {string} string "Internal server error" -// // @Router /v1/did [delete] -// func (r *DIDDHTRouter) DeleteDID(c *gin.Context) { -// // TODO(gabe): validate before removing record -// var request DeleteDIDRequest -// if err := Decode(c.Request, &request); err != nil { -// LoggingRespondErrWithMsg(c, err, "invalid remove dht record request", http.StatusBadRequest) -// return -// } -// -// if err := r.service.DeleteDID(c, ""); err != nil { -// LoggingRespondErrWithMsg(c, err, "failed to remove", http.StatusInternalServerError) -// return -// } -// -// Respond(c, nil, http.StatusOK) -// } diff --git a/impl/pkg/server/relay.go b/impl/pkg/server/pkarr.go similarity index 75% rename from impl/pkg/server/relay.go rename to impl/pkg/server/pkarr.go index 339e5253..98f1d293 100644 --- a/impl/pkg/server/relay.go +++ b/impl/pkg/server/pkarr.go @@ -12,21 +12,21 @@ import ( "github.com/TBD54566975/did-dht-method/pkg/service" ) -// RelayRouter is the router for the Relay API -type RelayRouter struct { +// PKARRRouter is the router for the PKARR API +type PKARRRouter struct { service *service.PKARRService } -// NewRelayRouter returns a new instance of the Relay router -func NewRelayRouter(service *service.PKARRService) (*RelayRouter, error) { - return &RelayRouter{service: service}, nil +// NewPKARRRouter returns a new instance of the Relay router +func NewPKARRRouter(service *service.PKARRService) (*PKARRRouter, error) { + return &PKARRRouter{service: service}, nil } -// Get godoc +// GetRecord godoc // -// @Summary Get a PKARR from the DHT -// @Description Get a PKARR from the DHT -// @Tags Relay +// @Summary GetRecord a PKARR record from the DHT +// @Description GetRecord a PKARR record from the DHT +// @Tags PKARR // @Accept octet-stream // @Produce octet-stream // @Param id path string true "ID to get" @@ -35,7 +35,7 @@ func NewRelayRouter(service *service.PKARRService) (*RelayRouter, error) { // @Failure 404 {string} string "Not found" // @Failure 500 {string} string "Internal server error" // @Router /{id} [get] -func (r *RelayRouter) Get(c *gin.Context) { +func (r *PKARRRouter) GetRecord(c *gin.Context) { id := GetParam(c, IDParam) if id == nil || *id == "" { LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) @@ -44,11 +44,11 @@ func (r *RelayRouter) Get(c *gin.Context) { resp, err := r.service.GetPKARR(c, *id) if err != nil { - LoggingRespondErrWithMsg(c, err, "failed to get pkarr", http.StatusInternalServerError) + LoggingRespondErrWithMsg(c, err, "failed to get pkarr record", http.StatusInternalServerError) return } if resp == nil { - LoggingRespondErrMsg(c, "pkarr not found", http.StatusNotFound) + LoggingRespondErrMsg(c, "pkarr record not found", http.StatusNotFound) return } @@ -61,19 +61,19 @@ func (r *RelayRouter) Get(c *gin.Context) { RespondBytes(c, res, http.StatusOK) } -// Put godoc +// PutRecord godoc // -// @Summary Put a PKARR record into the DHT -// @Description Put a PKARR record into the DHT -// @Tags Relay +// @Summary PutRecord a PKARR record into the DHT +// @Description PutRecord a PKARR record into the DHT +// @Tags PKARR // @Accept octet-stream -// @Param id path string true "ID to put" +// @Param id path string true "ID of the record to put" // @Param request body []byte true "64 bytes sig, 8 bytes u64 big-endian seq, 0-1000 bytes of v." // @Success 200 // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" // @Router /{id} [put] -func (r *RelayRouter) Put(c *gin.Context) { +func (r *PKARRRouter) PutRecord(c *gin.Context) { id := GetParam(c, IDParam) if id == nil || *id == "" { LoggingRespondErrMsg(c, "missing id param", http.StatusBadRequest) @@ -116,7 +116,7 @@ func (r *RelayRouter) Put(c *gin.Context) { Seq: seq, } if _, err = r.service.PublishPKARR(c, request); err != nil { - LoggingRespondErrWithMsg(c, err, "failed to publish pkarr request", http.StatusInternalServerError) + LoggingRespondErrWithMsg(c, err, "failed to publish pkarr record", http.StatusInternalServerError) return } diff --git a/impl/pkg/server/server.go b/impl/pkg/server/server.go index 4e26a1aa..0f27ccb4 100644 --- a/impl/pkg/server/server.go +++ b/impl/pkg/server/server.go @@ -29,7 +29,7 @@ type Server struct { shutdown chan os.Signal cfg *config.Config - svc *service.DIDService + svc *service.PKARRService } // NewServer returns a new instance of Server with the given db and host. @@ -47,10 +47,6 @@ func NewServer(cfg *config.Config, shutdown chan os.Signal) (*Server, error) { if err != nil { return nil, util.LoggingErrorMsg(err, "could not instantiate pkarr service") } - didDHTService, err := service.NewDIDService(cfg, db, *pkarrService) - if err != nil { - return nil, util.LoggingErrorMsg(err, "could not instantiate did dht service") - } handler.GET("/health", Health) @@ -73,7 +69,7 @@ func NewServer(cfg *config.Config, shutdown chan os.Signal) (*Server, error) { WriteTimeout: time.Second * 5, }, cfg: cfg, - svc: didDHTService, + svc: pkarrService, handler: handler, shutdown: shutdown, }, nil @@ -118,27 +114,12 @@ func setupHandler(env config.Environment) *gin.Engine { // PKARRAPI sets up the relay API routes according to https://github.com/Nuhvi/pkarr/blob/main/design/relays.md func PKARRAPI(rg *gin.RouterGroup, service *service.PKARRService) error { - relayRouter, err := NewRelayRouter(service) + relayRouter, err := NewPKARRRouter(service) if err != nil { return util.LoggingErrorMsg(err, "could not instantiate relay router") } - rg.PUT("/:id", relayRouter.Put) - rg.GET("/:id", relayRouter.Get) + rg.PUT("/:id", relayRouter.PutRecord) + rg.GET("/:id", relayRouter.GetRecord) return nil } - -// DIDDHTAPI sets up the DIDDHT API routes -// func DIDDHTAPI(rg *gin.RouterGroup, service *service.DIDService) error { -// didDHTRouter, err := NewDIDDHTRouter(service) -// if err != nil { -// return util.LoggingErrorMsg(err, "could not instantiate did:dht router") -// } -// -// didDHTAPI := rg.Group("/did") -// didDHTAPI.PUT("", didDHTRouter.PublishDID) -// didDHTAPI.GET("", didDHTRouter.ListDIDs) -// didDHTAPI.GET("/:id", didDHTRouter.GetDID) -// didDHTAPI.DELETE("/:id", didDHTRouter.DeleteDID) -// return nil -// } diff --git a/impl/pkg/server/server_pkarr_test.go b/impl/pkg/server/server_pkarr_test.go new file mode 100644 index 00000000..50598ded --- /dev/null +++ b/impl/pkg/server/server_pkarr_test.go @@ -0,0 +1,96 @@ +package server + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/did-dht-method/config" + "github.com/TBD54566975/did-dht-method/internal/did" + "github.com/TBD54566975/did-dht-method/pkg/dht" + "github.com/TBD54566975/did-dht-method/pkg/service" + "github.com/TBD54566975/did-dht-method/pkg/storage" +) + +func TestPKARRRouter(t *testing.T) { + pkarrSvc := testPKARRService(t) + pkarrRouter, err := NewPKARRRouter(&pkarrSvc) + require.NoError(t, err) + require.NotEmpty(t, pkarrRouter) + + t.Run("test put record", func(t *testing.T) { + didID, reqData := generateDIDPutRequest(t) + + w := httptest.NewRecorder() + suffix, err := did.DHT(didID).Suffix() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", testServerURL, suffix), bytes.NewReader(reqData)) + c := newRequestContextWithParams(w, req, map[string]string{IDParam: suffix}) + + pkarrRouter.PutRecord(c) + assert.True(t, is2xxResponse(w.Code)) + }) + + t.Run("test get record", func(t *testing.T) { + didID, reqData := generateDIDPutRequest(t) + + w := httptest.NewRecorder() + suffix, err := did.DHT(didID).Suffix() + assert.NoError(t, err) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("%s/%s", testServerURL, suffix), bytes.NewReader(reqData)) + c := newRequestContextWithParams(w, req, map[string]string{IDParam: suffix}) + + pkarrRouter.PutRecord(c) + assert.True(t, is2xxResponse(w.Code)) + + w = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", testServerURL, suffix), nil) + c = newRequestContextWithParams(w, req, map[string]string{IDParam: suffix}) + + pkarrRouter.GetRecord(c) + assert.True(t, is2xxResponse(w.Code)) + + resp, err := io.ReadAll(w.Body) + assert.NoError(t, err) + assert.NotEmpty(t, resp) + assert.Equal(t, reqData, resp) + }) +} + +func testPKARRService(t *testing.T) service.PKARRService { + defaultConfig := config.GetDefaultConfig() + db, err := storage.NewStorage(defaultConfig.ServerConfig.DBFile) + require.NoError(t, err) + require.NotEmpty(t, db) + pkarrService, err := service.NewPKARRService(&defaultConfig, db) + require.NoError(t, err) + require.NotEmpty(t, pkarrService) + return *pkarrService +} + +func generateDIDPutRequest(t *testing.T) (string, []byte) { + // generate a DID Document + sk, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc) + assert.NoError(t, err) + assert.NotEmpty(t, packet) + + bep44Put, err := dht.CreatePKARRPublishRequest(sk, *packet) + assert.NoError(t, err) + assert.NotEmpty(t, bep44Put) + + // prepare request as sig:seq:v + var seqBuf [8]byte + binary.BigEndian.PutUint64(seqBuf[:], uint64(bep44Put.Seq)) + return doc.ID, append(bep44Put.Sig[:], append(seqBuf[:], bep44Put.V.([]byte)...)...) +} diff --git a/impl/pkg/server/server_test.go b/impl/pkg/server/server_test.go new file mode 100644 index 00000000..0b47f136 --- /dev/null +++ b/impl/pkg/server/server_test.go @@ -0,0 +1,76 @@ +package server + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/did-dht-method/config" +) + +const ( + testServerURL = "https://diddht-service.com" +) + +func TestMain(t *testing.M) { + os.Exit(t.Run()) +} + +func TestHealthCheckAPI(t *testing.T) { + shutdown := make(chan os.Signal, 1) + serviceConfig, err := config.LoadConfig("") + serviceConfig.ServerConfig.DBFile = "health-check.db" + serviceConfig.ServerConfig.BaseURL = testServerURL + assert.NoError(t, err) + server, err := NewServer(serviceConfig, shutdown) + assert.NoError(t, err) + assert.NotEmpty(t, server) + + req := httptest.NewRequest(http.MethodGet, testServerURL+"/health", nil) + w := httptest.NewRecorder() + + c := newRequestContext(w, req) + Health(c) + assert.True(t, is2xxResponse(w.Code)) + + var resp GetHealthCheckResponse + err = json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Equal(t, HealthOK, resp.Status) +} + +// Is2xxResponse returns true if the given status code is a 2xx response +func is2xxResponse(statusCode int) bool { + return statusCode/100 == 2 +} + +func newJSONRequestValue(t *testing.T, data any) io.Reader { + dataBytes, err := json.Marshal(data) + require.NoError(t, err) + require.NotEmpty(t, dataBytes) + return bytes.NewReader(dataBytes) +} + +// construct a context value as expected by our handler +func newRequestContext(w http.ResponseWriter, req *http.Request) *gin.Context { + c, _ := gin.CreateTestContext(w) + c.Request = req + return c +} + +// construct a context value with query params as expected by our handler +func newRequestContextWithParams(w http.ResponseWriter, req *http.Request, params map[string]string) *gin.Context { + c := newRequestContext(w, req) + for k, v := range params { + c.AddParam(k, v) + } + return c +} diff --git a/impl/pkg/service/did.go b/impl/pkg/service/did.go deleted file mode 100644 index 4aeba7dd..00000000 --- a/impl/pkg/service/did.go +++ /dev/null @@ -1,53 +0,0 @@ -package service - -import ( - "context" - - "github.com/TBD54566975/ssi-sdk/did" - "github.com/TBD54566975/ssi-sdk/util" - "github.com/pkg/errors" - - "github.com/TBD54566975/did-dht-method/config" - "github.com/TBD54566975/did-dht-method/pkg/storage" -) - -// DIDService is the DID DHT service responsible for managing the DHT and reading/writing records -type DIDService struct { - cfg *config.Config - db *storage.Storage - pkarr PKARRService -} - -// NewDIDService returns a new instance of the DHT service -func NewDIDService(cfg *config.Config, db *storage.Storage, pkarr PKARRService) (*DIDService, error) { - if cfg == nil { - return nil, util.LoggingNewError("config is required") - } - if db == nil && !db.IsOpen() { - return nil, util.LoggingNewError("storage is required be non-nil and to be open") - } - return &DIDService{ - cfg: cfg, - db: db, - pkarr: pkarr, - }, nil -} - -type PublishDIDRequest struct { -} - -func (s *DIDService) PublishDID(_ context.Context, _ PublishDIDRequest) error { - return errors.New("unimplemented") -} - -func (s *DIDService) GetDID(_ context.Context, did string) (*did.Document, error) { - return nil, errors.New("unimplemented") -} - -func (s *DIDService) ListDIDs(_ context.Context) ([]did.Document, error) { - return nil, errors.New("unimplemented") -} - -func (s *DIDService) DeleteDID(_ context.Context, _ string) error { - return errors.New("unimplemented") -} diff --git a/impl/pkg/service/pkarr.go b/impl/pkg/service/pkarr.go index d938046f..63d0cc18 100644 --- a/impl/pkg/service/pkarr.go +++ b/impl/pkg/service/pkarr.go @@ -2,21 +2,25 @@ package service import ( "context" + "encoding/base64" "github.com/TBD54566975/ssi-sdk/util" "github.com/anacrolix/dht/v2/bep44" "github.com/anacrolix/torrent/bencode" + "github.com/sirupsen/logrus" "github.com/TBD54566975/did-dht-method/config" + dhtint "github.com/TBD54566975/did-dht-method/internal/dht" "github.com/TBD54566975/did-dht-method/pkg/dht" "github.com/TBD54566975/did-dht-method/pkg/storage" ) // PKARRService is the PKARR service responsible for managing the PKARR DHT and reading/writing records type PKARRService struct { - cfg *config.Config - db *storage.Storage - dht *dht.DHT + cfg *config.Config + db *storage.Storage + dht *dht.DHT + scheduler *dhtint.Scheduler } // NewPKARRService returns a new instance of the PKARR service @@ -27,27 +31,50 @@ func NewPKARRService(cfg *config.Config, db *storage.Storage) (*PKARRService, er if db == nil && !db.IsOpen() { return nil, util.LoggingNewError("storage is required be non-nil and to be open") } - dht, err := dht.NewDHT(cfg.DHTConfig.BootstrapPeers) + d, err := dht.NewDHT(cfg.DHTConfig.BootstrapPeers) if err != nil { return nil, util.LoggingErrorMsg(err, "failed to instantiate dht") } - return &PKARRService{ - cfg: cfg, - db: db, - dht: dht, - }, nil + scheduler := dhtint.NewScheduler() + service := PKARRService{ + cfg: cfg, + db: db, + dht: d, + scheduler: &scheduler, + } + if err = scheduler.Schedule(cfg.PKARRConfig.RepublishCRON, service.republish); err != nil { + return nil, util.LoggingErrorMsg(err, "failed to start republisher") + } + return &service, nil } // PublishPKARRRequest is the request to publish a PKARR record type PublishPKARRRequest struct { - V []byte `json:"v" validate:"required"` - K [32]byte `json:"k" validate:"required"` - Sig [64]byte `json:"sig" validate:"required"` - Seq int64 `json:"seq" validate:"required"` + V []byte `validate:"required"` + K [32]byte `validate:"required"` + Sig [64]byte `validate:"required"` + Seq int64 `validate:"required"` } -// PublishPKARR publishes the given PKARR to the DHT +func (p PublishPKARRRequest) toRecord() storage.PKARRRecord { + encoding := base64.RawURLEncoding + return storage.PKARRRecord{ + V: encoding.EncodeToString(p.V), + K: encoding.EncodeToString(p.K[:]), + Sig: encoding.EncodeToString(p.Sig[:]), + Seq: p.Seq, + } +} + +// PublishPKARR stores the record in the db, publishes the given PKARR to the DHT, and returns the z-base-32 encoded ID func (s *PKARRService) PublishPKARR(ctx context.Context, request PublishPKARRRequest) (string, error) { + if err := util.IsValidStruct(request); err != nil { + return "", err + } + // TODO(gabe): if putting to the DHT fails we should note that in the db and retry later + if err := s.db.WriteRecord(request.toRecord()); err != nil { + return "", err + } return s.dht.Put(ctx, bep44.Put{ V: request.V, K: &request.K, @@ -63,11 +90,37 @@ type GetPKARRResponse struct { Sig [64]byte `validate:"required"` } +func fromPKARRRecord(record storage.PKARRRecord) (*GetPKARRResponse, error) { + encoding := base64.RawURLEncoding + vBytes, err := encoding.DecodeString(record.V) + if err != nil { + return nil, err + } + sigBytes, err := encoding.DecodeString(record.Sig) + if err != nil { + return nil, err + } + return &GetPKARRResponse{ + V: vBytes, + Seq: record.Seq, + Sig: [64]byte(sigBytes), + }, nil +} + // GetPKARR returns the full PKARR (including sig data) for the given z-base-32 encoded ID func (s *PKARRService) GetPKARR(ctx context.Context, id string) (*GetPKARRResponse, error) { got, err := s.dht.GetFull(ctx, id) if err != nil { - return nil, err + // try to resolve from storage before returning and error + // if we detect this and have the record we should republish to the DHT + logrus.WithError(err).Warnf("failed to get pkarr<%s> from dht, attempting to resolve from storage", id) + record, err := s.db.ReadRecord(id) + if err != nil || record == nil { + logrus.WithError(err).Errorf("failed to resolve pkarr<%s> from storage", id) + return nil, err + } + logrus.Debugf("resolved pkarr<%s> from storage", id) + return fromPKARRRecord(*record) } bBytes, err := got.V.MarshalBencode() if err != nil { @@ -83,3 +136,54 @@ func (s *PKARRService) GetPKARR(ctx context.Context, id string) (*GetPKARRRespon Sig: got.Sig, }, nil } + +// TODO(gabe) make this more efficient. create a publish schedule based on each individual record, not all records +func (s *PKARRService) republish() { + allRecords, err := s.db.ListRecords() + if err != nil { + logrus.WithError(err).Error("failed to list record(s) for republishing") + return + } + if len(allRecords) == 0 { + logrus.Info("No records to republish") + return + } + logrus.Infof("Republishing %d record(s)", len(allRecords)) + errCnt := 0 + for _, record := range allRecords { + put, err := recordToBEP44Put(record) + if err != nil { + logrus.WithError(err).Error("failed to convert record to bep44 put") + errCnt++ + continue + } + if _, err = s.dht.Put(context.Background(), *put); err != nil { + logrus.WithError(err).Error("failed to republish record") + errCnt++ + continue + } + } + logrus.Infof("Republishing complete. Successfully republished %d out of %d record(s)", len(allRecords)-errCnt, len(allRecords)) +} + +func recordToBEP44Put(record storage.PKARRRecord) (*bep44.Put, error) { + encoding := base64.RawURLEncoding + vBytes, err := encoding.DecodeString(record.V) + if err != nil { + return nil, err + } + kBytes, err := encoding.DecodeString(record.K) + if err != nil { + return nil, err + } + sigBytes, err := encoding.DecodeString(record.Sig) + if err != nil { + return nil, err + } + return &bep44.Put{ + V: vBytes, + K: (*[32]byte)(kBytes), + Sig: [64]byte(sigBytes), + Seq: record.Seq, + }, nil +} diff --git a/impl/pkg/service/pkarr_test.go b/impl/pkg/service/pkarr_test.go new file mode 100644 index 00000000..357aa512 --- /dev/null +++ b/impl/pkg/service/pkarr_test.go @@ -0,0 +1,73 @@ +package service + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/did-dht-method/config" + "github.com/TBD54566975/did-dht-method/internal/did" + "github.com/TBD54566975/did-dht-method/pkg/dht" + "github.com/TBD54566975/did-dht-method/pkg/storage" +) + +func TestPKARRService(t *testing.T) { + svc := newPKARRService(t) + require.NotEmpty(t, svc) + + t.Run("test put bad record", func(t *testing.T) { + _, err := svc.PublishPKARR(context.Background(), PublishPKARRRequest{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validation for 'V' failed on the 'required' tag") + }) + + t.Run("test get non existent record", func(t *testing.T) { + got, err := svc.GetPKARR(context.Background(), "test") + assert.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("test put and get record", func(t *testing.T) { + // create a did doc as a packet to store + sk, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc) + assert.NoError(t, err) + assert.NotEmpty(t, packet) + + putMsg, err := dht.CreatePKARRPublishRequest(sk, *packet) + require.NoError(t, err) + require.NotEmpty(t, putMsg) + + id, err := svc.PublishPKARR(context.Background(), PublishPKARRRequest{ + V: putMsg.V.([]byte), + K: *putMsg.K, + Sig: putMsg.Sig, + Seq: putMsg.Seq, + }) + assert.NoError(t, err) + assert.NotEmpty(t, id) + + got, err := svc.GetPKARR(context.Background(), id) + assert.NoError(t, err) + assert.NotEmpty(t, got) + assert.Equal(t, putMsg.V, got.V) + assert.Equal(t, putMsg.Sig, got.Sig) + assert.Equal(t, putMsg.Seq, got.Seq) + }) +} + +func newPKARRService(t *testing.T) PKARRService { + defaultConfig := config.GetDefaultConfig() + db, err := storage.NewStorage(defaultConfig.ServerConfig.DBFile) + require.NoError(t, err) + require.NotEmpty(t, db) + pkarrService, err := NewPKARRService(&defaultConfig, db) + require.NoError(t, err) + require.NotEmpty(t, pkarrService) + return *pkarrService +} diff --git a/impl/pkg/storage/pkarr.go b/impl/pkg/storage/pkarr.go new file mode 100644 index 00000000..71d8e09d --- /dev/null +++ b/impl/pkg/storage/pkarr.go @@ -0,0 +1,68 @@ +package storage + +import ( + "encoding/json" +) + +const ( + pkarrNamespace = "pkarr" +) + +type PKARRRecord struct { + // Up to an 1000 byte base64URL encoded string + V string `json:"v" validate:"required"` + // 32 byte base64URL encoded string + K string `json:"k" validate:"required"` + // 64 byte base64URL encoded string + Sig string `json:"sig" validate:"required"` + Seq int64 `json:"seq" validate:"required"` +} + +type PKARRStorage interface { + WriteRecord(record PKARRRecord) error + ReadRecord(id string) (*PKARRRecord, error) + ListRecords() ([]PKARRRecord, error) +} + +// WriteRecord writes the given record to the storage +// TODO: don't overwrite existing records, store unique seq numbers +func (s *Storage) WriteRecord(record PKARRRecord) error { + recordBytes, err := json.Marshal(record) + if err != nil { + return err + } + return s.Write(pkarrNamespace, record.K, recordBytes) +} + +// ReadRecord reads the record with the given id from the storage +func (s *Storage) ReadRecord(id string) (*PKARRRecord, error) { + recordBytes, err := s.Read(pkarrNamespace, id) + if err != nil { + return nil, err + } + if len(recordBytes) == 0 { + return nil, nil + } + var record PKARRRecord + if err = json.Unmarshal(recordBytes, &record); err != nil { + return nil, err + } + return &record, nil +} + +// ListRecords lists all records in the storage +func (s *Storage) ListRecords() ([]PKARRRecord, error) { + recordsMap, err := s.ReadAll(pkarrNamespace) + if err != nil { + return nil, err + } + var records []PKARRRecord + for _, recordBytes := range recordsMap { + var record PKARRRecord + if err = json.Unmarshal(recordBytes, &record); err != nil { + return nil, err + } + records = append(records, record) + } + return records, nil +} diff --git a/impl/pkg/storage/pkarr_test.go b/impl/pkg/storage/pkarr_test.go new file mode 100644 index 00000000..e41d8722 --- /dev/null +++ b/impl/pkg/storage/pkarr_test.go @@ -0,0 +1,54 @@ +package storage + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/TBD54566975/did-dht-method/internal/did" + "github.com/TBD54566975/did-dht-method/pkg/dht" +) + +func TestPKARRStorage(t *testing.T) { + db := setupBoltDB(t) + defer db.Close() + require.NotEmpty(t, db) + + // create a did doc as a packet to store + sk, doc, err := did.GenerateDIDDHT(did.CreateDIDDHTOpts{}) + require.NoError(t, err) + require.NotEmpty(t, doc) + + packet, err := did.DHT(doc.ID).ToDNSPacket(*doc) + assert.NoError(t, err) + assert.NotEmpty(t, packet) + + putMsg, err := dht.CreatePKARRPublishRequest(sk, *packet) + require.NoError(t, err) + require.NotEmpty(t, putMsg) + + // create record + encoding := base64.RawURLEncoding + record := PKARRRecord{ + V: encoding.EncodeToString(putMsg.V.([]byte)), + K: encoding.EncodeToString(putMsg.K[:]), + Sig: encoding.EncodeToString(putMsg.Sig[:]), + Seq: putMsg.Seq, + } + + err = db.WriteRecord(record) + assert.NoError(t, err) + + // read it back + readRecord, err := db.ReadRecord(record.K) + assert.NoError(t, err) + assert.Equal(t, record, *readRecord) + + // list and confirm it's there + records, err := db.ListRecords() + assert.NoError(t, err) + assert.NotEmpty(t, records) + assert.Equal(t, record, records[0]) +}