From f2ce23bfb31ff628acc5977dd19be63b2c3ad78a Mon Sep 17 00:00:00 2001 From: Derek Anderson Date: Wed, 22 Mar 2023 14:03:26 -0500 Subject: [PATCH] Refactor (#54) * Add executor component * Add node type + refactor models + refactor executables * Creating a "host" module to contain all of the libp2p host functionality (#27) * Add store package (#28) * Add peerstore (#30) * Add API (#33) * Add function handler (#31) * Remove obsolete files (#37) * Remove src as it contains obsoleted (or refactored and moved) files * Go mod tidy * Add Readme files for executables (#39) * Add Readme files for executables * Remove replace directive from go.mod * Add tests for `store` and `peerstore` packages (#38) * Add tests for "store" package * Rename test * Move pebble DB setup to an external package * Add tests for peerstore * Add tests with a baseline cache to cover all storage failure scenarios * Adding tests for the `executor` (#40) * Using config for executor * Use afero to abstract FS operations * Add test for paths created by the executor (manifest, entry etc) * Add tests for executor creation * Add tests for configuration * Add more tests for executor - cmd creation and manifest writing * Update executor creation * Split imports * Add tests for function handler package (#41) * Add tests for downloading JSON and parsing/merging deployment info * Add tests for downloader * Add tests for archive unpacking code * Adding external test for function handler - Get() * Update function handler usage * Add tests for the `node` package (#44) * Add mock Executor + add tests for node config * Add mocks for more component, test node creation and handling unsupported functions * Add tests for handlers - health - roll call response - install function response * Add a WaitMap for better handling of roll-call/execute responses * Add a WaitFor to enable waiting for a limited time period * Use time.After instead of a ticker * Refactor roll call response handling and add tests for notifiee * Add test for sending message * Add test for publishing to a topic * Add a method for subscribing - internalizes topic handle handling * Add tests for worker execution * Add tests for function install * Handle roll call request * Adding test for issuing roll call * Health interval is configurable + tests for health ping * Create a const for loopback address * Add test for head node execute - roll call timeout scenario * Roll call timeout is configurable * Simplify handling of configurable options by having cfg as part of `Node` instead of individual fields * Simplify peer address handling code * Add head execute test case + fix handling of worker execution flow * Keep a single package for execution result caching * Decrease timeout * Each component with a logger sets its own "component" tag (#47) * Host discovery phase - skip connected peers (#46) * Add integration test for node - function install and execution request (#45) * Add integration test for node (function install + execute) * Move function server to the helper package * Log files created in appropriate directories * Blockless runtime path set as environment variable and proper closing of log files * Add integration flags * Process requests in parallel (#48) * Concurrency level for node is controlled via CLI flag (#49) * update makefile (#50) * add usage flags to readme * [executor] Collect CPU usage info and add integration tests (#53) * Add a config option for setting executable name * Collect resource usage info - CPU time in nanoseconds and max memory on *ux * Fix type assertion * Add a small md5sum test program * Add integration test for executor * update docker files (#55) --------- Co-authored-by: Maelkum --- .gitignore | 3 + Makefile | 14 +- README.md | 28 +- api/api.go | 22 + api/execute.go | 36 ++ api/install.go | 66 +++ api/node.go | 13 + api/result.go | 27 ++ cmd/keygen/README.md | 23 + cmd/keygen/main.go | 89 ++++ cmd/node/README.md | 72 +++ cmd/node/address.go | 44 ++ cmd/node/flags.go | 42 ++ cmd/node/main.go | 231 ++++++++++ cmd/node/parse.go | 22 + config/config.go | 27 ++ config/model.go | 28 ++ docker/Dockerfile | 1 + docker/run.sh | 6 +- executor/config.go | 51 +++ executor/config_internal_test.go | 56 +++ executor/execute.go | 125 +++++ executor/execute_internal_test.go | 190 ++++++++ executor/executor.go | 45 ++ executor/executor_integration_test.go | 156 +++++++ executor/executor_test.go | 46 ++ executor/function.go | 34 ++ executor/manifest.go | 57 +++ executor/memory.go | 20 + executor/memory_win.go | 14 + executor/params.go | 11 + executor/path.go | 27 ++ executor/path_internal_test.go | 38 ++ executor/testdata/md5sum/README.md | 12 + executor/testdata/md5sum/md5sum.wasm | Bin 0 -> 685866 bytes executor/usage.go | 18 + function/deployment.go | 40 ++ function/deployment_internal_test.go | 121 +++++ function/function.go | 42 ++ function/get.go | 84 ++++ function/get_test.go | 192 ++++++++ function/gzip.go | 101 +++++ function/gzip_internal_test.go | 50 ++ function/http.go | 84 ++++ function/http_internal_test.go | 282 ++++++++++++ function/params.go | 10 + function/store.go | 6 + .../testdata/testFunction.tar.gz | Bin go.mod | 109 +---- go.sum | 323 ++----------- host/config.go | 67 +++ host/dht.go | 166 +++++++ host/host.go | 93 ++++ host/params.go | 6 + host/publish.go | 20 + host/send.go | 28 ++ host/subscribe.go | 32 ++ models/api/request/request.go | 14 + models/api/response/response.go | 13 + models/blockless/errors.go | 10 + .../blockless/function.go | 54 ++- models/blockless/message.go | 29 ++ src/models/db.go => models/blockless/peer.go | 5 +- models/blockless/protocol.go | 9 + models/blockless/role.go | 33 ++ models/execute/request.go | 36 ++ models/execute/response.go | 21 + models/request/execute.go | 21 + models/request/install_function.go | 13 + models/request/roll_call.go | 13 + models/response/code.go | 17 + models/response/execute.go | 14 + models/response/health.go | 12 + models/response/install_function.go | 13 + models/response/roll_call.go | 15 + node/config.go | 71 +++ node/config_internal_test.go | 74 +++ node/execute.go | 281 ++++++++++++ node/execute_integration_test.go | 208 +++++++++ node/execute_internal_test.go | 426 ++++++++++++++++++ node/executor.go | 9 + node/function.go | 13 + node/function_manifest.go | 21 + node/handlers.go | 84 ++++ node/handlers_internal_test.go | 203 +++++++++ node/health.go | 39 ++ node/health_internal_test.go | 79 ++++ node/internal/waitmap/waitmap.go | 111 +++++ .../internal/waitmap/waitmap_internal_test.go | 187 ++++++++ node/message.go | 56 +++ node/message_internal_test.go | 106 +++++ node/node.go | 119 +++++ node/node_integration_test.go | 241 ++++++++++ node/node_internal_test.go | 160 +++++++ node/notifiee.go | 60 +++ node/notifiee_internal_test.go | 58 +++ node/params.go | 22 + node/peerstore.go | 11 + node/process.go | 46 ++ node/queue.go | 51 +++ node/rest.go | 109 +++++ node/roll_call.go | 110 +++++ node/roll_call_internal_test.go | 214 +++++++++ node/run.go | 118 +++++ node/store.go | 6 + node/testdata/hello.tar.gz | Bin 0 -> 27142 bytes peerstore/params.go | 5 + peerstore/peerstore.go | 115 +++++ peerstore/peerstore_test.go | 244 ++++++++++ peerstore/store.go | 6 + src/README.md | 23 - src/chain/README.md | 3 - src/chain/chain.go | 17 - src/config/config.go | 48 -- src/config/config_test.go | 28 -- src/controller/README.md | 5 - src/controller/controller.go | 113 ----- src/controller/controller_test.go | 146 ------ src/controller/head.go | 111 ----- src/controller/install_function.go | 90 ---- src/controller/install_function_test.go | 42 -- src/controller/worker.go | 30 -- src/daemon/README.md | 3 - src/daemon/channels.go | 110 ----- src/daemon/daemon.go | 102 ----- src/db/db.go | 102 ----- src/db/db_test.go | 40 -- src/dht/README.md | 5 - src/dht/dht.go | 136 ------ src/enums/enums.go | 73 --- src/executor/README.md | 5 - src/executor/executor.go | 157 ------- src/executor/executor_test.go | 54 --- src/health/health.go | 17 - src/host/README.md | 3 - src/host/host.go | 61 --- src/host/notifee.go | 113 ----- src/http/README.md | 3 - src/http/client.go | 81 ---- src/http/client_test.go | 86 ---- src/http/http.go | 1 - src/http/testdata/testfile | 1 - src/keygen/README.md | 3 - src/keygen/keygen.go | 75 --- src/keygen/keygen_test.go | 38 -- src/main.go | 73 --- src/memstore/memstore.go | 1 - src/memstore/results.go | 55 --- src/memstore/results_test.go | 31 -- src/messaging/README.md | 3 - src/messaging/handler.go | 34 -- src/messaging/handlers/msgexecute.go | 42 -- src/messaging/handlers/msghealthcheck.go | 13 - src/messaging/handlers/msginstall.go | 38 -- src/messaging/handlers/msginstallresponse.go | 14 - src/messaging/handlers/msgrollcall.go | 53 --- src/messaging/messaging.go | 95 ---- src/models/config.go | 74 --- src/models/messages.go | 117 ----- src/models/models.go | 7 - src/models/rest.go | 48 -- src/repository/README.md | 3 - src/repository/base.go | 5 - src/repository/controller.go | 12 - src/repository/repository.go | 200 -------- src/repository/repository_test.go | 94 ---- src/restapi/README.md | 5 - src/restapi/api.go | 150 ------ src/restapi/api_test.go | 260 ----------- src/restapi/params.go | 9 - src/restapi/rest.go | 46 -- src/restapi/rest_test.go | 32 -- src/selection/README.md | 3 - src/selection/selection.go | 38 -- src/selection/selection_test.go | 67 --- store/get.go | 53 +++ store/set.go | 35 ++ store/store.go | 20 + store/store_test.go | 204 +++++++++ testing/helpers/function_server.go | 71 +++ testing/helpers/pebble.go | 21 + testing/mocks/executor.go | 27 ++ testing/mocks/function_store.go | 27 ++ testing/mocks/generic.go | 53 +++ testing/mocks/peerstore.go | 46 ++ testing/mocks/store.go | 49 ++ 186 files changed, 7742 insertions(+), 3966 deletions(-) create mode 100644 api/api.go create mode 100644 api/execute.go create mode 100644 api/install.go create mode 100644 api/node.go create mode 100644 api/result.go create mode 100644 cmd/keygen/README.md create mode 100644 cmd/keygen/main.go create mode 100644 cmd/node/README.md create mode 100644 cmd/node/address.go create mode 100644 cmd/node/flags.go create mode 100644 cmd/node/main.go create mode 100644 cmd/node/parse.go create mode 100644 config/config.go create mode 100644 config/model.go create mode 100644 executor/config.go create mode 100644 executor/config_internal_test.go create mode 100644 executor/execute.go create mode 100644 executor/execute_internal_test.go create mode 100644 executor/executor.go create mode 100644 executor/executor_integration_test.go create mode 100644 executor/executor_test.go create mode 100644 executor/function.go create mode 100644 executor/manifest.go create mode 100644 executor/memory.go create mode 100644 executor/memory_win.go create mode 100644 executor/params.go create mode 100644 executor/path.go create mode 100644 executor/path_internal_test.go create mode 100644 executor/testdata/md5sum/README.md create mode 100755 executor/testdata/md5sum/md5sum.wasm create mode 100644 executor/usage.go create mode 100644 function/deployment.go create mode 100644 function/deployment_internal_test.go create mode 100644 function/function.go create mode 100644 function/get.go create mode 100644 function/get_test.go create mode 100644 function/gzip.go create mode 100644 function/gzip_internal_test.go create mode 100644 function/http.go create mode 100644 function/http_internal_test.go create mode 100644 function/params.go create mode 100644 function/store.go rename {src/repository => function}/testdata/testFunction.tar.gz (100%) create mode 100644 host/config.go create mode 100644 host/dht.go create mode 100644 host/host.go create mode 100644 host/params.go create mode 100644 host/publish.go create mode 100644 host/send.go create mode 100644 host/subscribe.go create mode 100644 models/api/request/request.go create mode 100644 models/api/response/response.go create mode 100644 models/blockless/errors.go rename src/models/repository.go => models/blockless/function.go (65%) create mode 100644 models/blockless/message.go rename src/models/db.go => models/blockless/peer.go (65%) create mode 100644 models/blockless/protocol.go create mode 100644 models/blockless/role.go create mode 100644 models/execute/request.go create mode 100644 models/execute/response.go create mode 100644 models/request/execute.go create mode 100644 models/request/install_function.go create mode 100644 models/request/roll_call.go create mode 100644 models/response/code.go create mode 100644 models/response/execute.go create mode 100644 models/response/health.go create mode 100644 models/response/install_function.go create mode 100644 models/response/roll_call.go create mode 100644 node/config.go create mode 100644 node/config_internal_test.go create mode 100644 node/execute.go create mode 100644 node/execute_integration_test.go create mode 100644 node/execute_internal_test.go create mode 100644 node/executor.go create mode 100644 node/function.go create mode 100644 node/function_manifest.go create mode 100644 node/handlers.go create mode 100644 node/handlers_internal_test.go create mode 100644 node/health.go create mode 100644 node/health_internal_test.go create mode 100644 node/internal/waitmap/waitmap.go create mode 100644 node/internal/waitmap/waitmap_internal_test.go create mode 100644 node/message.go create mode 100644 node/message_internal_test.go create mode 100644 node/node.go create mode 100644 node/node_integration_test.go create mode 100644 node/node_internal_test.go create mode 100644 node/notifiee.go create mode 100644 node/notifiee_internal_test.go create mode 100644 node/params.go create mode 100644 node/peerstore.go create mode 100644 node/process.go create mode 100644 node/queue.go create mode 100644 node/rest.go create mode 100644 node/roll_call.go create mode 100644 node/roll_call_internal_test.go create mode 100644 node/run.go create mode 100644 node/store.go create mode 100644 node/testdata/hello.tar.gz create mode 100644 peerstore/params.go create mode 100644 peerstore/peerstore.go create mode 100644 peerstore/peerstore_test.go create mode 100644 peerstore/store.go delete mode 100644 src/README.md delete mode 100644 src/chain/README.md delete mode 100644 src/chain/chain.go delete mode 100644 src/config/config.go delete mode 100644 src/config/config_test.go delete mode 100644 src/controller/README.md delete mode 100644 src/controller/controller.go delete mode 100644 src/controller/controller_test.go delete mode 100644 src/controller/head.go delete mode 100644 src/controller/install_function.go delete mode 100644 src/controller/install_function_test.go delete mode 100644 src/controller/worker.go delete mode 100644 src/daemon/README.md delete mode 100644 src/daemon/channels.go delete mode 100644 src/daemon/daemon.go delete mode 100644 src/db/db.go delete mode 100644 src/db/db_test.go delete mode 100644 src/dht/README.md delete mode 100644 src/dht/dht.go delete mode 100644 src/enums/enums.go delete mode 100644 src/executor/README.md delete mode 100644 src/executor/executor.go delete mode 100644 src/executor/executor_test.go delete mode 100644 src/health/health.go delete mode 100644 src/host/README.md delete mode 100644 src/host/host.go delete mode 100644 src/host/notifee.go delete mode 100644 src/http/README.md delete mode 100644 src/http/client.go delete mode 100644 src/http/client_test.go delete mode 100644 src/http/http.go delete mode 100644 src/http/testdata/testfile delete mode 100644 src/keygen/README.md delete mode 100644 src/keygen/keygen.go delete mode 100644 src/keygen/keygen_test.go delete mode 100644 src/main.go delete mode 100644 src/memstore/memstore.go delete mode 100644 src/memstore/results.go delete mode 100644 src/memstore/results_test.go delete mode 100644 src/messaging/README.md delete mode 100644 src/messaging/handler.go delete mode 100644 src/messaging/handlers/msgexecute.go delete mode 100644 src/messaging/handlers/msghealthcheck.go delete mode 100644 src/messaging/handlers/msginstall.go delete mode 100644 src/messaging/handlers/msginstallresponse.go delete mode 100644 src/messaging/handlers/msgrollcall.go delete mode 100644 src/messaging/messaging.go delete mode 100644 src/models/config.go delete mode 100644 src/models/messages.go delete mode 100644 src/models/models.go delete mode 100644 src/models/rest.go delete mode 100644 src/repository/README.md delete mode 100644 src/repository/base.go delete mode 100644 src/repository/controller.go delete mode 100644 src/repository/repository.go delete mode 100644 src/repository/repository_test.go delete mode 100644 src/restapi/README.md delete mode 100644 src/restapi/api.go delete mode 100644 src/restapi/api_test.go delete mode 100644 src/restapi/params.go delete mode 100644 src/restapi/rest.go delete mode 100644 src/restapi/rest_test.go delete mode 100644 src/selection/README.md delete mode 100644 src/selection/selection.go delete mode 100644 src/selection/selection_test.go create mode 100644 store/get.go create mode 100644 store/set.go create mode 100644 store/store.go create mode 100644 store/store_test.go create mode 100644 testing/helpers/function_server.go create mode 100644 testing/helpers/pebble.go create mode 100644 testing/mocks/executor.go create mode 100644 testing/mocks/function_store.go create mode 100644 testing/mocks/generic.go create mode 100644 testing/mocks/peerstore.go create mode 100644 testing/mocks/store.go diff --git a/.gitignore b/.gitignore index 35f4d73a..08aea3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +cmd/node/node +cmd/keygen/keygen + dist/ runtime/ *.env diff --git a/Makefile b/Makefile index 51959648..ce0c9962 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: all -all: clean build +all: clean build-node build-keygen .PHONY: test test: @@ -8,10 +8,16 @@ test: go test ./src/... @echo "\n✅ Done.\n" -.PHONY: build -build: +.PHONY: build-node +build-node: @echo "\n🛠 Building node...\n" - cd src && go build -o ../dist/b7s + cd cmd/node && go build -o ../../dist/b7s + @echo "\n✅ Done.\n" + +.PHONY: build-keygen +build-keygen: + @echo "\n🛠 Building node...\n" + cd cmd/keygen && go build -o ../../dist/b7s-keygen @echo "\n✅ Done.\n" .PHONY: clean diff --git a/README.md b/README.md index 97f273fc..c70ba326 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ ![Coverage](https://img.shields.io/badge/Coverage-48.7%25-yellow) - # b7s daemon b7s is a peer-to-peer networking daemon for the blockless network. It is supported on Windows, Linux, and MacOS platforms for both x64 and arm64 architectures. @@ -19,18 +18,21 @@ sudo sh -c "wget https://raw.githubusercontent.com/blocklessnetwork/b7s/main/dow You can also use Docker to install b7s. See the [Docker documentation](/docker/README.md) for more information. -Usage -b7s can be run with a number of commands and flags: - -Commands: - -- `help`: display the help menu -- `keygen`: generate identity keys for the node - Flags: - -- `config`: path to the configuration file -- `out`: style of logging used in the daemon (rich, text, or json) - For example: +## Usage + +| Flag | Short Form | Default Value | Description | +| ----------- | ---------- | ----------------------- | --------------------------------------------------------------------------------------------- | +| log-level | -l | "info" | Specifies the level of logging to use. | +| db | -d | "db" | Specifies the path to the database used for persisting node data. | +| role | -r | "worker" | Specifies the role this node will have in the Blockless protocol (head or worker). | +| address | -a | "0.0.0.0" | Specifies the address that the libp2p host will use. | +| port | -p | 0 | Specifies the port that the libp2p host will use. | +| private-key | N/A | N/A | Specifies the private key that the libp2p host will use. | +| concurrency | -c | node.DefaultConcurrency | Specifies the maximum number of requests the node will process in parallel. | +| rest-api | N/A | N/A | Specifies the address where the head node REST API will listen on. | +| boot-nodes | N/A | N/A | Specifies a list of addresses that this node will connect to on startup, in multiaddr format. | +| workspace | N/A | "./workspace" | Specifies the directory that the node can use for file storage. | +| runtime | N/A | N/A | Specifies the runtime address used by the worker node. | ## Dependencies diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..3e48af4c --- /dev/null +++ b/api/api.go @@ -0,0 +1,22 @@ +package api + +import ( + "github.com/rs/zerolog" +) + +// API provides REST API functionality for the Blockless head node. +type API struct { + log zerolog.Logger + node Node +} + +// New creates a new instance of a Blockless head node REST API. Access to node data is provided by the provided `node`. +func New(log zerolog.Logger, node Node) *API { + + api := API{ + log: log.With().Str("component", "api").Logger(), + node: node, + } + + return &api +} diff --git a/api/execute.go b/api/execute.go new file mode 100644 index 00000000..0f27e85f --- /dev/null +++ b/api/execute.go @@ -0,0 +1,36 @@ +package api + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + + "github.com/blocklessnetworking/b7s/models/api/request" + "github.com/blocklessnetworking/b7s/models/execute" +) + +// Execute implements the REST API endpoint for function execution. +func (a *API) Execute(ctx echo.Context) error { + + // Unpack the API request. + var req request.Execute + err := ctx.Bind(&req) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("could not unpack request: %w", err)) + } + + // TODO: Check - We perhaps want to return the request ID and not wait for the execution, right? + // It's probable that it will time out anyway, right? + + // Get the execution result. + result, err := a.node.ExecuteFunction(ctx.Request().Context(), execute.Request(req)) + // Determine status code. + code := http.StatusOK + if err != nil { + code = http.StatusInternalServerError + } + + // Send the response. + return ctx.JSON(code, result) +} diff --git a/api/install.go b/api/install.go new file mode 100644 index 00000000..2f5a3b7d --- /dev/null +++ b/api/install.go @@ -0,0 +1,66 @@ +package api + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/labstack/echo/v4" + + "github.com/blocklessnetworking/b7s/models/api/request" +) + +const ( + functionInstallTimeout = 10 * time.Second +) + +func (a *API) Install(ctx echo.Context) error { + + // Unpack the API request. + var req request.InstallFunction + err := ctx.Bind(&req) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("could not unpack request: %w", err)) + } + + if req.URI == "" && req.CID == "" { + return echo.NewHTTPError(http.StatusBadRequest, errors.New("URI or CID are required")) + } + + // Add a deadline to the context. + reqCtx, cancel := context.WithTimeout(ctx.Request().Context(), functionInstallTimeout) + defer cancel() + + // Start function install in a separate goroutine and signal when it's done. + fnErr := make(chan error) + go func() { + err = a.node.FunctionInstall(reqCtx, req.URI, req.CID) + fnErr <- err + }() + + // Wait until either function install finishes, or request times out. + select { + + // Context timed out. + case <-reqCtx.Done(): + + status := http.StatusRequestTimeout + if !errors.Is(reqCtx.Err(), context.DeadlineExceeded) { + status = http.StatusInternalServerError + } + return ctx.NoContent(status) + + // Work done. + case err = <-fnErr: + break + } + + // Check if function install succeeded and handle error or return response. + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("function installation failed: %w", err)) + } + + return ctx.NoContent(http.StatusOK) +} diff --git a/api/node.go b/api/node.go new file mode 100644 index 00000000..7fc657b2 --- /dev/null +++ b/api/node.go @@ -0,0 +1,13 @@ +package api + +import ( + "context" + + "github.com/blocklessnetworking/b7s/models/execute" +) + +type Node interface { + ExecuteFunction(context.Context, execute.Request) (execute.Result, error) + ExecutionResult(id string) (execute.Result, bool) + FunctionInstall(ctx context.Context, uri string, cid string) error +} diff --git a/api/result.go b/api/result.go new file mode 100644 index 00000000..a7f5a6a5 --- /dev/null +++ b/api/result.go @@ -0,0 +1,27 @@ +package api + +import ( + "errors" + "net/http" + + "github.com/labstack/echo/v4" +) + +// ExecutionResult implements the REST API endpoint for retrieving the result of a function execution. +func (a *API) ExecutionResult(ctx echo.Context) error { + + // Get the request ID. + requestID := ctx.Param("id") + if requestID == "" { + return echo.NewHTTPError(http.StatusBadRequest, errors.New("missing request ID")) + } + + // Lookup execution result. + result, ok := a.node.ExecutionResult(requestID) + if !ok { + return ctx.NoContent(http.StatusNotFound) + } + + // Send the response back. + return ctx.JSON(http.StatusOK, result) +} diff --git a/cmd/keygen/README.md b/cmd/keygen/README.md new file mode 100644 index 00000000..90fbeb75 --- /dev/null +++ b/cmd/keygen/README.md @@ -0,0 +1,23 @@ +# KeyGen + +## Description + +The `keygen` utility can be used to create keys that will determine Blockless b7s Node identity. + +## Usage + +```console +Usage of keygen: + -o, --output string directory where keys should be stored (default ".") +``` + +## Examples + +Create keys in the `keys` directory of the users `home` directory: + +```console +$ ./keygen --output ~/keys +generated private key: /home/user/keys/priv.bin +generated public key: /home/user/keys/pub.bin +generated identity file: /home/user/keys/identity +``` diff --git a/cmd/keygen/main.go b/cmd/keygen/main.go new file mode 100644 index 00000000..a74a4912 --- /dev/null +++ b/cmd/keygen/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + "github.com/spf13/pflag" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +const ( + // Names used for created files. + privKeyName = "priv.bin" + pubKeyName = "pub.bin" + identityName = "identity" +) + +const ( + // Permissions used for created files. + privKeyPermissions = 0600 + pubKeyPermissions = 0644 +) + +func main() { + + var ( + flagOutputDir string + ) + + pflag.StringVarP(&flagOutputDir, "output", "o", ".", "directory where keys should be stored") + + pflag.Parse() + + // Create output directory, if it doesn't exist. + err := os.MkdirAll(flagOutputDir, os.ModePerm) + if err != nil { + log.Fatalf("could not create output directory: %s", err) + } + + // Generate key pair. + priv, pub, err := crypto.GenerateKeyPair(crypto.Ed25519, 0) + if err != nil { + log.Fatalf("could not generate key pair: %s", err) + } + + // Encode keys and extract peer ID from key. + privPayload, err := crypto.MarshalPrivateKey(priv) + if err != nil { + log.Fatalf("could not marshal private key: %s", err) + } + + pubPayload, err := crypto.MarshalPublicKey(pub) + if err != nil { + log.Fatalf("could not marshal public key: %s", err) + } + + identity, err := peer.IDFromPublicKey(pub) + if err != nil { + log.Fatalf("failed to get peer identity from public key: %s", err) + } + + // Write keys and identity to files. + + pubKeyFile := filepath.Join(flagOutputDir, pubKeyName) + err = os.WriteFile(pubKeyFile, pubPayload, pubKeyPermissions) + if err != nil { + log.Fatalf("could not write private key to file: %s", err) + } + + idFile := filepath.Join(flagOutputDir, identityName) + err = os.WriteFile(idFile, []byte(identity), pubKeyPermissions) + if err != nil { + log.Fatalf("could not write private key to file: %s", err) + } + + privKeyFile := filepath.Join(flagOutputDir, privKeyName) + err = os.WriteFile(privKeyFile, privPayload, privKeyPermissions) + if err != nil { + log.Fatalf("could not write private key to file: %s", err) + } + + fmt.Printf("generated private key: %s\n", privKeyFile) + fmt.Printf("generated public key: %s\n", pubKeyFile) + fmt.Printf("generated identity file: %s\n", idFile) +} diff --git a/cmd/node/README.md b/cmd/node/README.md new file mode 100644 index 00000000..b35298a3 --- /dev/null +++ b/cmd/node/README.md @@ -0,0 +1,72 @@ + +# Blockless Node + +## Description + +Blockless b7s Node is a peer-to-peer networking daemon for the blockless network. + +A Node in the Blockless network can have one of two roles - it can be a Head Node or a Worker Node. + +In short, Worker Nodes are nodes that will be doing the actual execution of work within the Blockless P2P network. +Worker Nodes do this by relying on the Blockless Runtime. +Blockless Runtime needs to be available locally on the machine where the Node is run. + +Head Nodes are nodes that are performing coordination of work between a number of Worker Nodes. +When a Head Node receives an execution request to execute a piece of work (a Blockless Function), it will start a process of finding a Worker Node most suited to do this work. +Head Node does not need to have access to Blockless Runtime. + +Head Nodes also serve a REST API that can be used to query or trigger certain actions. + +## Usage + +```console +Usage of node: + -a, --address string address that the libp2p host will use (default "0.0.0.0") + --boot-nodes strings list of addresses that this node will connect to on startup, in multiaddr format + -c, --concurrency uint maximum number of requests node will process in parallel (default 10) + -d, --db string path to the database used for persisting node data (default "db") + -l, --log-level string log level to use (default "info") + -p, --port uint port that the libp2p host will use + --private-key string private key that the libp2p host will use + --rest-api string address where the head node REST API will listen on + -r, --role string role this note will have in the Blockless protocol (head or worker) (default "worker") + --runtime string runtime address (used by the worker node) + --workspace string directory that the node can use for file storage (default "./workspace") +``` + +You can find more information about `multiaddr` format for network addresses [here](https://github.com/multiformats/multiaddr) and [here](https://multiformats.io/multiaddr/). + +Private key path relates to the private key created by the [keygen](/cmd/keygen/README.md) utility. +Using the same private key in multiple `node` runs will ensure the node has the same identity on the network. +If a private key is not specified the node will start with a randomly generated identity. + +## Examples + +### Starting a Worker Node + +```console +$ ./node --db ./database --log-level debug --port 9000 --role worker --runtime ~/.local/bin --workspace workspace --private-key ./keys/priv.bin +``` + +The created `node` will listen on all addresses on TCP port 9000. +Database used to persist Node data between runs will be created in the `database` subdirectory. + +Blockless Runtime path is given as `~/.local/bin`. +At startup, node will check if the Blockless Runtime is actually found there, namely the [blockless-cli](https://blockless.network/docs/cli). + +Node Identity will be determined by the private key found in `priv.bin` file in the `keys` subdirectory. + +Any transient files needed for node operation will be created in the `workspace` subdirectory. + +### Starting a Head Node + +```console +$ ./node --db /var/tmp/b7s/db --log-level debug --port 9002 -r head --workspace /var/tmp/b7s/workspace --private-key ~/keys/priv.bin --rest-api ':8080' +``` + +The created `node` will listen on all addresses on TCP port 9002. +Database used to persist Node data between runs will be created at `/var/tmp/b7s/db`. + +Any transient files needed for node operation will be created in the `/var/tmp/b7s/workspace` directory. + +Head Node REST API will be available on all addresses on port 8080. diff --git a/cmd/node/address.go b/cmd/node/address.go new file mode 100644 index 00000000..3adb5678 --- /dev/null +++ b/cmd/node/address.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + + "github.com/multiformats/go-multiaddr" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// getPeerAddresses returns the list of the multiaddreses for the peer list. +func getPeerAddresses(peers []blockless.Peer) ([]multiaddr.Multiaddr, error) { + + var addrs []multiaddr.Multiaddr + + for _, peer := range peers { + + addr, err := multiaddr.NewMultiaddr(peer.MultiAddr) + if err != nil { + return nil, fmt.Errorf("could not parse multiaddress (addr: %s): %w", peer.MultiAddr, err) + } + + addrs = append(addrs, addr) + } + + return addrs, nil +} + +// parse list of strings with multiaddresses +func getBootNodeAddresses(addrs []string) ([]multiaddr.Multiaddr, error) { + + var out []multiaddr.Multiaddr + for _, addr := range addrs { + + addr, err := multiaddr.NewMultiaddr(addr) + if err != nil { + return nil, fmt.Errorf("could not parse multiaddress (addr: %s): %w", addr, err) + } + + out = append(out, addr) + } + + return out, nil +} diff --git a/cmd/node/flags.go b/cmd/node/flags.go new file mode 100644 index 00000000..c4d0971a --- /dev/null +++ b/cmd/node/flags.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/spf13/pflag" + + "github.com/blocklessnetworking/b7s/config" + "github.com/blocklessnetworking/b7s/node" +) + +// Default values. +const ( + defaultPort = 0 + defaultAddress = "0.0.0.0" + defaultDB = "db" + defaultConcurrency = uint(node.DefaultConcurrency) + + defaultRole = "worker" +) + +func parseFlags() *config.Config { + + var cfg config.Config + + pflag.StringVarP(&cfg.Log.Level, "log-level", "l", "info", "log level to use") + pflag.StringVarP(&cfg.DatabasePath, "db", "d", defaultDB, "path to the database used for persisting node data") + + // Node configuration. + pflag.StringVarP(&cfg.Role, "role", "r", defaultRole, "role this note will have in the Blockless protocol (head or worker)") + pflag.StringVarP(&cfg.Host.Address, "address", "a", defaultAddress, "address that the libp2p host will use") + pflag.UintVarP(&cfg.Host.Port, "port", "p", defaultPort, "port that the libp2p host will use") + pflag.StringVar(&cfg.Host.PrivateKey, "private-key", "", "private key that the libp2p host will use") + pflag.UintVarP(&cfg.Concurrency, "concurrency", "c", defaultConcurrency, "maximum number of requests node will process in parallel") + pflag.StringVar(&cfg.API, "rest-api", "", "address where the head node REST API will listen on") + pflag.StringSliceVar(&cfg.BootNodes, "boot-nodes", nil, "list of addresses that this node will connect to on startup, in multiaddr format") + + pflag.StringVar(&cfg.Workspace, "workspace", "./workspace", "directory that the node can use for file storage") + pflag.StringVar(&cfg.Runtime, "runtime", "", "runtime address (used by the worker node)") + + pflag.Parse() + + return &cfg +} diff --git a/cmd/node/main.go b/cmd/node/main.go new file mode 100644 index 00000000..1ac155d5 --- /dev/null +++ b/cmd/node/main.go @@ -0,0 +1,231 @@ +package main + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + + "github.com/cockroachdb/pebble" + "github.com/labstack/echo/v4" + "github.com/rs/zerolog" + "github.com/ziflex/lecho/v3" + + "github.com/blocklessnetworking/b7s/api" + "github.com/blocklessnetworking/b7s/executor" + "github.com/blocklessnetworking/b7s/function" + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/node" + "github.com/blocklessnetworking/b7s/peerstore" + "github.com/blocklessnetworking/b7s/store" +) + +const ( + success = 0 + failure = 1 +) + +func main() { + os.Exit(run()) +} + +func run() int { + + // Signal catching for clean shutdown. + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + + // Initialize logging. + log := zerolog.New(os.Stderr).With().Timestamp().Logger().Level(zerolog.DebugLevel) + + // Parse CLI flags and validate that the configuration is valid. + cfg := parseFlags() + + // Set log level. + level, err := zerolog.ParseLevel(cfg.Log.Level) + if err != nil { + log.Error().Err(err).Str("level", cfg.Log.Level).Msg("could not parse log level") + return failure + } + log = log.Level(level) + + // Determine node role. + role, err := parseNodeRole(cfg.Role) + if err != nil { + log.Error().Err(err).Str("role", cfg.Role).Msg("invalid node role specified") + return failure + } + + // Open the pebble database. + pdb, err := pebble.Open(cfg.DatabasePath, &pebble.Options{}) + if err != nil { + log.Error().Err(err).Str("db", cfg.DatabasePath).Msg("could not open pebble database") + return failure + } + defer pdb.Close() + + // Create a new store. + store := store.New(pdb) + + peerstore := peerstore.New(store) + + // Get the list of dial back peers. + peers, err := peerstore.Peers() + if err != nil { + log.Error().Err(err).Msg("could not get list of dial-back peers") + return failure + } + peerAddrs, err := getPeerAddresses(peers) + if err != nil { + log.Error().Err(err).Msg("could not get peer addresses") + return failure + } + + // Get the list of boot nodes addresses. + bootNodeAddrs, err := getBootNodeAddresses(cfg.BootNodes) + if err != nil { + log.Error().Err(err).Msg("could not get boot node addresses") + return failure + } + + // Create libp2p host. + host, err := host.New(log, cfg.Host.Address, cfg.Host.Port, + host.WithPrivateKey(cfg.Host.PrivateKey), + host.WithBootNodes(bootNodeAddrs), + host.WithDialBackPeers(peerAddrs), + ) + if err != nil { + log.Error().Err(err).Str("key", cfg.Host.PrivateKey).Msg("could not create host") + return failure + } + + log.Info(). + Str("id", host.ID().String()). + Strs("addresses", host.Addresses()). + Int("boot_nodes", len(bootNodeAddrs)). + Int("dial_back_peers", len(peerAddrs)). + Msg("created host") + + // Set node options. + opts := []node.Option{ + node.WithRole(role), + node.WithConcurrency(cfg.Concurrency), + } + + // If this is a worker node, initialize an executor. + if role == blockless.WorkerNode { + + // Crete an executor. + executor, err := executor.New(log, + executor.WithWorkDir(cfg.Workspace), + executor.WithRuntimeDir(cfg.Runtime), + ) + if err != nil { + log.Error(). + Err(err). + Str("workspace", cfg.Workspace). + Str("runtime", cfg.Runtime). + Msg("could not create an executor") + return failure + } + + opts = append(opts, node.WithExecutor(executor)) + } + + // Create function store. + functionStore := function.NewHandler(log, store, cfg.Workspace) + + // Instantiate node. + node, err := node.New(log, host, store, peerstore, functionStore, opts...) + if err != nil { + log.Error().Err(err).Msg("could not create node") + return failure + } + + // Create the main context. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + done := make(chan struct{}) + failed := make(chan struct{}) + + // Start node main loop in a separate goroutine. + go func() { + + log.Info(). + Str("role", role.String()). + Msg("Blockless Node starting") + + err := node.Run(ctx) + if err != nil { + log.Error().Err(err).Msg("Blockless Node failed") + close(failed) + } else { + close(done) + } + + log.Info().Msg("Blockless Node stopped") + }() + + // If we're a head node - start the REST API. + if role == blockless.HeadNode { + + if cfg.API == "" { + log.Error().Err(err).Msg("REST API address is required") + return failure + } + + // Create echo server and iniialize logging. + server := echo.New() + server.HideBanner = true + server.HidePort = true + + elog := lecho.From(log) + server.Logger = elog + server.Use(lecho.Middleware(lecho.Config{Logger: elog})) + + // Create an API handler. + api := api.New(log, node) + + // Set endpoint handlers. + server.POST("/api/v1/functions/execute", api.Execute) + server.GET("/api/v1/functions/:id/install", api.Install) + server.GET("/api/v1/functions/requests/:id/result", api.ExecutionResult) + + // Start API in a separate goroutine. + go func() { + + log.Info().Msg("Node API starting") + err := server.Start(cfg.API) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Warn().Err(err).Msg("Node API failed") + close(failed) + } else { + close(done) + } + + log.Info().Msg("Node API stopped") + }() + } + + select { + case <-sig: + log.Info().Msg("Blockless Node stopping") + case <-done: + log.Info().Msg("Blockless Node done") + case <-failed: + log.Info().Msg("Blockless Node aborted") + return failure + } + + // If we receive a second interrupt signal, exit immediately. + go func() { + <-sig + log.Warn().Msg("forcing exit") + os.Exit(1) + }() + + return success +} diff --git a/cmd/node/parse.go b/cmd/node/parse.go new file mode 100644 index 00000000..794a3998 --- /dev/null +++ b/cmd/node/parse.go @@ -0,0 +1,22 @@ +package main + +import ( + "errors" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +func parseNodeRole(role string) (blockless.NodeRole, error) { + + switch role { + + case blockless.HeadNodeLabel: + return blockless.HeadNode, nil + + case blockless.WorkerNodeLabel: + return blockless.WorkerNode, nil + + default: + return 0, errors.New("invalid node role") + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..bb102bd4 --- /dev/null +++ b/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v2" +) + +// Load will load the config file from the given location. +func Load(file string) (*Config, error) { + + // Read config file. + payload, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("could not read file: %w", err) + } + + // Unmarshal file. + var config Config + err = yaml.Unmarshal(payload, &config) + if err != nil { + return nil, fmt.Errorf("could not unmarshal file: %w", err) + } + + return &config, nil +} diff --git a/config/model.go b/config/model.go new file mode 100644 index 00000000..a9856f69 --- /dev/null +++ b/config/model.go @@ -0,0 +1,28 @@ +package config + +// Config describes the Blockless configuration options. +type Config struct { + Log Log + DatabasePath string + Role string + BootNodes []string + Concurrency uint + + Host Host + API string + Runtime string + + Workspace string +} + +// Host describes the libp2p host that the node will use. +type Host struct { + Port uint + Address string + PrivateKey string +} + +// Log describes the logging configuration. +type Log struct { + Level string +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 75717c1e..45e86688 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,6 +28,7 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2 ## setup COPY ./dist/b7s b7s +COPY ./dist/b7s-keygen b7s-keygen COPY ./configs/docker-config.yaml docker-config.yaml ## run script diff --git a/docker/run.sh b/docker/run.sh index 18223df2..707977a5 100644 --- a/docker/run.sh +++ b/docker/run.sh @@ -60,7 +60,7 @@ else if [ -n "$KEY_PASSWORD" ]; then echo $KEY_PASSWORD | blsd keys export node --keyring-backend=test --home=/app/.blockless-chain > /app/keys/wallet.key fi - ../b7s keygen + ../b7s-keygen # Backup keys if [ -n "$KEY_PATH" ]; then # backup the on chain node identity @@ -72,8 +72,8 @@ fi # run template against the config file # load env var array as a data source -/app/gomplate -d 'boot_nodes=env:///BOOT_NODES?type=text/csv' -f /app/docker-config.yaml -o /app/docker-config-env.yaml +# /app/gomplate -d 'boot_nodes=env:///BOOT_NODES?type=text/csv' -f /app/docker-config.yaml -o /app/docker-config-env.yaml # run the node cd /app -./b7s -c docker-config-env.yaml \ No newline at end of file +./b7s \ No newline at end of file diff --git a/executor/config.go b/executor/config.go new file mode 100644 index 00000000..eae10ea4 --- /dev/null +++ b/executor/config.go @@ -0,0 +1,51 @@ +package executor + +import ( + "github.com/spf13/afero" +) + +// defaultConfig used to create Executor. +var defaultConfig = Config{ + WorkDir: "workspace", + RuntimeDir: "", + ExecutableName: blocklessCli, + FS: afero.NewOsFs(), +} + +// Config represents the Executor configuration. +type Config struct { + WorkDir string // directory where files needed for the execution are stored + RuntimeDir string // directory where the executable can be found + ExecutableName string // name for the executable + FS afero.Fs // FS accessor +} + +type Option func(*Config) + +// WithWorkDir sets the workspace directory for the executor. +func WithWorkDir(dir string) Option { + return func(cfg *Config) { + cfg.WorkDir = dir + } +} + +// WithRuntimeDir sets the runtime directory for the executor. +func WithRuntimeDir(dir string) Option { + return func(cfg *Config) { + cfg.RuntimeDir = dir + } +} + +// WithFS sets the FS handler used by the executor. +func WithFS(fs afero.Fs) Option { + return func(cfg *Config) { + cfg.FS = fs + } +} + +// WithExecutableName sets the name of the executable that should be ran. +func WithExecutableName(name string) Option { + return func(cfg *Config) { + cfg.ExecutableName = name + } +} diff --git a/executor/config_internal_test.go b/executor/config_internal_test.go new file mode 100644 index 00000000..956efaf7 --- /dev/null +++ b/executor/config_internal_test.go @@ -0,0 +1,56 @@ +package executor + +import ( + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func TestWithWorkDir(t *testing.T) { + + const workdir = "/var/tmp/b7s" + + cfg := Config{ + WorkDir: "", + } + + WithWorkDir(workdir)(&cfg) + require.Equal(t, workdir, cfg.WorkDir) +} + +func TestWithRuntimeDir(t *testing.T) { + + const runtimeDir = "/usr/local/bin" + + cfg := Config{ + RuntimeDir: "", + } + + WithRuntimeDir(runtimeDir)(&cfg) + require.Equal(t, runtimeDir, cfg.RuntimeDir) +} + +func TestWithFS(t *testing.T) { + + var fs = afero.NewOsFs() + + cfg := Config{ + FS: afero.NewMemMapFs(), + } + + WithFS(fs)(&cfg) + require.Equal(t, fs, cfg.FS) +} + +func TestWithExecutableName(t *testing.T) { + + var name = "super-special-executable" + + cfg := Config{ + ExecutableName: "", + } + + WithExecutableName(name)(&cfg) + require.Equal(t, name, cfg.ExecutableName) +} diff --git a/executor/execute.go b/executor/execute.go new file mode 100644 index 00000000..d54f3632 --- /dev/null +++ b/executor/execute.go @@ -0,0 +1,125 @@ +package executor + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/blocklessnetworking/b7s/models/execute" +) + +// execute handles the actual execution of the Blockless function. It returns the +// standard output of the blockless-cli that handled the execution. `Function` +// typically takes this output and uses it to create the appropriate execution response. +func (e *Executor) execute(requestID string, req execute.Request) (string, execute.Usage, error) { + + e.log.Info(). + Str("id", req.FunctionID). + Str("request_id", requestID). + Msg("processing execution request") + + // Generate paths for execution request. + paths := e.generateRequestPaths(requestID, req.FunctionID, req.Method) + + err := e.cfg.FS.MkdirAll(paths.workdir, defaultPermissions) + if err != nil { + return "", execute.Usage{}, fmt.Errorf("could not setup working directory for execution (dir: %s): %w", paths.workdir, err) + } + // Remove all temporary files after we're done. + defer func() { + err := e.cfg.FS.RemoveAll(paths.workdir) + if err != nil { + e.log.Error().Err(err).Str("dir", paths.workdir). + Msg("could not remove request working directory") + } + }() + + e.log.Debug(). + Str("dir", paths.workdir). + Str("request_id", requestID). + Msg("working directory for the request") + + err = e.writeExecutionManifest(req, paths) + if err != nil { + return "", execute.Usage{}, fmt.Errorf("could not write execution manifest: %w", err) + } + + // Create command that will be executed. + cmd := e.createCmd(paths, req) + + e.log.Debug(). + Str("request_id", requestID). + Int("env_vars_set", len(cmd.Env)). + Str("cmd", cmd.String()). + Msg("command ready for execution") + + // Execute the command and collect output. + start := time.Now() + out, err := cmd.Output() + end := time.Now() + if err != nil { + return "", execute.Usage{}, fmt.Errorf("command execution failed: %w", err) + } + + e.log.Info(). + Str("request_id", requestID). + Msg("command executed successfully") + + // Create usage information. + duration := end.Sub(start) + usage := procStateToUsage(cmd.ProcessState) + usage.WallClockTime = duration + + return string(out), usage, nil +} + +// createCmd will create the command to be executed, prepare working directory, environment, standard input and all else. +func (e *Executor) createCmd(paths requestPaths, req execute.Request) *exec.Cmd { + + // Prepare command to be executed. + exePath := filepath.Join(e.cfg.RuntimeDir, e.cfg.ExecutableName) + + // Prepare CLI arguments. + var args []string + args = append(args, paths.manifest) + for _, param := range req.Parameters { + if param.Value != "" { + args = append(args, param.Value) + } + } + + cmd := exec.Command(exePath, args...) + cmd.Dir = paths.workdir + + // Setup stdin of the command. + var stdin io.Reader + if req.Config.Stdin != nil { + stdin = strings.NewReader(*req.Config.Stdin) + } + cmd.Stdin = stdin + + // Setup environment. + // First, pass through our environment variables. + cmd.Env = os.Environ() + + // Second, set the variables set in the execution request. + names := make([]string, 0, len(req.Config.Environment)) + for _, env := range req.Config.Environment { + e := fmt.Sprintf("%s=%s", env.Name, env.Value) + cmd.Env = append(cmd.Env, e) + + names = append(names, env.Name) + } + + // Third and final - set the `BLS_LIST_VARS` variable with + // the list of names of the variables from the execution request. + blsList := strings.Join(names, ";") + blsEnv := fmt.Sprintf("%s=%s", blsListEnvName, blsList) + cmd.Env = append(cmd.Env, blsEnv) + + return cmd +} diff --git a/executor/execute_internal_test.go b/executor/execute_internal_test.go new file mode 100644 index 00000000..91660878 --- /dev/null +++ b/executor/execute_internal_test.go @@ -0,0 +1,190 @@ +package executor + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestExecute_CreateCMD(t *testing.T) { + + var ( + runtimeDir = "/usr/local/bin" + workdir = "/var/tmp/b7s" + functionID = "function-id" + functionMethod = "function-method" + + executablePath = filepath.Join(runtimeDir, blocklessCli) + + requestID = mocks.GenericUUID.String() + stdin = "dummy stdin payload" + environment = getEnvVars(t) + + request = execute.Request{ + Config: execute.Config{ + Stdin: &stdin, + Environment: environment, + }, + } + ) + + executor := Executor{ + log: mocks.NoopLogger, + cfg: Config{ + RuntimeDir: runtimeDir, + WorkDir: workdir, + ExecutableName: blocklessCli, + }, + } + paths := executor.generateRequestPaths(requestID, functionID, functionMethod) + + // Create command. + cmd := executor.createCmd(paths, request) + require.NotNil(t, cmd) + + // Verify command to be executed is correct. + require.Equal(t, executablePath, cmd.Path) + + // Verify CLI arguments are correct. + require.Len(t, cmd.Args, 2) + require.Equal(t, executablePath, cmd.Args[0]) + require.Equal(t, paths.manifest, cmd.Args[1]) + + // Verify working directory is correct. + require.Equal(t, paths.workdir, cmd.Dir) + + // Verify the environment variables are as expected (sort the slices). + expectedEnv := getExpectedEnvVars(t, environment) + cmdEnv := cmd.Env + + sort.Strings(expectedEnv) + sort.Strings(cmdEnv) + + require.Equal(t, expectedEnv, cmdEnv) +} + +func TestExecute_WriteManifest(t *testing.T) { + + const ( + manifestPath = "/manifest.json" + entry = "entry-path-value" + fsRoot = "fs-root-path-value" + ) + + const expectedManifest = `{ + "fs_root_path": "fs-root-path-value", + "entry": "entry-path-value", + "limited_fuel": 100000000, + "limited_memory": 200, + "permissions": [ + "permission-string-a", + "permission-string-b", + "permission-string-c" + ] +}` + + request := execute.Request{ + Config: execute.Config{ + Permissions: []string{ + "permission-string-a", + "permission-string-b", + "permission-string-c", + }, + }, + } + paths := requestPaths{ + manifest: manifestPath, + entry: entry, + fsRoot: fsRoot, + } + + fs := afero.NewMemMapFs() + executor := Executor{ + log: mocks.NoopLogger, + cfg: Config{ + FS: fs, + }, + } + + err := executor.writeExecutionManifest(request, paths) + require.NoError(t, err) + + read, err := afero.ReadFile(fs, manifestPath) + require.NoError(t, err) + require.Equal(t, expectedManifest, string(read)) +} + +func TestExecute_WriteFile(t *testing.T) { + + const ( + filename = "dummy-file.txt" + content = "This is the content of the file we want to create." + ) + fs := afero.NewMemMapFs() + + executor := Executor{ + log: mocks.NoopLogger, + cfg: Config{ + FS: fs, + }, + } + + err := executor.writeFile(filename, []byte(content)) + require.NoError(t, err) + + read, err := afero.ReadFile(fs, filename) + require.NoError(t, err) + require.Equal(t, content, string(read)) +} + +func getEnvVars(t *testing.T) []execute.EnvVar { + t.Helper() + + const ( + nameRoot = "executor-env-var-name" + valueRoot = "executor-env-var-value" + count = 10 + ) + + env := make([]execute.EnvVar, 0, count) + for i := 0; i < count; i++ { + + e := execute.EnvVar{ + Name: fmt.Sprintf("%s-%d", nameRoot, i), + Value: fmt.Sprintf("%s-%d", valueRoot, i), + } + + env = append(env, e) + } + + return env +} + +// getExpectedEnvVars return the complete list of environment variables expected by the CLI. +func getExpectedEnvVars(t *testing.T, environment []execute.EnvVar) []string { + t.Helper() + + out := os.Environ() + + names := make([]string, 0, len(environment)) + for _, env := range environment { + e := fmt.Sprintf("%s=%s", env.Name, env.Value) + out = append(out, e) + + names = append(names, env.Name) + } + + list := fmt.Sprintf("%s=%s", blsListEnvName, strings.Join(names, ";")) + out = append(out, list) + + return out +} diff --git a/executor/executor.go b/executor/executor.go new file mode 100644 index 00000000..2939f43e --- /dev/null +++ b/executor/executor.go @@ -0,0 +1,45 @@ +package executor + +import ( + "fmt" + "path/filepath" + + "github.com/rs/zerolog" +) + +// Executor provides the capabilities to run external applications. +type Executor struct { + log zerolog.Logger + cfg Config +} + +// New creates a new Executor with the specified working directory. +func New(log zerolog.Logger, options ...Option) (*Executor, error) { + + cfg := defaultConfig + for _, option := range options { + option(&cfg) + } + + // We need the absolute path for the runtime, since we'll be changing + // the working directory on execution. + runtime, err := filepath.Abs(cfg.RuntimeDir) + if err != nil { + return nil, fmt.Errorf("could not get absolute path for runtime (path: %s): %w", cfg.RuntimeDir, err) + } + cfg.RuntimeDir = runtime + + // Verify the runtime path is valid. + cliPath := filepath.Join(cfg.RuntimeDir, cfg.ExecutableName) + _, err = cfg.FS.Stat(cliPath) + if err != nil { + return nil, fmt.Errorf("invalid runtime path, cli not found (path: %s): %w", cliPath, err) + } + + e := Executor{ + log: log.With().Str("component", "executor").Logger(), + cfg: cfg, + } + + return &e, nil +} diff --git a/executor/executor_integration_test.go b/executor/executor_integration_test.go new file mode 100644 index 00000000..0a48c941 --- /dev/null +++ b/executor/executor_integration_test.go @@ -0,0 +1,156 @@ +//go:build integration +// +build integration + +package executor_test + +import ( + "crypto/md5" + "crypto/rand" + "fmt" + "io/ioutil" + "os" + "path" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/executor" + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/models/response" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +const ( + runtimeDirEnv = "B7S_INTEG_RUNTIME_DIR" + cleanupDisableEnv = "B7S_INTEG_CLEANUP_DISABLE" +) + +func TestExecutor_Execute(t *testing.T) { + + const ( + dirPattern = "b7s-executor-integration-test-" + + testFunction = "./testdata/md5sum/md5sum.wasm" + + functionID = "function-id" + requestID = "dummy-request-id" + + chunkSize = 128 + fileSize = 256 + ) + + // Setup directories. + workspace, err := os.MkdirTemp("", dirPattern) + require.NoError(t, err) + if !cleanupDisabled() { + defer os.RemoveAll(workspace) + } + + var ( + workdir = path.Join(workspace, "t", requestID) // request work directory + fsRoot = path.Join(workdir, "fs") // function FS root + functiondir = path.Join(workspace, functionID) // function location + ) + + t.Logf("working directory: %v", workspace) + createDirs(t, workdir, fsRoot, functiondir) + + // Stage executable to working directory. + copyFunction(t, testFunction, functiondir) + + // Create a random testfile. + testfile, hash := createTestFile(t, fsRoot, fileSize) + + // Create executor. + executor, err := executor.New( + mocks.NoopLogger, + executor.WithWorkDir(workspace), + executor.WithRuntimeDir(os.Getenv(runtimeDirEnv)), + ) + require.NoError(t, err) + + // Execute the function. + req := execute.Request{ + FunctionID: functionID, + Method: path.Base(testFunction), + Parameters: []execute.Parameter{ + {Value: "--chunk"}, + {Value: fmt.Sprintf("%v", chunkSize)}, + {Value: "--file"}, + {Value: path.Base(testfile)}, // Specify name only because the path is relative to FS root. + }, + } + + res, err := executor.Function(requestID, req) + require.NoError(t, err) + + // Verify the execution result. + require.Equal(t, response.CodeOK, res.Code) + require.Equal(t, requestID, res.RequestID) + require.Equal(t, hash, res.Result) + + // Verify usage info - for now, only that they are non-zero. + cpuTimeTotal := res.Usage.CPUSysTime + res.Usage.CPUUserTime + require.Greater(t, cpuTimeTotal, time.Duration(0)) + require.NotZero(t, res.Usage.WallClockTime) +} + +func createTestFile(t *testing.T, dir string, size int) (string, string) { + t.Helper() + + const ( + filePattern = "testfile-" + ) + + f, err := ioutil.TempFile(dir, filePattern) + require.NoError(t, err) + defer f.Close() + + buf := make([]byte, size) + + _, err = rand.Read(buf) + require.NoError(t, err) + + n, err := f.Write(buf) + require.NoError(t, err) + require.Equal(t, size, n) + + // Calculate the hash of the file payload. + hash := md5.New() + _, err = hash.Write(buf) + require.NoError(t, err) + + md5sum := fmt.Sprintf("%x", hash.Sum(nil)) + + return f.Name(), md5sum +} + +func createDirs(t *testing.T, dirs ...string) { + t.Helper() + + // Create directory structure. + for _, dir := range dirs { + err := os.MkdirAll(dir, os.ModePerm) + require.NoError(t, err) + } + + return +} + +func copyFunction(t *testing.T, filepath string, target string) { + t.Helper() + + payload, err := os.ReadFile(filepath) + require.NoError(t, err) + + _, name := path.Split(filepath) + targetPath := path.Join(target, name) + + err = os.WriteFile(targetPath, payload, os.ModePerm) + require.NoError(t, err) +} + +func cleanupDisabled() bool { + return os.Getenv(cleanupDisableEnv) == "yes" +} diff --git a/executor/executor_test.go b/executor/executor_test.go new file mode 100644 index 00000000..14b1dd73 --- /dev/null +++ b/executor/executor_test.go @@ -0,0 +1,46 @@ +package executor_test + +import ( + "path" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/executor" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestExecutor_Create(t *testing.T) { + t.Run("nominal case", func(t *testing.T) { + + var ( + runtimeDir = "/usr/local/bin" + cliPath = path.Join(runtimeDir, "blockless-cli") + ) + + fs := afero.NewMemMapFs() + fs.Create(cliPath) + + _, err := executor.New(mocks.NoopLogger, + executor.WithRuntimeDir(runtimeDir), + executor.WithFS(fs), + ) + require.NoError(t, err) + }) + t.Run("missing runtime path", func(t *testing.T) { + + const ( + runtimeDir = "/usr/local/bin" + ) + + // Use empty FS that surely will not have the runtime anywhere. + executor, err := executor.New(mocks.NoopLogger, + executor.WithRuntimeDir(runtimeDir), + executor.WithFS(afero.NewMemMapFs()), + ) + require.Error(t, err) + require.Nil(t, executor) + }) + +} diff --git a/executor/function.go b/executor/function.go new file mode 100644 index 00000000..cf27a1f6 --- /dev/null +++ b/executor/function.go @@ -0,0 +1,34 @@ +package executor + +import ( + "fmt" + + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/models/response" +) + +// Function will execute the Blockless function defined by the execution request. +func (e *Executor) Function(requestID string, req execute.Request) (execute.Result, error) { + + // Execute the function. + out, usage, err := e.execute(requestID, req) + if err != nil { + + res := execute.Result{ + Code: response.CodeError, + RequestID: requestID, + Usage: usage, + } + + return res, fmt.Errorf("function execution failed: %w", err) + } + + res := execute.Result{ + Code: response.CodeOK, + RequestID: requestID, + Result: out, + Usage: usage, + } + + return res, nil +} diff --git a/executor/manifest.go b/executor/manifest.go new file mode 100644 index 00000000..bd659bb1 --- /dev/null +++ b/executor/manifest.go @@ -0,0 +1,57 @@ +package executor + +import ( + "encoding/json" + "fmt" + + "github.com/blocklessnetworking/b7s/models/execute" +) + +// writeExecutionManifest will write a predefined execution manifest to disk. +func (e *Executor) writeExecutionManifest(req execute.Request, paths requestPaths) error { + + manifest := struct { + FSRootPath string `json:"fs_root_path,omitempty"` + Entry string `json:"entry,omitempty"` + LimitedFuel int `json:"limited_fuel,omitempty"` + LimitedMemory int `json:"limited_memory,omitempty"` + Permissions []string `json:"permissions,omitempty"` + }{ + FSRootPath: paths.fsRoot, + Entry: paths.entry, + LimitedFuel: 100_000_000, + LimitedMemory: 200, + Permissions: req.Config.Permissions, + } + + // Serialize manifest. + encoded, err := json.MarshalIndent(manifest, "", "\t") + if err != nil { + return fmt.Errorf("could not marshal function manifest: %w", err) + } + + // Write manifest to disk. + err = e.writeFile(paths.manifest, encoded) + if err != nil { + return fmt.Errorf("could not write manifest to disk: %w", err) + } + + return nil +} + +// writeFile is a helper function wrapping the three OS-level calls - os.Create, file Write() and file Close(). +func (e *Executor) writeFile(name string, data []byte) error { + + f, err := e.cfg.FS.Create(name) + if err != nil { + return fmt.Errorf("could not create file: %w", err) + } + defer f.Close() + + _, err = f.Write(data) + if err != nil { + return fmt.Errorf("could not write to file: %w", err) + } + + return nil +} diff --git a/executor/memory.go b/executor/memory.go new file mode 100644 index 00000000..eecd36eb --- /dev/null +++ b/executor/memory.go @@ -0,0 +1,20 @@ +//go:build !windows +// +build !windows + +package executor + +import ( + "os" + "syscall" +) + +// getMemUsage returns process max memory usage in kilobytes. +func getMemUsage(ps *os.ProcessState) int64 { + + usage, ok := ps.SysUsage().(*syscall.Rusage) + if !ok { + return 0 + } + + return usage.Maxrss +} diff --git a/executor/memory_win.go b/executor/memory_win.go new file mode 100644 index 00000000..0dccf490 --- /dev/null +++ b/executor/memory_win.go @@ -0,0 +1,14 @@ +//go:build windows +// +build windows + +package executor + +import ( + "os" +) + +// getMemUsage returns process max memory usage in kilobytes. +func getMemUsage(ps *os.ProcessState) int64 { + // FIXME: See how to retrieve memory usage on windows. + return 0 +} diff --git a/executor/params.go b/executor/params.go new file mode 100644 index 00000000..49539ffd --- /dev/null +++ b/executor/params.go @@ -0,0 +1,11 @@ +package executor + +import ( + "os" +) + +const ( + defaultPermissions = os.ModePerm + blocklessCli = "blockless-cli" + blsListEnvName = "BLS_LIST_VARS" +) diff --git a/executor/path.go b/executor/path.go new file mode 100644 index 00000000..3ffe9644 --- /dev/null +++ b/executor/path.go @@ -0,0 +1,27 @@ +package executor + +import ( + "path" +) + +// requestPaths defines a number of path components relevant to a request. +type requestPaths struct { + workdir string + fsRoot string + manifest string + entry string +} + +func (e *Executor) generateRequestPaths(requestID string, functionID string, method string) requestPaths { + + // Workdir Should be the root for all other paths. + workdir := path.Join(e.cfg.WorkDir, "t", requestID) + paths := requestPaths{ + workdir: workdir, + fsRoot: path.Join(workdir, "fs"), + manifest: path.Join(workdir, "runtime-manifest.json"), + entry: path.Join(e.cfg.WorkDir, functionID, method), + } + + return paths +} diff --git a/executor/path_internal_test.go b/executor/path_internal_test.go new file mode 100644 index 00000000..8832dcbd --- /dev/null +++ b/executor/path_internal_test.go @@ -0,0 +1,38 @@ +package executor + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestExecutor_RequestPaths(t *testing.T) { + + const ( + workdir = "/var/tmp/b7s/workspace" + requestID = "request-id" + functionID = "function-id" + functionMethod = "function-method" + + // Expected paths. + expectedRequestWorkdir = workdir + "/t/request-id" + expectedFSRoot = workdir + "/t/request-id/fs" + expectedManifestPath = workdir + "/t/request-id/runtime-manifest.json" + expectedEntry = workdir + "/function-id/function-method" + ) + + executor := &Executor{ + log: mocks.NoopLogger, + cfg: Config{ + WorkDir: workdir, + }, + } + + paths := executor.generateRequestPaths(requestID, functionID, functionMethod) + assert.Equal(t, expectedRequestWorkdir, paths.workdir) + assert.Equal(t, expectedEntry, paths.entry) + assert.Equal(t, expectedFSRoot, paths.fsRoot) + assert.Equal(t, expectedManifestPath, paths.manifest) +} diff --git a/executor/testdata/md5sum/README.md b/executor/testdata/md5sum/README.md new file mode 100644 index 00000000..2597f618 --- /dev/null +++ b/executor/testdata/md5sum/README.md @@ -0,0 +1,12 @@ +# md5sum + +## Description + +md5sum is a small wasi executable that returns the md5 checksum of a file. + +## Usage + +``` + -c, --chunk int buffer size when copying (default 1024) + -f, --file string filename to checksum +``` diff --git a/executor/testdata/md5sum/md5sum.wasm b/executor/testdata/md5sum/md5sum.wasm new file mode 100755 index 0000000000000000000000000000000000000000..4636cd2ee967a547756e5a074e9933ee5c6558e5 GIT binary patch literal 685866 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

Z7Qs*KhR5m3MAFezu`$Sdy@>Q7lFpVN9zdjy zk`dnbtc??Km^`s1<7eX#A6{g(+It^=a7n8rP~gd!#IX7 zekFL&TPN-m^px69@DTS3oc`TkFtJ&o?TvkxT7ez5QgOsy z(O#n&INE|eJEXk|&?3{Gf-vnR6p=y}A&z(v4@rAwuojPil(gqb5{?TH}EX>TSx zy@`{7Jk|ezl(gp$@^#w#g|zpv;6ZPlxXMj?Yh~@-L1b~0qCL$GkYyD|w>pr*1|<>pe7rrLMFFfvlpIPokq%?`Y1f4+^K&6D&#y)?$g*!y-#l$df2S-cr;@$$zb7>;XEyeMdg%0f^O zrD*a{8L%%N@*&k-Zm}OAcBULV4zVe#f`TZqx8Md0UN1t$TuDB%%IiBL7^jkDx(~xp zaUv2{^0HwH$6b)QQpgnmoC!e8BWeW0mbqpiRISOG!xd1d4!ma2T$sF8TMx&#sQcr? z)M6`SU6%^N17*bp)rV_{o?bfM->@E~G=~BPat5zLj$2H4u!_^Y3&cnXm{#Ml8#PH- zD#$jc^#FT1;J{neVm#P;P?e_2A+}XZ?ToeoTv(8bYOyUzK&g1@sDbsMwu`EpxI`_r zQ?MH8rzq>51I%Yq$`+PB_uXKlURDufeZHY{syXVhGJ2eoF*oz#KZ@u7&yiw zl``n_6y#E3kxDFl|3M_iF{d?3!S4t7Ks_8kB+{Rnw9BCV>jL?k5W3sxAtA*;L;x2+ zfy^i=vMW9(IB+BAH_3+!>d*&H4M+yQLUnpJB&Ae{g97=cR6-R($+ayg8V81Pb5kC;4NJgoHOfTkJaD`4XH}v$YUPM>?l|V3pbDA`1A+41_JZ%Tq3e5 z$Px-XzXLMsV+Hvh*~Gh)nAcn;HZXG>xW9>LiZ+8O&T^K(A|-+4EP>NX0wpYg?MebG zi1fV+ayunZN(uOSB2iWnYl)SwR*gNTRJi+zRS19!!qCPS2VXclDhR)hVuj3xFyn#v z+Ti|4D%{s60`r}R2P?S^6y0|($mf*SMv4_cidco1RAc#1OrcXiht9*p|7Qd&1wy}| z3zgtK;&YIEFk?1?m<10fMk9!kq;=+6feAI9Tr1QEYkn%#5l$W45*37hqaS1^6J$o~ zPlbFsY-m-y9{L%O(Mzd7A2$GMrSm|(Rgrt9jI(5x)=WGqV)Sqxn_c-I`%(s5B1Ah2 z$a5sJ^=*nZ25wBwOJuJ2u{v;04@^hW=*U^u0}mt8<4ozmuKGj1Ty-US>x7}|$}KUV zpvov@tGUWIz;QZ42{~#NXIcm>m%8-I z#!I2A0w`3d1z7_q#rwS21UU5;{CbttTL`4hF64Vz6zQ~9B>ApZ<$;SxcVA56ghZcnK&As73x!c>Zj3bRacvBFe}nhLW_QG>PgW366mUntC^ zhXfiC4KOQAiTqn57pAETWYT;A(i8ok9QZ8Awp6w(Fv`|C50y_0pqmABLEywn&!3d1 ziv-EaHgFI)p@K_4GKl#=#Y)#HQVv25s|PZIiriIxxN3&GPnD^zN>A^2!K5<$qC&3= z55WuLcR!f+5+eLnIzt#rwv>oqmBpZL%W(&Zu&M$L`_Yq}Afl?uYS>wrpmEY=3j^mZ%z0MLHHDL60}gyxty)lK)PaGJj&#;tQo!U2ic695lExfNoFJ%{~_`gij~>~!FY|xb0BHz zA{b8-`6bAfC>OzW0Hk&q$Tx?B^kif(Sdrlf797BU5WDf9sUF`;B~A6j@8CkiCcdpo z8aC<9!9}=;hn!*E4RW}JhX-?&MvI~bd6$O8BAIL%rExkHDduxG#k8M*zwuIDG1Q{I zwi2rEq7PC9@|sPsX;mQgBbrmhF{lU9Pfv}Pks;{srcS_@}1B72nx z_tF^!WGIqwZ;R0v%}ss6eFKgE+F>Y!`-K?$I&gCd;r>>mu#DrV0n`RmF`ia<28J64 zKo?~8+MpO?ec(e0uTbB(7(Lqj=P=iWL11p-t@n5N=Ih;5g zF3ZW~lw#8q=4Yg0R2zKJfm=Ma#y`el6$Z|pI?i|uI7RaG&~c1kO|UkpO%%;@KOQ1y zmL@S4EuyP?Qx%IhOUR!Mt>@@~Y0f^pY3yC{_4Cs&#=Ekh=CV9=hvz zC{5xc;8K$)s$fE8+kzTMT*du^hDm88A5Y-8j$Y*&$^YCI2wjnbCIP68UyEXY;APU# zEC>bnHZ?eOb)_)tGC+x@I_UW&>tbXWNXfeBrXsnIX#Gt2sRnp@ zS0LKyx)X#dTXqF1nUap$Esqd96a%8tmHX6{6a{xX;wdkaswQzBJJ~@{})2yE(B=uD>oq< zg4;~kA5CkNpCHx*s;Y;F)E~(UBIs$QpQ7O(!cqH}Jla@@M;aD9Ii#U$1M*B1NDE1; z8!8Jg1k(_m6KCR~dJm5}^PppuM{T^qMYEnbP<+vg)29!G7vbJIeN6~dm2f|cz75nt z>w}`dum1f$G9D19Z=|>fhUnKB(X&yQV5{DygbAsl6s>_4$m39BRks8c&77nTS}o1n z#OiR7%UsnMxHK4&*O0`vaPgo7EE=8O2W?a#95f!BK0J-%NM{Q7WOts$dQWvkW#l^QX{s3`v`ZaB237*+R)SnY&imnkm@;T*0~ z$6cl+Wb>NB0F=dFqWYyGe2dx=I$@J;B2kn|+G{^&5^6e&q zYzDG%E0F%*k?#PI-l#BQKz;Zc7lUNl4kgaUgCNP)#Yz(q*l2?FQI!xYr%>PqAgNY} z;mp?tk}883!N`dq!-7H9>_ve$ft*FGBAM?skWmykijig%DOEf%CLMuI7m9#R^E^&6 z)PPq@Gw3)LvKPo+a2FenA-11_>_%*xFyCh&Szl4~jC>>d)dPgR6 zBN?(6=S_yEs?Ca<9-L1%bTjHMI<&N$j`&EAO!8CB23@8xK-o(_rA2Jm59xamC%-AV zgkGSOG+*H9e+Um{Mf@ai$;hMtmnpz?JOWtDqSXab%i@b1YCX6>YFWrowR&A3wJdLv zJGCrfK&WNWsO7dRh!Uv~OPUFm8W>Fz3d}Ot37C!^oQ6`>kODPS0vbwLLs7*7kkm2? zXc>vS)G~_GfFAXSb4M|R-ho0rG4v_st7IlZBasUwc@Gm2FX}>*>vJ3pXoUpP5=zqF zA)>wLDWnp#OAo1+P_ikl7dQ5u3S zlX0sNgeub}A{6^6B{L2!1#iT#jT7&~P=0%#NJQjB+$|yv4kX{NK`vgT_^J#rdDicm z77{;Ya4;RXb`CkngM*d8NkTDOw-G2cJFdaO6S$~QpIkC?Q{azAxs1%zj=z5L3s3e_ zaYRmlzpQ?N~=!9N}E8wR`{B! z3TXF%WEI)>SCC43tvdtU<0o>Em)`yYE)@_P@RsR;xP26m8|Rk&9?1Tp^4Uiy_ER!= zffd;-xQSKKGVDjw${QJJ25GSq`7ZfBf{?VFDf?alNlWsQuc{FOOC`9P*efNt7`S#h zImjir8Ms`6yMdN1K@npl6?exi3;l|*r2z%5l;B)4aq$M)#Eyu~TQ$ax?DO9K`W)Ar zTqhwtrU=SMDmNb0HAcE>=#uEiv5EY55A_=ukyC7JY8_zHo&oJWYL46Wm zi9&FOe{+C0a0}glg{IYM$=QQCL%tD(@L(Tz8d6f6(SjNl%_C7-`L0J&)}k<`B^G&D zUW*u+l0hRcu5O3-d7QpHaGB%yv%12 z=jD3&fSuI`@>Ua!NUHd}T^A z;NhNnYaAStPQD^<+nab(bmf> z!t0e%RD{Rtq!3GLv_gz^H(gi4zMp(H8NnG`gm zW>VOkQe=`8>0ahe@Oq^bL*emyDSm)2Ka5Fn5Ga#k7?YwDB$Hy8OA0%aA`Frm|8S5ox`{B)NltOpoJFySiekN&IqP-F zPW_L`{*LJlzk?+kim?Yvc84q3T`bu~@L@UL=}dMrCA-VZ{OjwIeeoZY{f*l9gDlwv zO0oxC$sT6OeySvUh?4dD1>}M##5+#DWi8;am$?tTU#a&I@OYixg_4`3fb&d}9_Y!L z0?v~px&Ugf&W}RFB^AL%CX^k4m@Y0jb#a7ranZ{h1g}?0P!ArjlYlSzktFyd+XD0d z4gqPh%Kt$UNXh=mlKoRj_J%XrOO))NUgmkPOZMackgTfY5+(b9Wjr+y9d`uCe_2zI z0FtJ7{KMa2`lf0ir&R~}=tm+wK++VCe>}ulF`h9Q_riy%=BZQ0ha}@OFY|bKy;3zL z@NicR%1ZjNek-6YqU(6#9#99v`Q1!tN9Y)}&Xm?R!NiBf@D z3%Yon*YjTHpI;}-qjIuni!n~(BPexwa1uc&OM{aL*Fd5sgqhk<4TJBbcl=Rnv17B+AR<;Y)>I?{vV!b$1?-p{-Vks83EY~Nqg(|i;j zZW_d#Qu+nXAhyAhLjIs6iFE~+LfR7RU_L11RRt5`#V>Yrm(<&5%E<@ zH~S%$TYk&flvon~2I6;=WSfPMOty}aY1Xo2F+xX!b`FZ zl4M%~KPFqUOSU5<+aSo649{1}HU=I`IoJ5ty%iBl5~wcbNV45W$nIn;Ig;Mag7$Hv zcYr7X_? z&9W@hIiOjVrSMWVCox_@6~T+ut%~(X`I2m-L1&Qcvi|_kOjg;y8E7V}C68I;d!@Zfmsr}6l^ zhJ=Tz;T zv(oomDhC6;=d!T$Jy)-;NIWYE=odt;vi!lu6O`TukFf80p>;BPS{Gp8NsNqU&N@rM zEjmusfPyxqpaWYWs7dEc)8xTf_aV(>c$%vdLICH{_ZtP(rbYF5i1@+x)(_ctk9oazFe zQfib0_r~9Ns_xR`UwTk#DjpR^Pi4!kq$u1$GdW5pR$ffP*9LUU$}$)NaA=36pR$tI z7jqi<+v^;wyxJ;#nH>p|b$QaK*wrA}THXK6r`SVeR9ro(C9;tBrg7Z2ZbA-HLC9Fh z7zIauro07TXbBf-d=sny;P?ANbCk#q z@KmH4My#S;tRm}xj&VtKk)(=cQmHG-QpHKEo|I$Nl2|o%v0_p+DJNA^CRJsHRs5?n z*q>OnbFmtTrblS6f{Bst_!7pt!)3}~bA?r>SF>73ta_GX)yu`|dR_SUzK&4DT98=x zVXTuC*8NJ9s?G`eK);>oYD`eIOsu`oC zGQ^b%5^a226XS1khm8f&2sQsW@cazgmi7YM`^?g@{Sm7>dMN-^XSS@?C6dkQ#wWb`a zQevfDNnUG9h>K4p*>KmtnoD#Df__?#%T|{_jZ)#iZ39?I<#?+b=)> za;(m~SZ$Ao{{>f_SVQzhqO~nZt-lOJJFl6cr1GmP6%Ie8X%UJL^hUW<{&b}>6wRgZ zS0PwQY8_A2uo`iU!PruL^F zQ!WlGte!Jgx?^B<#4Q)q&_N1Xo_Kk%cAM359Z$^2fiIz6N$}vQjU)%5WD{jX&mV__ z{+hzx$V>c5L-bR$^y5GT_AAfd-;F=hQvhGxSK-eT708pOC_!0MK|HC79As5h;7M0{ zv9fIaA+iLE5${Jg2||k?LDljSyjfm?YCIK{ltU&#xSIq_p%GGo6_o^Qye7;4gd@S) zouZlMj`f~@@b_=k}g(ar@kik$d-tmT|Rb>o20BHb0uLuQ`qIp>^fn1=AQ8f zWV|EGvm52cjv4Rh^2YmCdD9sq72Fvm!EvvVzZiBayj?!QX>JL!+Blu3j}rQOV8<%s zOqQPBtBPC|X1#{}kJN4~EYJQuH})&72(gG?D;J(1)&9jSJ;^ee0^jGUP1i>wK_$CFgpS!VV#`D50 z!_0U%LFs!%$#{5y)%I;jkQt9CP}itG1)3R;N}}&50;z>pcf|%O0Z+lz)wA@?j5`%D zU&5@(;!IzV()W_mr+qLieIF^kNYE^OUy1GjnpuF~YtkQ{iS*xaOP?hd=1fi=YFMS@ zvcQhDEtMqpQx*1=-PpU2`mXc_`2Qn?DP*Eim$FAkWP~W;pltp^$@x$?L<=;M zp%8Lc6(g{L#cCzNT2g|>K>_%D2%AxcHuLatM`$EaOs=F)D-ex^w^3m^9f19mkQMCk zY|TW|f2pu-=avr&tpuI~X@qWW`C!!`Nl*r0yi^UkGqLr*D(rf`hMga_X$r}1?3mUE zF|8X`TH%?3?H!T=&!2h@P&vl2B- zp~Qy?tD!Q|poF;0V2q| ztxU9l_4LoBZ2OSg@aUB{7GMn(;qNyDsdG^4A&bNwN zMKe3+DlQi-bk21?Jn#poI7UIJsu|NFnIt8WV?3u8Chr zqR~!G@rA(L=_Hz{Rx$DE+>Q9P$V%o5D3Wh$8ijP0}uJO6SH#LiG^x2m8iHD ze;0{5G%8tk+lgr_McR)CodXwh)n5v5pf>eLg&HRaVR)E2NeSc*#Rv7-7p=QE z8`eOprPQ^F^I;9uVGUODS)|fi{=g`{CK1b3FhDXi9#eC%jfxqgmW1kp(nN?OR27O< zW_nx1LY1Wo#;eSBMk0lkR&4brK4VonNyKU-;t-W72d-WPczBM*qY-iOP@Cx;BR+GJ zQ;G<;&sTBex6f2f*Lvce}@6Kcv(TecAl$v;XbOxu^m*r_}$jV7Bip+Bwh3 zycF!xz6UdZcSrDc;?4$dBJS%*pSz2 z+1`!{@+}SRuo-*@;oL$3z$ssXxSSP$*jU2`jY>1V;b%hy7~yArdEDt!Kc{GL$=ON` z>5}wTa#?12xBCNsi6+$v%S`GLV0bv@+RgcdIX%8V)8Y=V?$y0hN)m0jq zC%Mz|db!P2MEQY}shdD$y^M&x)_CcnwQ;A6FOll|7MEZK)${$1OGrbcyVmp(tCyIh zR;O6wx^tCUS#7NGTO9ve5^SFT)`QzJ9lt9_gn>^^{0Q2R~<=^F!5yv~0n$vGg|NIH$o z$~guXzcGxn5;%Dn`7&A-xt^m&1ii^8Y^U;nO z7vQ2EL%`=zFqwNwm!ZFxKrEI19!j4D4$JnCr_!KW7|6bmxeoyx#7=8pOR@*ZwaD+1 zHb~Rq*CEvz3et4=yGWi0(sX!|%Kac++Lge28CTvxN~eY0mI&MjoKFApoAFo0{jAAp zDK)7+RjGOuz?$$k1RPcs++R=7b~((cCyYe2E$U5}MHb&axHwaAVe$58w{2a$tp4Z$ zdW4&KL^AUT$()vje7b_zWvL)bKYYFEx-) z^|n!ryl;iS^3ywgIjEA#?FO?CU{p4$)&A4>I$_O)-Ty>C;N*@(29t7HJ3ESaCX{m_ z=)8-NOxMremYQ0bk-G#5I~likN>x>k#H##Cq~*XP^s}yNyZ&0W=Y0`-b)an_*u$35 zgju~GzR6)~_QqdlepVeS@J=JYtqO$O|CFBW8(5+zL`WVwQN*Opwy*v)Ejrbs)t!cK~Tl}Y5XtXLy℘&P7VWhE|!_UhgG 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�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 ./src - github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 -) +replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 diff --git a/go.sum b/go.sum index 117c9d47..0709bb26 100644 --- a/go.sum +++ b/go.sum @@ -19,18 +19,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= -cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -41,43 +37,24 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= -cosmossdk.io/errors v1.0.0-beta.7 h1:gypHW76pTQGVnHKo6QBkb4yFOJjC+sUGRc5Al3Odj1w= -cosmossdk.io/errors v1.0.0-beta.7/go.mod h1:mz6FQMJRku4bY7aqS/Gwfcmr/ue91roMEKAmDUDpBfE= -cosmossdk.io/math v1.0.0-beta.3 h1:TbZxSopz2LqjJ7aXYfn7nJSb8vNaBklW6BLpcei1qwM= -cosmossdk.io/math v1.0.0-beta.3/go.mod h1:3LYasri3Zna4XpbrTNdKsWmD5fHHkaNAod/mNT9XdE4= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= -filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= -filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= -github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= -github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= -github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -86,11 +63,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.4.0 h1:yCQqn7dwca4ITXb+CbubHmedzaQYHhNhrEXLYUeEe8Q= -github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= -github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= @@ -99,52 +72,22 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blocklessnetwork/orchestration-chain v0.0.4 h1:PQCi8nMf9UnuAvp6jeX2gjS0rz8UnKZ2Aqq2Lc/rrnM= -github.com/blocklessnetwork/orchestration-chain v0.0.4/go.mod h1:bbGcHebexzjAWdJKeJTOfzOZsnF67fQsFNF3OkoRzwk= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= -github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= -github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= -github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= -github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4= github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= github.com/cockroachdb/errors v1.8.1 h1:A5+txlVZfOqFBDa4mGz2bUWSp0aHElvHX2bKkdbQu+Y= @@ -158,9 +101,6 @@ github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/coinbase/rosetta-sdk-go v0.7.9 h1:lqllBjMnazTjIqYrOGv8h8jxjg9+hJazIGZr9ZvoCcA= -github.com/confio/ics23/go v0.7.0 h1:00d2kukk7sPoHWL4zZBZwzxnpA2pec1NPdwbSokJ5w8= -github.com/confio/ics23/go v0.7.0/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= @@ -169,36 +109,13 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU= github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= -github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= -github.com/cosmos/cosmos-proto v1.0.0-alpha7 h1:yqYUOHF2jopwZh4dVQp3xgqwftE5/2hkrwIV6vkUbO0= -github.com/cosmos/cosmos-proto v1.0.0-alpha7/go.mod h1:dosO4pSAbJF8zWCzCoTWP7nNsjcvSUBQmniFxDg5daw= -github.com/cosmos/cosmos-sdk v0.46.6 h1:K9EZsqOZ2jQX3bIQUpn7Hk/YCoaJWRLU56PzvpX8INk= -github.com/cosmos/cosmos-sdk v0.46.6/go.mod h1:JNklMfXo7MhDF1j/jxZCmDyOYyqhVoKB22e8p1ATEqA= -github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= -github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= -github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= -github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4Y= -github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= -github.com/cosmos/iavl v0.19.4 h1:t82sN+Y0WeqxDLJRSpNd8YFX5URIrT+p8n6oJbJ2Dok= -github.com/cosmos/iavl v0.19.4/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= -github.com/cosmos/ibc-go/v5 v5.1.0 h1:m1NHXFkwwvNeJegZqtyox1WLinh+PMy4ivU/Cs9KjeA= -github.com/cosmos/ibc-go/v5 v5.1.0/go.mod h1:H6sV0/CkNRDtvSrhbsIgiog1WnSwhguGfg8x34MOVEk= -github.com/cosmos/ledger-cosmos-go v0.11.1 h1:9JIYsGnXP613pb2vPjFeMMjBI5lEDsEaF6oYorTy6J4= -github.com/cosmos/ledger-cosmos-go v0.11.1/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY= -github.com/cosmos/ledger-go v0.9.2 h1:Nnao/dLwaVTk1Q5U9THldpUMMXU94BOTWPddSmVB6pI= -github.com/cosmos/ledger-go v0.9.2/go.mod h1:oZJ2hHAZROdlHiwTg4t7kP+GKIIkBT+o6c9QWFanOyI= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= -github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -207,25 +124,13 @@ github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6Uh github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= -github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= -github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= -github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= -github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= -github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg= -github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= -github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= @@ -237,18 +142,13 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= -github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= -github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= @@ -257,7 +157,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= @@ -270,16 +169,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= -github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -288,23 +182,21 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= -github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -316,7 +208,6 @@ github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -333,14 +224,11 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -352,12 +240,10 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= @@ -365,7 +251,6 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -380,60 +265,29 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= -github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= -github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= -github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= -github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= -github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= -github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= -github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-getter v1.6.1 h1:NASsgP4q6tL94WH6nJxKWj8As2H/2kop/bB1d8JMyRY= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= -github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU= -github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= @@ -442,13 +296,8 @@ github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3 github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ignite/cli v0.25.2 h1:6VF4Ygx67gtQpJmzmXQTqvZvb+Qy+bzao+zxlqSEWro= -github.com/ignite/cli v0.25.2/go.mod h1:zVKEE1Qox7DbluU+bFNw3EyWfDrMzmUkS66lfrNicrw= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/go-cid v0.3.2 h1:OGgOd+JCFM+y1DjWPmVH+2/4POtpDzwcr7VgnB7mZXc= github.com/ipfs/go-cid v0.3.2/go.mod h1:gQ8pKqT/sUxGY+tIwy1RPpAojYu7jAyCp5Tz1svoupw= github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= @@ -478,14 +327,8 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= -github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -506,10 +349,8 @@ github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6i github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -534,8 +375,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA= +github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= @@ -546,8 +390,6 @@ github.com/libp2p/go-libp2p v0.23.2 h1:yqyTeKQJyofWXxEv/eEVUvOrGdt/9x+0PIQ4N1kax github.com/libp2p/go-libp2p v0.23.2/go.mod h1:s9DEa5NLR4g+LZS+md5uGU4emjMWFiqkZr6hBTY8UxI= github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= -github.com/libp2p/go-libp2p-core v0.20.1 h1:fQz4BJyIFmSZAiTbKV8qoYhEH5Dtv/cVhZbG3Ib/+Cw= -github.com/libp2p/go-libp2p-core v0.20.1/go.mod h1:6zR8H7CvQWgYLsbG4on6oLNSGcyKaYFSEYyDt51+bIY= github.com/libp2p/go-libp2p-kad-dht v0.18.0 h1:akqO3gPMwixR7qFSFq70ezRun97g5hrA/lBW9jrjUYM= github.com/libp2p/go-libp2p-kad-dht v0.18.0/go.mod h1:Gb92MYIPm3K2pJLGn8wl0m8wiKDvHrYpg+rOd0GzzPA= github.com/libp2p/go-libp2p-kbucket v0.5.0 h1:g/7tVm8ACHDxH29BGrpsQlnNeu+6OF1A9bno/4/U1oA= @@ -575,10 +417,7 @@ github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmW github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM= github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= @@ -588,7 +427,10 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8 github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/marten-seemann/webtransport-go v0.1.1 h1:TnyKp3pEXcDooTaNn4s9dYpMJ7kMnTp7k5h+SgYP/mc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= @@ -614,20 +456,12 @@ github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUM github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= -github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= -github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94= -github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= -github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -638,8 +472,6 @@ github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjW github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= -github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= @@ -678,34 +510,22 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -720,7 +540,6 @@ github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e/go.mod h1:uIp+gprXx github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= @@ -733,7 +552,6 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= @@ -742,34 +560,25 @@ github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJ github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/regen-network/cosmos-proto v0.3.1 h1:rV7iM4SSFAagvy8RiyhiACbWEGotmqzywPxOvwMdxcg= github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= +github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= -github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -800,8 +609,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.13.0 h1:Dx1kYM01xsSqKPno3aqLnrwac2LetPvN23diwyr69Qs= github.com/smartystreets/assertions v1.13.0/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8= @@ -811,31 +618,22 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= -github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -843,32 +641,21 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s= -github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= -github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI= -github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= -github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= -github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -github.com/tendermint/tendermint v0.34.23 h1:JZYsdc59aOiT5efou+BHILJv8x6FlRyvlor84Xq9Tb0= -github.com/tendermint/tendermint v0.34.23/go.mod h1:rXVrl4OYzmIa1I91av3iLv2HS0fGSiucyW9J4aMTpKI= -github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= -github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -890,11 +677,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= -github.com/zondax/hid v0.9.1-0.20220302062450-5552068d2266 h1:O9XLFXGkVswDFmH9LaYpqu+r/AAFWqr0DL6V00KEVFg= -github.com/zondax/hid v0.9.1-0.20220302062450-5552068d2266/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +github.com/ziflex/lecho/v3 v3.5.0 h1:Z4TBr8SbUUnfaVc8tGJf1Jhu0G9Jxjl77lPW0riXKak= +github.com/ziflex/lecho/v3 v3.5.0/go.mod h1:+eInrytYHxVPI6NQbua9xXGerB1x0ujj9jAV33yBIko= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -904,27 +688,23 @@ 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.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= -golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -935,16 +715,13 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= +golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1024,7 +801,6 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1039,8 +815,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1054,7 +830,6 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1117,9 +892,7 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1137,19 +910,17 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1158,12 +929,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= +golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1232,10 +1005,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -1260,7 +1033,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1269,7 +1041,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -1298,10 +1069,8 @@ google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1315,8 +1084,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 h1:jCw9YRd2s40X9Vxi4zKsPRvSPlHWNqadVkpbMsCPzPQ= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -1334,12 +1101,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1352,7 +1116,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 h1:KR8+MyP7/qOlV+8Af01LtjL04bu7on42eVsxT4EyBQk= google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -1360,7 +1123,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1368,14 +1130,11 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1397,13 +1156,9 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= -pgregory.net/rapid v0.4.7/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/host/config.go b/host/config.go new file mode 100644 index 00000000..77c09ea7 --- /dev/null +++ b/host/config.go @@ -0,0 +1,67 @@ +package host + +import ( + "time" + + "github.com/multiformats/go-multiaddr" +) + +// defaultConfig used to create Host. +var defaultConfig = Config{ + PrivateKey: "", + ConnectionThreshold: 20, + DialBackPeersLimit: 100, + DiscoveryInterval: 10 * time.Second, +} + +// Config represents the Host configuration. +type Config struct { + PrivateKey string + ConnectionThreshold uint + BootNodes []multiaddr.Multiaddr + DialBackPeers []multiaddr.Multiaddr + DialBackPeersLimit uint + DiscoveryInterval time.Duration +} + +// WithPrivateKey specifies the private key for the Host. +func WithPrivateKey(filepath string) func(*Config) { + return func(cfg *Config) { + cfg.PrivateKey = filepath + } +} + +// WithConnectionThreshold specifies how many connections should the host wait for on peer discovery. +func WithConnectionThreshold(n uint) func(*Config) { + return func(cfg *Config) { + cfg.ConnectionThreshold = n + } +} + +// WithBootNodes specifies boot nodes that the host initially tries to connect to. +func WithBootNodes(nodes []multiaddr.Multiaddr) func(*Config) { + return func(cfg *Config) { + cfg.BootNodes = nodes + } +} + +// WithDialBackPeers specifies dial-back peers that the host initially tries to connect to. +func WithDialBackPeers(peers []multiaddr.Multiaddr) func(*Config) { + return func(cfg *Config) { + cfg.DialBackPeers = peers + } +} + +// WithDialBackPeersLimit specifies the maximum number of dial-back peers to use. +func WithDialBackPeersLimit(n uint) func(*Config) { + return func(cfg *Config) { + cfg.DialBackPeersLimit = n + } +} + +// WithDiscoveryInterval specifies how often we should try to discover new peers during the discovery phase. +func WithDiscoveryInterval(d time.Duration) func(*Config) { + return func(cfg *Config) { + cfg.DiscoveryInterval = d + } +} diff --git a/host/dht.go b/host/dht.go new file mode 100644 index 00000000..a2e679ba --- /dev/null +++ b/host/dht.go @@ -0,0 +1,166 @@ +package host + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + dht "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/discovery/routing" + "github.com/libp2p/go-libp2p/p2p/discovery/util" +) + +func (h *Host) DiscoverPeers(ctx context.Context, topic string) error { + + // Initialize DHT. + dht, err := h.initDHT(ctx) + if err != nil { + return fmt.Errorf("could not initialize DHT: %w", err) + } + + discovery := routing.NewRoutingDiscovery(dht) + util.Advertise(ctx, discovery, topic) + + h.log.Debug().Msg("host started peer discovery") + + connected := uint(0) +findPeers: + for { + peers, err := discovery.FindPeers(ctx, topic) + if err != nil { + return fmt.Errorf("could not find peers: %w", err) + } + + for peer := range peers { + // Skip self. + if peer.ID == h.ID() { + continue + } + + // Skip peers we're already connected to. + connections := h.Network().ConnsToPeer(peer.ID) + if len(connections) > 0 { + h.log.Debug(). + Str("peer", peer.String()). + Msg("skipping connected peer") + continue + } + + err = h.Connect(ctx, peer) + if err != nil { + h.log.Debug(). + Err(err). + Str("peer", peer.String()). + Msg("could not connect to peer") + continue + } + + h.log.Info().Str("peer", peer.String()).Msg("connected to peer") + + connected++ + + // Stop when we have reached connection threshold. + if connected >= h.cfg.ConnectionThreshold { + break findPeers + } + } + + time.Sleep(h.cfg.DiscoveryInterval) + } + + h.log.Info().Msg("peer discovery complete") + return nil +} + +func (h *Host) initDHT(ctx context.Context) (*dht.IpfsDHT, error) { + + // Start a DHT for use in peer discovery. Set the DHT to server mode. + kademliaDHT, err := dht.New(ctx, h.Host, dht.Mode(dht.ModeServer)) + if err != nil { + return nil, fmt.Errorf("could not create DHT: %w", err) + } + + // Bootstrap the DHT. + err = kademliaDHT.Bootstrap(ctx) + if err != nil { + return nil, fmt.Errorf("could not bootstrap the DHT: %w", err) + } + + bootNodes := h.cfg.BootNodes + peers := h.cfg.DialBackPeers + + // Add the dial-back peers to the list of bootstrap nodes if they do not already exist. + // TODO: Limit to workers. + + // We may want to limit the number of dial back peers we use. + added := uint(0) + addLimit := h.cfg.DialBackPeersLimit + + for _, peer := range peers { + peer := peer + + // If the limit of dial-back peers is set and we've reached it - stop now. + if addLimit != 0 && added >= addLimit { + h.log.Info().Uint("limit", addLimit).Msg("reached limit for dial-back peers") + break + } + + addr := peer.String() + addr = strings.Replace(addr, "127.0.0.1", "0.0.0.0", 1) + + // Check if the peer is already among the boot nodes. + exists := false + for _, bootNode := range bootNodes { + if bootNode.String() == addr { + exists = true + break + } + } + + // If the peer is not among the boot nodes - add it now. + if !exists { + bootNodes = append(bootNodes, peer) + added++ + } + } + + // Connect to the bootstrap nodes. + var wg sync.WaitGroup + for _, bootNode := range bootNodes { + + addrInfo, err := peer.AddrInfoFromP2pAddr(bootNode) + if err != nil { + h.log.Warn(). + Err(err). + Str("address", bootNode.String()). + Msg("could not get addrinfo for boot node - skipping") + continue + } + + wg.Add(1) + + go func() { + defer wg.Done() + + peerAddr := addrInfo + + err := h.Host.Connect(ctx, *peerAddr) + if err != nil { + if err.Error() != errNoGoodAddresses { + h.log.Error(). + Err(err). + Str("addrinfo", peerAddr.String()). + Msg("could not connect to bootstrap node") + } + } + }() + } + + // Wait until we know the outcome of all connection attempts. + wg.Wait() + + return kademliaDHT, nil +} diff --git a/host/host.go b/host/host.go new file mode 100644 index 00000000..172e43ac --- /dev/null +++ b/host/host.go @@ -0,0 +1,93 @@ +package host + +import ( + "fmt" + "os" + + "github.com/rs/zerolog" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/host" +) + +// Host represents a new libp2p host. +type Host struct { + log zerolog.Logger + host.Host // TODO: Once the use cases cristalize - reconsider embedding vs private field + + cfg Config +} + +// New creates a new Host. +func New(log zerolog.Logger, address string, port uint, options ...func(*Config)) (*Host, error) { + + cfg := defaultConfig + for _, option := range options { + option(&cfg) + } + + hostAddress := fmt.Sprintf("/ip4/%v/tcp/%v", address, port) + opts := []libp2p.Option{ + libp2p.ListenAddrStrings(hostAddress), + libp2p.DefaultTransports, + libp2p.DefaultMuxers, + libp2p.DefaultSecurity, + libp2p.NATPortMap(), + } + + // Read private key, if provided. + if cfg.PrivateKey != "" { + key, err := readPrivateKey(cfg.PrivateKey) + if err != nil { + return nil, fmt.Errorf("could not read private key: %w", err) + } + + opts = append(opts, libp2p.Identity(key)) + } + + // Create libp2p host. + h, err := libp2p.New(opts...) + if err != nil { + return nil, fmt.Errorf("could not create libp2p host: %w", err) + } + + host := Host{ + log: log.With().Str("component", "host").Logger(), + cfg: cfg, + } + host.Host = h + + return &host, nil +} + +// Addresses returns the list of p2p addresses of the host. +func (h *Host) Addresses() []string { + + addrs := h.Addrs() + out := make([]string, 0, len(addrs)) + + hostID := h.ID() + + for _, addr := range addrs { + addr := fmt.Sprintf("%s/p2p/%s", addr.String(), hostID.String()) + out = append(out, addr) + } + + return out +} + +func readPrivateKey(filepath string) (crypto.PrivKey, error) { + + payload, err := os.ReadFile(filepath) + if err != nil { + return nil, fmt.Errorf("could not read file: %w", err) + } + + key, err := crypto.UnmarshalPrivateKey(payload) + if err != nil { + return nil, fmt.Errorf("could not unmarshal private key: %w", err) + } + + return key, nil +} diff --git a/host/params.go b/host/params.go new file mode 100644 index 00000000..331c767d --- /dev/null +++ b/host/params.go @@ -0,0 +1,6 @@ +package host + +const ( + // Sentinel error for DHT. + errNoGoodAddresses = "no good addresses" +) diff --git a/host/publish.go b/host/publish.go new file mode 100644 index 00000000..e34fe97b --- /dev/null +++ b/host/publish.go @@ -0,0 +1,20 @@ +package host + +import ( + "context" + "fmt" + + pubsub "github.com/libp2p/go-libp2p-pubsub" +) + +// Publish will publish the message on the provided gossipsub topic. +func (h *Host) Publish(ctx context.Context, topic *pubsub.Topic, payload []byte) error { + + // Publish the message. + err := topic.Publish(ctx, payload) + if err != nil { + return fmt.Errorf("could not publish message: %w", err) + } + + return nil +} diff --git a/host/send.go b/host/send.go new file mode 100644 index 00000000..c4ccdbe2 --- /dev/null +++ b/host/send.go @@ -0,0 +1,28 @@ +package host + +import ( + "context" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// SendMessage sends a message directly to the specified peer. +func (h *Host) SendMessage(ctx context.Context, to peer.ID, payload []byte) error { + + stream, err := h.Host.NewStream(ctx, to, blockless.ProtocolID) + if err != nil { + return fmt.Errorf("could not create stream: %w", err) + } + defer stream.Close() + + _, err = stream.Write(payload) + if err != nil { + stream.Reset() + return fmt.Errorf("could not write payload: %w", err) + } + + return nil +} diff --git a/host/subscribe.go b/host/subscribe.go new file mode 100644 index 00000000..64e9a9f1 --- /dev/null +++ b/host/subscribe.go @@ -0,0 +1,32 @@ +package host + +import ( + "context" + "fmt" + + pubsub "github.com/libp2p/go-libp2p-pubsub" +) + +// Subscribe will have the host start listening to a specified gossipsub topic. +func (h *Host) Subscribe(ctx context.Context, topic string) (*pubsub.Topic, *pubsub.Subscription, error) { + + // Get a new PubSub object with the default router. + pubsub, err := pubsub.NewGossipSub(ctx, h) + if err != nil { + return nil, nil, fmt.Errorf("could not create new gossipsub: %w", err) + } + + // Join the specified topic. + th, err := pubsub.Join(topic) + if err != nil { + return nil, nil, fmt.Errorf("could not join topic: %w", err) + } + + // Subscribe to the topic. + subscription, err := th.Subscribe() + if err != nil { + return nil, nil, fmt.Errorf("could not subscribe to topic: %w", err) + } + + return th, subscription, nil +} diff --git a/models/api/request/request.go b/models/api/request/request.go new file mode 100644 index 00000000..02639e5f --- /dev/null +++ b/models/api/request/request.go @@ -0,0 +1,14 @@ +package request + +import ( + "github.com/blocklessnetworking/b7s/models/execute" +) + +// Execute describes the payload for the REST API request for function execution. +type Execute execute.Request + +// InstallFunction describes the payload for the REST API request for function install. +type InstallFunction struct { + CID string `query:"cid"` + URI string `query:"uri"` +} diff --git a/models/api/response/response.go b/models/api/response/response.go new file mode 100644 index 00000000..6cdb99c8 --- /dev/null +++ b/models/api/response/response.go @@ -0,0 +1,13 @@ +package response + +import ( + "github.com/blocklessnetworking/b7s/models/execute" +) + +// Execute describes the REST API response for function execution. +type Execute execute.Result + +// InstallFunction describes the REST API response for the function install. +type InstallFunction struct { + Code string `json:"code"` +} diff --git a/models/blockless/errors.go b/models/blockless/errors.go new file mode 100644 index 00000000..0646ffba --- /dev/null +++ b/models/blockless/errors.go @@ -0,0 +1,10 @@ +package blockless + +import ( + "errors" +) + +// Sentinel errors. +var ( + ErrNotFound = errors.New("not found") +) diff --git a/src/models/repository.go b/models/blockless/function.go similarity index 65% rename from src/models/repository.go rename to models/blockless/function.go index 5f2f5f86..63352e8c 100644 --- a/src/models/repository.go +++ b/models/blockless/function.go @@ -1,26 +1,32 @@ -package models +package blockless +// FunctionManifest describes some important configuration options for a Blockless function. type FunctionManifest struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` Function Function `json:"function,omitempty"` Deployment Deployment `json:"deployment,omitempty"` Runtime Runtime `json:"runtime,omitempty"` Cached bool `json:"cached,omitempty"` - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` Hooks []interface{} `json:"hooks,omitempty"` - Description string `json:"description,omitempty"` - FsRootPath string `json:"fs_root_path,omitempty"` + FSRootPath string `json:"fs_root_path,omitempty"` Entry string `json:"entry,omitempty"` ContentType string `json:"contentType,omitempty"` Permissions []string `json:"permissions,omitempty"` + + DriversRootPath string `json:"drivers_root_path,omitempty"` + LimitedFuel uint `json:"limited_fuel,omitempty"` + LimitedMemory uint `json:"limited_memory,omitempty"` } -// legacy manifest support +// Runtime is here to support legacy manifests. type Runtime struct { Checksum string `json:"checksum,omitempty"` - Url string `json:"url,omitempty"` + URL string `json:"url,omitempty"` } +// Function represents a Blockless function that can be executed. type Function struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -28,27 +34,27 @@ type Function struct { Runtime string `json:"runtime,omitempty"` Extensions []string `json:"extensions,omitempty"` } -type Arguments struct { - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` -} -type Envvars struct { - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` -} -type Methods struct { - Name string `json:"name,omitempty"` - Entry string `json:"entry,omitempty"` - Arguments []Arguments `json:"arguments,omitempty"` - Envvars []Envvars `json:"envvars,omitempty"` - ResultType string `json:"result_type,omitempty"` -} + type Deployment struct { - Cid string `json:"cid,omitempty"` + CID string `json:"cid,omitempty"` Checksum string `json:"checksum,omitempty"` - Uri string `json:"uri,omitempty"` + URI string `json:"uri,omitempty"` Methods []Methods `json:"methods,omitempty"` Aggregation string `json:"aggregation,omitempty"` Nodes int `json:"nodes,omitempty"` File string `json:"file,omitempty"` } + +type Methods struct { + Name string `json:"name,omitempty"` + Entry string `json:"entry,omitempty"` + Arguments []Parameter `json:"arguments,omitempty"` + EnvVars []Parameter `json:"envvars,omitempty"` + ResultType string `json:"result_type,omitempty"` +} + +// Parameter represents a generic name-value pair. +type Parameter struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/models/blockless/message.go b/models/blockless/message.go new file mode 100644 index 00000000..a7c0083b --- /dev/null +++ b/models/blockless/message.go @@ -0,0 +1,29 @@ +package blockless + +// Message types in the Blockless protocol. +const ( + MessageHealthCheck = "MsgHealthCheck" + MessageExecute = "MsgExecute" + MessageExecuteResult = "MsgExecuteResult" + MessageExecuteError = "MsgExecuteError" + MessageExecuteTimeout = "MsgExecuteTimeout" + MessageExecuteUnknown = "MsgExecuteUnknown" + MessageExecuteInvalid = "MsgExecuteInvalid" + MessageExecuteNotFound = "MsgExecuteNotFound" + MessageExecuteNotSupported = "MsgExecuteNotSupported" + MessageExecuteNotImplemented = "MsgExecuteNotImplemented" + MessageExecuteNotAuthorized = "MsgExecuteNotAuthorized" + MessageExecuteNotPermitted = "MsgExecuteNotPermitted" + MessageExecuteNotAvailable = "MsgExecuteNotAvailable" + MessageExecuteNotReady = "MsgExecuteNotReady" + MessageExecuteNotConnected = "MsgExecuteNotConnected" + MessageExecuteNotInitialized = "MsgExecuteNotInitialized" + MessageExecuteNotConfigured = "MsgExecuteNotConfigured" + MessageExecuteNotInstalled = "MsgExecuteNotInstalled" + MessageExecuteNotUpgraded = "MsgExecuteNotUpgraded" + MessageRollCall = "MsgRollCall" + MessageRollCallResponse = "MsgRollCallResponse" + MessageExecuteResponse = "MsgExecuteResponse" + MessageInstallFunction = "MsgInstallFunction" + MessageInstallFunctionResponse = "MsgInstallFunctionResponse" +) diff --git a/src/models/db.go b/models/blockless/peer.go similarity index 65% rename from src/models/db.go rename to models/blockless/peer.go index cf1a597c..f0e8b845 100644 --- a/src/models/db.go +++ b/models/blockless/peer.go @@ -1,12 +1,13 @@ -package models +package blockless import ( "github.com/libp2p/go-libp2p/core/peer" ) +// Peer identifies another node in the Blockless network. type Peer struct { Type string `json:"type,omitempty"` - Id peer.ID `json:"id,omitempty"` + ID peer.ID `json:"id,omitempty"` MultiAddr string `json:"multiaddress,omitempty"` AddrInfo peer.AddrInfo `json:"addrinfo,omitempty"` } diff --git a/models/blockless/protocol.go b/models/blockless/protocol.go new file mode 100644 index 00000000..7976e2e5 --- /dev/null +++ b/models/blockless/protocol.go @@ -0,0 +1,9 @@ +package blockless + +import ( + "github.com/libp2p/go-libp2p/core/protocol" +) + +const ( + ProtocolID protocol.ID = "/b7s/work/1.0.0" +) diff --git a/models/blockless/role.go b/models/blockless/role.go new file mode 100644 index 00000000..ca63f1db --- /dev/null +++ b/models/blockless/role.go @@ -0,0 +1,33 @@ +package blockless + +// TODO: Reconsider the package name - typically I'd use the name of the project - `b7s`. +// Package `blockless` might be too wide. + +// NodeRole is a representation of the node's role. +type NodeRole uint8 + +// The following are all possible node roles. +const ( + HeadNode NodeRole = iota + 1 + WorkerNode +) + +// The following are labels for the node roles, used when parsing the node role as a string. +const ( + HeadNodeLabel = "head" + WorkerNodeLabel = "worker" +) + +// String returns the string representation of the node role. +func (n NodeRole) String() string { + + switch n { + + case HeadNode: + return HeadNodeLabel + case WorkerNode: + return WorkerNodeLabel + default: + return "invalid" + } +} diff --git a/models/execute/request.go b/models/execute/request.go new file mode 100644 index 00000000..c83163c2 --- /dev/null +++ b/models/execute/request.go @@ -0,0 +1,36 @@ +package execute + +// Request describes an execution request. +type Request struct { + FunctionID string `json:"function_id"` + Method string `json:"method"` + Parameters []Parameter `json:"parameters"` + Config Config `json:"config"` +} + +// Parameter represents an execution parameter, modeled as a key-value pair. +type Parameter struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Config represents the configurable options for an execution request. +type Config struct { + Environment []EnvVar `json:"env_vars"` + NodeCount int `json:"number_of_nodes"` + ResultAggregation ResultAggregation `json:"result_aggregation"` + Stdin *string `json:"stdin"` + Permissions []string `json:"permissions"` +} + +// EnvVar represents the name and value of the environment variables set for the execution. +type EnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type ResultAggregation struct { + Enable bool `json:"enable"` + Type string `json:"type"` + Parameters []Parameter `json:"parameters"` +} diff --git a/models/execute/response.go b/models/execute/response.go new file mode 100644 index 00000000..6643eb95 --- /dev/null +++ b/models/execute/response.go @@ -0,0 +1,21 @@ +package execute + +import ( + "time" +) + +// Result describes an execution result. +type Result struct { + Code string `json:"code"` + Result string `json:"result"` + RequestID string `json:"request_id"` + Usage Usage `json:"usage,omitempty"` +} + +// Usage represents the resource usage information for a particular execution. +type Usage struct { + WallClockTime time.Duration `json:"wall_clock_time,omitempty"` + CPUUserTime time.Duration `json:"cpu_user_time,omitempty"` + CPUSysTime time.Duration `json:"cpu_sys_time,omitempty"` + MemoryMaxKB int64 `json:"memory_max_kb,omitempty"` +} diff --git a/models/request/execute.go b/models/request/execute.go new file mode 100644 index 00000000..06e1fd13 --- /dev/null +++ b/models/request/execute.go @@ -0,0 +1,21 @@ +package request + +import ( + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/blocklessnetworking/b7s/models/execute" +) + +// Execute describes the `MessageExecute` request payload. +type Execute struct { + Type string `json:"type,omitempty"` + From peer.ID `json:"from,omitempty"` + Code string `json:"code,omitempty"` + FunctionID string `json:"function_id,omitempty"` + Method string `json:"method,omitempty"` + Parameters []execute.Parameter `json:"parameters,omitempty"` + Config execute.Config `json:"config,omitempty"` + + // RequestID may be set initially, if the execution request is relayed via roll-call. + RequestID string `json:"request_id,omitempty"` +} diff --git a/models/request/install_function.go b/models/request/install_function.go new file mode 100644 index 00000000..56ec54ea --- /dev/null +++ b/models/request/install_function.go @@ -0,0 +1,13 @@ +package request + +import ( + "github.com/libp2p/go-libp2p/core/peer" +) + +// InstallFunction describes the `MessageInstallFunction` request payload. +type InstallFunction struct { + Type string `json:"type,omitempty"` + From peer.ID `json:"from,omitempty"` + ManifestURL string `json:"manifest_url,omitempty"` + CID string `json:"cid,omitempty"` +} diff --git a/models/request/roll_call.go b/models/request/roll_call.go new file mode 100644 index 00000000..f30d5bc2 --- /dev/null +++ b/models/request/roll_call.go @@ -0,0 +1,13 @@ +package request + +import ( + "github.com/libp2p/go-libp2p/core/peer" +) + +// RollCall describes the `MessageRollCall` message payload. +type RollCall struct { + From peer.ID `json:"from,omitempty"` + Type string `json:"type,omitempty"` + FunctionID string `json:"function_id,omitempty"` + RequestID string `json:"request_id,omitempty"` +} diff --git a/models/response/code.go b/models/response/code.go new file mode 100644 index 00000000..69647014 --- /dev/null +++ b/models/response/code.go @@ -0,0 +1,17 @@ +package response + +// Response codes. +const ( + CodeOK = "200" + CodeAccepted = "202" + CodeError = "500" + CodeTimeout = "408" + CodeUnknown = "520" + CodeInvalid = "400" + CodeNotFound = "404" + CodeNotSupported = "501" + CodeNotImplemented = "501" + CodeNotAuthorized = "401" + CodeNotPermitted = "403" + CodeNotAvailable = "503" +) diff --git a/models/response/execute.go b/models/response/execute.go new file mode 100644 index 00000000..7eaf7920 --- /dev/null +++ b/models/response/execute.go @@ -0,0 +1,14 @@ +package response + +import ( + "github.com/libp2p/go-libp2p/core/peer" +) + +// Execute describes the `MessageExecuteResponse` message payload. +type Execute struct { + Type string `json:"type,omitempty"` + RequestID string `json:"request_id,omitempty"` + From peer.ID `json:"from,omitempty"` + Code string `json:"code,omitempty"` + Result string `json:"result,omitempty"` +} diff --git a/models/response/health.go b/models/response/health.go new file mode 100644 index 00000000..3e913803 --- /dev/null +++ b/models/response/health.go @@ -0,0 +1,12 @@ +package response + +import ( + "github.com/libp2p/go-libp2p/core/peer" +) + +// Health describes the message sent as a health ping. +type Health struct { + Type string `json:"type,omitempty"` + From peer.ID `json:"from,omitempty"` + Code string `json:"code,omitempty"` +} diff --git a/models/response/install_function.go b/models/response/install_function.go new file mode 100644 index 00000000..38aaf1f2 --- /dev/null +++ b/models/response/install_function.go @@ -0,0 +1,13 @@ +package response + +import ( + "github.com/libp2p/go-libp2p/core/peer" +) + +// InstallFunction describes the response to the `MessageInstallFunction` message. +type InstallFunction struct { + Type string `json:"type,omitempty"` + From peer.ID `json:"from,omitempty"` + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} diff --git a/models/response/roll_call.go b/models/response/roll_call.go new file mode 100644 index 00000000..6322d3e0 --- /dev/null +++ b/models/response/roll_call.go @@ -0,0 +1,15 @@ +package response + +import ( + "github.com/libp2p/go-libp2p/core/peer" +) + +// RollCall describes the `MessageRollCall` response payload. +type RollCall struct { + Type string `json:"type,omitempty"` + From peer.ID `json:"from,omitempty"` + Code string `json:"code,omitempty"` + Role string `json:"role,omitempty"` + FunctionID string `json:"function_id,omitempty"` + RequestID string `json:"request_id,omitempty"` +} diff --git a/node/config.go b/node/config.go new file mode 100644 index 00000000..d37e098f --- /dev/null +++ b/node/config.go @@ -0,0 +1,71 @@ +package node + +import ( + "time" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// Option can be used to set Node configuration options. +type Option func(*Config) + +// DefaultConfig represents the default settings for the node. +var DefaultConfig = Config{ + Role: blockless.WorkerNode, + Topic: DefaultTopic, + HealthInterval: DefaultHealthInterval, + RollCallTimeout: DefaultRollCallTimeout, + Concurrency: DefaultConcurrency, +} + +// Config represents the Node configuration. +type Config struct { + Role blockless.NodeRole // Node role. + Topic string // Topic to subscribe to. + Execute Executor // Executor to use for running functions. + HealthInterval time.Duration // How often should we emit the health ping. + RollCallTimeout time.Duration // How long do we wait for roll call responses. + Concurrency uint // How many requests should the node process in parallel. +} + +// WithRole specifies the role for the node. +func WithRole(role blockless.NodeRole) Option { + return func(cfg *Config) { + cfg.Role = role + } +} + +// WithTopic specifies the p2p topic to which node should subscribe. +func WithTopic(topic string) Option { + return func(cfg *Config) { + cfg.Topic = topic + } +} + +// WithExecutor specifies the executor to be used for running Blockless functions +func WithExecutor(execute Executor) Option { + return func(cfg *Config) { + cfg.Execute = execute + } +} + +// WithHealthInterval specifies how often we should emit the health signal. +func WithHealthInterval(d time.Duration) Option { + return func(cfg *Config) { + cfg.HealthInterval = d + } +} + +// WithRollCallTimeout specifies how long do we wait for roll call responses. +func WithRollCallTimeout(d time.Duration) Option { + return func(cfg *Config) { + cfg.RollCallTimeout = d + } +} + +// WithConcurrency specifies how many requests the node should process in parallel. +func WithConcurrency(n uint) Option { + return func(cfg *Config) { + cfg.Concurrency = n + } +} diff --git a/node/config_internal_test.go b/node/config_internal_test.go new file mode 100644 index 00000000..276218c7 --- /dev/null +++ b/node/config_internal_test.go @@ -0,0 +1,74 @@ +package node + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestConfig_NodeRole(t *testing.T) { + + const role = blockless.WorkerNode + + cfg := Config{ + Role: blockless.WorkerNode, + } + + WithRole(role)(&cfg) + require.Equal(t, role, cfg.Role) +} + +func TestConfig_Topic(t *testing.T) { + + const topic = "super-secret-topic" + + cfg := Config{ + Topic: "", + } + + WithTopic(topic)(&cfg) + require.Equal(t, topic, cfg.Topic) +} + +func TestConfig_Executor(t *testing.T) { + + executor := mocks.BaselineExecutor(t) + + cfg := Config{ + Execute: nil, + } + + WithExecutor(executor)(&cfg) + + require.Equal(t, executor, cfg.Execute) +} + +func TestConfig_HealthInterval(t *testing.T) { + + const interval = 30 * time.Second + + cfg := Config{ + HealthInterval: 0, + } + + WithHealthInterval(interval)(&cfg) + + require.Equal(t, interval, cfg.HealthInterval) +} + +func TestConfig_RollCallTimeout(t *testing.T) { + + const timeout = 10 * time.Second + + cfg := Config{ + RollCallTimeout: 0, + } + + WithRollCallTimeout(timeout)(&cfg) + + require.Equal(t, timeout, cfg.RollCallTimeout) +} diff --git a/node/execute.go b/node/execute.go new file mode 100644 index 00000000..bd683db2 --- /dev/null +++ b/node/execute.go @@ -0,0 +1,281 @@ +package node + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/models/response" +) + +// executeFunc is a function that handles an execution request. In case of a worker node, +// the function is executed locally. In case of a head node, a roll call request is issued, +// and the execution request is relayed to, and retrieved from, a worker node that volunteers. +// NOTE: By using `execute.Result` here as the type, if this is executed on the head node we are +// losing the information about `who` is the peer that sent us the result - the `from` field. +type executeFunc func(context.Context, peer.ID, string, execute.Request) (execute.Result, error) + +func (n *Node) processExecute(ctx context.Context, from peer.ID, payload []byte) error { + + // Unpack the request. + var req request.Execute + err := json.Unmarshal(payload, &req) + if err != nil { + return fmt.Errorf("could not unpack the request: %w", err) + } + req.From = from + + requestID := req.RequestID + if requestID == "" { + requestID, err = newRequestID() + if err != nil { + return fmt.Errorf("could not generate new request ID: %w", err) + } + } + + // Create execute request. + execReq := execute.Request{ + FunctionID: req.FunctionID, + Method: req.Method, + Parameters: req.Parameters, + Config: req.Config, + } + + // Call the appropriate function that executes the request in the appropriate way. + // NOTE: In case of an error, we do not return from this function. + // Instead, we send the response back to the caller, whatever it may be. + var execFunc executeFunc + if n.cfg.Role == blockless.WorkerNode { + execFunc = n.workerExecute + } else { + execFunc = n.headExecute + } + + result, err := execFunc(ctx, from, requestID, execReq) + if err != nil { + n.log.Error(). + Err(err). + Str("peer", from.String()). + Str("function_id", req.FunctionID). + Msg("execution failed") + } + + // Cache the execution result. + n.executeResponses.Set(result.RequestID, result) + + // Create the execution response from the execution result. + res := response.Execute{ + Type: blockless.MessageExecuteResponse, + RequestID: result.RequestID, + Code: result.Code, + Result: result.Result, + } + + // Send the response, whatever it may be (success or failure). + err = n.send(ctx, req.From, res) + if err != nil { + return fmt.Errorf("could not send response: %w", err) + } + + return nil +} + +func (n *Node) workerExecute(ctx context.Context, from peer.ID, requestID string, req execute.Request) (execute.Result, error) { + + // Check if we have function in store. + functionInstalled, err := n.isFunctionInstalled(req.FunctionID) + if err != nil { + res := execute.Result{ + Code: response.CodeError, + } + return res, fmt.Errorf("could not lookup function in store: %w", err) + } + + if !functionInstalled { + res := execute.Result{ + Code: response.CodeNotFound, + } + + return res, nil + } + + // Execute the function. + res, err := n.execute.Function(requestID, req) + if err != nil { + return res, fmt.Errorf("execution failed: %w", err) + } + + return res, nil +} + +func (n *Node) headExecute(ctx context.Context, from peer.ID, requestID string, req execute.Request) (execute.Result, error) { + + err := n.issueRollCall(ctx, requestID, req.FunctionID) + if err != nil { + + res := execute.Result{ + Code: response.CodeError, + } + + return res, fmt.Errorf("could not issue roll call: %w", err) + } + + n.log.Info(). + Str("function_id", req.FunctionID). + Str("request_id", requestID). + Msg("roll call published") + + // Limit for how long we wait for responses. + tctx, cancel := context.WithTimeout(ctx, n.cfg.RollCallTimeout) + defer cancel() + + // Peer that reports to roll call first. + var reportingPeer peer.ID +rollCallResponseLoop: + for { + // Wait for responses from nodes who want to work on the request. + select { + // Request timed out. + case <-tctx.Done(): + + n.log.Info(). + Str("function_id", req.FunctionID). + Str("request_id", requestID). + Msg("roll call timed out") + + res := execute.Result{ + Code: response.CodeTimeout, + } + + return res, errRollCallTimeout + + case reply := <-n.rollCall.responses(requestID): + + n.log.Debug(). + Str("peer", reply.From.String()). + Str("function_id", req.FunctionID). + Str("request_id", requestID). + Str("code", reply.Code). + Msg("peer reported for roll call") + + // Check if this is the reply we want. + if reply.Code != response.CodeAccepted || + reply.FunctionID != req.FunctionID || + reply.RequestID != requestID { + + n.log.Debug(). + Str("peer", reply.From.String()). + Str("request_id", requestID). + Str("code", reply.Code). + Msg("skipping inadequate roll call response") + + continue + } + + // Check if we are connected to this peer. + connections := n.host.Network().ConnsToPeer(reply.From) + if len(connections) == 0 { + continue + } + + reportingPeer = reply.From + break rollCallResponseLoop + } + } + + n.log.Info(). + Str("peer", reportingPeer.String()). + Str("function_id", req.FunctionID). + Str("request_id", requestID). + Msg("peer reported for roll call") + + // Request execution from the peer who reported back first. + reqExecute := request.Execute{ + Type: blockless.MessageExecute, + FunctionID: req.FunctionID, + Method: req.Method, + Parameters: req.Parameters, + Config: req.Config, + RequestID: requestID, + } + + // Send message to reporting peer to execute the function. + err = n.send(ctx, reportingPeer, reqExecute) + if err != nil { + + res := execute.Result{ + Code: response.CodeError, + } + + return res, fmt.Errorf("could not send execution request to peer (peer: %s, function: %s, request: %s): %w", + reportingPeer.String(), + req.FunctionID, + requestID, + err) + } + + n.log.Debug(). + Str("request_id", requestID). + Msg("waiting for execution response") + + // TODO: Verify that the response came from the peer that reported for the roll call. + resExecute := n.executeResponses.Wait(requestID).(response.Execute) + + n.log.Info(). + Str("request_id", requestID). + Str("peer", resExecute.From.String()). + Str("code", resExecute.Code). + Msg("received execution response") + + // Return the execution result. + result := execute.Result{ + Code: resExecute.Code, + Result: resExecute.Result, + RequestID: resExecute.RequestID, + } + + return result, nil +} + +func (n *Node) processExecuteResponse(ctx context.Context, from peer.ID, payload []byte) error { + + // Unpack the message. + var res response.Execute + err := json.Unmarshal(payload, &res) + if err != nil { + return fmt.Errorf("could not not unpack execute response: %w", err) + } + res.From = from + + n.log.Debug(). + Str("request_id", res.RequestID). + Str("from", from.String()). + Msg("received execution response") + + // Record execution response. + n.executeResponses.Set(res.RequestID, res) + + return nil +} + +// isFuncitonInstalled looks up the function in the store by using the functionID/CID as key. +func (n *Node) isFunctionInstalled(functionID string) (bool, error) { + + _, err := n.function.Get("", functionID, true) + if err != nil { + + if errors.Is(err, blockless.ErrNotFound) { + return false, nil + } + + return false, fmt.Errorf("could not lookup function in store: %w", err) + } + + return true, nil +} diff --git a/node/execute_integration_test.go b/node/execute_integration_test.go new file mode 100644 index 00000000..b05ebf96 --- /dev/null +++ b/node/execute_integration_test.go @@ -0,0 +1,208 @@ +//go:build integration +// +build integration + +package node_test + +import ( + "context" + "fmt" + "os" + "sync" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/response" +) + +func TestNode_ExecuteComplete(t *testing.T) { + + const ( + testTimeLimit = 1 * time.Minute + + dirPattern = "b7s-node-execute-integration-test-" + + cid = "whatever-cid" + + // Paths where files will be hosted on the test server. + manifestEndpoint = "/hello-manifest.json" + archiveEndpoint = "/hello-deployment.tar.gz" + testFunctionToServe = "testdata/hello.tar.gz" + functionMethod = "hello.wasm" + + expectedExecutionResult = `This is the start of my program +The answer is 42 +This is the end of my program +` + ) + + cleanupDisabled := cleanupDisabled() + + var verifiedExecution bool + + t.Log("starting test") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Set a hard limit for test duration. + // This looks a bit sketchy as tests can have the time limit + // set externally, but as there's a lot of moving pieces here, + // include it for better usability. + go func() { + <-time.After(testTimeLimit) + cancel() + t.Log("cancelling test") + }() + + // Phase 0: Create libp2p hosts, loggers, temporary directories and nodes. + + head := instantiateNode(t, dirPattern, blockless.HeadNode) + defer head.db.Close() + defer head.logFile.Close() + if !cleanupDisabled { + defer os.RemoveAll(head.dir) + } + + worker := instantiateNode(t, dirPattern, blockless.WorkerNode) + defer worker.db.Close() + defer worker.logFile.Close() + if !cleanupDisabled { + defer os.RemoveAll(worker.dir) + } + + t.Log("created nodes") + + // Phase 1: Setup connections and start node main loops. + + // Client that will issue and receive request. + client := createClient(t) + + // Add hosts to each others peer stores so that they know how to contact each other. + hostAddNewPeer(t, client.host, head.host) + hostAddNewPeer(t, client.host, worker.host) + hostAddNewPeer(t, head.host, worker.host) + + // Establish a connection so that hosts disseminate topic subscription info. + headInfo := hostGetAddrInfo(t, head.host) + err := worker.host.Connect(ctx, *headInfo) + require.NoError(t, err) + + t.Log("setup addressing") + + // Phase 2: Start nodes. + + t.Log("starting nodes") + + // We start nodes in separate goroutines. + var nodesWG sync.WaitGroup + nodesWG.Add(1) + go func() { + defer nodesWG.Done() + + err = head.node.Run(ctx) + require.NoError(t, err) + + t.Log("head node stopped") + }() + nodesWG.Add(1) + go func() { + defer nodesWG.Done() + + err = worker.node.Run(ctx) + require.NoError(t, err) + + t.Log("worker node stopped") + }() + + // Add a delay for the hosts to subscribe to topics, + // diseminate subscription information etc. + time.Sleep(startupDelay) + + t.Log("starting function server") + + // Phase 3: Create the server hosting the manifest and the function. + + srv := createFunctionServer(t, manifestEndpoint, archiveEndpoint, testFunctionToServe, cid) + defer srv.Close() + + // Phase 4: Have the worker install the function. + // That way, when he receives the execution request - he will have the function needed to execute it. + + t.Log("instructing worker node to install function") + + var installWG sync.WaitGroup + installWG.Add(1) + + // Setup verifier for the response we expect. + client.host.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer installWG.Done() + defer stream.Close() + + var res response.InstallFunction + getStreamPayload(t, stream, &res) + + require.Equal(t, blockless.MessageInstallFunctionResponse, res.Type) + require.Equal(t, response.CodeAccepted, res.Code) + require.Equal(t, "installed", res.Message) + + t.Log("client received function install response") + }) + + manifestURL := fmt.Sprintf("%v%v", srv.URL, manifestEndpoint) + err = client.sendInstallMessage(ctx, worker.host.ID(), manifestURL, cid) + require.NoError(t, err) + + // Wait for the installation request to be processed. + installWG.Wait() + + t.Log("worker node installed function") + + // Phase 5: Request execution from the head node. + + t.Log("sending execution request") + + // Setup verifier for the response we expect. + var executeWG sync.WaitGroup + + executeWG.Add(1) + client.host.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer executeWG.Done() + defer stream.Close() + + t.Log("client received execution response") + + var res response.Execute + getStreamPayload(t, stream, &res) + + require.Equal(t, blockless.MessageExecuteResponse, res.Type) + require.Equal(t, response.CodeOK, res.Code) + require.NotEmpty(t, res.RequestID) + require.Equal(t, expectedExecutionResult, res.Result) + + t.Log("client verified execution response") + + verifiedExecution = true + }) + + err = client.sendExecutionMessage(ctx, head.host.ID(), cid, functionMethod) + require.NoError(t, err) + + executeWG.Wait() + + t.Log("execution request processed") + + // Since we're done, we can cancel the context, leading to stopping of the nodes. + cancel() + + nodesWG.Wait() + + t.Log("nodes shutdown") + + t.Log("test complete") + + require.True(t, verifiedExecution) +} diff --git a/node/execute_internal_test.go b/node/execute_internal_test.go new file mode 100644 index 00000000..807ded3c --- /dev/null +++ b/node/execute_internal_test.go @@ -0,0 +1,426 @@ +package node + +import ( + "context" + "encoding/json" + "sync" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/models/response" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestNode_WorkerExecute(t *testing.T) { + + const ( + functionID = "dummy-function-id" + functionMethod = "dummy-function-method" + ) + + executionRequest := request.Execute{ + Type: blockless.MessageExecute, + FunctionID: functionID, + Method: functionMethod, + Parameters: []execute.Parameter{}, + Config: execute.Config{}, + } + + payload := serialize(t, executionRequest) + + t.Run("handles correct execution", func(t *testing.T) { + t.Parallel() + + var ( + requestID string + ) + + node := createNode(t, blockless.WorkerNode) + + // Use a custom executor to verify all execution parameters are correct. + executor := mocks.BaselineExecutor(t) + executor.ExecFunctionFunc = func(reqID string, req execute.Request) (execute.Result, error) { + require.NotEmpty(t, reqID) + require.Equal(t, executionRequest.FunctionID, req.FunctionID) + require.Equal(t, executionRequest.Method, req.Method) + require.ElementsMatch(t, executionRequest.Parameters, req.Parameters) + require.Equal(t, executionRequest.Config, req.Config) + + requestID = reqID + res := mocks.GenericExecutionResult + res.RequestID = requestID + + return res, nil + } + node.execute = executor + + // Create a host that will serve as a receiver of the execution response. + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + var wg sync.WaitGroup + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received response.Execute + getStreamPayload(t, stream, &received) + + require.Equal(t, blockless.MessageExecuteResponse, received.Type) + + // We should receive the response the baseline executor will return. + expected := mocks.GenericExecutionResult + require.Equal(t, requestID, received.RequestID) + require.Equal(t, expected.Code, received.Code) + require.Equal(t, expected.Result, received.Result) + }) + + err = node.processExecute(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("handles execution failure", func(t *testing.T) { + t.Parallel() + + var ( + faultyExecutionResult = execute.Result{ + Code: response.CodeError, + Result: "something horrible has happened", + RequestID: mocks.GenericUUID.String(), + } + ) + + node := createNode(t, blockless.WorkerNode) + + // Use a custom executor to verify all execution parameters are correct. + executor := mocks.BaselineExecutor(t) + executor.ExecFunctionFunc = func(requestID string, req execute.Request) (execute.Result, error) { + return faultyExecutionResult, mocks.GenericError + } + node.execute = executor + + // Create a host that will serve as a receiver of the execution response. + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + var wg sync.WaitGroup + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received response.Execute + getStreamPayload(t, stream, &received) + + require.Equal(t, blockless.MessageExecuteResponse, received.Type) + + require.Equal(t, faultyExecutionResult.RequestID, received.RequestID) + require.Equal(t, faultyExecutionResult.Code, received.Code) + require.Equal(t, faultyExecutionResult.Result, received.Result) + }) + + err = node.processExecute(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("handles function store errors", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.WorkerNode) + + // Error retrieving function. + fstore := mocks.BaselineFunctionHandler(t) + fstore.GetFunc = func(string, string, bool) (*blockless.FunctionManifest, error) { + return nil, mocks.GenericError + } + node.function = fstore + + // Create a host that will serve as a receiver of the execution response. + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + var wg sync.WaitGroup + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received response.Execute + getStreamPayload(t, stream, &received) + + require.Equal(t, blockless.MessageExecuteResponse, received.Type) + + require.Equal(t, received.Code, response.CodeError) + }) + + err = node.processExecute(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + + // Function is not installed. + fstore.GetFunc = func(string, string, bool) (*blockless.FunctionManifest, error) { + return nil, blockless.ErrNotFound + } + node.function = fstore + + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received response.Execute + getStreamPayload(t, stream, &received) + + require.Equal(t, blockless.MessageExecuteResponse, received.Type) + + require.Equal(t, received.Code, response.CodeNotFound) + }) + + err = node.processExecute(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("handles malformed request", func(t *testing.T) { + t.Parallel() + + const ( + // JSON without closing brace. + malformedJSON = `{ + "type": "MsgExecute", + "function_id": "dummy-function-id", + "method": "dummy-function-method", + "config": {}` + ) + + node := createNode(t, blockless.WorkerNode) + + err := node.processExecute(context.Background(), mocks.GenericPeerID, []byte(malformedJSON)) + require.Error(t, err) + }) +} + +func TestNode_HeadExecute(t *testing.T) { + + const ( + functionID = "dummy-function-id" + functionMethod = "dummy-function-method" + ) + + executionRequest := request.Execute{ + Type: blockless.MessageExecute, + FunctionID: functionID, + Method: functionMethod, + Parameters: []execute.Parameter{}, + Config: execute.Config{}, + } + + payload := serialize(t, executionRequest) + + t.Run("handles roll call timeout", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.HeadNode) + + ctx := context.Background() + _, err := node.subscribe(ctx) + require.NoError(t, err) + + // Create a host that will receive the execution response. + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + var wg sync.WaitGroup + + wg.Add(1) + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received response.Execute + getStreamPayload(t, stream, &received) + + require.Equal(t, blockless.MessageExecuteResponse, received.Type) + require.Equal(t, response.CodeTimeout, received.Code) + }) + + // Since no one will respond to a roll call, this is bound to time out. + err = node.processExecute(ctx, receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("handles correct execution", func(t *testing.T) { + t.Parallel() + + const ( + topic = DefaultTopic + executionResult = "dummy-execution-result" + ) + + var ( + requestID string + ) + + ctx, cancel := context.WithCancel(context.Background()) + + node := createNode(t, blockless.HeadNode) + node.listenDirectMessages(ctx) + + defer cancel() + _, err := node.subscribe(ctx) + require.NoError(t, err) + + // Create a host that will simulate a worker. + // It will listen to a roll call request and reply, + // as well as feign execution. + mockWorker, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + _, subscription, err := mockWorker.Subscribe(ctx, topic) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, mockWorker) + + // Connect to the node so they exchange topic subscription info. + info := hostGetAddrInfo(t, node.host) + err = mockWorker.Connect(ctx, *info) + + // Mock worker will feign execution. + mockWorker.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer stream.Close() + + var req request.Execute + getStreamPayload(t, stream, &req) + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + require.Equal(t, blockless.MessageExecute, req.Type) + + res := response.Execute{ + Type: blockless.MessageExecuteResponse, + RequestID: requestID, + Result: executionResult, + Code: response.CodeOK, + } + payload := serialize(t, res) + err = mockWorker.SendMessage(ctx, node.host.ID(), payload) + require.NoError(t, err) + }) + + // Create a host that will receive the execution response. + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + var receiverWG sync.WaitGroup + + receiverWG.Add(1) + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer receiverWG.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var res response.Execute + getStreamPayload(t, stream, &res) + require.Equal(t, blockless.MessageExecuteResponse, res.Type) + + require.Equal(t, response.CodeOK, res.Code) + require.Equal(t, requestID, res.RequestID) + require.Equal(t, executionResult, res.Result) + }) + + var nodeWG sync.WaitGroup + nodeWG.Add(1) + + // Start the node request asynchronously. + go func() { + defer nodeWG.Done() + + time.Sleep(subscriptionDiseminationPause) + + err = node.processExecute(ctx, receiver.ID(), payload) + require.NoError(t, err) + }() + + // Mock worker workflow. + + deadlineCtx, dcancel := context.WithTimeout(ctx, publishTimeout) + defer dcancel() + + // Mock worker should wait for the roll call to be broadcast. + msg, err := subscription.Next(deadlineCtx) + require.NoError(t, err) + + from := msg.ReceivedFrom + require.Equal(t, node.host.ID(), from) + + var received request.RollCall + err = json.Unmarshal(msg.Data, &received) + + require.Equal(t, blockless.MessageRollCall, received.Type) + require.Equal(t, functionID, received.FunctionID) + + requestID = received.RequestID + require.NotEmpty(t, requestID) + + // Reply to the server that we can do the work. + res := response.RollCall{ + Type: blockless.MessageRollCallResponse, + Code: response.CodeAccepted, + FunctionID: received.FunctionID, + RequestID: requestID, + } + + rcPayload := serialize(t, res) + + // Mock worker should respond to an execution request. + err = mockWorker.SendMessage(ctx, node.host.ID(), rcPayload) + require.NoError(t, err) + + receiverWG.Wait() + nodeWG.Wait() + }) +} diff --git a/node/executor.go b/node/executor.go new file mode 100644 index 00000000..f9326852 --- /dev/null +++ b/node/executor.go @@ -0,0 +1,9 @@ +package node + +import ( + "github.com/blocklessnetworking/b7s/models/execute" +) + +type Executor interface { + Function(string, execute.Request) (execute.Result, error) +} diff --git a/node/function.go b/node/function.go new file mode 100644 index 00000000..61bae1aa --- /dev/null +++ b/node/function.go @@ -0,0 +1,13 @@ +package node + +import ( + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// FunctionStore provides retrieval of function manifest. +// TODO: Somewhat of a name discrepancy - function store vs function handler.. settle down on a name. +type FunctionStore interface { + // Get retrieves a function manifest based on the address or CID. `useCached` boolean + // determines if function manifest should be refetched or previously cached data can be returned. + Get(address string, cid string, useCached bool) (*blockless.FunctionManifest, error) +} diff --git a/node/function_manifest.go b/node/function_manifest.go new file mode 100644 index 00000000..98c5b91b --- /dev/null +++ b/node/function_manifest.go @@ -0,0 +1,21 @@ +package node + +import ( + "fmt" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// getFunctionManifest retrieves the function manifest for the function with the given ID. +func (n *Node) getFunctionManifest(id string) (*blockless.FunctionManifest, error) { + + // Try to get function manifest from the store. + var manifest blockless.FunctionManifest + err := n.store.GetRecord(id, &manifest) + if err != nil { + // TODO: Check - error not found. + return nil, fmt.Errorf("could not retrieve function manifest: %w", err) + } + + return &manifest, nil +} diff --git a/node/handlers.go b/node/handlers.go new file mode 100644 index 00000000..44ad5c54 --- /dev/null +++ b/node/handlers.go @@ -0,0 +1,84 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/models/response" +) + +// TODO: peerID of the sender is a good candidate to move on to the context + +type HandlerFunc func(context.Context, peer.ID, []byte) error + +func (n *Node) processHealthCheck(ctx context.Context, from peer.ID, payload []byte) error { + n.log.Debug(). + Str("from", from.String()). + Msg("peer health check received") + return nil +} + +func (n *Node) processRollCallResponse(ctx context.Context, from peer.ID, payload []byte) error { + + // Unpack the roll call response. + var res response.RollCall + err := json.Unmarshal(payload, &res) + if err != nil { + return fmt.Errorf("could not unpack the roll call response: %w", err) + } + res.From = from + + // Record the response. + n.rollCall.add(res.RequestID, res) + + return nil +} + +func (n *Node) processInstallFunction(ctx context.Context, from peer.ID, payload []byte) error { + + // Only workers should respond to function install requests. + if n.cfg.Role != blockless.WorkerNode { + n.log.Debug(). + Msg("received function install request, ignoring") + return nil + } + + // Unpack the request. + var req request.InstallFunction + err := json.Unmarshal(payload, &req) + if err != nil { + return fmt.Errorf("could not unpack request: %w", err) + } + req.From = from + + // Get the function manifest. + _, err = n.function.Get(req.ManifestURL, req.CID, true) + if err != nil { + return fmt.Errorf("could not retrieve function (manifest_url: %s, cid: %s): %w", req.ManifestURL, req.CID, err) + } + + // Create the response. + res := response.InstallFunction{ + Type: blockless.MessageInstallFunctionResponse, + Code: response.CodeAccepted, + Message: "installed", + } + + // Reply to the caller. + err = n.send(ctx, from, res) + if err != nil { + return fmt.Errorf("could not send the response (peer: %s): %w", from, err) + } + + return nil +} + +func (n *Node) processInstallFunctionResponse(ctx context.Context, from peer.ID, payload []byte) error { + n.log.Debug().Msg("function install response received") + return nil +} diff --git a/node/handlers_internal_test.go b/node/handlers_internal_test.go new file mode 100644 index 00000000..55d1fad8 --- /dev/null +++ b/node/handlers_internal_test.go @@ -0,0 +1,203 @@ +package node + +import ( + "context" + "sync" + "testing" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/models/response" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestNode_Handlers(t *testing.T) { + + node := createNode(t, blockless.HeadNode) + + t.Run("health check", func(t *testing.T) { + t.Parallel() + + msg := response.Health{ + Type: blockless.MessageHealthCheck, + Code: response.CodeOK, + } + + payload := serialize(t, msg) + err := node.processHealthCheck(context.Background(), mocks.GenericPeerID, payload) + require.NoError(t, err) + }) + t.Run("roll call response", func(t *testing.T) { + t.Parallel() + + const ( + requestID = "dummy-request-id" + ) + + res := response.RollCall{ + Type: blockless.MessageRollCallResponse, + Code: response.CodeOK, + Role: "dummy-role", + FunctionID: "dummy-function-id", + RequestID: requestID, + } + + // Record response asynchronously. + var wg sync.WaitGroup + var recordedResponse response.RollCall + go func() { + defer wg.Done() + recordedResponse = <-node.rollCall.responses(requestID) + }() + + wg.Add(1) + + payload := serialize(t, res) + err := node.processRollCallResponse(context.Background(), mocks.GenericPeerID, payload) + require.NoError(t, err) + + wg.Wait() + + expected := res + expected.From = mocks.GenericPeerID + require.Equal(t, expected, recordedResponse) + }) + t.Run("function install response", func(t *testing.T) { + t.Parallel() + + msg := response.InstallFunction{ + Type: blockless.MessageInstallFunctionResponse, + Code: response.CodeOK, + Message: "dummy-message", + } + + payload := serialize(t, msg) + err := node.processInstallFunctionResponse(context.Background(), mocks.GenericPeerID, payload) + require.NoError(t, err) + }) +} + +func TestNode_InstallFunction(t *testing.T) { + + const ( + manifestURL = "https://example.com/manifest-url" + cid = "dummy-cid" + ) + + installReq := request.InstallFunction{ + ManifestURL: manifestURL, + CID: cid, + } + + payload := serialize(t, installReq) + + t.Run("head node handles install", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.HeadNode) + + err := node.processInstallFunction(context.Background(), mocks.GenericPeerID, payload) + require.NoError(t, err) + }) + t.Run("worker node handles install", func(t *testing.T) { + t.Parallel() + + const ( + expectedMessage = "installed" + ) + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + node := createNode(t, blockless.WorkerNode) + hostAddNewPeer(t, node.host, receiver) + + var wg sync.WaitGroup + + wg.Add(1) + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received response.InstallFunction + getStreamPayload(t, stream, &received) + + require.Equal(t, blockless.MessageInstallFunctionResponse, received.Type) + require.Equal(t, response.CodeAccepted, received.Code) + require.Equal(t, expectedMessage, received.Message) + }) + + err = node.processInstallFunction(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("worker node handles function install error", func(t *testing.T) { + t.Parallel() + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + node := createNode(t, blockless.WorkerNode) + hostAddNewPeer(t, node.host, receiver) + + fstore := mocks.BaselineFunctionHandler(t) + fstore.GetFunc = func(string, string, bool) (*blockless.FunctionManifest, error) { + return nil, mocks.GenericError + } + node.function = fstore + + // NOTE: In reality, this is more "documenting" current behavior. + // In reality it sounds more correct that we *should* get a response back. + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + require.Fail(t, "unexpected response") + }) + + err = node.processInstallFunction(context.Background(), receiver.ID(), payload) + require.Error(t, err) + }) + t.Run("worker node handles invalid function install requeset", func(t *testing.T) { + t.Parallel() + + const ( + // JSON without closing brace. + brokenPayload = `{ + "type": "MsgInstallFunction", + "manifest_url": "https://example.com/manifest-url", + "cid": "dummy-cid"` + ) + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + node := createNode(t, blockless.WorkerNode) + hostAddNewPeer(t, node.host, receiver) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + require.Fail(t, "unexpected response") + }) + + err = node.processInstallFunction(context.Background(), receiver.ID(), []byte(brokenPayload)) + require.Error(t, err) + }) + t.Run("worker node handles failure to send response", func(t *testing.T) { + t.Parallel() + + // Receiver exists but not added to peer store - the node doesn't know + // the receivers addresses so `send` will fail. + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + node := createNode(t, blockless.WorkerNode) + + err = node.processInstallFunction(context.Background(), receiver.ID(), payload) + require.Error(t, err) + }) +} diff --git a/node/health.go b/node/health.go new file mode 100644 index 00000000..4adbe36a --- /dev/null +++ b/node/health.go @@ -0,0 +1,39 @@ +package node + +import ( + "context" + "time" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/response" +) + +// HealthPing will run a long running loop, publishing health signal until cancelled. +func (n *Node) HealthPing(ctx context.Context) { + + ticker := time.NewTicker(n.cfg.HealthInterval) + + for { + select { + + case <-ticker.C: + + msg := response.Health{ + Type: blockless.MessageHealthCheck, + Code: response.CodeOK, + } + + err := n.publish(ctx, msg) + if err != nil { + n.log.Warn().Err(err).Msg("could not publish health signal") + } + + n.log.Debug().Msg("emitted health ping") + + case <-ctx.Done(): + ticker.Stop() + n.log.Info().Msg("stopping health ping") + return + } + } +} diff --git a/node/health_internal_test.go b/node/health_internal_test.go new file mode 100644 index 00000000..6e30d016 --- /dev/null +++ b/node/health_internal_test.go @@ -0,0 +1,79 @@ +package node + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/response" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestNode_Health(t *testing.T) { + + const ( + healthInterval = 20 * time.Millisecond + topic = DefaultTopic + + expectedPingCount = 3 + ) + + var ( + logger = mocks.NoopLogger + store = mocks.BaselineStore(t) + peerstore = mocks.BaselinePeerStore(t) + functionHandler = mocks.BaselineFunctionHandler(t) + ) + + // Create a node with a short health interval that will issue quick pings. + // Then we'll create a host to subscribe to the same topic and verify a few pings before cancelling. + + nhost, err := host.New(logger, loopback, 0) + require.NoError(t, err) + + node, err := New(logger, nhost, store, peerstore, functionHandler, WithRole(blockless.HeadNode), WithHealthInterval(healthInterval), WithTopic(topic)) + require.NoError(t, err) + + // Create a host that will listen on the the topic to verify health pings + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Establish a connection between node and receiver. + hostAddNewPeer(t, nhost, receiver) + info := hostGetAddrInfo(t, receiver) + + err = node.host.Connect(ctx, *info) + require.NoError(t, err) + + // Have both client and node subscribe to the same topic. + _, subscription, err := receiver.Subscribe(ctx, topic) + require.NoError(t, err) + + _, err = node.subscribe(ctx) + require.NoError(t, err) + + go node.HealthPing(ctx) + + // Wait for subscribed messages and verify a few pings came in. + for i := 0; i < expectedPingCount; i++ { + msg, err := subscription.Next(ctx) + require.NoError(t, err) + + require.Equal(t, node.host.ID(), msg.ReceivedFrom) + + var received response.Health + err = json.Unmarshal(msg.Data, &received) + require.NoError(t, err) + + require.Equal(t, blockless.MessageHealthCheck, received.Type) + require.Equal(t, response.CodeOK, received.Code) + } +} diff --git a/node/internal/waitmap/waitmap.go b/node/internal/waitmap/waitmap.go new file mode 100644 index 00000000..c7ae00d8 --- /dev/null +++ b/node/internal/waitmap/waitmap.go @@ -0,0 +1,111 @@ +package waitmap + +import ( + "sync" + "time" +) + +// NOTE: Perhaps enable an option to say how long to wait for? + +// WaitMap is a key-value store that enables not only setting and getting +// values from a map, but also waiting until value for a key becomes available. +// Important: Since this implementation is tied pretty closely to how it will be used, +// (as an internal package), it has the peculiar behavior of only the first `Set` setting +// the value. Subsequent `Sets()` are recorded, but don't change the returned value. +type WaitMap struct { + sync.Mutex + + m map[string][]any + subs map[string][]chan any +} + +// New creates a new WaitMap. +func New() *WaitMap { + + wm := WaitMap{ + m: make(map[string][]any), + subs: make(map[string][]chan any), + } + + return &wm +} + +// Set sets the value for a key. If the value already exists, we append it to a list. +func (w *WaitMap) Set(key string, value any) { + w.Lock() + defer w.Unlock() + + _, ok := w.m[key] + if !ok { + w.m[key] = make([]any, 0) + } + + w.m[key] = append(w.m[key], value) + + // Send the new value to any waiting subscribers of the key. + for _, sub := range w.subs[key] { + sub <- value + } + delete(w.subs, key) +} + +// Wait will wait until the value for a key becomes available. +func (w *WaitMap) Wait(key string) any { + w.Lock() + // Unlock cannot be deferred so we can ublock Set() while waiting. + + values, ok := w.m[key] + if ok { + w.Unlock() + return values[0] + } + + // If there's no value yet, subscribe to any new values for this key. + ch := make(chan any) + w.subs[key] = append(w.subs[key], ch) + w.Unlock() + + return <-ch +} + +// WaitFor will wait for the value for a key to become available, but no longer than the specified duration. +func (w *WaitMap) WaitFor(key string, d time.Duration) (any, bool) { + w.Lock() + // Unlock cannot be deferred so we can ublock Set() while waiting. + + values, ok := w.m[key] + if ok { + w.Unlock() + return values[0], true + } + + // If there's no value yet, subscribe to any new values for this key. + // Use a bufferred channel since we might bail before collecting our value. + ch := make(chan any, 1) + w.subs[key] = append(w.subs[key], ch) + w.Unlock() + + select { + case <-time.After(d): + return nil, false + case value := <-ch: + return value, true + } +} + +// Get will return the current value for the key, if any. +func (w *WaitMap) Get(key string) (any, bool) { + w.Lock() + defer w.Unlock() + + values, ok := w.m[key] + if !ok { + return values, ok + } + + // As noted in the comment at the beginning of this file, + // this is special behavior because of the way this map will be used. + // Get will always return the first value. + value := values[0] + return value, true +} diff --git a/node/internal/waitmap/waitmap_internal_test.go b/node/internal/waitmap/waitmap_internal_test.go new file mode 100644 index 00000000..13c6ae37 --- /dev/null +++ b/node/internal/waitmap/waitmap_internal_test.go @@ -0,0 +1,187 @@ +package waitmap + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestWaitMap(t *testing.T) { + + const ( + timeout = 5 * time.Millisecond + ) + + t.Run("setting and getting a value works", func(t *testing.T) { + t.Parallel() + + const ( + key = "dummy-key" + value = "dummy-value" + ) + + wm := New() + + wm.Set(key, value) + require.Len(t, wm.m, 1) + require.Equal(t, value, wm.m[key][0]) + + retrieved, ok := wm.Get(key) + require.True(t, ok) + require.Equal(t, value, retrieved) + }) + t.Run("getting a value not yet set works", func(t *testing.T) { + t.Parallel() + + const ( + key = "dummy-key" + ) + + wm := New() + + _, ok := wm.Get(key) + require.False(t, ok) + }) + t.Run("waiting on a value works", func(t *testing.T) { + t.Parallel() + + const ( + key = "dummy-key" + value = "dummy-value" + ) + + var ( + retrieved string + ) + + wm := New() + + var wg sync.WaitGroup + wg.Add(1) + // Spin up a goroutine that will asynchronously wait for a value to be set. + go func() { + defer wg.Done() + waited := wm.Wait(key) + + retrieved = waited.(string) + }() + + // Delay so that the goroutine actually has to wait. + time.Sleep(timeout) + + // Confirm that there is no value set yet. + _, ok := wm.Get(key) + require.False(t, ok) + + wm.Set(key, value) + + // Make sure to wait for the goroutine to complete. + wg.Wait() + + require.Equal(t, value, retrieved) + }) + t.Run("wait returns immediately if the value exists", func(*testing.T) { + t.Parallel() + + const ( + key = "dummy-key" + value = "dummy-value" + ) + + wm := New() + + wm.Set(key, value) + + retrieved := wm.Wait(key) + require.Equal(t, value, retrieved) + }) + t.Run("multiple waiters get notified", func(t *testing.T) { + t.Parallel() + + const ( + key = "dummy-key" + value = "dummy-value" + ) + + wm := New() + + var wg sync.WaitGroup + wg.Add(3) + + fn := func() { + defer wg.Done() + waited := wm.Wait(key) + + require.Equal(t, value, waited.(string)) + } + + // Spin up three goroutines - they should all get the same result. + go fn() + go fn() + go fn() + + wm.Set(key, value) + + wg.Wait() + }) + t.Run("limited wait receives a value", func(t *testing.T) { + t.Parallel() + + const ( + key = "dummy-key" + value = "dummy-value" + ) + + wm := New() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + retrieved, ok := wm.WaitFor(key, 100*time.Millisecond) + require.True(t, ok) + require.Equal(t, value, retrieved.(string)) + }() + + // Delay so that the goroutine actually has to wait. + time.Sleep(timeout) + + wm.Set(key, value) + + wg.Wait() + }) + t.Run("limited wait times out", func(t *testing.T) { + t.Parallel() + const ( + key = "dummy-key" + value = "dummy-value" + + timeout = 10 * time.Millisecond + ) + + wm := New() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + _, ok := wm.WaitFor(key, 5*time.Millisecond) + require.False(t, ok) + }() + + // Wait so the initial `WaitFor` times out. + time.Sleep(timeout) + wm.Set(key, value) + + wg.Wait() + + // Confirm that a second `WaitFor` will succeed. + retrieved, ok := wm.WaitFor(key, 5*time.Millisecond) + require.True(t, ok) + require.Equal(t, value, retrieved.(string)) + }) +} diff --git a/node/message.go b/node/message.go new file mode 100644 index 00000000..83dd9c6b --- /dev/null +++ b/node/message.go @@ -0,0 +1,56 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" +) + +func (n *Node) subscribe(ctx context.Context) (*pubsub.Subscription, error) { + + topic, subscription, err := n.host.Subscribe(ctx, n.cfg.Topic) + if err != nil { + return nil, fmt.Errorf("could not subscribe to topic: %w", err) + } + n.topic = topic + + return subscription, nil +} + +// send serializes the message and sends it to the specified peer. +func (n *Node) send(ctx context.Context, to peer.ID, msg interface{}) error { + + // Serialize the message. + payload, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("could not encode record: %w", err) + } + + // Send message. + err = n.host.SendMessage(ctx, to, payload) + if err != nil { + return fmt.Errorf("could not send message: %w", err) + } + + return nil +} + +func (n *Node) publish(ctx context.Context, msg interface{}) error { + + // Serialize the message. + payload, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("could not encode record: %w", err) + } + + // Publish message. + err = n.host.Publish(ctx, n.topic, payload) + if err != nil { + return fmt.Errorf("could not publish message: %w", err) + } + + return nil +} diff --git a/node/message_internal_test.go b/node/message_internal_test.go new file mode 100644 index 00000000..bcd5017a --- /dev/null +++ b/node/message_internal_test.go @@ -0,0 +1,106 @@ +package node + +import ( + "context" + "encoding/json" + "sync" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestNode_Messaging(t *testing.T) { + + const ( + topic = DefaultTopic + ) + + var ( + rec = dummyRecord{ + ID: mocks.GenericUUID.String(), + Value: 19846, + Description: "dummy-description", + } + ) + + client, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + node := createNode(t, blockless.HeadNode) + hostAddNewPeer(t, node.host, client) + + t.Run("sending single message", func(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + wg.Add(1) + + client.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + var received dummyRecord + getStreamPayload(t, stream, &received) + + require.Equal(t, rec, received) + }) + + err = node.send(context.Background(), client.ID(), rec) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("publishing to a topic", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Establish a connection between peers. + clientInfo := hostGetAddrInfo(t, client) + err = node.host.Connect(ctx, *clientInfo) + require.NoError(t, err) + + // Have both client and node subscribe to the same topic. + _, subscription, err := client.Subscribe(ctx, topic) + require.NoError(t, err) + + _, err = node.subscribe(ctx) + require.NoError(t, err) + + time.Sleep(subscriptionDiseminationPause) + + err = node.publish(ctx, rec) + require.NoError(t, err) + + deadlineCtx, cancel := context.WithTimeout(ctx, publishTimeout) + defer cancel() + msg, err := subscription.Next(deadlineCtx) + require.NoError(t, err) + + from := msg.ReceivedFrom + require.Equal(t, node.host.ID(), from) + require.NotNil(t, msg.Topic) + require.Equal(t, topic, *msg.Topic) + + var received dummyRecord + err = json.Unmarshal(msg.Data, &received) + require.NoError(t, err) + + require.Equal(t, rec, received) + }) +} + +type dummyRecord struct { + ID string `json:"id"` + Value uint64 `json:"value"` + Description string `json:"description"` +} diff --git a/node/node.go b/node/node.go new file mode 100644 index 00000000..b3a61313 --- /dev/null +++ b/node/node.go @@ -0,0 +1,119 @@ +package node + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/google/uuid" + pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/rs/zerolog" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/node/internal/waitmap" +) + +// Node is the entity that actually provides the main Blockless node functionality. +// It listens for messages coming from the wire and processes them. Depending on the +// node role, which is determined on construction, it may process messages in different ways. +// For example, upon receiving a message requesting execution of a Blockless function, +// a Worker Node will use the `Execute` component to fullfill the execution request. +// On the other hand, a Head Node will issue a roll call and eventually +// delegate the execution to the chosend Worker Node. +type Node struct { + cfg Config + + log zerolog.Logger + host *host.Host + store Store + execute Executor + function FunctionStore + + topic *pubsub.Topic + sema chan struct{} + wg *sync.WaitGroup + + rollCall *rollCallQueue + executeResponses *waitmap.WaitMap +} + +// New creates a new Node. +func New(log zerolog.Logger, host *host.Host, store Store, peerStore PeerStore, function FunctionStore, options ...Option) (*Node, error) { + + // Initialize config. + cfg := DefaultConfig + for _, option := range options { + option(&cfg) + } + + // If we're a head node, we don't have an executor. + if cfg.Role == blockless.HeadNode && cfg.Execute != nil { + return nil, errors.New("head node does not support execution") + } + // If we're a worker node, we require an executor. + if cfg.Role == blockless.WorkerNode && cfg.Execute == nil { + return nil, errors.New("worker node requires an executor component") + } + + n := Node{ + cfg: cfg, + + log: log.With().Str("component", "node").Logger(), + host: host, + store: store, + function: function, + execute: cfg.Execute, + + wg: &sync.WaitGroup{}, + sema: make(chan struct{}, cfg.Concurrency), + + rollCall: newQueue(rollCallQueueBufferSize), + executeResponses: waitmap.New(), + } + + // Create a notifiee with a backing peerstore. + cn := newConnectionNotifee(log, peerStore) + host.Network().Notify(cn) + + return &n, nil +} + +// getHandler returns the appropriate handler function for the given message. +func (n Node) getHandler(msgType string) HandlerFunc { + + switch msgType { + case blockless.MessageHealthCheck: + return n.processHealthCheck + case blockless.MessageExecute: + return n.processExecute + case blockless.MessageExecuteResponse: + return n.processExecuteResponse + case blockless.MessageRollCall: + return n.processRollCall + case blockless.MessageRollCallResponse: + return n.processRollCallResponse + case blockless.MessageInstallFunction: + return n.processInstallFunction + case blockless.MessageInstallFunctionResponse: + return n.processInstallFunctionResponse + + default: + return func(_ context.Context, from peer.ID, _ []byte) error { + return ErrUnsupportedMessage + } + } +} + +func newRequestID() (string, error) { + + // Generate a new request/executionID. + uuid, err := uuid.NewRandom() + if err != nil { + return "", fmt.Errorf("could not generate new request ID: %w", err) + } + + return uuid.String(), nil +} diff --git a/node/node_integration_test.go b/node/node_integration_test.go new file mode 100644 index 00000000..a2e22377 --- /dev/null +++ b/node/node_integration_test.go @@ -0,0 +1,241 @@ +//go:build integration +// +build integration + +package node_test + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os" + "path" + "testing" + "time" + + "github.com/cockroachdb/pebble" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + ps "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/multiformats/go-multiaddr" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/executor" + "github.com/blocklessnetworking/b7s/function" + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/node" + "github.com/blocklessnetworking/b7s/peerstore" + "github.com/blocklessnetworking/b7s/store" + "github.com/blocklessnetworking/b7s/testing/helpers" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +const ( + loopback = "127.0.0.1" + + startupDelay = 5 * time.Second + + cleanupDisableEnv = "B7S_INTEG_CLEANUP_DISABLE" + runtimeDirEnv = "B7S_INTEG_RUNTIME_DIR" +) + +type nodeScaffolding struct { + dir string + db *pebble.DB + host *host.Host + logFile *os.File + node *node.Node +} + +func instantiateNode(t *testing.T, dirnamePattern string, role blockless.NodeRole) *nodeScaffolding { + t.Helper() + + nodeDir := fmt.Sprintf("%v-%v-", dirnamePattern, role.String()) + + // Bootstrap node directory. + dir, err := os.MkdirTemp("", nodeDir) + require.NoError(t, err) + + // Create logger. + logName := path.Join(dir, fmt.Sprintf("%v-log.json", role.String())) + logFile, err := os.Create(logName) + require.NoError(t, err) + + logger := zerolog.New(logFile) + + // Create head node libp2p host. + host, err := host.New(logger, loopback, 0) + require.NoError(t, err) + + // Create head node. + db, node := createNode(t, dir, logger, host, role) + + ns := nodeScaffolding{ + dir: dir, + db: db, + logFile: logFile, + host: host, + node: node, + } + + return &ns +} + +func createNode(t *testing.T, dir string, logger zerolog.Logger, host *host.Host, role blockless.NodeRole) (*pebble.DB, *node.Node) { + t.Helper() + + var ( + dbDir = path.Join(dir, "db") + workdir = path.Join(dir, "workdir") + ) + + db, err := pebble.Open(dbDir, &pebble.Options{}) + require.NoError(t, err) + + var ( + store = store.New(db) + peerstore = peerstore.New(store) + fstore = function.NewHandler(logger, store, workdir) + ) + + opts := []node.Option{ + node.WithRole(role), + } + + if role == blockless.WorkerNode { + + runtimeDir := os.Getenv(runtimeDirEnv) + + executor, err := executor.New(logger, + executor.WithRuntimeDir(runtimeDir), + executor.WithWorkDir(workdir), + ) + require.NoError(t, err) + + opts = append(opts, node.WithExecutor(executor)) + } + + node, err := node.New(logger, host, store, peerstore, fstore, opts...) + require.NoError(t, err) + + return db, node +} + +// client is an external actor that can interact with the nodes. +type client struct { + host *host.Host +} + +func createClient(t *testing.T) *client { + t.Helper() + + host, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + c := client{ + host: host, + } + + return &c +} + +func (c *client) sendInstallMessage(ctx context.Context, to peer.ID, manifestURL string, cid string) error { + + req := request.InstallFunction{ + Type: blockless.MessageInstallFunction, + ManifestURL: manifestURL, + CID: cid, + } + + payload, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("could not encode message: %w", err) + } + + err = c.host.SendMessage(ctx, to, payload) + if err != nil { + return fmt.Errorf("could not send message: %w", err) + } + + return nil +} + +func (c *client) sendExecutionMessage(ctx context.Context, to peer.ID, cid string, method string) error { + + req := request.Execute{ + Type: blockless.MessageExecute, + FunctionID: cid, + Method: method, + } + + payload, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("could not encode message: %w", err) + } + + err = c.host.SendMessage(ctx, to, payload) + if err != nil { + return fmt.Errorf("could not send message: %w", err) + } + + return nil +} + +func createFunctionServer(t *testing.T, manifestPath string, deploymentPath string, archivePath string, cid string) *helpers.FunctionServer { + + manifest := blockless.FunctionManifest{ + Name: "hello", + FSRootPath: "./", + DriversRootPath: "", + LimitedFuel: 200_000_000, + LimitedMemory: 120, + Entry: "hello.wasm", + } + + fs := helpers.CreateFunctionServer(t, manifestPath, manifest, deploymentPath, archivePath, cid) + + return fs +} + +func hostAddNewPeer(t *testing.T, host *host.Host, newPeer *host.Host) { + t.Helper() + + info := hostGetAddrInfo(t, newPeer) + host.Peerstore().AddAddrs(info.ID, info.Addrs, ps.PermanentAddrTTL) +} + +func hostGetAddrInfo(t *testing.T, host *host.Host) *peer.AddrInfo { + t.Helper() + + addresses := host.Addresses() + require.NotEmpty(t, addresses) + + addr := addresses[0] + + maddr, err := multiaddr.NewMultiaddr(addr) + require.NoError(t, err) + + info, err := peer.AddrInfoFromP2pAddr(maddr) + require.NoError(t, err) + + return info +} + +func getStreamPayload(t *testing.T, stream network.Stream, output any) { + t.Helper() + + buf := bufio.NewReader(stream) + payload, err := buf.ReadBytes('\n') + require.ErrorIs(t, err, io.EOF) + + err = json.Unmarshal(payload, output) + require.NoError(t, err) +} + +func cleanupDisabled() bool { + return os.Getenv(cleanupDisableEnv) == "yes" +} diff --git a/node/node_internal_test.go b/node/node_internal_test.go new file mode 100644 index 00000000..de3de416 --- /dev/null +++ b/node/node_internal_test.go @@ -0,0 +1,160 @@ +package node + +import ( + "bufio" + "context" + "encoding/json" + "io" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +const ( + loopback = "127.0.0.1" + + // How long can the client wait for a published message before giving up. + publishTimeout = 10 * time.Second + + // It seems like a delay is needed so that the hosts exchange information about the fact + // that they are subscribed to the same topic. If that does not happen, node might publish + // a message too soon and the client might miss it. It will then wait for a published message in vain. + // This is the pause we make after subscribing to the topic and before publishing a message. + // In reality as little as 250ms is enough, but lets allow a longer time for when + // tests are executed in parallel or on weaker machines. + subscriptionDiseminationPause = 1 * time.Second +) + +func TestNode_New(t *testing.T) { + + var ( + logger = mocks.NoopLogger + store = mocks.BaselineStore(t) + peerstore = mocks.BaselinePeerStore(t) + functionHandler = mocks.BaselineFunctionHandler(t) + executor = mocks.BaselineExecutor(t) + ) + + host, err := host.New(logger, loopback, 0) + require.NoError(t, err) + + t.Run("create a head node", func(t *testing.T) { + t.Parallel() + + node, err := New(logger, host, store, peerstore, functionHandler, WithRole(blockless.HeadNode)) + require.NoError(t, err) + require.NotNil(t, node) + + // Creating a head node with executor fails. + _, err = New(logger, host, store, peerstore, functionHandler, WithRole(blockless.HeadNode), WithExecutor(executor)) + require.Error(t, err) + }) + t.Run("create a worker node", func(t *testing.T) { + t.Parallel() + + node, err := New(logger, host, store, peerstore, functionHandler, WithRole(blockless.WorkerNode), WithExecutor(executor)) + require.NoError(t, err) + require.NotNil(t, node) + + // Creating a worker node without executor fails. + _, err = New(logger, host, store, peerstore, functionHandler, WithRole(blockless.WorkerNode)) + require.Error(t, err) + }) +} + +func TestNode_MessageHandler(t *testing.T) { + t.Run("unsupported messages should fail", func(t *testing.T) { + t.Parallel() + + const ( + msgType = "jibberish" + ) + + node := createNode(t, blockless.HeadNode) + + handlerFunc := node.getHandler(msgType) + + err := handlerFunc(context.Background(), mocks.GenericPeerID, []byte{}) + require.Error(t, err) + + require.ErrorIs(t, err, ErrUnsupportedMessage) + }) +} + +func createNode(t *testing.T, role blockless.NodeRole) *Node { + t.Helper() + + var ( + logger = mocks.NoopLogger + store = mocks.BaselineStore(t) + peerstore = mocks.BaselinePeerStore(t) + functionHandler = mocks.BaselineFunctionHandler(t) + ) + + host, err := host.New(logger, loopback, 0) + require.NoError(t, err) + + opts := []Option{ + WithRole(role), + } + + if role == blockless.WorkerNode { + executor := mocks.BaselineExecutor(t) + opts = append(opts, WithExecutor(executor)) + } + + node, err := New(logger, host, store, peerstore, functionHandler, opts...) + require.NoError(t, err) + + return node +} + +func hostAddNewPeer(t *testing.T, host *host.Host, newPeer *host.Host) { + t.Helper() + + info := hostGetAddrInfo(t, newPeer) + host.Peerstore().AddAddrs(info.ID, info.Addrs, peerstore.PermanentAddrTTL) +} + +func hostGetAddrInfo(t *testing.T, host *host.Host) *peer.AddrInfo { + + addresses := host.Addresses() + require.NotEmpty(t, addresses) + + addr := addresses[0] + + maddr, err := multiaddr.NewMultiaddr(addr) + require.NoError(t, err) + + info, err := peer.AddrInfoFromP2pAddr(maddr) + require.NoError(t, err) + + return info +} + +func getStreamPayload(t *testing.T, stream network.Stream, output any) { + t.Helper() + + buf := bufio.NewReader(stream) + payload, err := buf.ReadBytes('\n') + require.ErrorIs(t, err, io.EOF) + + err = json.Unmarshal(payload, output) + require.NoError(t, err) +} + +func serialize(t *testing.T, message any) []byte { + payload, err := json.Marshal(message) + require.NoError(t, err) + + return payload +} diff --git a/node/notifiee.go b/node/notifiee.go new file mode 100644 index 00000000..5f831342 --- /dev/null +++ b/node/notifiee.go @@ -0,0 +1,60 @@ +package node + +import ( + "github.com/libp2p/go-libp2p/core/network" + "github.com/multiformats/go-multiaddr" + "github.com/rs/zerolog" +) + +// TODO: Potentially move to internal package. +type connectionNotifiee struct { + log zerolog.Logger + peers PeerStore +} + +func newConnectionNotifee(log zerolog.Logger, peerStore PeerStore) *connectionNotifiee { + + cn := connectionNotifiee{ + log: log.With().Str("component", "notifiee").Logger(), + peers: peerStore, + } + + return &cn +} + +func (n *connectionNotifiee) Connected(network network.Network, conn network.Conn) { + + // Get peer information. + peerID := conn.RemotePeer() + maddr := conn.RemoteMultiaddr() + addrInfo := network.Peerstore().PeerInfo(peerID) + + n.log.Debug(). + Str("id", peerID.String()). + Str("addr", maddr.String()). + Msg("peer connected") + + // Store the peer info. + err := n.peers.Store(peerID, maddr, addrInfo) + if err != nil { + n.log.Warn().Err(err).Str("id", peerID.String()).Msg("could not add peer to peerstore") + } + + // Update peer list. + err = n.peers.UpdatePeerList(peerID, maddr, addrInfo) + if err != nil { + n.log.Warn().Err(err).Str("id", peerID.String()).Msg("could not update peers in peerstore") + } +} + +func (n *connectionNotifiee) Disconnected(_ network.Network, _ network.Conn) { + // TBD: Not implemented +} + +func (n *connectionNotifiee) Listen(_ network.Network, _ multiaddr.Multiaddr) { + // TBD: Not implemented +} + +func (n *connectionNotifiee) ListenClose(_ network.Network, _ multiaddr.Multiaddr) { + // TBD: Not implemented +} diff --git a/node/notifiee_internal_test.go b/node/notifiee_internal_test.go new file mode 100644 index 00000000..b5e97629 --- /dev/null +++ b/node/notifiee_internal_test.go @@ -0,0 +1,58 @@ +package node + +import ( + "context" + "testing" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestNode_Notifiee(t *testing.T) { + + var ( + logger = mocks.NoopLogger + store = mocks.BaselineStore(t) + functionHandler = mocks.BaselineFunctionHandler(t) + ) + + server, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + var ( + storedPeer bool + updatedPeerList bool + ) + + peerstore := mocks.BaselinePeerStore(t) + // Override the peerstore methods so we know if the node correctly handled incoming connection. + peerstore.StoreFunc = func(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error { + storedPeer = true + return nil + } + peerstore.UpdatePeerListFunc = func(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error { + updatedPeerList = true + return nil + } + + node, err := New(logger, server, store, peerstore, functionHandler, WithRole(blockless.HeadNode)) + require.NoError(t, err) + + client, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, client, node.host) + + serverInfo := hostGetAddrInfo(t, server) + err = client.Connect(context.Background(), *serverInfo) + require.NoError(t, err) + + // Verify that peer store was updated. + require.True(t, storedPeer) + require.True(t, updatedPeerList) +} diff --git a/node/params.go b/node/params.go new file mode 100644 index 00000000..5b28c1af --- /dev/null +++ b/node/params.go @@ -0,0 +1,22 @@ +package node + +import ( + "errors" + "time" +) + +const ( + DefaultTopic = "blockless/b7s/general" + DefaultHealthInterval = 1 * time.Minute + DefaultRollCallTimeout = 5 * time.Second + DefaultConcurrency = 10 + + functionInstallTimeout = 10 * time.Second + + rollCallQueueBufferSize = 1000 +) + +var ( + ErrUnsupportedMessage = errors.New("unsupported message") + errRollCallTimeout = errors.New("roll call timed out") +) diff --git a/node/peerstore.go b/node/peerstore.go new file mode 100644 index 00000000..52509f0e --- /dev/null +++ b/node/peerstore.go @@ -0,0 +1,11 @@ +package node + +import ( + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" +) + +type PeerStore interface { + Store(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error + UpdatePeerList(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error +} diff --git a/node/process.go b/node/process.go new file mode 100644 index 00000000..6c6cfb3f --- /dev/null +++ b/node/process.go @@ -0,0 +1,46 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" +) + +// processMessage will determine which message was received and how to process it. +func (n *Node) processMessage(ctx context.Context, from peer.ID, payload []byte) error { + + // Determine message type. + msgType, err := getMessageType(payload) + if err != nil { + return fmt.Errorf("could not determine message type: %w", err) + } + + n.log.Debug(). + Str("peer", from.String()). + Str("message", msgType). + Msg("received message from peer") + + // Get the registered handler for the message. + handler := n.getHandler(msgType) + + // Invoke the aprropriate handler to process the message. + return handler(ctx, from, payload) +} + +type baseMessage struct { + Type string `json:"type,omitempty"` +} + +// getMessageType will return the `type` string field from the JSON payload. +func getMessageType(payload []byte) (string, error) { + + var message baseMessage + err := json.Unmarshal(payload, &message) + if err != nil { + return "", fmt.Errorf("could not unmarshal message: %w", err) + } + + return message.Type, nil +} diff --git a/node/queue.go b/node/queue.go new file mode 100644 index 00000000..78b4e608 --- /dev/null +++ b/node/queue.go @@ -0,0 +1,51 @@ +package node + +import ( + "sync" + + "github.com/blocklessnetworking/b7s/models/response" +) + +type rollCallQueue struct { + sync.Mutex + + size uint + m map[string]chan response.RollCall +} + +func newQueue(bufSize uint) *rollCallQueue { + + q := rollCallQueue{ + size: bufSize, + m: make(map[string]chan response.RollCall), + } + + return &q +} + +// add records a new response to a roll call. +func (q *rollCallQueue) add(id string, res response.RollCall) { + q.Lock() + defer q.Unlock() + + _, ok := q.m[id] + if !ok { + q.m[id] = make(chan response.RollCall, q.size) + } + + q.m[id] <- res +} + +// responses will return a channel that can be used to iterate through all of the responses. +func (q *rollCallQueue) responses(id string) <-chan response.RollCall { + q.Lock() + + _, ok := q.m[id] + if !ok { + q.m[id] = make(chan response.RollCall, q.size) + } + + q.Unlock() + + return q.m[id] +} diff --git a/node/rest.go b/node/rest.go new file mode 100644 index 00000000..5072ba60 --- /dev/null +++ b/node/rest.go @@ -0,0 +1,109 @@ +package node + +import ( + "context" + "crypto/sha256" + "fmt" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/models/request" +) + +// TODO: Consider introducing an entity - a `delegator`. This could be like an Executor, only +// instead of local execution, it would issue a roll call and delegate work to the worker nodes. +// Problem is that delegator would need to be notified when an execution result has arrived. +// Doing this way would make the execution flow more streamlined and would not differentiate as much between +// worker and head node. +func (n *Node) ExecuteFunction(ctx context.Context, req execute.Request) (execute.Result, error) { + + requestID, err := newRequestID() + if err != nil { + return execute.Result{}, fmt.Errorf("could not generate request ID: %w", err) + } + + switch n.cfg.Role { + case blockless.WorkerNode: + return n.workerExecute(ctx, n.host.ID(), requestID, req) + + case blockless.HeadNode: + return n.headExecute(ctx, n.host.ID(), requestID, req) + } + + panic(fmt.Errorf("invalid node role: %s", n.cfg.Role)) +} + +// ExecutionResult fetches the execution result from the node cache. +func (n *Node) ExecutionResult(id string) (execute.Result, bool) { + res, ok := n.executeResponses.Get(id) + return res.(execute.Result), ok +} + +// FunctionInstall initiates function install process. +func (n *Node) FunctionInstall(ctx context.Context, uri string, cid string) error { + + var req request.InstallFunction + if uri != "" { + var err error + req, err = createInstallMessageFromURI(uri) + if err != nil { + return fmt.Errorf("could not create install message from URI: %W", err) + } + } else { + req = createInstallMessageFromCID(cid) + } + + n.log.Debug(). + Str("url", req.ManifestURL). + Str("cid", req.CID). + Msg("publishing function install message") + + err := n.publish(ctx, req) + if err != nil { + return fmt.Errorf("could not publish message: %w", err) + } + + return nil +} + +// createInstallMessageFromURI creates a MsgInstallFunction from the given URI. +// CID is calculated as a SHA-256 hash of the URI. +func createInstallMessageFromURI(uri string) (request.InstallFunction, error) { + + cid, err := deriveCIDFromURI(uri) + if err != nil { + return request.InstallFunction{}, fmt.Errorf("could not determine cid: %w", err) + } + + msg := request.InstallFunction{ + Type: blockless.MessageInstallFunction, + ManifestURL: uri, + CID: cid, + } + + return msg, nil +} + +// createInstallMessageFromCID creates the MsgInstallFunction from the given CID. +func createInstallMessageFromCID(cid string) request.InstallFunction { + + req := request.InstallFunction{ + Type: blockless.MessageInstallFunction, + ManifestURL: fmt.Sprintf("https://%s.ipfs.w3s.link/manifest.json", cid), + CID: cid, + } + + return req +} + +func deriveCIDFromURI(uri string) (string, error) { + + h := sha256.New() + _, err := h.Write([]byte(uri)) + if err != nil { + return "", fmt.Errorf("could not calculate hash: %w", err) + } + cid := fmt.Sprintf("%x", h.Sum(nil)) + + return cid, nil +} diff --git a/node/roll_call.go b/node/roll_call.go new file mode 100644 index 00000000..6954c78f --- /dev/null +++ b/node/roll_call.go @@ -0,0 +1,110 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/models/response" +) + +func (n *Node) processRollCall(ctx context.Context, from peer.ID, payload []byte) error { + + // Only workers respond to roll calls at the moment. + if n.cfg.Role != blockless.WorkerNode { + n.log.Debug().Msg("skipping roll call as a non-worker node") + return nil + } + + // Unpack the request. + var req request.RollCall + err := json.Unmarshal(payload, &req) + if err != nil { + return fmt.Errorf("could not unpack request: %w", err) + } + req.From = from + + // Check if we have this manifest. + functionInstalled, err := n.isFunctionInstalled(req.FunctionID) + if err != nil { + // We could not lookup the manifest. + res := response.RollCall{ + Type: blockless.MessageRollCallResponse, + FunctionID: req.FunctionID, + RequestID: req.RequestID, + Code: response.CodeError, + } + + sendErr := n.send(ctx, req.From, res) + // Log send error but choose to return the original error. + n.log.Error(). + Err(sendErr). + Str("to", req.From.String()). + Msg("could not send response") + + return fmt.Errorf("could not check if function is installed: %w", err) + } + + // We don't have this function. + if !functionInstalled { + + res := response.RollCall{ + Type: blockless.MessageRollCallResponse, + FunctionID: req.FunctionID, + RequestID: req.RequestID, + Code: response.CodeNotFound, + } + + err = n.send(ctx, req.From, res) + if err != nil { + return fmt.Errorf("could not send response: %w", err) + } + + // TODO: In the original code we create a function install call here. + // However, we do it with the CID only, but the function install code + // requires manifestURL + CID. So at the moment this code path is not + // present here. + + return nil + } + + // Create response. + res := response.RollCall{ + Type: blockless.MessageRollCallResponse, + FunctionID: req.FunctionID, + RequestID: req.RequestID, + Code: response.CodeAccepted, + } + + // Send message. + err = n.send(ctx, req.From, res) + if err != nil { + return fmt.Errorf("could not send response: %w", err) + } + + return nil +} + +// issueRollCall will create a roll call request for executing the given function. +// On successful issuance of the roll call request, we return the ID of the issued request. +func (n *Node) issueRollCall(ctx context.Context, requestID string, functionID string) error { + + // Create a roll call request. + rollCall := request.RollCall{ + Type: blockless.MessageRollCall, + FunctionID: functionID, + RequestID: requestID, + } + + // Publish the mssage. + err := n.publish(ctx, rollCall) + if err != nil { + return fmt.Errorf("could not publish to topic: %w", err) + } + + return nil +} diff --git a/node/roll_call_internal_test.go b/node/roll_call_internal_test.go new file mode 100644 index 00000000..350ad3c0 --- /dev/null +++ b/node/roll_call_internal_test.go @@ -0,0 +1,214 @@ +package node + +import ( + "context" + "encoding/json" + "sync" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/host" + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/request" + "github.com/blocklessnetworking/b7s/models/response" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func TestNode_RollCall(t *testing.T) { + + var ( + rollCallReq = request.RollCall{ + Type: blockless.MessageRollCall, + FunctionID: "dummy-function-id", + RequestID: mocks.GenericUUID.String(), + } + ) + + payload := serialize(t, rollCallReq) + + t.Run("head node handles roll call", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.HeadNode) + err := node.processRollCall(context.Background(), mocks.GenericPeerID, payload) + require.NoError(t, err) + }) + + t.Run("worker node handles roll call", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.WorkerNode) + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + var wg sync.WaitGroup + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + var received response.RollCall + getStreamPayload(t, stream, &received) + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + require.Equal(t, blockless.MessageRollCallResponse, received.Type) + + require.Equal(t, rollCallReq.FunctionID, received.FunctionID) + require.Equal(t, rollCallReq.RequestID, received.RequestID) + require.Equal(t, response.CodeAccepted, received.Code) + }) + + err = node.processRollCall(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("worker node handles failure to check function store", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.WorkerNode) + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + // Function store fails to check function presence. + fstore := mocks.BaselineFunctionHandler(t) + fstore.GetFunc = func(string, string, bool) (*blockless.FunctionManifest, error) { + return nil, mocks.GenericError + } + node.function = fstore + + var wg sync.WaitGroup + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + var received response.RollCall + getStreamPayload(t, stream, &received) + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + require.Equal(t, blockless.MessageRollCallResponse, received.Type) + + require.Equal(t, rollCallReq.FunctionID, received.FunctionID) + require.Equal(t, rollCallReq.RequestID, received.RequestID) + require.Equal(t, response.CodeError, received.Code) + }) + + err = node.processRollCall(context.Background(), receiver.ID(), payload) + require.Error(t, err) + + wg.Wait() + }) + t.Run("worker node handles function not installed", func(t *testing.T) { + t.Parallel() + + node := createNode(t, blockless.WorkerNode) + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + // Function store works okay but function is not found. + fstore := mocks.BaselineFunctionHandler(t) + fstore.GetFunc = func(string, string, bool) (*blockless.FunctionManifest, error) { + return nil, blockless.ErrNotFound + } + node.function = fstore + + var wg sync.WaitGroup + wg.Add(1) + + receiver.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer wg.Done() + defer stream.Close() + + var received response.RollCall + getStreamPayload(t, stream, &received) + + from := stream.Conn().RemotePeer() + require.Equal(t, node.host.ID(), from) + + require.Equal(t, blockless.MessageRollCallResponse, received.Type) + + require.Equal(t, rollCallReq.FunctionID, received.FunctionID) + require.Equal(t, rollCallReq.RequestID, received.RequestID) + require.Equal(t, response.CodeNotFound, received.Code) + }) + + err = node.processRollCall(context.Background(), receiver.ID(), payload) + require.NoError(t, err) + + wg.Wait() + }) + t.Run("node issues roll call ok", func(t *testing.T) { + t.Parallel() + + const ( + topic = DefaultTopic + functionID = "super-secret-function-id" + ) + + ctx := context.Background() + + node := createNode(t, blockless.WorkerNode) + + receiver, err := host.New(mocks.NoopLogger, loopback, 0) + require.NoError(t, err) + + hostAddNewPeer(t, node.host, receiver) + + info := hostGetAddrInfo(t, receiver) + err = node.host.Connect(ctx, *info) + require.NoError(t, err) + + // Have both client and node subscribe to the same topic. + _, subscription, err := receiver.Subscribe(ctx, topic) + require.NoError(t, err) + + _, err = node.subscribe(ctx) + require.NoError(t, err) + + time.Sleep(subscriptionDiseminationPause) + + requestID, err := newRequestID() + require.NoError(t, err) + + err = node.issueRollCall(ctx, requestID, functionID) + require.NoError(t, err) + + deadlineCtx, cancel := context.WithTimeout(ctx, publishTimeout) + defer cancel() + + msg, err := subscription.Next(deadlineCtx) + require.NoError(t, err) + + from := msg.ReceivedFrom + require.Equal(t, node.host.ID(), from) + require.NotNil(t, msg.Topic) + require.Equal(t, topic, *msg.Topic) + + var received request.RollCall + err = json.Unmarshal(msg.Data, &received) + require.NoError(t, err) + + require.Equal(t, blockless.MessageRollCall, received.Type) + require.Equal(t, functionID, received.FunctionID) + require.Equal(t, requestID, received.RequestID) + }) +} diff --git a/node/run.go b/node/run.go new file mode 100644 index 00000000..da17eebe --- /dev/null +++ b/node/run.go @@ -0,0 +1,118 @@ +package node + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + + "github.com/libp2p/go-libp2p/core/network" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// Run will start the main loop for the node. +func (n *Node) Run(ctx context.Context) error { + + // Subscribe to the specified topic. + subscription, err := n.subscribe(ctx) + if err != nil { + return fmt.Errorf("could not subscribe to topic: %w", err) + } + + // Set the handler for direct messages. + n.listenDirectMessages(ctx) + + // Discover peers. + // NOTE: Potentially signal any error here so that we abort the node + // run loop if anything failed. + go func() { + err = n.host.DiscoverPeers(ctx, n.cfg.Topic) + if err != nil { + n.log.Error(). + Err(err). + Msg("could not discover peers") + } + }() + + // Start the health signal emitter in a separate goroutine. + go n.HealthPing(ctx) + + n.log.Info(). + Uint("concurrency", n.cfg.Concurrency). + Msg("starting node main loop") + + // Message processing loop. + for { + + // Retrieve next message. + msg, err := subscription.Next(ctx) + if err != nil { + // NOTE: Cancelling the context will lead us here. + n.log.Error().Err(err).Msg("could not receive message") + break + } + + // Skip messages we published. + if msg.ReceivedFrom == n.host.ID() { + continue + } + + n.log.Debug(). + Str("message_id", msg.ID). + Str("peer_id", msg.ReceivedFrom.String()). + Msg("received message") + + // Try to get a slot for processing the request. + n.sema <- struct{}{} + n.wg.Add(1) + + go func() { + // Free up slot after we're done. + defer n.wg.Done() + defer func() { <-n.sema }() + + err = n.processMessage(ctx, msg.ReceivedFrom, msg.Data) + if err != nil { + n.log.Error(). + Err(err). + Str("id", msg.ID). + Str("peer_id", msg.ReceivedFrom.String()). + Msg("could not process message") + } + }() + } + + n.log.Debug().Msg("waiting for messages being processed") + + n.wg.Wait() + + return nil +} + +// listenDirectMessages will process messages sent directly to the peer (as opposed to published messages). +func (n *Node) listenDirectMessages(ctx context.Context) { + + n.host.SetStreamHandler(blockless.ProtocolID, func(stream network.Stream) { + defer stream.Close() + + from := stream.Conn().RemotePeer() + + buf := bufio.NewReader(stream) + msg, err := buf.ReadBytes('\n') + if err != nil && !errors.Is(err, io.EOF) { + stream.Reset() + n.log.Error().Err(err).Msg("error receiving direct message") + return + } + + err = n.processMessage(ctx, from, msg) + if err != nil { + n.log.Error(). + Err(err). + Str("peer_id", from.String()). + Msg("could not process direct message") + } + }) +} diff --git a/node/store.go b/node/store.go new file mode 100644 index 00000000..b70b7ae6 --- /dev/null +++ b/node/store.go @@ -0,0 +1,6 @@ +package node + +type Store interface { + GetRecord(string, interface{}) error + SetRecord(string, interface{}) error +} diff --git a/node/testdata/hello.tar.gz b/node/testdata/hello.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a9688e1d7398227bd42802ad3735c76ca39e0383 GIT binary patch literal 27142 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 AK>z>% literal 0 HcmV?d00001 diff --git a/peerstore/params.go b/peerstore/params.go new file mode 100644 index 00000000..b6ef76d8 --- /dev/null +++ b/peerstore/params.go @@ -0,0 +1,5 @@ +package peerstore + +const ( + peersKey = "peers" +) diff --git a/peerstore/peerstore.go b/peerstore/peerstore.go new file mode 100644 index 00000000..aba560dc --- /dev/null +++ b/peerstore/peerstore.go @@ -0,0 +1,115 @@ +package peerstore + +import ( + "errors" + "fmt" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// PeerStore takes care of storing and reading peer information to and from persistent storage. +type PeerStore struct { + store Store +} + +// New creates a new PeerStore handler. +func New(store Store) *PeerStore { + + ps := PeerStore{ + store: store, + } + + return &ps +} + +// Store will persist the peer information. +func (p *PeerStore) Store(peerID peer.ID, addr multiaddr.Multiaddr, info peer.AddrInfo) error { + + // Check if we already have this peer stored. + var peer blockless.Peer + err := p.store.GetRecord(peerID.String(), &peer) + // If we don't have an error it means that the peer is already stored in the DB. We're done. + if err == nil { + return nil + } + + // Check if we failed to retrieve the record. If the error is `not found` - that's okay, + // and we want to store the peer info now. If it's any other error - halt. + if err != nil && !errors.Is(err, blockless.ErrNotFound) { + return fmt.Errorf("could not retrieve peer: %w", err) + } + + // New peer - create peer info record and store it. + peerInfo := blockless.Peer{ + Type: "peer", + ID: peerID, + MultiAddr: addr.String(), + AddrInfo: info, + } + + // Store the peer in the DB. + // NOTE: This may not be necessary, in case we already had this peer info. + // Check if we should re-store this data - perhaps we'd be updating part of it? + err = p.store.SetRecord(peerID.String(), peerInfo) + if err != nil { + return fmt.Errorf("could not store peer: %w", err) + } + + return nil +} + +// UpdatePeerList will check if the specified peer is found in the peer list. If not - it will be added. +// NOTE: We're basically duplicating knowledge here - if we have peer stored under its ID, +// we will have it in the `peers` list; do we need to duplicate it? +func (p *PeerStore) UpdatePeerList(peerID peer.ID, addr multiaddr.Multiaddr, info peer.AddrInfo) error { + + // Get list of peers from the store. + var peers []blockless.Peer + err := p.store.GetRecord(peersKey, &peers) + if err != nil && !errors.Is(err, blockless.ErrNotFound) { + return fmt.Errorf("could not retrieve peer list: %w", err) + } + + // Check if this is a known peer. + // NOTE: List iteration, might be slow with a long peer list and frequent connections. + for _, peer := range peers { + + // If the peer is already known, we're done. + if peer.ID == peerID { + return nil + } + } + + // New peer - add it to the list of peers. + peerInfo := blockless.Peer{ + Type: "peer", + ID: peerID, + MultiAddr: addr.String(), + AddrInfo: info, + } + peers = append(peers, peerInfo) + + // Store the updated peer list. + err = p.store.SetRecord(peersKey, peers) + if err != nil { + return fmt.Errorf("could not update peer list: %w", err) + } + + return nil +} + +// Peers returns the list of peers from the peer store. +func (p *PeerStore) Peers() ([]blockless.Peer, error) { + + // Get list of peers from the store. + var peers []blockless.Peer + err := p.store.GetRecord(peersKey, &peers) + if err != nil && !errors.Is(err, blockless.ErrNotFound) { + return nil, fmt.Errorf("could not retrieve peer list: %w", err) + } + + return peers, nil +} diff --git a/peerstore/peerstore_test.go b/peerstore/peerstore_test.go new file mode 100644 index 00000000..14075484 --- /dev/null +++ b/peerstore/peerstore_test.go @@ -0,0 +1,244 @@ +package peerstore_test + +import ( + "testing" + + "github.com/cockroachdb/pebble" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/peerstore" + "github.com/blocklessnetworking/b7s/store" + "github.com/blocklessnetworking/b7s/testing/helpers" + "github.com/blocklessnetworking/b7s/testing/mocks" +) + +func Test_PeerStore(t *testing.T) { + t.Run("empty peer store", func(t *testing.T) { + t.Parallel() + + peerstore, _, db := setupPeerStore(t) + defer db.Close() + + peers, err := peerstore.Peers() + require.NoError(t, err) + require.Empty(t, peers) + }) + t.Run("store peer", func(t *testing.T) { + t.Parallel() + + peerstore, store, db := setupPeerStore(t) + defer db.Close() + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.Store(mocks.GenericPeerID, addr, info) + require.NoError(t, err) + + // Verify peer is written to the underlying store. + var peer blockless.Peer + err = store.GetRecord(peerID.String(), &peer) + require.NoError(t, err) + + require.Equal(t, peerID, peer.ID) + require.Equal(t, addr.String(), peer.MultiAddr) + require.Equal(t, info, peer.AddrInfo) + }) + t.Run("update peer list", func(t *testing.T) { + t.Parallel() + + peerstore, _, db := setupPeerStore(t) + defer db.Close() + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.UpdatePeerList(peerID, addr, info) + require.NoError(t, err) + + peers, err := peerstore.Peers() + require.NoError(t, err) + require.Len(t, peers, 1) + + peer := peers[0] + require.Equal(t, peerID, peer.ID) + require.Equal(t, addr.String(), peer.MultiAddr) + require.Equal(t, info, peer.AddrInfo) + }) + t.Run("adding known peer to peer list", func(t *testing.T) { + t.Parallel() + + peerstore, _, db := setupPeerStore(t) + defer db.Close() + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.UpdatePeerList(peerID, addr, info) + require.NoError(t, err) + + // Add the same peer again - we should still only have one peer in the list. + err = peerstore.UpdatePeerList(peerID, addr, info) + require.NoError(t, err) + + peers, err := peerstore.Peers() + require.NoError(t, err) + require.Len(t, peers, 1) + }) +} + +func Test_PeerStore_Store(t *testing.T) { + + t.Run("handles failure to store peer", func(t *testing.T) { + + store := mocks.BaselineStore(t) + store.GetRecordFunc = func(string, interface{}) error { + // We first check if the peer exists - make sure it doesn't. + return blockless.ErrNotFound + } + store.SetRecordFunc = func(string, interface{}) error { + return mocks.GenericError + } + + peerstore := peerstore.New(store) + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.Store(peerID, addr, info) + require.ErrorIs(t, err, mocks.GenericError) + }) + t.Run("handles failure to get existing peer", func(t *testing.T) { + + store := mocks.BaselineStore(t) + store.GetRecordFunc = func(string, interface{}) error { + return mocks.GenericError + } + peerstore := peerstore.New(store) + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.Store(peerID, addr, info) + require.ErrorIs(t, err, mocks.GenericError) + }) + t.Run("handles noop on existing peer", func(t *testing.T) { + + store := mocks.BaselineStore(t) + peerstore := peerstore.New(store) + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.Store(peerID, addr, info) + require.NoError(t, err) + }) + t.Run("handles failure to get existing peer list", func(t *testing.T) { + store := mocks.BaselineStore(t) + store.GetRecordFunc = func(string, interface{}) error { + return mocks.GenericError + } + + peerstore := peerstore.New(store) + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.UpdatePeerList(peerID, addr, info) + require.ErrorIs(t, err, mocks.GenericError) + }) + t.Run("handles failure to update peer list", func(t *testing.T) { + store := mocks.BaselineStore(t) + store.SetRecordFunc = func(string, interface{}) error { + return mocks.GenericError + } + + peerstore := peerstore.New(store) + + var ( + peerID = mocks.GenericPeerID + addr = genericMultiAddr(t) + info = peer.AddrInfo{ + ID: peerID, + Addrs: []multiaddr.Multiaddr{addr}, + } + ) + + err := peerstore.UpdatePeerList(peerID, addr, info) + require.ErrorIs(t, err, mocks.GenericError) + }) + t.Run("handles peer list retrieval error", func(t *testing.T) { + store := mocks.BaselineStore(t) + store.GetRecordFunc = func(string, interface{}) error { + return mocks.GenericError + } + + peerstore := peerstore.New(store) + + _, err := peerstore.Peers() + require.ErrorIs(t, err, mocks.GenericError) + }) +} +func setupPeerStore(t *testing.T) (*peerstore.PeerStore, *store.Store, *pebble.DB) { + t.Helper() + + db := helpers.InMemoryDB(t) + store := store.New(db) + ps := peerstore.New(store) + + return ps, store, db +} + +func genericMultiAddr(t *testing.T) multiaddr.Multiaddr { + t.Helper() + + addr, err := multiaddr.NewMultiaddr(mocks.GenericAddress) + require.NoError(t, err) + + return addr +} diff --git a/peerstore/store.go b/peerstore/store.go new file mode 100644 index 00000000..fcc35588 --- /dev/null +++ b/peerstore/store.go @@ -0,0 +1,6 @@ +package peerstore + +type Store interface { + SetRecord(key string, value interface{}) error + GetRecord(key string, out interface{}) error +} diff --git a/src/README.md b/src/README.md deleted file mode 100644 index d26758f7..00000000 --- a/src/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# contributing - -- golang 1.18 -- clone repo -- `make` - -## structure - -The main entry uses `spf13/cobra`, and that setup can be found in `main.go`. The cobra `rootCMD` is set to start the `daemon`. While other sub commands, may run another service. - -``` -src/ -├─ main.go -├─ controller/ -├─ enums/ -├─ models/ -├─ messaging/ -Makefile -``` - -`controller` package will contain all the methods that are used inside the daemon to do something. `models` contain structs, but also contains helpers to extend those models, importantly all `message` structs are defined here to pass through the `p2p` channels. `enums` makes our communication consistent. `messaging` contains the wiring to send messages on the network, but also has handlers defined to react to messages sent on the network. - -## messaging paths diff --git a/src/chain/README.md b/src/chain/README.md deleted file mode 100644 index 5f9f2500..00000000 --- a/src/chain/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## chain client - -this part of the application responds to chain events. It is responsible for managing the identity of the Node on the Blockless Blockchain. diff --git a/src/chain/chain.go b/src/chain/chain.go deleted file mode 100644 index 740f4120..00000000 --- a/src/chain/chain.go +++ /dev/null @@ -1,17 +0,0 @@ -package chain - -import ( - "context" -) - -func registerNode(ctx context.Context) { - -} - -func startClient(ctx context.Context) { - -} - -func Start(ctx context.Context) { - -} diff --git a/src/config/config.go b/src/config/config.go deleted file mode 100644 index d4fdf0fe..00000000 --- a/src/config/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package config - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/blocklessnetworking/b7s/src/models" - log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v2" -) - -var ( - C models.Config -) - -func parseYamlFile(file string, o interface{}) error { - if file == "" { - file = "config.yaml" - } - - if !filepath.IsAbs(file) { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("get current working directory failed: %v", err) - } - file = filepath.Join(cwd, file) - } - - log.WithFields(log.Fields{ - "configPath": file, - }).Info("config path set") - - b, err := ioutil.ReadFile(file) - if err != nil { - return fmt.Errorf("read file %s failed (%s)", file, err.Error()) - } - - return yaml.Unmarshal(b, o) -} - -func Load(cfgFile string) error { - if err := parseYamlFile(cfgFile, &C); err != nil { - return err - } - return nil -} diff --git a/src/config/config_test.go b/src/config/config_test.go deleted file mode 100644 index 6977c505..00000000 --- a/src/config/config_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package config - -import ( - "io/ioutil" - "os" - "testing" -) - -func TestLoad(t *testing.T) { - // Create a test config.yaml file - configYaml := []byte(` -node: - workspace_root: "./testdata" - `) - ioutil.WriteFile("/tmp/b7_test_config.yaml", configYaml, 0644) - defer os.Remove("/tmp/b7_test_config.yaml") - - // Load config - err := Load("/tmp/b7_test_config.yaml") - if err != nil { - t.Errorf("Error loading config: %v", err) - } - - // Check that config values are correct - if C.Node.WorkspaceRoot != "./testdata" { - t.Errorf("Expected workspace root to be './testdata', got %s", C.Node.WorkspaceRoot) - } -} diff --git a/src/controller/README.md b/src/controller/README.md deleted file mode 100644 index 14b1f4e4..00000000 --- a/src/controller/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## controller - -This part of the application contains orchestration logic to complete various tasks. When required tasks are run through the controller as a single point, where `role` of the node is then determined. - -Code has been siloed into files specifying if the role is type `head` or type `worker` diff --git a/src/controller/controller.go b/src/controller/controller.go deleted file mode 100644 index fecb77b1..00000000 --- a/src/controller/controller.go +++ /dev/null @@ -1,113 +0,0 @@ -package controller - -import ( - "context" - "encoding/json" - "errors" - - "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/memstore" - "github.com/blocklessnetworking/b7s/src/messaging" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/blocklessnetworking/b7s/src/repository" - pubsub "github.com/libp2p/go-libp2p-pubsub" - log "github.com/sirupsen/logrus" -) - -func IsFunctionInstalled(ctx context.Context, functionId string) (models.FunctionManifest, error) { - functionManifestString, err := db.GetString(ctx, functionId) - functionManifest := models.FunctionManifest{} - - json.Unmarshal([]byte(functionManifestString), &functionManifest) - - if err != nil { - if err.Error() == "pebble: not found" { - return functionManifest, errors.New("function not installed") - } else { - return functionManifest, err - } - } - - return functionManifest, nil -} - -func ExecuteFunction(ctx context.Context, request models.RequestExecute) (models.ExecutorResponse, error) { - config := ctx.Value("config").(models.Config) - executorRole := config.Protocol.Role - - // if the role is a peer, then we need to send the request to the peer - if executorRole == enums.RoleWorker { - return WorkerExecuteFunction(ctx, request) - } else { - return HeadExecuteFunction(ctx, request) - } -} - -func InstallFunction(ctx context.Context, installMessage models.MsgInstallFunction) error { - if _, err := repository.GetPackage(ctx, installMessage); err != nil { - return err - } - - msg, err := json.Marshal(models.NewMsgInstallFunctionResponse(enums.ResponseCodeAccepted, "installed")) - if err != nil { - log.Error("Error marshalling install function response", err) - return err - } - - if err := messaging.SendMessage(ctx, installMessage.From, msg); err != nil { - log.Error("Error sending message to peer", err) - return err - } - - return nil -} - -func RollCall(ctx context.Context, functionId string) *models.MsgRollCall { - msgRollCall := models.NewMsgRollCall(functionId) - messaging.PublishMessage(ctx, ctx.Value("topic").(*pubsub.Topic), msgRollCall) - return msgRollCall -} - -func RollCallResponse(ctx context.Context, msg models.MsgRollCall) { - _, err := IsFunctionInstalled(ctx, msg.FunctionId) - - response := models.MsgRollCallResponse{ - Type: enums.MsgRollCallResponse, - FunctionId: msg.FunctionId, - Code: enums.ResponseCodeAccepted, - RequestId: msg.RequestId, - } - - if err != nil { - err := InstallFunction(ctx, models.MsgInstallFunction{ - Type: enums.MsgInstallFunction, - From: msg.From, - Cid: msg.FunctionId, - }) - - if err != nil { - response.Code = enums.ResponseCodeNotFound - } - } - - responseJSON, err := json.Marshal(response) - - if err != nil { - log.Debug("error marshalling roll call response") - return - } - - messaging.SendMessage(ctx, msg.From, responseJSON) -} - -func HealthStatus(ctx context.Context) { - message := models.NewMsgHealthPing(enums.ResponseCodeOk) - messaging.PublishMessage(ctx, ctx.Value("topic").(*pubsub.Topic), message) -} - -func GetExecutionResponse(ctx context.Context, reqId string) *models.MsgExecuteResponse { - memstore := ctx.Value("executionResponseMemStore").(memstore.ReqRespStore) - response := memstore.Get(reqId) - return response -} diff --git a/src/controller/controller_test.go b/src/controller/controller_test.go deleted file mode 100644 index b9ded953..00000000 --- a/src/controller/controller_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package controller - -import ( - "context" - "encoding/json" - "fmt" - "os/exec" - "strings" - "testing" - - "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/memstore" - "github.com/blocklessnetworking/b7s/src/models" -) - -func TestIsFunctionInstalled(t *testing.T) { - - // set up a mock function manifest to store in the database - mockManifest := models.FunctionManifest{ - Function: models.Function{ - ID: "test-function", - Name: "Test Function", - Version: "1.0.0", - Runtime: "go", - }, - Deployment: models.Deployment{ - Cid: "Qmabcdef", - Checksum: "123456789", - Uri: "https://ipfs.io/ipfs/Qmabcdef", - Methods: []models.Methods{ - { - Name: "TestMethod", - Entry: "main.TestMethod", - }, - }, - }, - Runtime: models.Runtime{ - Checksum: "987654321", - Url: "https://ipfs.io/ipfs/Qmzyxwvu", - }, - } - mockManifestBytes, _ := json.Marshal(mockManifest) - - appDb := db.GetDb("/tmp/b7s") - ctx := context.WithValue(context.Background(), "appDb", appDb) - defer db.Close(ctx) - - // Insert a test value into the database - db.Set(ctx, "test_key", string(mockManifestBytes)) - - // Call IsFunctionInstalled - functionManifest, err := IsFunctionInstalled(ctx, "test_key") - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Compare the function manifests as strings to account for potential encoding issues - functionManifestBytes, _ := json.Marshal(functionManifest) - expectedManifestBytes, _ := json.Marshal(mockManifest) - if string(functionManifestBytes) != string(expectedManifestBytes) { - t.Errorf("Unexpected function manifest. Got %v, expected %v", functionManifest, mockManifest) - } -} -func TestExecuteFunction(t *testing.T) { - // Create a mock Config value to pass to the context - mockConfig := models.Config{ - Protocol: models.ConfigProtocol{ - Role: enums.RoleWorker, - }, - Node: models.ConfigNode{ - WorkspaceRoot: "/tmp/b7s_tests", - }, - } - ctx := context.WithValue(context.Background(), "config", mockConfig) - testStringValue := "foo" - testString := fmt.Sprintf("echo %s", testStringValue) - // Inject a mock execCommand function - mockExecCommand := func(command string, args ...string) *exec.Cmd { - cs := []string{"-c", testString} - cmd := exec.Command("bash", cs...) - return cmd - } - ctx = context.WithValue(ctx, "execCommand", mockExecCommand) - - // Set up a mock function manifest to store in the database - mockManifest := models.FunctionManifest{ - Function: models.Function{ - ID: "test-function", - Name: "Test Function", - Version: "1.0.0", - Runtime: "go", - }, - Deployment: models.Deployment{ - Cid: "Qmabcdef", - Checksum: "123456789", - Uri: "https://ipfs.io/ipfs/Qmabcdef", - Methods: []models.Methods{ - { - Name: "TestMethod", - Entry: "main.TestMethod", - }, - }, - }, - Runtime: models.Runtime{ - Checksum: "987654321", - Url: "https://ipfs.io/ipfs/Qmzyxwvu", - }, - } - mockManifestBytes, _ := json.Marshal(mockManifest) - - appDb := db.GetDb("/tmp/b7s") - ctx = context.WithValue(ctx, "appDb", appDb) - defer db.Close(ctx) - - // response memstore - executionResponseMemStore := memstore.NewReqRespStore() - ctx = context.WithValue(ctx, "executionResponseMemStore", executionResponseMemStore) - - // Insert the mock function manifest into the database - db.Set(ctx, "test-function", string(mockManifestBytes)) - - // Create a mock RequestExecute value to pass to ExecuteFunction - mockRequest := models.RequestExecute{ - FunctionId: "test-function", - Method: "TestMethod", - Parameters: []models.RequestExecuteParameters{ - { - Name: "param1", - Value: "value1", - }, - }, - Config: models.ExecutionRequestConfig{}, - } - - // Call ExecuteFunction with the mock context and request - response, err := ExecuteFunction(ctx, mockRequest) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Assert that the correct function was called (Worker`Exec`uteFunction in this case) - if strings.Trim(response.Result, "\n") != testStringValue { - t.Errorf("Unexpected response. Got %v, expected %v", response.Result, testStringValue) - } -} diff --git a/src/controller/head.go b/src/controller/head.go deleted file mode 100644 index 0cbae392..00000000 --- a/src/controller/head.go +++ /dev/null @@ -1,111 +0,0 @@ -package controller - -import ( - "context" - "encoding/json" - "time" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/messaging" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p-core/host" - "github.com/libp2p/go-libp2p-core/peer" - log "github.com/sirupsen/logrus" -) - -func HeadExecuteFunction(ctx context.Context, request models.RequestExecute) (models.ExecutorResponse, error) { - // perform rollcall to see who is available - rollcallMessage := RollCall(ctx, request.FunctionId) - - type rollcalled struct { - From peer.ID - Code string - } - rollCalledChannel := make(chan rollcalled) - - go func(ctx context.Context) { - // collect responses of nodes who want to work on the request - host := ctx.Value("host").(host.Host) - rollcallResponseChannel := ctx.Value(enums.ChannelMsgRollCallResponse).(chan models.MsgRollCallResponse) - _, timeoutCancel := context.WithCancel(ctx) - // time out - // should we retry this? - // possible no ne responds back - go func() { - timeout := time.After(5 * time.Second) - LOOP: - select { - case <-timeout: - rollCalledChannel <- rollcalled{ - Code: enums.ResponseCodeTimeout, - } - return - case msg := <-rollcallResponseChannel: - conns := host.Network().ConnsToPeer(msg.From) - - if msg.Code == enums.ResponseCodeAccepted && msg.FunctionId == request.FunctionId && len(conns) > 0 && rollcallMessage.RequestId == msg.RequestId { - timeoutCancel() - rollCalledChannel <- rollcalled{ - From: msg.From, - } - return - } else { - goto LOOP - } - case <-ctx.Done(): - log.Warn("timeout cancelled") - return - } - }() - }(ctx) - - msgRollCall := <-rollCalledChannel - - if msgRollCall.Code == enums.ResponseCodeTimeout { - return models.ExecutorResponse{ - Code: enums.ResponseCodeTimeout, - }, nil - } - - // we got a message back before the timeout went off - // request an execution from first responding node - // we should queue these responses into a pool first - // for selection - msgExecute := models.MsgExecute{ - Type: enums.MsgExecute, - FunctionId: request.FunctionId, - Method: request.Method, - Parameters: request.Parameters, - Config: request.Config, - } - - jsonBytes, err := json.Marshal(msgExecute) - - if err != nil { - return models.ExecutorResponse{ - Code: enums.ResponseCodeError, - }, err - } - - // send execute message to node - messaging.SendMessage(ctx, msgRollCall.From, jsonBytes) - - // wait for response - executeResponseChannel := ctx.Value(enums.ChannelMsgExecuteResponse).(chan models.MsgExecuteResponse) - msgExec := <-executeResponseChannel - - // too many models here ? - out := models.ExecutorResponse{ - Code: msgExec.Code, - Result: msgExec.Result, - RequestId: msgExec.RequestId, - } - - // log.WithFields(log.Fields{ - // "msg": msgExec, - // }).Info("execute response") - - defer close(rollCalledChannel) - return out, nil - -} diff --git a/src/controller/install_function.go b/src/controller/install_function.go deleted file mode 100644 index 1bd6d552..00000000 --- a/src/controller/install_function.go +++ /dev/null @@ -1,90 +0,0 @@ -package controller - -import ( - "context" - "crypto/sha256" - "errors" - "fmt" - - pubsub "github.com/libp2p/go-libp2p-pubsub" - log "github.com/sirupsen/logrus" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/messaging" - "github.com/blocklessnetworking/b7s/src/models" -) - -// MsgInstallFunction publishes the function install message to pubsub. -func MsgInstallFunction(ctx context.Context, req models.RequestFunctionInstall) error { - - if req.Uri == "" && req.Cid == "" { - return errors.New("invalid request - URI and CID are empty") - } - - var msg models.MsgInstallFunction - if req.Uri != "" { - var err error - msg, err = createInstallMessageFromURI(req.Uri) - if err != nil { - return fmt.Errorf("could not create install message from URI: %W", err) - } - } else { - msg = createInstallMessageFromCID(req.Cid) - } - - log.WithField("url", msg.ManifestUrl). - Info("Requesting to message peer for function installation") - - // Get the pubsub topic from the context. - topic, ok := ctx.Value("topic").(*pubsub.Topic) - if !ok { - return errors.New("could not get pubsub topic from context") - } - - // Write the message to pubsub topic. - messaging.PublishMessage(ctx, topic, msg) - - return nil -} - -// createInstallMessageFromURI creates a MsgInstallFunction from the given URI. -// CID is calculated as a SHA-256 hash of the URI. -func createInstallMessageFromURI(uri string) (models.MsgInstallFunction, error) { - - cid, err := deriveCIDFromURI(uri) - if err != nil { - return models.MsgInstallFunction{}, fmt.Errorf("could not determine cid: %w", err) - } - - msg := models.MsgInstallFunction{ - Type: enums.MsgInstallFunction, - ManifestUrl: uri, - Cid: cid, - } - - return msg, nil -} - -func deriveCIDFromURI(uri string) (string, error) { - - h := sha256.New() - _, err := h.Write([]byte(uri)) - if err != nil { - return "", fmt.Errorf("could not calculate hash: %w", err) - } - cid := fmt.Sprintf("%x", h.Sum(nil)) - - return cid, nil -} - -// createInstallMessageFromCID creates the MsgInstallFunction from the given CID. -func createInstallMessageFromCID(cid string) models.MsgInstallFunction { - - msg := models.MsgInstallFunction{ - Type: enums.MsgInstallFunction, - ManifestUrl: fmt.Sprintf("https://%s.ipfs.w3s.link/manifest.json", cid), - Cid: cid, - } - - return msg -} diff --git a/src/controller/install_function_test.go b/src/controller/install_function_test.go deleted file mode 100644 index f060083f..00000000 --- a/src/controller/install_function_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package controller - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/blocklessnetworking/b7s/src/enums" -) - -func TestCreateInstallMessageFromURI(t *testing.T) { - - const ( - uri = "https://example.com/manifest.json" - ) - - req, err := createInstallMessageFromURI(uri) - require.NoError(t, err) - - assert.Equal(t, enums.MsgInstallFunction, req.Type) - assert.Equal(t, uri, req.ManifestUrl) - - cid, err := deriveCIDFromURI(uri) - require.NoError(t, err) - - assert.Equal(t, cid, req.Cid) -} - -func TestCreateInstallMessageFromCID(t *testing.T) { - - const ( - cid = "test-cid-value" - expectedManifestURL = `https://test-cid-value.ipfs.w3s.link/manifest.json` - ) - - req := createInstallMessageFromCID(cid) - - assert.Equal(t, enums.MsgInstallFunction, req.Type) - assert.Equal(t, cid, req.Cid) - assert.Equal(t, expectedManifestURL, req.ManifestUrl) -} diff --git a/src/controller/worker.go b/src/controller/worker.go deleted file mode 100644 index f724e9dc..00000000 --- a/src/controller/worker.go +++ /dev/null @@ -1,30 +0,0 @@ -package controller - -import ( - "context" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/executor" - "github.com/blocklessnetworking/b7s/src/models" -) - -func WorkerExecuteFunction(ctx context.Context, request models.RequestExecute) (models.ExecutorResponse, error) { - functionManifest, err := IsFunctionInstalled(ctx, request.FunctionId) - - // return if the function isn't installed - // maybe install it? - if err != nil { - out := models.ExecutorResponse{ - Code: enums.ResponseCodeNotFound, - } - return out, err - } - - out, err := executor.Execute(ctx, request, functionManifest) - - if err != nil { - return out, err - } - - return out, nil -} diff --git a/src/daemon/README.md b/src/daemon/README.md deleted file mode 100644 index 8e06b2ce..00000000 --- a/src/daemon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## daemon - -This part of the application is responsible for 'glueing' a running service together. It runs all other parts of the applications as `go functions` with the main `func` yielding to a `select {}` diff --git a/src/daemon/channels.go b/src/daemon/channels.go deleted file mode 100644 index ab198e16..00000000 --- a/src/daemon/channels.go +++ /dev/null @@ -1,110 +0,0 @@ -package daemon - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/blocklessnetworking/b7s/src/controller" - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/messaging" - "github.com/blocklessnetworking/b7s/src/models" - log "github.com/sirupsen/logrus" -) - -func setupChannels(ctx context.Context) context.Context { - // define channels before instanciating the host - // msgInstallFunctionChannel := make(chan models.MsgInstallFunction) - // msgExecute := make(chan models.MsgExecute) - msgExecuteResponse := make(chan models.MsgExecuteResponse) - // msgRollCallChannel := make(chan models.MsgRollCall) - msgRollCallResponseChannel := make(chan models.MsgRollCallResponse) - msgChannelLocal := make(chan models.Message) - // ctx = context.WithValue(ctx, enums.ChannelMsgExecute, msgExecute) - ctx = context.WithValue(ctx, enums.ChannelMsgExecuteResponse, msgExecuteResponse) - // ctx = context.WithValue(ctx, enums.ChannelMsgInstallFunction, msgInstallFunctionChannel) - // ctx = context.WithValue(ctx, enums.ChannelMsgRollCall, msgRollCallChannel) - ctx = context.WithValue(ctx, enums.ChannelMsgRollCallResponse, msgRollCallResponseChannel) - ctx = context.WithValue(ctx, enums.ChannelMsgLocal, msgChannelLocal) - - return ctx -} - -func listenToChannels(ctx context.Context) { - - msgChannel := ctx.Value(enums.ChannelMsgLocal).(chan models.Message) - - for { - select { - case msg := <-msgChannel: - switch msg.Type { - case enums.MsgInstallFunction: - m, ok := msg.Data.(models.MsgInstallFunction) - if ok { - controller.InstallFunction(ctx, m) - } else { - fmt.Println("The assertion failed.") - } - case enums.MsgExecuteResponse: - msg := msg.Data.(*models.MsgExecute) - requestExecute := models.RequestExecute{ - FunctionId: msg.FunctionId, - Method: msg.Method, - Parameters: msg.Parameters, - Config: msg.Config, - } - executorResponse, err := controller.ExecuteFunction(ctx, requestExecute) - if err != nil { - log.Error(err) - } - jsonBytes, err := json.Marshal(&models.MsgExecuteResponse{ - RequestId: executorResponse.RequestId, - Type: enums.MsgExecuteResponse, - Code: executorResponse.Code, - Result: executorResponse.Result, - }) - messaging.SendMessage(ctx, msg.From, jsonBytes) - case enums.MsgRollCall: - controller.RollCallResponse(ctx, msg.Data.(models.MsgRollCall)) - } - } - } -} - -// func listenToChannels(ctx context.Context) { -// msgInstallFunctionChannel := ctx.Value(enums.ChannelMsgInstallFunction).(chan models.MsgInstallFunction) -// msgExecute := ctx.Value(enums.ChannelMsgExecute).(chan models.MsgExecute) -// msgRollCallChannel := ctx.Value(enums.ChannelMsgRollCall).(chan models.MsgRollCall) - -// for { -// select { -// case msg := <-msgInstallFunctionChannel: -// controller.InstallFunction(ctx, msg) -// case msg := <-msgRollCallChannel: -// controller.RollCallResponse(ctx, msg) -// case msg := <-msgExecute: -// // todo no sir I don't like this -// // I think this is duplicated in the controller -// requestExecute := models.RequestExecute{ -// FunctionId: msg.FunctionId, -// Method: msg.Method, -// Parameters: msg.Parameters, -// Config: msg.Config, -// } -// executorResponse, err := controller.ExecuteFunction(ctx, requestExecute) -// if err != nil { -// log.Error(err) -// } - -// jsonBytes, err := json.Marshal(&models.MsgExecuteResponse{ -// RequestId: executorResponse.RequestId, -// Type: enums.MsgExecuteResponse, -// Code: executorResponse.Code, -// Result: executorResponse.Result, -// }) - -// // send exect response back to head node -// messaging.SendMessage(ctx, msg.From, jsonBytes) -// } -// } -// } diff --git a/src/daemon/daemon.go b/src/daemon/daemon.go deleted file mode 100644 index ed30566d..00000000 --- a/src/daemon/daemon.go +++ /dev/null @@ -1,102 +0,0 @@ -package daemon - -import ( - "context" - "strconv" - - "os" - "path/filepath" - "time" - - "github.com/blocklessnetworking/b7s/src/chain" - "github.com/blocklessnetworking/b7s/src/config" - "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/dht" - "github.com/blocklessnetworking/b7s/src/health" - "github.com/blocklessnetworking/b7s/src/host" - "github.com/blocklessnetworking/b7s/src/memstore" - "github.com/blocklessnetworking/b7s/src/messaging" - "github.com/blocklessnetworking/b7s/src/restapi" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -// the daemonm service loop -// also the rootCommand for cobra -func Run(cmd *cobra.Command, args []string, configPath string) { - topicName := "blockless/b7s/general" - ctx := context.Background() - ctx = context.WithValue(ctx, "topicName", topicName) - - ex, err := os.Executable() - if err != nil { - log.Warn(err) - } - - // get the path to the executable - exPath := filepath.Dir(ex) - - // load config - err = config.Load(configPath) - if err != nil { - log.Fatal(err) - } - - // set context config - ctx = context.WithValue(ctx, "config", config.C) - - // create a new node hode - port, err := strconv.Atoi(config.C.Node.Port) - if err != nil { - log.Fatal(err) - } - - // setup any daemon processing channels we need to listen to - ctx = setupChannels(ctx) - - // create a new libp2p host - h := host.NewHost(ctx, port, config.C.Node.IP) - ctx = context.WithValue(ctx, "host", h) - - // set appdb config - appDb := db.GetDb(exPath + "/" + h.ID().Pretty() + "_appDb") - ctx = context.WithValue(ctx, "appDb", appDb) - - n := &host.ConnectedNotifee{ - Ctx: ctx, - } - - h.Network().Notify(n) - - // response memstore - // todo flush memstore occasionally - executionResponseMemStore := memstore.NewReqRespStore() - ctx = context.WithValue(ctx, "executionResponseMemStore", executionResponseMemStore) - - // listen to channels and handle messages from other parts of the applications - // if coming from the network, these messages would processing through `messageHandlers` first - go listenToChannels(ctx) - - // pubsub topics from p2p - topic := messaging.Subscribe(ctx, h, topicName) - ctx = context.WithValue(ctx, "topic", topic) - - // start health monitoring - ticker := time.NewTicker(1 * time.Minute) - go health.StartPing(ctx, ticker) - - // start other services based on config - if config.C.Protocol.Role == "head" { - restapi.Start(ctx) - chain.Start(ctx) - } - - defer ticker.Stop() - - // discover peers - go dht.DiscoverPeers(ctx, h) - - // daemon is running - // waiting for ctrl-c to exit - select {} -} diff --git a/src/db/db.go b/src/db/db.go deleted file mode 100644 index 279f2c4a..00000000 --- a/src/db/db.go +++ /dev/null @@ -1,102 +0,0 @@ -package db - -import ( - "context" - "fmt" - "sync" - - log "github.com/sirupsen/logrus" - - "github.com/cockroachdb/pebble" -) - -var ( - mtx sync.RWMutex - db *pebble.DB - isClosed bool -) - -func GetDb(databaseID string) *pebble.DB { - mtx.Lock() - defer mtx.Unlock() - if db == nil { - log.Info("opening database: ", databaseID) - d, err := pebble.Open(databaseID, &pebble.Options{}) - if err != nil { - log.Warn(err) - } - db = d - } - isClosed = false - return db -} - -func Set(ctx context.Context, key string, value string) error { - mtx.Lock() - defer mtx.Unlock() - - if isClosed { - return fmt.Errorf("database is closed") - } - - d := ctx.Value("appDb").(*pebble.DB) - if err := d.Set([]byte(key), []byte(value), pebble.Sync); err != nil { - log.Warn(err) - return err - } - return nil -} - -func Get(ctx context.Context, key string) ([]byte, error) { - mtx.RLock() - defer mtx.RUnlock() - - if isClosed { - return nil, fmt.Errorf("database is closed") - } - - d := ctx.Value("appDb").(*pebble.DB) - value, closer, err := d.Get([]byte(key)) - if err != nil { - return nil, err - } - defer closer.Close() - return value, nil -} - -func GetString(ctx context.Context, key string) (string, error) { - mtx.RLock() - defer mtx.RUnlock() - - if isClosed { - return "", fmt.Errorf("database is closed") - } - - d := ctx.Value("appDb").(*pebble.DB) - value, closer, err := d.Get([]byte(key)) - if err != nil { - return "", err - } - stringVal := string(value) - defer closer.Close() - return stringVal, nil -} - -func Close(ctx context.Context) error { - mtx.Lock() - defer mtx.Unlock() - - if isClosed { - return fmt.Errorf("database is closed") - } - - d := ctx.Value("appDb").(*pebble.DB) - if db != nil { - if err := d.Close(); err != nil { - log.Warn(err) - } - db = nil - } - isClosed = true - return nil -} diff --git a/src/db/db_test.go b/src/db/db_test.go deleted file mode 100644 index 90c63f47..00000000 --- a/src/db/db_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package db - -import ( - "context" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestDb(t *testing.T) { - // setup - databaseID := "/tmp/test_db" - os.RemoveAll(databaseID) - - // test GetDb - db := GetDb(databaseID) - assert.NotNil(t, db) - - ctx := context.WithValue(context.Background(), "appDb", db) - - // test Set and Get - err := Set(ctx, "test_key", "test_value") - assert.Nil(t, err) - - value, err := Get(ctx, "test_key") - assert.Nil(t, err) - assert.Equal(t, "test_value", string(value)) - - // test GetString - stringValue, err := GetString(ctx, "test_key") - assert.Nil(t, err) - assert.Equal(t, "test_value", stringValue) - - // test Close - Close(ctx) - value, err = Get(ctx, "test_key") - assert.Nil(t, value) - assert.NotNil(t, err) -} diff --git a/src/dht/README.md b/src/dht/README.md deleted file mode 100644 index 91143700..00000000 --- a/src/dht/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## dht - -this part of the application details how we connect to other nodes, and the information we're doing for discovery of those nodes. - -for the most part, we leave the discovery up to the `KHD` engine. diff --git a/src/dht/dht.go b/src/dht/dht.go deleted file mode 100644 index 99cf709b..00000000 --- a/src/dht/dht.go +++ /dev/null @@ -1,136 +0,0 @@ -package dht - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "sync" - - db "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p-core/peer" - dht "github.com/libp2p/go-libp2p-kad-dht" - "github.com/libp2p/go-libp2p/core/host" - drouting "github.com/libp2p/go-libp2p/p2p/discovery/routing" - dutil "github.com/libp2p/go-libp2p/p2p/discovery/util" - "github.com/multiformats/go-multiaddr" - log "github.com/sirupsen/logrus" -) - -func InitDHT(ctx context.Context, h host.Host) *dht.IpfsDHT { - // Start a DHT, for use in peer discovery. - kademliaDHT, err := dht.New(ctx, h) - if err != nil { - log.Fatal(err) - } - - // Set the DHT to server mode. - dht.Mode(dht.ModeServer) - - // Bootstrap the DHT. - if err = kademliaDHT.Bootstrap(ctx); err != nil { - log.Fatal(err) - } - - // Get the list of bootstrap nodes from the configuration. - cfg := ctx.Value("config").(models.Config) - bootNodes := cfg.Node.BootNodes - - // Get the list of dial-back peers from the database. - var dialBackPeers []models.Peer - peersRecordString, err := db.Get(ctx, "peers") - if err != nil { - peersRecordString = []byte("[]") - } - if err = json.Unmarshal(peersRecordString, &dialBackPeers); err != nil { - log.WithError(err).Info("Error unmarshalling peers record") - } - - //log the length of dialBackPeers - log.WithField("dialBackPeers", len(dialBackPeers)).Info("dialBackPeers") - - // Convert the dial-back peers to multiaddrs and add them to the list of bootstrap nodes if they do not already exist. - // likely good to limit the number of dial-back peers to a small number. - // and we need to limit to workers - for _, peer := range dialBackPeers { - peerMultiAddr := fmt.Sprintf("%s/p2p/%s", peer.MultiAddr, peer.Id.Pretty()) - peerMultiAddr = strings.Replace(peerMultiAddr, "127.0.0.1", "0.0.0.0", 1) - //log peer add - log.WithField("peerMultiAddr", peerMultiAddr).Info("peerMultiAddr") - peerExists := false - for _, bootNode := range bootNodes { - if bootNode == peerMultiAddr { - peerExists = true - break - } - } - if !peerExists { - bootNodes = append(bootNodes, peerMultiAddr) - } - } - - // Connect to the bootstrap nodes. - var wg sync.WaitGroup - for _, bootNode := range bootNodes { - peerAddr, err := peer.AddrInfoFromP2pAddr(multiaddr.StringCast(bootNode)) - log.Info("booting from: ", peerAddr) - if err != nil { - log.WithFields(log.Fields{ - "bootNode": bootNode, - "error": err, - }).Warn("Invalid bootstrap node address") - continue - } - wg.Add(1) - go func() { - defer wg.Done() - if err := h.Connect(ctx, *peerAddr); err != nil { - if err.Error() != "no good addresses" { - log.WithFields(log.Fields{ - "localMultiAddr": h.Addrs(), - "peerID": h.ID(), - "err": err, - }).Warn("Error connecting to bootstrap node") - } - } - }() - } - wg.Wait() - return kademliaDHT -} - -func DiscoverPeers(ctx context.Context, h host.Host) { - topicName := ctx.Value("topicName").(string) - kademliaDHT := InitDHT(ctx, h) - routingDiscovery := drouting.NewRoutingDiscovery(kademliaDHT) - dutil.Advertise(ctx, routingDiscovery, topicName) - log.Info("starting peer discovery") - // Look for others who have announced and attempt to connect to them - numConnected := 0 - for numConnected < 20 { - peerChan, err := routingDiscovery.FindPeers(ctx, topicName) - if err != nil { - panic(err) - } - for peer := range peerChan { - if peer.ID == h.ID() { - continue // No self connection - } - err := h.Connect(ctx, peer) - if err != nil { - // this can be quite noisy with discovery - // fmt.Println("Failed connecting to ", peer.ID.Pretty(), ", error:", err) - } else { - log.WithFields(log.Fields{ - "peerID": peer.ID.Pretty(), - }).Info("connected to a peer") - numConnected++ - if numConnected >= 20 { - break - } - } - } - } - log.Info("Peer discovery complete") -} diff --git a/src/enums/enums.go b/src/enums/enums.go deleted file mode 100644 index e90fe6c4..00000000 --- a/src/enums/enums.go +++ /dev/null @@ -1,73 +0,0 @@ -package enums - -import ( - "github.com/libp2p/go-libp2p/core/protocol" -) - -var ( - MsgHealthCheck = "MsgHealthCheck" - MsgExecute = "MsgExecute" - MsgExecuteResult = "MsgExecuteResult" - MsgExecuteError = "MsgExecuteError" - MsgExecuteTimeout = "MsgExecuteTimeout" - MsgExecuteUnknown = "MsgExecuteUnknown" - MsgExecuteInvalid = "MsgExecuteInvalid" - MsgExecuteNotFound = "MsgExecuteNotFound" - MsgExecuteNotSupported = "MsgExecuteNotSupported" - MsgExecuteNotImplemented = "MsgExecuteNotImplemented" - MsgExecuteNotAuthorized = "MsgExecuteNotAuthorized" - MsgExecuteNotPermitted = "MsgExecuteNotPermitted" - MsgExecuteNotAvailable = "MsgExecuteNotAvailable" - MsgExecuteNotReady = "MsgExecuteNotReady" - MsgExecuteNotConnected = "MsgExecuteNotConnected" - MsgExecuteNotInitialized = "MsgExecuteNotInitialized" - MsgExecuteNotConfigured = "MsgExecuteNotConfigured" - MsgExecuteNotInstalled = "MsgExecuteNotInstalled" - MsgExecuteNotUpgraded = "MsgExecuteNotUpgraded" - MsgRollCall = "MsgRollCall" - MsgRollCallResponse = "MsgRollCallResponse" - MsgExecuteResponse = "MsgExecuteResponse" - MsgInstallFunction = "MsgInstallFunction" - MsgInstallFunctionResponse = "MsgInstallFunctionResponse" -) - -var ( - RequestExecute = "RequestExecute" - ResponseExecute = "ResponseExecute" - RequestInstall = "RequestInstall" - ResponseInstall = "ResponseInstall" -) - -var ( - ResponseCodeOk = "200" - ResponseCodeAccepted = "202" - ResponseCodeError = "500" - ResponseCodeTimeout = "408" - ResponseCodeUnknown = "520" - ResponseCodeInvalid = "400" - ResponseCodeNotFound = "404" - ResponseCodeNotSupported = "501" - ResponseCodeNotImplemented = "501" - ResponseCodeNotAuthorized = "401" - ResponseCodeNotPermitted = "403" - ResponseCodeNotAvailable = "503" -) - -var ( - WorkProtocolId protocol.ID = "/b7s/work/1.0.0" -) - -var ( - RoleWorker = "worker" - RoleHead = "head" -) - -var ( - ChannelMsgLocal = "ChannelMsgLocal" - // ChannelMsgInstallFunction = "ChannelMsgInstallFunction" - // ChannelMsgExecute = "ChannelMsgExecute" - ChannelMsgExecuteResponse = "ChannelMsgExecuteResponse" - // ChannelMsgRollCall = "ChannelMsgRollCall" - // ChannelMsgHealthCheck = "ChannelMsgHealthCheck" - ChannelMsgRollCallResponse = "ChannelMsgRollCallResponse" -) diff --git a/src/executor/README.md b/src/executor/README.md deleted file mode 100644 index 0a89cb4b..00000000 --- a/src/executor/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## executor - -controls executing the WASM binary against a runtime. - -currently that runtime is expected to be `blockless` but this package can be abstracted to run any runtime under. diff --git a/src/executor/executor.go b/src/executor/executor.go deleted file mode 100644 index 3765b9d7..00000000 --- a/src/executor/executor.go +++ /dev/null @@ -1,157 +0,0 @@ -// Executor provides functions for running and interacting with functions. -package executor - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/memstore" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/google/uuid" - log "github.com/sirupsen/logrus" -) - -// prepExecutionManifest creates the execution manifest file for the specified function. -func prepExecutionManifest(ctx context.Context, requestId string, request models.RequestExecute, manifest models.FunctionManifest) (string, error) { - config := ctx.Value("config").(models.Config) - - functionPath := filepath.Join(config.Node.WorkspaceRoot, request.FunctionId, request.Method) - manifestPath := filepath.Join(config.Node.WorkspaceRoot, "t", requestId, "runtime-manifest.json") - tempFS := filepath.Join(config.Node.WorkspaceRoot, "t", requestId, "fs") - - // Create the directory - os.MkdirAll(filepath.Dir(manifestPath), os.ModePerm) - - type Manifest struct { - FS_ROOT_PATH string `json:"fs_root_path,omitempty"` - ENTRY string `json:"entry,omitempty"` - LIMITED_FUEL int `json:"limited_fuel,omitempty"` - LIMITED_MEMORY int `json:"limited_memory,omitempty"` - PERMISSIONS []string `json:"permissions,omitempty"` - } - - data := Manifest{ - FS_ROOT_PATH: tempFS, - ENTRY: functionPath, - LIMITED_FUEL: 100000000, - LIMITED_MEMORY: 200, - PERMISSIONS: request.Config.Permissions, - } - - file, jsonError := json.MarshalIndent(data, "", " ") - - if jsonError != nil { - log.WithFields(log.Fields{ - "err": jsonError, - }).Warn("failed to marshal manifest") - return "", jsonError - } - - _ = ioutil.WriteFile(manifestPath, file, 0644) - return manifestPath, nil -} - -func queryRuntime(runtimePath string) error { - cmd := exec.Command(runtimePath + "/blockless-cli") - _, err := cmd.Output() - if err != nil { - log.Error(err) - return err - } - return nil -} - -func Execute(ctx context.Context, request models.RequestExecute, functionManifest models.FunctionManifest) (models.ExecutorResponse, error) { - requestID, _ := uuid.NewRandom() - config := ctx.Value("config").(models.Config) - tempFSPath := filepath.Join(config.Node.WorkspaceRoot, "t", requestID.String(), "fs") - os.MkdirAll(tempFSPath, os.ModePerm) - - var execCommand func(string, ...string) *exec.Cmd - if ctxExecCommand, ok := ctx.Value("execCommand").(func(string, ...string) *exec.Cmd); ok { - execCommand = ctxExecCommand - } else { - // Check if the runtime is available. - if err := queryRuntime(config.Node.RuntimePath); err != nil { - return models.ExecutorResponse{ - Code: enums.ResponseCodeError, - RequestId: requestID.String(), - Result: "Runtime not available", - }, err - } - execCommand = exec.Command - } - - // Prepare the execution manifest. - runtimeManifestPath, err := prepExecutionManifest(ctx, requestID.String(), request, functionManifest) - if err != nil { - return models.ExecutorResponse{ - Code: enums.ResponseCodeError, - RequestId: requestID.String(), - }, err - } - - // Build the input and environment variable strings. - input := "" - if request.Config.Stdin != nil { - input = *request.Config.Stdin - } - envVars := request.Config.EnvVars - envVarString, envVarKeys := "", "" - if len(envVars) > 0 { - for _, envVar := range envVars { - envVarString += envVar.Name + "=\"" + envVar.Value + "\" " - envVarKeys += envVar.Name + ";" - } - envVarString = "env " + envVarString - envVarKeys = envVarKeys[:len(envVarKeys)-1] - } - - // Build the command string. - cmd := fmt.Sprintf("echo \"%s\" | %s BLS_LIST_VARS=\"%s\" %s/blockless-cli %s", input, envVarString, envVarKeys, config.Node.RuntimePath, runtimeManifestPath) - - // Execute the command. - run := execCommand("bash", "-c", cmd) - run.Dir = tempFSPath - out, err := run.Output() - if err != nil { - log.WithFields(log.Fields{"err": err}).Error("failed to execute request") - return models.ExecutorResponse{ - Code: enums.ResponseCodeError, - RequestId: requestID.String(), - }, err - } - - // Store the result in the execution response memory store - - executionResponseMemStore := ctx.Value("executionResponseMemStore").(memstore.ReqRespStore) - err = executionResponseMemStore.Set(requestID.String(), &models.MsgExecuteResponse{ - Type: enums.MsgExecuteResponse, - Code: enums.ResponseCodeOk, - Result: string(out), - }) - - if err != nil { - log.WithFields(log.Fields{ - "err": err, - }).Error("failed to set execution response") - } - - log.WithFields(log.Fields{ - "requestId": requestID, - }).Info("function executed") - - executorResponse := models.ExecutorResponse{ - RequestId: requestID.String(), - Code: enums.ResponseCodeOk, - Result: string(out), - } - - return executorResponse, nil -} diff --git a/src/executor/executor_test.go b/src/executor/executor_test.go deleted file mode 100644 index b02e6aef..00000000 --- a/src/executor/executor_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package executor - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/blocklessnetworking/b7s/src/models" - "github.com/google/uuid" -) - -func TestPrepExecutionManifest(t *testing.T) { - // setup - config := models.Config{ - Node: models.ConfigNode{ - WorkspaceRoot: "/tmp/workspace", - }, - } - requestID, _ := uuid.NewRandom() - request := models.RequestExecute{ - Method: "testMethod", - Config: models.ExecutionRequestConfig{ - Permissions: []string{"permission1", "permission2"}, - }, - } - functionManifest := models.FunctionManifest{ - Function: models.Function{ - ID: "testFunction", - }, - } - ctx := context.WithValue(context.Background(), "config", config) - - // run the test - manifestPath, err := prepExecutionManifest(ctx, requestID.String(), request, functionManifest) - - // test the result - if err != nil { - t.Errorf("prepExecutionManifest returned an error: %v", err) - } - if manifestPath != filepath.Join(config.Node.WorkspaceRoot, "t", requestID.String(), "runtime-manifest.json") { - t.Errorf("Unexpected manifest path, got %s", manifestPath) - } - - // check that the file exists and can be read - file, fileErr := os.Open(manifestPath) - if fileErr != nil { - t.Errorf("Error opening the manifest file: %v", fileErr) - } - defer file.Close() - - // cleanup - os.RemoveAll(filepath.Join(config.Node.WorkspaceRoot, "t", requestID.String())) -} diff --git a/src/health/health.go b/src/health/health.go deleted file mode 100644 index e9f24b5f..00000000 --- a/src/health/health.go +++ /dev/null @@ -1,17 +0,0 @@ -package health - -import ( - "context" - "time" - - "github.com/blocklessnetworking/b7s/src/controller" -) - -func StartPing(ctx context.Context, ticker *time.Ticker) { - for { - select { - case <-ticker.C: - controller.HealthStatus(ctx) - } - } -} diff --git a/src/host/README.md b/src/host/README.md deleted file mode 100644 index 313d04fa..00000000 --- a/src/host/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## host - -This package describes the `libp2p` host that is driving the underlying communication network. diff --git a/src/host/host.go b/src/host/host.go deleted file mode 100644 index d7a0fd6f..00000000 --- a/src/host/host.go +++ /dev/null @@ -1,61 +0,0 @@ -package host - -import ( - "context" - "io/ioutil" - "strconv" - - "github.com/blocklessnetworking/b7s/src/messaging" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p-core/crypto" - "github.com/libp2p/go-libp2p-core/host" - log "github.com/sirupsen/logrus" -) - -// NewHost creates a new libp2p host -func NewHost(ctx context.Context, port int, address string) host.Host { - var privKey crypto.PrivKey - - // Read the private key file if it exists - keyPath := ctx.Value("config").(models.Config).Node.KeyPath - if keyPath != "" { - log.Println("loading private key from: ", keyPath) - privKeyBytes, err := ioutil.ReadFile(keyPath) - if err != nil { - log.Error("failed to load private key from: ", keyPath) - } - - key, err := crypto.UnmarshalPrivateKey(privKeyBytes) - if err != nil { - log.Error("failed to load private key from: ", keyPath) - } - privKey = key - } - - var hostAddress = "/ip4/" + address + "/tcp/" + strconv.FormatInt(int64(port), 10) - opts := []libp2p.Option{ - libp2p.ListenAddrStrings(hostAddress), - libp2p.DefaultTransports, - libp2p.DefaultMuxers, - libp2p.DefaultSecurity, - libp2p.NATPortMap(), - } - - // Use the private key if it exists, otherwise generate an identity when starting the host - if privKey != nil { - opts = append(opts, libp2p.Identity(privKey)) - } - - h, err := libp2p.New(opts...) - if err != nil { - panic(err) - } - - log.Info("host: ", hostAddress+"/p2p/"+h.ID().Pretty()) - - // Set a stream handler to listen for incoming streams - messaging.ListenMessages(ctx, h) - - return h -} diff --git a/src/host/notifee.go b/src/host/notifee.go deleted file mode 100644 index dc27129f..00000000 --- a/src/host/notifee.go +++ /dev/null @@ -1,113 +0,0 @@ -package host - -import ( - "context" - "encoding/json" - - db "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p/core/network" - ma "github.com/multiformats/go-multiaddr" - log "github.com/sirupsen/logrus" -) - -// ConnectedNotifee is a struct that implements the Notifee interface -type ConnectedNotifee struct { - Ctx context.Context -} - -func (n *ConnectedNotifee) Connected(network network.Network, connection network.Conn) { - - // get the peer id and multiaddr. - peerID := connection.RemotePeer() - - // this address doesn't give us the in bound port for a dialback - multiAddr := connection.RemoteMultiaddr() - - // Get the peer record from the database. - peerRecord, err := db.Get(n.Ctx, peerID.Pretty()) - if err != nil { - log.WithError(err).Info("Error getting peer record from database") - } - - // Get the list of peers from the database. - peersRecordString, err := db.Get(n.Ctx, "peers") - if err != nil { - peersRecordString = []byte("[]") - } - - var peers []models.Peer - if err = json.Unmarshal(peersRecordString, &peers); err != nil { - log.WithError(err).Info("Error unmarshalling peers record") - } - - // Create a new peer info struct. - peerInfo := models.Peer{ - Type: "peer", - Id: peerID, - MultiAddr: multiAddr.String(), - AddrInfo: network.Peerstore().PeerInfo(peerID), - } - - // Check if the peer is already in the list. - peerExists := false - for _, peer := range peers { - if peer.Id == peerInfo.Id { - peerExists = true - break - } - } - - // If the peer is not in the list, add it. - if !peerExists { - peers = append(peers, peerInfo) - } - - // Marshal the peer info struct to JSON. - peerJSON, err := json.Marshal(peerInfo) - if err != nil { - log.WithError(err).Info("Error marshalling peer info") - return - } - - // Marshal the list of peers to JSON. - peersJSON, err := json.Marshal(peers) - if err != nil { - log.WithError(err).Info("Error marshalling peers record") - } - - //log the peerInfo - log.WithFields(log.Fields{ - "peerInfo": peerInfo, - }).Info("Peer Info Stored") - - // If the peer record does not exist in the database, set it. - if peerRecord == nil { - if err := db.Set(n.Ctx, peerID.Pretty(), string(peerJSON)); err != nil { - log.WithError(err).Info("Error setting peer record in database") - } - } - - // Set the list of peers in the database. - if err := db.Set(n.Ctx, "peers", string(peersJSON)); err != nil { - log.WithError(err).Info("Error setting peers record in database") - } -} - -// Disconnected is called when a connection is closed -func (n *ConnectedNotifee) Disconnected(network network.Network, connection network.Conn) { - // A peer has been disconnected - // Do something with the disconnected peer -} - -// Listen is called when a new stream is opened -func (n *ConnectedNotifee) Listen(network.Network, ma.Multiaddr) { - // A new stream has been opened - // Do something with the stream -} - -// ListenClose is called when a stream is closed -func (n *ConnectedNotifee) ListenClose(network.Network, ma.Multiaddr) { - // A stream has been closed - // Do something with the closed stream -} diff --git a/src/http/README.md b/src/http/README.md deleted file mode 100644 index 8a3a04da..00000000 --- a/src/http/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## http client - -various http methods needed to complete tasks, such as downloading files. making rest api requests. diff --git a/src/http/client.go b/src/http/client.go deleted file mode 100644 index 163e1312..00000000 --- a/src/http/client.go +++ /dev/null @@ -1,81 +0,0 @@ -package http - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "net/http" - "os" - "time" - - "github.com/blocklessnetworking/b7s/src/models" - "github.com/cavaliergopher/grab/v3" - log "github.com/sirupsen/logrus" -) - -var RestClient = &http.Client{Timeout: 10 * time.Second} - -func GetJson(url string, target interface{}) error { - r, err := RestClient.Get(url) - if err != nil { - return err - } - defer r.Body.Close() - - return json.NewDecoder(r.Body).Decode(target) -} - -func Download(ctx context.Context, functionManifest models.FunctionManifest) (string, error) { - WorkSpaceRoot := ctx.Value("config").(models.Config).Node.WorkspaceRoot - WorkSpaceDirectory := WorkSpaceRoot + "/" + functionManifest.Function.ID - client := grab.NewClient() - - // ensure path exists - os.MkdirAll(WorkSpaceDirectory, os.ModePerm) - // download function - req, _ := grab.NewRequest(WorkSpaceDirectory, functionManifest.Deployment.Uri) - - client.UserAgent = "b7s" - - // set request checksum - sum, err := hex.DecodeString(functionManifest.Deployment.Checksum) - if err != nil { - panic(err) - } - - // check hash of the function after downloading - req.SetChecksum(sha256.New(), sum, true) - resp := client.Do(req) - - log.WithFields(log.Fields{ - "uri": functionManifest.Deployment.Uri, - }).Info("function scheduled for sync") - - // start UI loop - t := time.NewTicker(500 * time.Millisecond) - defer t.Stop() -Loop: - for { - select { - case <-t.C: - log.WithFields(log.Fields{ - "uri": functionManifest.Deployment.Uri, - }).Info("function sync progress") - - case <-resp.Done: - // download is complete - break Loop - } - } - - // check for errors - if err := resp.Err(); err != nil { - log.WithFields(log.Fields{ - "uri": functionManifest.Deployment.Uri, - }).Info("function sync field will try again") - return "", err - } - - return resp.Filename, nil -} diff --git a/src/http/client_test.go b/src/http/client_test.go deleted file mode 100644 index 54b0d48e..00000000 --- a/src/http/client_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/blocklessnetworking/b7s/src/models" - "github.com/stretchr/testify/assert" -) - -type testStruct struct { - Field1 string `json:"field1"` - Field2 int `json:"field2"` -} - -func TestGetJson(t *testing.T) { - // setup test server - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - jsonData := testStruct{ - Field1: "value1", - Field2: 2, - } - json.NewEncoder(w).Encode(jsonData) - })) - defer ts.Close() - - // test GetJson - var target testStruct - err := GetJson(ts.URL, &target) - assert.Nil(t, err) - assert.Equal(t, "value1", target.Field1) - assert.Equal(t, 2, target.Field2) -} -func TestDownload(t *testing.T) { - // setup test server - // create a test server to simulate the function repository - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - file, _ := os.Open("testdata/testfile") - defer file.Close() - - // Send the file to the client - io.Copy(w, file) - })) - defer ts.Close() - - // setup context - config := models.Config{ - Node: models.ConfigNode{ - WorkspaceRoot: "/tmp/test_workspace", - }, - } - ctx := context.WithValue(context.Background(), "config", config) - - // setup test function manifest - functionManifest := models.FunctionManifest{ - Function: models.Function{ - ID: "test_function", - }, - Deployment: models.Deployment{ - Uri: ts.URL + "/testfile", - Checksum: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", - }, - } - - // test Download - filepath, err := Download(ctx, functionManifest) - assert.Nil(t, err) - assert.Equal(t, "/tmp/test_workspace/test_function/testfile", filepath) - - // check if file exists - _, err = os.Stat(filepath) - assert.Nil(t, err) - - // check if file content is correct - content, _ := ioutil.ReadFile(filepath) - assert.Equal(t, "hello", string(content)) - - // cleanup - os.RemoveAll("/tmp/test_workspace") -} diff --git a/src/http/http.go b/src/http/http.go deleted file mode 100644 index d02cfda6..00000000 --- a/src/http/http.go +++ /dev/null @@ -1 +0,0 @@ -package http diff --git a/src/http/testdata/testfile b/src/http/testdata/testfile deleted file mode 100644 index b6fc4c62..00000000 --- a/src/http/testdata/testfile +++ /dev/null @@ -1 +0,0 @@ -hello \ No newline at end of file diff --git a/src/keygen/README.md b/src/keygen/README.md deleted file mode 100644 index bd6c0cbc..00000000 --- a/src/keygen/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## keygen - -This package contains the needed methods to generate a private and public key that can be loaded on to the node identity. diff --git a/src/keygen/keygen.go b/src/keygen/keygen.go deleted file mode 100644 index 5faabb6f..00000000 --- a/src/keygen/keygen.go +++ /dev/null @@ -1,75 +0,0 @@ -package keygen - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - log "github.com/sirupsen/logrus" -) - -var ( - bits int -) - -func GenerateKeys(outputFolder string) error { - - privKey, pubKey, err := crypto.GenerateKeyPair( - crypto.Ed25519, - bits, - ) - if err != nil { - log.Fatal("failed to generate key:" + err.Error()) - } - - privBytes, err := crypto.MarshalPrivateKey(privKey) - if err != nil { - log.Fatal("failed to marshal private key:" + err.Error()) - } - - pubBytes, err := crypto.MarshalPublicKey(pubKey) - if err != nil { - log.Fatal("failed to marshal public key:" + err.Error()) - - } - - identity, err := peer.IDFromPublicKey(pubKey) - if err != nil { - log.Fatal("failed to get peer identity from public key:" + err.Error()) - } - - if err := os.MkdirAll(outputFolder, os.ModePerm); err != nil { - log.Fatal(err) - } - - dir := filepath.Dir(outputFolder) - err = os.MkdirAll(dir, 0777) - - if err != nil { - log.Fatal("failed to write to folder" + err.Error()) - } - - pubKeyFile := fmt.Sprintf("%s/pub.bin", outputFolder) - privKeyFile := fmt.Sprintf("%s/priv.bin", outputFolder) - peerIdFile := fmt.Sprintf("%s/identity", outputFolder) - - if err := ioutil.WriteFile(pubKeyFile, pubBytes, 0644); err != nil { - log.Fatal("failed to save pub key to file:" + err.Error()) - } - - if err := ioutil.WriteFile(privKeyFile, privBytes, 0644); err != nil { - log.Fatal("failed to save private key to file:" + err.Error()) - } - - if err := ioutil.WriteFile(peerIdFile, []byte(identity.String()), 0644); err != nil { - log.Fatal("failed to save identity to file:" + err.Error()) - } - - log.Info("Keys are generated at: ", outputFolder) - log.Info("identity:", identity) - - return nil -} diff --git a/src/keygen/keygen_test.go b/src/keygen/keygen_test.go deleted file mode 100644 index 829c8a66..00000000 --- a/src/keygen/keygen_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package keygen - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGenerateKeys(t *testing.T) { - // setup - outputFolder := "/tmp/test_keygen" - os.RemoveAll(outputFolder) - - // test GenerateKeys - err := GenerateKeys(outputFolder) - assert.Nil(t, err) - - // check that the files exist - _, err = os.Stat(outputFolder + "/pub.bin") - assert.Nil(t, err) - _, err = os.Stat(outputFolder + "/priv.bin") - assert.Nil(t, err) - _, err = os.Stat(outputFolder + "/identity") - assert.Nil(t, err) - - // check that the files are not empty - pubBytes, err := ioutil.ReadFile(outputFolder + "/pub.bin") - assert.Nil(t, err) - assert.NotEmpty(t, pubBytes) - privBytes, err := ioutil.ReadFile(outputFolder + "/priv.bin") - assert.Nil(t, err) - assert.NotEmpty(t, privBytes) - peerIdBytes, err := ioutil.ReadFile(outputFolder + "/identity") - assert.Nil(t, err) - assert.NotEmpty(t, peerIdBytes) -} diff --git a/src/main.go b/src/main.go deleted file mode 100644 index 6c87b8cd..00000000 --- a/src/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "os" - - "github.com/blocklessnetworking/b7s/src/daemon" - "github.com/blocklessnetworking/b7s/src/keygen" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -func setLogging(logType string) { - if logType == "json" { - log.SetFormatter(&log.JSONFormatter{}) - } else if logType == "text" { - log.SetFormatter(&log.TextFormatter{}) - } - -} - -func main() { - - // flag values - var configPath string - var logType string - - // set the daemon loop to main command - var rootCmd = cobra.Command{ - Use: "b7s", - Short: "Blockless is a peer-to-peer network for compute.", - Long: `Blockless is a peer-to-peer network that allows you to earn by sharing your compute power.`, - Run: func(cmd *cobra.Command, args []string) { - setLogging(logType) - - log.Info("starting b7s") - daemon.Run(cmd, args, configPath) - }, - } - - // generate identity keys - var keyGenCmd = &cobra.Command{ - Use: "keygen", - Short: "Generate a new keypair", - Long: `Generate a new keypair`, - Run: func(cmd *cobra.Command, args []string) { - setLogging(logType) - - path, err := os.Getwd() - if err != nil { - log.Println(err) - } - - err = keygen.GenerateKeys(path) - if err != nil { - log.Fatal(err) - } - }, - } - - // add flags - rootCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "path of the config file") - rootCmd.Flags().StringVarP(&logType, "out", "o", "rich", "output format of logs json, text, rich") - - // add subcommands - rootCmd.AddCommand(keyGenCmd) - - // execute the cobra loop - err := rootCmd.Execute() - - if err != nil { - log.Fatal(err) - } -} diff --git a/src/memstore/memstore.go b/src/memstore/memstore.go deleted file mode 100644 index 8406c2a5..00000000 --- a/src/memstore/memstore.go +++ /dev/null @@ -1 +0,0 @@ -package memstore diff --git a/src/memstore/results.go b/src/memstore/results.go deleted file mode 100644 index 4bb23f91..00000000 --- a/src/memstore/results.go +++ /dev/null @@ -1,55 +0,0 @@ -package memstore - -import ( - "errors" - "sync" - - "github.com/blocklessnetworking/b7s/src/models" - log "github.com/sirupsen/logrus" -) - -type ReqRespStore interface { - Get(string) *models.MsgExecuteResponse - Set(string, *models.MsgExecuteResponse) error -} - -type respDB map[string]*models.MsgExecuteResponse - -type localStorageReqRespStore struct { - db respDB - lock sync.RWMutex -} - -func (rs *localStorageReqRespStore) Get(requestId string) *models.MsgExecuteResponse { - rs.lock.RLock() - resp, ok := rs.db[requestId] - rs.lock.RUnlock() - if !ok { - return nil - } - - return resp -} - -func (rs *localStorageReqRespStore) Set(requestId string, resp *models.MsgExecuteResponse) error { - rs.lock.Lock() - defer rs.lock.Unlock() - existing, ok := rs.db[requestId] - if !ok { - rs.db[requestId] = resp - return nil - } else { - log.WithFields(log.Fields{ - "requestId": requestId, - "resp": resp, - "existing": existing, - }).Error("Response already exists") - return errors.New("Response for request " + requestId + " already exists") - } -} - -func NewReqRespStore() ReqRespStore { - return &localStorageReqRespStore{ - db: make(respDB), - } -} diff --git a/src/memstore/results_test.go b/src/memstore/results_test.go deleted file mode 100644 index ff7c1078..00000000 --- a/src/memstore/results_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package memstore - -import ( - "testing" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/stretchr/testify/assert" -) - -func TestReqRespStore_Set(t *testing.T) { - store := NewReqRespStore() - - // Test inserting a new request-response pair - requestID := "123" - response := &models.MsgExecuteResponse{ - Code: enums.ResponseCodeOk, - Result: "hello world", - } - err := store.Set(requestID, response) - assert.Nil(t, err) - - // Test inserting a duplicate request-response pair - err = store.Set(requestID, response) - assert.NotNil(t, err) - assert.Equal(t, "Response for request 123 already exists", err.Error()) - - // Test getting the inserted request-response pair - storedResponse := store.Get(requestID) - assert.Equal(t, response, storedResponse) -} diff --git a/src/messaging/README.md b/src/messaging/README.md deleted file mode 100644 index 789f4492..00000000 --- a/src/messaging/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## messaging - -This package contains all the needed bits for the `b7s` node to communicate with each other using `json` over `libp2p` streams. diff --git a/src/messaging/handler.go b/src/messaging/handler.go deleted file mode 100644 index eca7ae59..00000000 --- a/src/messaging/handler.go +++ /dev/null @@ -1,34 +0,0 @@ -package messaging - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/messaging/handlers" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p/core/peer" -) - -func HandleMessage(ctx context.Context, message []byte, peerID peer.ID) { - var msg models.MsgBase - ctx = context.WithValue(ctx, "peerID", peerID) - if err := json.Unmarshal(message, &msg); err != nil { - fmt.Errorf("error unmarshalling message: %v", err) - } - - handlers := map[string]func(context.Context, []byte){ - enums.MsgHealthCheck: handlers.HandleMsgHealthCheck, - enums.MsgExecute: handlers.HandleMsgExecute, - enums.MsgExecuteResponse: handlers.HandleMsgExecuteResponse, - enums.MsgRollCall: handlers.HandleMsgRollCall, - enums.MsgRollCallResponse: handlers.HandleMsgRollCallResponse, - enums.MsgInstallFunction: handlers.HandleMsgInstall, - enums.MsgInstallFunctionResponse: handlers.HandleMsgInstallResponse, - } - - if handler, ok := handlers[msg.Type]; ok { - handler(ctx, message) - } -} diff --git a/src/messaging/handlers/msgexecute.go b/src/messaging/handlers/msgexecute.go deleted file mode 100644 index 13380737..00000000 --- a/src/messaging/handlers/msgexecute.go +++ /dev/null @@ -1,42 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p/core/peer" - log "github.com/sirupsen/logrus" -) - -func HandleMsgExecute(ctx context.Context, message []byte) { - msgExecute := &models.MsgExecute{} - json.Unmarshal(message, msgExecute) - msgExecute.From = ctx.Value("peerID").(peer.ID) - log.WithFields(log.Fields{ - "message": string(message), - }).Debug("message from peer") - - channel := ctx.Value(enums.ChannelMsgLocal).(chan models.Message) - - localMsg := models.Message{ - Type: enums.MsgExecuteResponse, - Data: msgExecute, - } - - channel <- localMsg -} - -func HandleMsgExecuteResponse(ctx context.Context, message []byte) { - - msgExecuteResponse := &models.MsgExecuteResponse{} - json.Unmarshal(message, msgExecuteResponse) - msgExecuteResponse.From = ctx.Value("peerID").(peer.ID) - log.WithFields(log.Fields{ - "message": string(message), - }).Debug("message from peer") - - channel := ctx.Value(enums.ChannelMsgExecuteResponse).(chan models.MsgExecuteResponse) - channel <- *msgExecuteResponse -} diff --git a/src/messaging/handlers/msghealthcheck.go b/src/messaging/handlers/msghealthcheck.go deleted file mode 100644 index c44fc7d4..00000000 --- a/src/messaging/handlers/msghealthcheck.go +++ /dev/null @@ -1,13 +0,0 @@ -package handlers - -import ( - "context" - - log "github.com/sirupsen/logrus" -) - -func HandleMsgHealthCheck(ctx context.Context, message []byte) { - log.WithFields(log.Fields{ - "message": string(message), - }).Debug("peer health check recieved") -} diff --git a/src/messaging/handlers/msginstall.go b/src/messaging/handlers/msginstall.go deleted file mode 100644 index b7fad2f9..00000000 --- a/src/messaging/handlers/msginstall.go +++ /dev/null @@ -1,38 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p/core/peer" - log "github.com/sirupsen/logrus" -) - -func HandleMsgInstall(ctx context.Context, message []byte) { - cfg := ctx.Value("config").(models.Config) - - // right now only workers should respond to install calls, so that - // they are not particpating in work just yet - if cfg.Protocol.Role != enums.RoleWorker { - return - } - - msgInstall := &models.MsgInstallFunction{} - json.Unmarshal(message, msgInstall) - msgInstall.From = ctx.Value("peerID").(peer.ID) - - channel := ctx.Value(enums.ChannelMsgLocal).(chan models.Message) - - log.WithFields(log.Fields{ - "message": string(message), - }).Info("message to install") - - localMsg := models.Message{ - Type: enums.MsgInstallFunction, - Data: *msgInstall, - } - - channel <- localMsg -} diff --git a/src/messaging/handlers/msginstallresponse.go b/src/messaging/handlers/msginstallresponse.go deleted file mode 100644 index fab9efc5..00000000 --- a/src/messaging/handlers/msginstallresponse.go +++ /dev/null @@ -1,14 +0,0 @@ -package handlers - -import ( - "context" - - log "github.com/sirupsen/logrus" -) - -func HandleMsgInstallResponse(ctx context.Context, message []byte) { - //log hello - log.WithFields(log.Fields{ - "message": string(message), - }).Info("message to install") -} diff --git a/src/messaging/handlers/msgrollcall.go b/src/messaging/handlers/msgrollcall.go deleted file mode 100644 index ffeda6eb..00000000 --- a/src/messaging/handlers/msgrollcall.go +++ /dev/null @@ -1,53 +0,0 @@ -package handlers - -import ( - "context" - "encoding/json" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p/core/peer" - log "github.com/sirupsen/logrus" -) - -// roll call is recieved from pubsub peer -// respond to the requestor of the roll call directly -func HandleMsgRollCall(ctx context.Context, message []byte) { - cfg := ctx.Value("config").(models.Config) - - // right now only workers should respond to roll calls - // todo : other nodes should be able to respond to roll calls - if cfg.Protocol.Role != enums.RoleWorker { - return - } - - msgRollCall := &models.MsgRollCall{} - json.Unmarshal(message, msgRollCall) - msgRollCall.From = ctx.Value("peerID").(peer.ID) - - log.WithFields(log.Fields{ - "message": string(message), - }).Info("rollcall message") - - channel := ctx.Value(enums.ChannelMsgLocal).(chan models.Message) - - localMsg := models.Message{ - Type: enums.MsgRollCall, - Data: *msgRollCall, - } - - channel <- localMsg -} - -func HandleMsgRollCallResponse(ctx context.Context, message []byte) { - msgRollCallResponse := &models.MsgRollCallResponse{} - json.Unmarshal(message, msgRollCallResponse) - msgRollCallResponse.From = ctx.Value("peerID").(peer.ID) - - log.WithFields(log.Fields{ - "message": string(message), - }).Info("rollcall response") - - rollcallResponseChannel := ctx.Value(enums.ChannelMsgRollCallResponse).(chan models.MsgRollCallResponse) - rollcallResponseChannel <- *msgRollCallResponse -} diff --git a/src/messaging/messaging.go b/src/messaging/messaging.go deleted file mode 100644 index dde6bcf0..00000000 --- a/src/messaging/messaging.go +++ /dev/null @@ -1,95 +0,0 @@ -package messaging - -import ( - "bufio" - "context" - "encoding/json" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/libp2p/go-libp2p-core/network" - "github.com/libp2p/go-libp2p-core/peer" - pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/libp2p/go-libp2p/core/host" - log "github.com/sirupsen/logrus" -) - -// subscribe to a gossipsub topic -func Subscribe(ctx context.Context, host host.Host, topicName string) *pubsub.Topic { - // make sure we're subscribed to the topic before we start publishing - ps, err := pubsub.NewGossipSub(ctx, host) - if err != nil { - panic(err) - } - - topic, err := ps.Join(topicName) - if err != nil { - panic(err) - } - - sub, err := topic.Subscribe() - if err != nil { - panic(err) - } - - ctx = context.WithValue(ctx, "topic", topic) - // listen to messages - go ListenPublishedMessages(ctx, sub, host) - return topic -} - -// publish messages on the gossipsub topic -func PublishMessage(ctx context.Context, topic *pubsub.Topic, message any) { - messageString, _ := json.Marshal(message) - if err := topic.Publish(ctx, []byte(messageString)); err != nil { - log.WithFields(log.Fields{ - "err": err, - }).Warn("message err") - } -} - -// listens to pubsub messages and send them to a message handler -func ListenPublishedMessages(ctx context.Context, sub *pubsub.Subscription, host host.Host) { - for { - message, err := sub.Next(ctx) - if err != nil { - log.Warn(err) - } - if message.ReceivedFrom != host.ID() { - HandleMessage(ctx, message.Data, message.ReceivedFrom) - } - } -} - -// listen to direct messages from peers -func ListenMessages(ctx context.Context, host host.Host) { - host.SetStreamHandler(enums.WorkProtocolId, func(s network.Stream) { - defer s.Close() - buf := bufio.NewReader(s) - response, err := buf.ReadString('\n') - - if err != nil && err.Error() != "EOF" { - s.Reset() - log.Warn(err) - } - - HandleMessage(ctx, []byte(response), s.Conn().RemotePeer()) - }) -} - -// sends a message directly to a peer -func SendMessage(ctx context.Context, peer peer.ID, message []byte) error { - host := ctx.Value("host").(host.Host) - s, err := host.NewStream(context.Background(), peer, enums.WorkProtocolId) - if err != nil { - log.Warn(err) - return err - } - _, err = s.Write(message) - if err != nil { - s.Reset() - log.Warn(err) - return err - } - defer s.Close() - return nil -} diff --git a/src/models/config.go b/src/models/config.go deleted file mode 100644 index 1014c88f..00000000 --- a/src/models/config.go +++ /dev/null @@ -1,74 +0,0 @@ -package models - -import ( - "strings" - - "github.com/multiformats/go-multiaddr" -) - -type AddrList []multiaddr.Multiaddr - -func (al *AddrList) String() string { - strs := make([]string, len(*al)) - for i, addr := range *al { - strs[i] = addr.String() - } - return strings.Join(strs, ",") -} - -func (al *AddrList) UnmarshalYAML(unmarshal func(interface{}) error) error { - addrs := make([]string, 0) - err := unmarshal(&addrs) - if err != nil { - return err - } - - *al = make([]multiaddr.Multiaddr, 0) - for _, addr := range addrs { - a, err := multiaddr.NewMultiaddr(addr) - if err != nil { - return err - } - - *al = append(*al, a) - } - - return nil -} - -type Config struct { - Node ConfigNode `yaml:"node"` - Rest ConfigRest `yaml:"rest"` - Protocol ConfigProtocol `yaml:"protocol"` - Logging ConfigLogging `yaml:"logging"` - Repository ConfigRepository `yaml:"repository"` - Chain ConfigChain `yaml:"chain"` -} -type ConfigNode struct { - Name string `yaml:"name"` - IP string `yaml:"ip"` - Port string `yaml:"port"` - BootNodes []string `yaml:"boot_nodes"` - KeyPath string `yaml:"key_path"` - WorkspaceRoot string `yaml:"workspace_root"` - RuntimePath string `yaml:"runtime_path"` -} -type ConfigRest struct { - IP string `yaml:"ip"` - Port string `yaml:"port"` -} -type ConfigProtocol struct { - Role string `yaml:"role"` -} -type ConfigLogging struct { - FilePath string `yaml:"file_path"` - Level string `yaml:"level"` -} -type ConfigRepository struct { - URL string `yaml:"url"` -} -type ConfigChain struct { - AddressKey string `yaml:"address_key"` - RPC string `yaml:"rpc"` - Home string `yaml:"home"` -} diff --git a/src/models/messages.go b/src/models/messages.go deleted file mode 100644 index 038f4cd8..00000000 --- a/src/models/messages.go +++ /dev/null @@ -1,117 +0,0 @@ -package models - -import ( - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/google/uuid" - "github.com/libp2p/go-libp2p/core/peer" -) - -type Message struct { - Type string - Data interface{} -} - -type MsgBase struct { - Type string `json:"type,omitempty"` - From peer.ID `json:"from,omitempty"` -} - -type MsgHealthPing struct { - Type string `json:"type,omitempty"` - From peer.ID `json:"from,omitempty"` - Code string `json:"code,omitempty"` -} - -func NewMsgHealthPing(code string) *MsgHealthPing { - return &MsgHealthPing{ - Type: enums.MsgHealthCheck, - Code: code, - } -} - -type MsgExecute struct { - Type string `json:"type,omitempty"` - From peer.ID `json:"from,omitempty"` - Code string `json:"code,omitempty"` - FunctionId string `json:"functionId,omitempty"` - Method string `json:"method,omitempty"` - Parameters []RequestExecuteParameters `json:"parameters,omitempty"` - Config ExecutionRequestConfig `json:"config,omitempty"` -} - -func NewMsgExecute(code string) *MsgExecute { - return &MsgExecute{ - Type: enums.MsgExecute, - Code: code, - } -} - -type MsgExecuteResponse struct { - Type string `json:"type,omitempty"` - RequestId string `json:"requestId,omitempty"` - From peer.ID `json:"from,omitempty"` - Code string `json:"code,omitempty"` - Result string `json:"result,omitempty"` -} - -type MsgRollCall struct { - From peer.ID `json:"from,omitempty"` - Type string `json:"type,omitempty"` - FunctionId string `json:"functionId,omitempty"` - RequestId string `json:"request_id,omitempty"` -} - -func NewMsgRollCall(functionId string) *MsgRollCall { - requestId, _ := uuid.NewRandom() - return &MsgRollCall{ - Type: enums.MsgRollCall, - FunctionId: functionId, - RequestId: requestId.String(), - } -} - -type MsgRollCallResponse struct { - Type string `json:"type,omitempty"` - From peer.ID `json:"from,omitempty"` - Code string `json:"code,omitempty"` - Role string `json:"role,omitempty"` - FunctionId string `json:"functionId,omitempty"` - RequestId string `json:"request_id,omitempty"` -} - -func NewMsgRollCallResponse(code string, role string) *MsgRollCallResponse { - return &MsgRollCallResponse{ - Type: enums.MsgRollCallResponse, - Code: code, - Role: role, - } -} - -type MsgInstallFunction struct { - Type string `json:"type,omitempty"` - From peer.ID `json:"from,omitempty"` - ManifestUrl string `json:"manifestUrl,omitempty"` - Cid string `json:"cid,omitempty"` -} - -func NewMsgInstallFunction(manifestUrl string) *MsgInstallFunction { - return &MsgInstallFunction{ - Type: enums.MsgInstallFunction, - ManifestUrl: manifestUrl, - } -} - -type MsgInstallFunctionResponse struct { - Type string `json:"type,omitempty"` - From peer.ID `json:"from,omitempty"` - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} - -func NewMsgInstallFunctionResponse(code string, message string) *MsgInstallFunctionResponse { - return &MsgInstallFunctionResponse{ - Type: enums.MsgInstallFunctionResponse, - Code: code, - Message: message, - } -} diff --git a/src/models/models.go b/src/models/models.go deleted file mode 100644 index 4540b84b..00000000 --- a/src/models/models.go +++ /dev/null @@ -1,7 +0,0 @@ -package models - -type ExecutorResponse struct { - Code string `json:"code"` - Result string `json:"result"` - RequestId string `json:"request_id"` -} diff --git a/src/models/rest.go b/src/models/rest.go deleted file mode 100644 index 68e8c5d0..00000000 --- a/src/models/rest.go +++ /dev/null @@ -1,48 +0,0 @@ -package models - -type RequestExecute struct { - FunctionId string `json:"function_id"` - Method string `json:"method"` - Parameters []RequestExecuteParameters `json:"parameters"` - Config ExecutionRequestConfig `json:"config"` -} -type RequestExecuteEnvVars struct { - Name string `json:"name"` - Value string `json:"value"` -} -type RequestExecuteParameters struct { - Name string `json:"name"` - Value string `json:"value"` -} -type RequestExecuteResultAggregation struct { - Enable bool `json:"enable"` - Type string `json:"type"` - Parameters []RequestExecuteParameters `json:"parameters"` -} -type ExecutionRequestConfig struct { - EnvVars []RequestExecuteEnvVars `json:"env_vars"` - NumberOfNodes int `json:"number_of_nodes"` - ResultAggregation RequestExecuteResultAggregation `json:"result_aggregation"` - Stdin *string `json:"stdin"` - Permissions []string `json:"permissions"` -} - -type ResponseExecute struct { - Code string `json:"code"` - Id string `json:"id"` - Result string `json:"result"` -} - -type RequestFunctionInstall struct { - Cid string `json:"cid"` - Uri string `json:"uri"` - Count int `json:"count"` -} - -type ResponseInstall struct { - Code string `json:"code"` -} - -type RequestFunctionResponse struct { - Id string `json:"id"` -} diff --git a/src/repository/README.md b/src/repository/README.md deleted file mode 100644 index c46460b4..00000000 --- a/src/repository/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## repository - -The JSONRepository module is a Go module that provides functionality for interacting with a JSON-based function repository. It includes a struct called JSONRepository that has a method called "Get()" which takes in a context and a manifest path. The Get method retrieves the function manifest from the repository by making a GET request to the provided manifest path, it then verifies if the runtime url is present in the manifest, if not it populates the deployment url and gets the ID from the hostname. It then checks if the function is present in cache, if not it downloads the function and caches it, if present it skips the download process and returns the function manifest. It also includes a function called UnGzip which extracts a gzipped archive to a specified destination. diff --git a/src/repository/base.go b/src/repository/base.go deleted file mode 100644 index f807bcf2..00000000 --- a/src/repository/base.go +++ /dev/null @@ -1,5 +0,0 @@ -package repository - -type Repo interface { - Get() string -} diff --git a/src/repository/controller.go b/src/repository/controller.go deleted file mode 100644 index 5c4a788f..00000000 --- a/src/repository/controller.go +++ /dev/null @@ -1,12 +0,0 @@ -package repository - -import ( - "context" - - "github.com/blocklessnetworking/b7s/src/models" -) - -func GetPackage(ctx context.Context, manifest models.MsgInstallFunction) (models.FunctionManifest, error) { - repo := JSONRepository{} - return repo.Get(ctx, manifest) -} diff --git a/src/repository/repository.go b/src/repository/repository.go deleted file mode 100644 index aa7c4bcb..00000000 --- a/src/repository/repository.go +++ /dev/null @@ -1,200 +0,0 @@ -package repository - -import ( - "archive/tar" - "compress/gzip" - "context" - "encoding/json" - "errors" - "io" - "net/url" - "os" - "path/filepath" - - "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/http" - "github.com/blocklessnetworking/b7s/src/models" - log "github.com/sirupsen/logrus" -) - -type JSONRepository struct{} - -// get the manifest from the repository -// downloads the binary -func (r JSONRepository) Get(ctx context.Context, installMsg models.MsgInstallFunction) (models.FunctionManifest, error) { - WorkSpaceRoot := ctx.Value("config").(models.Config).Node.WorkspaceRoot - - functionManifest := models.FunctionManifest{} - err := http.GetJson(installMsg.ManifestUrl, &functionManifest) - - if err != nil { - log.Warn(err) - } - - if functionManifest.Runtime.Url != "" { - DeploymentUrl, err := url.Parse(functionManifest.Runtime.Url) - if err != nil { - log.Warn(err) - } - - ManifestUrl, err := url.Parse(installMsg.ManifestUrl) - if err != nil { - log.Warn(err) - } - - if DeploymentUrl.Host == "" { - DeploymentUrl.Host = ManifestUrl.Host - } - - if DeploymentUrl.Scheme == "" { - DeploymentUrl.Scheme = ManifestUrl.Scheme - } - - functionManifest.Deployment = models.Deployment{ - Uri: DeploymentUrl.String(), - Checksum: functionManifest.Runtime.Checksum, - } - } - - cachedFunction, err := db.GetString(ctx, installMsg.Cid) - WorkSpaceDirectory := WorkSpaceRoot + "/" + installMsg.Cid - - if err != nil { - if err.Error() == "pebble: not found" { - log.Info("function not found in cache, syncing") - } else { - log.Warn(err) - } - } - - if cachedFunction == "" { - // download the function - fileName, err := http.Download(ctx, functionManifest) - - UnGzip(fileName, WorkSpaceDirectory) - if err != nil { - log.Warn(err) - } - - functionManifest.Deployment.File = fileName - functionManifest.Cached = true - - functionManifestJson, error := json.Marshal(functionManifest) - - if error != nil { - log.Warn(error) - return functionManifest, error - } - - db.Set(ctx, installMsg.Cid, string(functionManifestJson)) - - log.WithFields(log.Fields{ - "uri": functionManifest.Deployment.Uri, - }).Info("function sync completed") - - } else { - - err := json.Unmarshal([]byte(cachedFunction), &functionManifest) - if err != nil { - log.Warn(err) - } - - log.WithFields(log.Fields{ - "uri": functionManifest.Deployment.Uri, - }).Info("function sync skipped, already present") - } - - return functionManifest, nil -} - -func UnGzip(archive, destination string) error { - dest := destination - if len(dest) == 0 { - log.WithFields(log.Fields{ - "archive": archive, - }).Warn("extract archive destination is not specified and cwd is used.") - dest = "./" - } else { - - if err := os.MkdirAll(dest, os.ModePerm); err != nil { - return err - } - } - gzipStream, err := os.Open(archive) - if err != nil { - return err - } - defer gzipStream.Close() - uncompressedStream, err := gzip.NewReader(gzipStream) - if err != nil { - log.WithFields(log.Fields{ - "archive": archive, - "err": err, - }).Error("extract archive failed.") - return err - } - defer uncompressedStream.Close() - - tarReader := tar.NewReader(uncompressedStream) - - for { - header, err := tarReader.Next() - - if err == io.EOF { - break - } - - if err != nil { - log.WithFields(log.Fields{ - "archive": archive, - "err": err, - }).Error("extract archive failed.") - return err - } - - switch header.Typeflag { - case tar.TypeDir: - dir := filepath.Join(dest, header.Name) - if err := os.MkdirAll(dir, 0755); err != nil { - log.WithFields(log.Fields{ - "archive": archive, - "err": err, - "dir": dir, - }).Error("extract archive mkdir failed") - return err - } - case tar.TypeReg: - file := filepath.Join(dest, header.Name) - outFile, err := os.Create(file) - if err != nil { - log.WithFields(log.Fields{ - "archive": archive, - "err": err, - "file": file, - }).Error("extract archive create new file failed") - return err - } - if _, err := io.Copy(outFile, tarReader); err != nil { - log.WithFields(log.Fields{ - "archive": archive, - "err": err, - "file": file, - }).Error("extract archive copy content to new file failed") - outFile.Close() - return err - } - outFile.Close() - - default: - log.WithFields(log.Fields{ - "archive": archive, - "typeflag": header.Typeflag, - "header": header.Name, - }).Error("extract archive unknown header") - return errors.New("extract tgz file failed: unknown header") - } - - } - - return nil -} diff --git a/src/repository/repository_test.go b/src/repository/repository_test.go deleted file mode 100644 index 03513428..00000000 --- a/src/repository/repository_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package repository - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" -) - -func TestJSONRepository_Get(t *testing.T) { - // remove b7s_test folder before starting the test - os.RemoveAll("/tmp/b7s_test") - - mockConfig := models.Config{ - Protocol: models.ConfigProtocol{ - Role: enums.RoleWorker, - }, - Node: models.ConfigNode{ - WorkspaceRoot: "/tmp/b7s_tests", - }, - } - ctx := context.WithValue(context.Background(), "config", mockConfig) - - appDb := db.GetDb("/tmp/b7s_test") - ctx = context.WithValue(ctx, "appDb", appDb) - defer db.Close(ctx) - - mockManifest := models.FunctionManifest{ - Function: models.Function{ - ID: "testFunction", - }, - Deployment: models.Deployment{ - Uri: "", - }, - Runtime: models.Runtime{ - Url: "", - Checksum: "fb1f409f1044844020c0aed9d8fe670484ce4af98c3768a72516d62cbf6a3c02", - }, - } - - // create a test server to simulate the function repository - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/manifest.json" { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(mockManifest) - } else if r.URL.Path == "/testFunction.tar.gz" { - // Open the testFunction.tar.gz file - file, _ := os.Open("testdata/testFunction.tar.gz") - defer file.Close() - - // Send the file to the client - io.Copy(w, file) - } - })) - - mockManifest.Deployment.Uri = ts.URL + "/testFunction.tar.gz" - mockManifest.Runtime.Url = ts.URL + "/testFunction.tar.gz" - defer ts.Close() - - // create a new JSONRepository struct - repo := JSONRepository{} - - mockManifestBytes, _ := json.Marshal(mockManifest) - - // insert mock manifest into the database - db.Set(ctx, "test_key", string(mockManifestBytes)) - installMsg := &models.MsgInstallFunction{ - ManifestUrl: ts.URL + "/manifest.json", - } - - // call the Get method with the test server URL - manifest, err := repo.Get(ctx, *installMsg) - - if err != nil { - t.Errorf("Expected no error, got %s", err) - } - - // check that the function ID is correct - if manifest.Function.ID != "testFunction" { - t.Errorf("Expected function ID to be 'testFunction', got %s", manifest.Function.ID) - } - - // check that the deployment URI is correct - if manifest.Deployment.Uri != mockManifest.Deployment.Uri { - t.Errorf("Expected deployment URI to be %s, got %s", mockManifest.Deployment.Uri, manifest.Deployment.Uri) - } -} diff --git a/src/restapi/README.md b/src/restapi/README.md deleted file mode 100644 index 40cffa19..00000000 --- a/src/restapi/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## rest api - -this is the externally accessible API that a consumer can use to request work from the node, deploy functions. - -currently public it is soon to be gated by mMTLs diff --git a/src/restapi/api.go b/src/restapi/api.go deleted file mode 100644 index b372f9b7..00000000 --- a/src/restapi/api.go +++ /dev/null @@ -1,150 +0,0 @@ -package restapi - -import ( - "context" - "encoding/json" - "errors" - "net/http" - - log "github.com/sirupsen/logrus" - - "github.com/blocklessnetworking/b7s/src/controller" - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" -) - -func handleRequestExecute(w http.ResponseWriter, r *http.Request) { - // body decode - request := models.RequestExecute{} - json.NewDecoder(r.Body).Decode(&request) - - // execute the function - out, err := controller.ExecuteFunction(r.Context(), request) - - if err != nil { - response := models.ResponseExecute{ - Code: enums.ResponseCodeError, - Id: out.RequestId, - } - json.NewEncoder(w).Encode(response) - return - } - - response := models.ResponseExecute{ - Code: out.Code, - Id: out.RequestId, - Result: out.Result, - } - - json.NewEncoder(w).Encode(response) -} - -type MsgInstallFunctionFunc func(context.Context, models.RequestFunctionInstall) error - -func handleInstallFunction(w http.ResponseWriter, r *http.Request) { - - // Make sure that the request body is there. - if r.Body == nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - // Unmarshal request. - var request models.RequestFunctionInstall - err := json.NewDecoder(r.Body).Decode(&request) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - // TODO: Could be done using validators. - if request.Uri == "" && request.Cid == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - // Initialize the msgInstallFunction function - get the value from the context if set, - // else use the default one. - var msgInstallFunc MsgInstallFunctionFunc = controller.MsgInstallFunction - - // NOTE: At the moment, this function is no longer set on the context (for tests only). - val := r.Context().Value("msgInstallFunc") - if val != nil { - // Assert that the context value is of the expected type. - fn, ok := val.(MsgInstallFunctionFunc) - if !ok { - // Should never happen. - w.WriteHeader(http.StatusInternalServerError) - return - } - - msgInstallFunc = fn - } - - // Add a deadline to the context. - ctx, cancel := context.WithTimeout(r.Context(), functionInstallTimeout) - defer cancel() - - // Start function install in a separate goroutine and signal when it's done. - fnErr := make(chan error) - go func() { - err = msgInstallFunc(ctx, request) - fnErr <- err - }() - - // Wait until either function install finishes, or request times out. - select { - - // Context timed out. - case <-ctx.Done(): - - status := http.StatusRequestTimeout - if !errors.Is(ctx.Err(), context.DeadlineExceeded) { - status = http.StatusInternalServerError - } - - w.WriteHeader(status) - return - - // Work done. - case err = <-fnErr: - break - } - - // Check if function install succeeded and handle error or return response. - if err != nil { - - log.WithError(err). - WithField("uri", request.Uri). - WithField("cid", request.Cid). - Error("failed to install function") - - w.WriteHeader(http.StatusInternalServerError) - return - } - - response := models.ResponseInstall{ - Code: enums.ResponseCodeOk, - } - - // Write response. - err = json.NewEncoder(w).Encode(response) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func handleRootRequest(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("ok")) -} - -func handleGetExecuteResponse(w http.ResponseWriter, r *http.Request) { - // body decode - request := models.RequestFunctionResponse{} - json.NewDecoder(r.Body).Decode(&request) - - // get the response - response := controller.GetExecutionResponse(r.Context(), request.Id) - json.NewEncoder(w).Encode(response) -} diff --git a/src/restapi/api_test.go b/src/restapi/api_test.go deleted file mode 100644 index 94daaff9..00000000 --- a/src/restapi/api_test.go +++ /dev/null @@ -1,260 +0,0 @@ -package restapi - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/blocklessnetworking/b7s/src/db" - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/memstore" - "github.com/blocklessnetworking/b7s/src/models" -) - -func TestHandleRequestExecute(t *testing.T) { - // setup - req, err := http.NewRequest("POST", "/function/request", bytes.NewBuffer([]byte(`{"functionId": "test_function", "input": "test_input"}`))) - if err != nil { - t.Fatal(err) - } - - mockConfig := models.Config{ - Protocol: models.ConfigProtocol{ - Role: enums.RoleWorker, - }, - Node: models.ConfigNode{ - WorkspaceRoot: "/tmp/b7s_tests", - }, - } - - appDb := db.GetDb("/tmp/b7s") - - // mock the context - ctx := context.WithValue(req.Context(), "config", models.Config{}) - ctx = context.WithValue(ctx, "config", mockConfig) - ctx = context.WithValue(ctx, "appDb", appDb) - req = req.WithContext(ctx) - defer db.Close(ctx) - - // mock the response writer - rr := httptest.NewRecorder() - - // call the function - handleRequestExecute(rr, req) - - // check the response - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) - } - - expected := "{\"code\":\"500\",\"id\":\"\",\"result\":\"\"}\n" - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) - } -} - -func TestHandleRootRequest(t *testing.T) { - // Create a request to pass to our handler - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } - - // Create a ResponseRecorder to record the response - rr := httptest.NewRecorder() - handler := http.HandlerFunc(handleRootRequest) - - // Serve the request to our handler - handler.ServeHTTP(rr, req) - - // Check the status code is what we expect - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - // Check the response body is what we expect - expected := "ok" - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} -func TestHandleGetExecuteResponse(t *testing.T) { - // setup - store := memstore.NewReqRespStore() - requestID := "123" - response := &models.MsgExecuteResponse{ - Code: enums.ResponseCodeOk, - Result: "hello world", - } - store.Set(requestID, response) - - // create a request with the requestID - req, err := http.NewRequest("POST", "/", bytes.NewBuffer([]byte(`{"id":"`+requestID+`"}`))) - if err != nil { - t.Fatal(err) - } - - // create a response recorder - rr := httptest.NewRecorder() - - // create a context with the store - ctx := context.WithValue(req.Context(), "executionResponseMemStore", store) - - // attach the context to the request - req = req.WithContext(ctx) - - // call the handler - handleGetExecuteResponse(rr, req) - - // check the status code - if status := rr.Code; status != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - status, http.StatusOK) - } - - // check the response body - expected := "{\"code\":\"200\",\"result\":\"hello world\"}\n" - if rr.Body.String() != expected { - t.Errorf("handler returned unexpected body: got %v want %v", - rr.Body.String(), expected) - } -} -func TestHandleInstallFunction(t *testing.T) { - - const ( - contentTypeJSON = "application/json" - requestPayload = `{ "uri": "https://example.com/manifest.json" }` - ) - - var installFn MsgInstallFunctionFunc = func(ctx context.Context, req models.RequestFunctionInstall) error { - return nil - } - - ctx := context.WithValue(context.Background(), "msgInstallFunc", installFn) - - // Function that processes the HTTP request. - handler := func(w http.ResponseWriter, r *http.Request) { - handleInstallFunction(w, r.WithContext(ctx)) - } - - // Create a test server to handle the request. - srv := httptest.NewServer(http.HandlerFunc(handler)) - defer srv.Close() - - // Send the request to the test server. - res, err := http.Post(srv.URL, contentTypeJSON, strings.NewReader(requestPayload)) - if err != nil { - t.Fatalf("could not execute POST request: %s", err) - } - - // Check the response status code. - if res.StatusCode != http.StatusOK { - t.Errorf("unexpected status code (want: %v, got %v)", http.StatusOK, res.StatusCode) - } - - // Unpack the response body. - var response models.ResponseInstall - err = json.NewDecoder(res.Body).Decode(&response) - if err != nil { - t.Fatalf("could not decode server response: %s", err) - } - - // Verify the response - if response.Code != enums.ResponseCodeOk { - t.Errorf("unexpected response code (want: %v, got %v)", enums.ResponseCodeOk, response.Code) - } -} - -func TestHandleInstallFunction_HandlesErrors(t *testing.T) { - - const ( - contentTypeJSON = "application/json" - - malformedJSON = `{ "uri": "https://example.com/manifest.json" ` // JSON with missing closing brace. - emptyRequest = `{ "uri": "", "cid": ""} ` // Both URI and CID are empty. - validJSON = `{ "uri": "https://example.com/manifest.json" }` - ) - - var ( - installFn MsgInstallFunctionFunc = func(context.Context, models.RequestFunctionInstall) error { - return nil - } - failingInstallFn MsgInstallFunctionFunc = func(context.Context, models.RequestFunctionInstall) error { - return errors.New("stop") - } - ) - - tests := []struct { - name string - - payload io.Reader - installFn MsgInstallFunctionFunc - - expectedCode int - }{ - { - name: "missing response body", - payload: nil, - installFn: installFn, - expectedCode: http.StatusBadRequest, - }, - { - name: "malformed JSON payload", - payload: strings.NewReader(malformedJSON), - installFn: installFn, - expectedCode: http.StatusBadRequest, - }, - { - name: "missing URI and CID", - payload: strings.NewReader(emptyRequest), - installFn: installFn, - expectedCode: http.StatusBadRequest, - }, - { - name: "install function failed", - payload: strings.NewReader(validJSON), - installFn: failingInstallFn, - expectedCode: http.StatusInternalServerError, - }, - } - - for _, test := range tests { - - test := test - - t.Run(test.name, func(t *testing.T) { - - t.Parallel() - - ctx := context.WithValue(context.Background(), "msgInstallFunc", test.installFn) - - // Function that processes the HTTP request. - handler := func(w http.ResponseWriter, r *http.Request) { - handleInstallFunction(w, r.WithContext(ctx)) - } - - // Create a test server to handle the request. - srv := httptest.NewServer(http.HandlerFunc(handler)) - defer srv.Close() - - // Send the request to the test server. - res, err := http.Post(srv.URL, contentTypeJSON, test.payload) - if err != nil { - t.Fatalf("could not execute POST request: %s", err) - } - - // Check the response status code. - if res.StatusCode != test.expectedCode { - t.Fatalf("unexpected status code (want: %v, got %v)", test.expectedCode, res.StatusCode) - } - }) - } -} diff --git a/src/restapi/params.go b/src/restapi/params.go deleted file mode 100644 index d5668007..00000000 --- a/src/restapi/params.go +++ /dev/null @@ -1,9 +0,0 @@ -package restapi - -import ( - "time" -) - -const ( - functionInstallTimeout = 10 * time.Second -) diff --git a/src/restapi/rest.go b/src/restapi/rest.go deleted file mode 100644 index 94d4026e..00000000 --- a/src/restapi/rest.go +++ /dev/null @@ -1,46 +0,0 @@ -package restapi - -import ( - "context" - "net/http" - - "github.com/blocklessnetworking/b7s/src/models" - "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" -) - -func handleWeb(w http.ResponseWriter, r *http.Request) { - // params - w.Write([]byte("ok")) -} - -func startServer(ctx context.Context) { - var config = ctx.Value("config").(models.Config) - // router for api - myRouter := mux.NewRouter().StrictSlash(true).PathPrefix("/api/v1").Subrouter() - myRouter.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r.WithContext(ctx)) - }) - }) - - // router declaration - myRouter.HandleFunc("/", handleRootRequest) - - myRouter.HandleFunc("/function/request", handleRequestExecute).Methods("POST") - myRouter.HandleFunc("/function/install", handleInstallFunction).Methods("POST") - myRouter.HandleFunc("/function/result", handleGetExecuteResponse).Methods("POST") - - log.Info(http.ListenAndServe(":"+config.Rest.Port, myRouter)) -} - -func Start(ctx context.Context) { - var config = ctx.Value("config").(models.Config) - - log.WithFields(log.Fields{ - "port": config.Rest.Port, - "address": config.Rest.IP, - }).Info("starting rest server") - - go startServer(ctx) -} diff --git a/src/restapi/rest_test.go b/src/restapi/rest_test.go deleted file mode 100644 index 7abae6d3..00000000 --- a/src/restapi/rest_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package restapi - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHandleWeb(t *testing.T) { - // create a request to pass to our handler - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - t.Fatal(err) - } - - // create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response - rr := httptest.NewRecorder() - handler := http.HandlerFunc(handleWeb) - - // our handlers satisfy http.Handler, so we can call their ServeHTTP method - // directly and pass in our Request and ResponseRecorder. - handler.ServeHTTP(rr, req) - - // Check the status code is what we expect - assert.Equal(t, http.StatusOK, rr.Code, "status code should be 200") - - // Check the response body is what we expect - expected := "ok" - assert.Equal(t, expected, rr.Body.String(), "response should be 'ok'") -} diff --git a/src/selection/README.md b/src/selection/README.md deleted file mode 100644 index 2363fd63..00000000 --- a/src/selection/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## selection - -This package contains the required bits to perform selection for the network. diff --git a/src/selection/selection.go b/src/selection/selection.go deleted file mode 100644 index bcc6d7a6..00000000 --- a/src/selection/selection.go +++ /dev/null @@ -1,38 +0,0 @@ -package selection - -import ( - "context" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p-core/peer" -) - -type Network interface { - ConnsToPeer(id peer.ID) map[peer.ID]bool -} - -type Host interface { - Network() Network -} - -func SelectWorkerFromRollCall( - ctx context.Context, - rollcallResponse models.MsgRollCallResponse, - request models.RequestExecute, - rollcallRequest models.MsgRollCall, -) bool { - - h := ctx.Value("host").(Host) - conns := h.Network().ConnsToPeer(rollcallResponse.From) - - // pop off all the responses that don't match our first found connection - // worker with function - // worker responsed accepted has resources and is ready to execute - // worker knows RequestID - if rollcallResponse.Code == enums.ResponseCodeAccepted && rollcallResponse.FunctionId == request.FunctionId && len(conns) > 0 && rollcallRequest.RequestId == rollcallResponse.RequestId { - return false - } - - return true -} diff --git a/src/selection/selection_test.go b/src/selection/selection_test.go deleted file mode 100644 index 8ba383d1..00000000 --- a/src/selection/selection_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package selection - -import ( - "context" - "testing" - - "github.com/blocklessnetworking/b7s/src/enums" - "github.com/blocklessnetworking/b7s/src/models" - "github.com/libp2p/go-libp2p-core/peer" - "github.com/stretchr/testify/assert" -) - -type mockNetwork struct { - conns map[peer.ID]bool -} - -func (n *mockNetwork) ConnsToPeer(id peer.ID) map[peer.ID]bool { - return map[peer.ID]bool{id: n.conns[id]} -} - -type mockHost struct { - network Network -} - -func (h *mockHost) Network() Network { - return h.network -} - -func TestSelectWorkerFromRollCall(t *testing.T) { - network := &mockNetwork{ - conns: map[peer.ID]bool{ - "peer1": true, - }, - } - - host := &mockHost{ - network: network, - } - - rollcallResponse := &models.MsgRollCallResponse{ - From: "peer1", - Code: enums.ResponseCodeAccepted, - FunctionId: "function1", - RequestId: "request1", - } - - request := &models.RequestExecute{ - FunctionId: "function1", - } - - rollcallRequest := &models.MsgRollCall{ - RequestId: "request1", - } - - ctx := context.WithValue(context.Background(), "host", host) - - result := SelectWorkerFromRollCall(ctx, *rollcallResponse, *request, *rollcallRequest) - assert.Equal(t, false, result) - - rollcallResponse.FunctionId = "function2" - result = SelectWorkerFromRollCall(ctx, *rollcallResponse, *request, *rollcallRequest) - assert.Equal(t, true, result) - - rollcallResponse.Code = enums.ResponseCodeError - result = SelectWorkerFromRollCall(ctx, *rollcallResponse, *request, *rollcallRequest) - assert.Equal(t, true, result) -} diff --git a/store/get.go b/store/get.go new file mode 100644 index 00000000..d2a979e7 --- /dev/null +++ b/store/get.go @@ -0,0 +1,53 @@ +package store + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/cockroachdb/pebble" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +// Get retrieves the value for a key. +func (s *Store) Get(key string) (string, error) { + + value, closer, err := s.db.Get([]byte(key)) + if err != nil { + if errors.Is(err, pebble.ErrNotFound) { + return "", blockless.ErrNotFound + } + + return "", fmt.Errorf("could not retrieve value: %w", err) + } + // Closer must be called else a memory leak occurs. + defer closer.Close() + + // After closer is done, the slice is no longer valid, so we need to copy it. + dup := make([]byte, len(value)) + copy(dup, value) + + return string(dup), nil +} + +// GetRecord will read and JSON-decode the record associated with the provided key. +func (s *Store) GetRecord(key string, out interface{}) error { + + value, closer, err := s.db.Get([]byte(key)) + if err != nil { + if errors.Is(err, pebble.ErrNotFound) { + return blockless.ErrNotFound + } + return fmt.Errorf("could not retrieve value: %w", err) + } + // Closer must be called else a memory leak occurs. + defer closer.Close() + + err = json.Unmarshal(value, out) + if err != nil { + return fmt.Errorf("could not decode record: %w", err) + } + + return nil +} diff --git a/store/set.go b/store/set.go new file mode 100644 index 00000000..f0469d42 --- /dev/null +++ b/store/set.go @@ -0,0 +1,35 @@ +package store + +import ( + "encoding/json" + "fmt" + + "github.com/cockroachdb/pebble" +) + +// Set sets the value for a key. +func (s *Store) Set(key string, value string) error { + + err := s.db.Set([]byte(key), []byte(value), pebble.Sync) + if err != nil { + return fmt.Errorf("could not store value: %w", err) + } + + return nil +} + +// SetRecord will JSON-encode the provided record and store it in the DB. +func (s *Store) SetRecord(key string, value interface{}) error { + + encoded, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("could not serialize the record: %w", err) + } + + err = s.db.Set([]byte(key), encoded, pebble.Sync) + if err != nil { + return fmt.Errorf("could not store value: %w", err) + } + + return nil +} diff --git a/store/store.go b/store/store.go new file mode 100644 index 00000000..6bca45e8 --- /dev/null +++ b/store/store.go @@ -0,0 +1,20 @@ +package store + +import ( + "github.com/cockroachdb/pebble" +) + +// Store enables interaction with a database. +type Store struct { + db *pebble.DB +} + +// New creates a new Store backed by the database at the given path. +func New(db *pebble.DB) *Store { + + store := Store{ + db: db, + } + + return &store +} diff --git a/store/store_test.go b/store/store_test.go new file mode 100644 index 00000000..e83b9ce8 --- /dev/null +++ b/store/store_test.go @@ -0,0 +1,204 @@ +package store_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/store" + "github.com/blocklessnetworking/b7s/testing/helpers" +) + +func Test_Store(t *testing.T) { + t.Run("setting value", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + const ( + key = "some-key" + value = "some-value" + ) + + err := store.Set(key, value) + require.NoError(t, err) + + read, err := store.Get(key) + require.NoError(t, err) + + require.Equal(t, value, read) + }) + t.Run("missing value correctly reported", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + read, err := store.Get("missing-key") + require.Equal(t, "", read) + require.ErrorIs(t, err, blockless.ErrNotFound) + }) + t.Run("overwriting value", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + const ( + key = "some-key" + valueV1 = "some-value-v1" + valueV2 = "some-value-v2" + ) + + // Set value V1. + err := store.Set(key, valueV1) + require.NoError(t, err) + + read, err := store.Get(key) + require.NoError(t, err) + + require.Equal(t, valueV1, read) + + // Set value V2. + err = store.Set(key, valueV2) + require.NoError(t, err) + + read, err = store.Get(key) + require.NoError(t, err) + + require.Equal(t, read, valueV2) + }) + t.Run("setting record", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + const ( + key = "some-key" + ) + + type person struct { + Name string + Age uint + } + + var value = person{ + Name: "John", + Age: 30, + } + + err := store.SetRecord(key, value) + require.NoError(t, err) + + var read person + err = store.GetRecord(key, &read) + require.NoError(t, err) + + require.Equal(t, value, read) + }) + t.Run("handling missing record", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + const ( + key = "some-key" + ) + + type person struct { + Name string + Age uint + } + + var read person + err := store.GetRecord(key, &read) + require.Equal(t, person{}, read) + require.ErrorIs(t, err, blockless.ErrNotFound) + }) + t.Run("overwriting record", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + const ( + key = "some-key" + ) + + type person struct { + Name string + Age uint + } + + var value = person{ + Name: "John", + Age: 30, + } + + err := store.SetRecord(key, value) + require.NoError(t, err) + + var read person + err = store.GetRecord(key, &read) + require.NoError(t, err) + + require.Equal(t, value, read) + + // Change record values. + valueV2 := person{ + Name: "Paul", + Age: 20, + } + + err = store.SetRecord(key, valueV2) + require.NoError(t, err) + + err = store.GetRecord(key, &read) + require.NoError(t, err) + require.Equal(t, valueV2, read) + }) + t.Run("handle invalid output type", func(t *testing.T) { + t.Parallel() + + db := helpers.InMemoryDB(t) + defer db.Close() + store := store.New(db) + + const ( + key = "some-key" + ) + + type person struct { + Name string + Age uint + } + + var value = person{ + Name: "John", + Age: 30, + } + + err := store.SetRecord(key, value) + require.NoError(t, err) + + type invalidModel struct { + Name float64 + Age string + } + + var rec invalidModel + err = store.GetRecord(key, &rec) + require.Error(t, err) + }) + +} diff --git a/testing/helpers/function_server.go b/testing/helpers/function_server.go new file mode 100644 index 00000000..338d3fc3 --- /dev/null +++ b/testing/helpers/function_server.go @@ -0,0 +1,71 @@ +package helpers + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +type FunctionServer struct { + *httptest.Server +} + +func CreateFunctionServer(t *testing.T, manifestEndpoint string, manifest blockless.FunctionManifest, deploymentEndpoint string, archivePath string, cid string) *FunctionServer { + t.Helper() + + // Archive to serve. + archive, err := os.ReadFile(archivePath) + require.NoError(t, err) + + // Checksum of the archive we serve. + checksum := sha256.Sum256(archive) + + srv := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + + path := req.URL.Path + switch path { + // Manifest request. + case manifestEndpoint: + + // Link to a URL on our own server where we'll serve the function archive. + deploymentURL := url.URL{ + Scheme: "http", + Host: req.Host, + Path: deploymentEndpoint, + } + + manifest.Deployment = blockless.Deployment{ + CID: cid, + Checksum: fmt.Sprintf("%x", checksum), + URI: deploymentURL.String(), + } + + payload, err := json.Marshal(manifest) + require.NoError(t, err) + w.Write(payload) + + // Archive download request. + case deploymentEndpoint: + w.Write(archive) + + default: + w.WriteHeader(http.StatusNotFound) + } + })) + + fs := FunctionServer{ + Server: srv, + } + + return &fs +} diff --git a/testing/helpers/pebble.go b/testing/helpers/pebble.go new file mode 100644 index 00000000..7fb81961 --- /dev/null +++ b/testing/helpers/pebble.go @@ -0,0 +1,21 @@ +package helpers + +import ( + "testing" + + "github.com/cockroachdb/pebble" + "github.com/cockroachdb/pebble/vfs" + "github.com/stretchr/testify/require" +) + +func InMemoryDB(t *testing.T) *pebble.DB { + t.Helper() + + opts := pebble.Options{ + FS: vfs.NewMem(), + } + db, err := pebble.Open("", &opts) + require.NoError(t, err) + + return db +} diff --git a/testing/mocks/executor.go b/testing/mocks/executor.go new file mode 100644 index 00000000..3fa78128 --- /dev/null +++ b/testing/mocks/executor.go @@ -0,0 +1,27 @@ +package mocks + +import ( + "testing" + + "github.com/blocklessnetworking/b7s/models/execute" +) + +type Executor struct { + ExecFunctionFunc func(string, execute.Request) (execute.Result, error) +} + +func BaselineExecutor(t *testing.T) *Executor { + t.Helper() + + executor := Executor{ + ExecFunctionFunc: func(string, execute.Request) (execute.Result, error) { + return GenericExecutionResult, nil + }, + } + + return &executor +} + +func (e *Executor) Function(requestID string, req execute.Request) (execute.Result, error) { + return e.ExecFunctionFunc(requestID, req) +} diff --git a/testing/mocks/function_store.go b/testing/mocks/function_store.go new file mode 100644 index 00000000..e6fa40e9 --- /dev/null +++ b/testing/mocks/function_store.go @@ -0,0 +1,27 @@ +package mocks + +import ( + "testing" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +type FunctionHandler struct { + GetFunc func(string, string, bool) (*blockless.FunctionManifest, error) +} + +func BaselineFunctionHandler(t *testing.T) *FunctionHandler { + t.Helper() + + fh := FunctionHandler{ + GetFunc: func(string, string, bool) (*blockless.FunctionManifest, error) { + return &GenericManifest, nil + }, + } + + return &fh +} + +func (f *FunctionHandler) Get(address string, cid string, useCached bool) (*blockless.FunctionManifest, error) { + return f.GetFunc(address, cid, useCached) +} diff --git a/testing/mocks/generic.go b/testing/mocks/generic.go new file mode 100644 index 00000000..3dc5ba77 --- /dev/null +++ b/testing/mocks/generic.go @@ -0,0 +1,53 @@ +package mocks + +import ( + "errors" + "io" + + "github.com/google/uuid" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/rs/zerolog" + + "github.com/blocklessnetworking/b7s/models/blockless" + "github.com/blocklessnetworking/b7s/models/execute" + "github.com/blocklessnetworking/b7s/models/response" +) + +// Global variables that can be used for testing. They are valid non-nil values for commonly needed types. +var ( + NoopLogger = zerolog.New(io.Discard) + + GenericError = errors.New("dummy test") + + GenericPeerID = peer.ID([]byte{0x0, 0x24, 0x8, 0x1, 0x12, 0x20, 0x56, 0x77, 0x86, 0x82, 0x76, 0xa, 0xc5, 0x9, 0x63, 0xde, 0xe4, 0x31, 0xfc, 0x44, 0x75, 0xdd, 0x5a, 0x27, 0xee, 0x6b, 0x94, 0x13, 0xed, 0xe2, 0xa3, 0x6d, 0x8a, 0x1d, 0x57, 0xb6, 0xb8, 0x91}) + + GenericAddress = "/ip4/127.0.0.1/tcp/9000/p2p/12D3KooWRp3AVk7qtc2Av6xiqgAza1ZouksQaYcS2cvN94kHSCoa" + + GenericString = "test" + + GenericUUID = uuid.UUID{0xd1, 0xc2, 0x44, 0xaf, 0xa3, 0x1d, 0x48, 0x87, 0x93, 0x9d, 0xd6, 0xc7, 0xf, 0xe, 0x4f, 0xd0} + + GenericExecutionResult = execute.Result{ + Code: response.CodeUnknown, + Result: "generic-execution-result", + RequestID: GenericUUID.String(), + } + + GenericManifest = blockless.FunctionManifest{ + ID: "generic-id", + Name: "generic-name", + Description: "generic-description", + Function: blockless.Function{ + ID: "function-id", + Name: "function-name", + Runtime: "generic-runtime", + }, + Deployment: blockless.Deployment{ + CID: "generic-cid", + Checksum: "1234567890", + URI: "generic-uri", + }, + FSRootPath: "/var/tmp/blockless/", + Entry: "/var/tmp/blockless/app.wasm", + } +) diff --git a/testing/mocks/peerstore.go b/testing/mocks/peerstore.go new file mode 100644 index 00000000..e7c114c6 --- /dev/null +++ b/testing/mocks/peerstore.go @@ -0,0 +1,46 @@ +package mocks + +import ( + "testing" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + + "github.com/blocklessnetworking/b7s/models/blockless" +) + +type PeerStore struct { + StoreFunc func(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error + UpdatePeerListFunc func(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error + PeersFunc func() ([]blockless.Peer, error) +} + +func BaselinePeerStore(t *testing.T) *PeerStore { + t.Helper() + + peerstore := PeerStore{ + StoreFunc: func(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error { + return nil + }, + UpdatePeerListFunc: func(peer.ID, multiaddr.Multiaddr, peer.AddrInfo) error { + return nil + }, + PeersFunc: func() ([]blockless.Peer, error) { + return []blockless.Peer{}, nil + }, + } + + return &peerstore +} + +func (p *PeerStore) Store(id peer.ID, addr multiaddr.Multiaddr, info peer.AddrInfo) error { + return p.StoreFunc(id, addr, info) +} + +func (p *PeerStore) UpdatePeerList(id peer.ID, addr multiaddr.Multiaddr, info peer.AddrInfo) error { + return p.UpdatePeerListFunc(id, addr, info) +} + +func (p *PeerStore) Peers() ([]blockless.Peer, error) { + return p.PeersFunc() +} diff --git a/testing/mocks/store.go b/testing/mocks/store.go new file mode 100644 index 00000000..e8cbfdee --- /dev/null +++ b/testing/mocks/store.go @@ -0,0 +1,49 @@ +package mocks + +import ( + "testing" +) + +type Store struct { + GetFunc func(key string) (string, error) + SetFunc func(key string, value string) error + GetRecordFunc func(key string, value interface{}) error + SetRecordFunc func(key string, value interface{}) error +} + +func BaselineStore(t *testing.T) *Store { + t.Helper() + + store := Store{ + GetFunc: func(string) (string, error) { + return GenericString, nil + }, + SetFunc: func(string, string) error { + return nil + }, + GetRecordFunc: func(string, interface{}) error { + return nil + }, + SetRecordFunc: func(string, interface{}) error { + return nil + }, + } + + return &store +} + +func (s *Store) Get(key string) (string, error) { + return s.GetFunc(key) +} + +func (s *Store) Set(key string, value string) error { + return s.SetFunc(key, value) +} + +func (s *Store) GetRecord(key string, value interface{}) error { + return s.GetRecordFunc(key, value) +} + +func (s *Store) SetRecord(key string, value interface{}) error { + return s.SetRecordFunc(key, value) +}