From dadfd90dcd6387f71fb477532372a4b6dba5789d Mon Sep 17 00:00:00 2001 From: alexvanin Date: Fri, 10 Jul 2020 17:17:51 +0300 Subject: [PATCH] Initial commit Initial public review release v0.10.0 --- .dockerignore | 8 + .gitattributes | 1 + .gitignore | 8 + .golangci.yml | 136 ++ CHANGELOG.md | 6 + CONTRIBUTING.md | 3 + Dockerfile | 21 + LICENSE | 674 +++++++++ Makefile | 99 ++ cmd/neofs-node/defaults.go | 346 +++++ cmd/neofs-node/main.go | 146 ++ go.mod | 48 + go.sum | Bin 0 -> 66535 bytes internal/error.go | 7 + lib/.gitkeep | 0 lib/acl/action.go | 94 ++ lib/acl/action_test.go | 163 +++ lib/acl/basic.go | 179 +++ lib/acl/basic_test.go | 116 ++ lib/acl/binary.go | 129 ++ lib/acl/binary_test.go | 27 + lib/acl/extended.go | 29 + lib/acl/header.go | 234 ++++ lib/acl/headers_test.go | 60 + lib/acl/match.go | 94 ++ lib/acl/match_test.go | 192 +++ lib/blockchain/event/event.go | 31 + lib/blockchain/event/handler.go | 22 + lib/blockchain/event/listener.go | 309 +++++ lib/blockchain/event/netmap/epoch.go | 39 + lib/blockchain/event/netmap/epoch_test.go | 47 + lib/blockchain/event/parser.go | 53 + lib/blockchain/event/utils.go | 34 + lib/blockchain/goclient/client.go | 190 +++ lib/blockchain/goclient/client_test.go | 33 + lib/blockchain/goclient/util.go | 131 ++ lib/blockchain/goclient/util_test.go | 145 ++ lib/blockchain/subscriber/subscriber.go | 151 ++ lib/boot/bootstrap_test.go | 24 + lib/boot/bootstrapper.go | 31 + lib/boot/storage.go | 46 + lib/buckets/boltdb/boltdb.go | 109 ++ lib/buckets/boltdb/methods.go | 94 ++ lib/buckets/boltdb/methods_test.go | 95 ++ lib/buckets/boltdb/plugin/main.go | 25 + lib/buckets/fsbucket/bucket.go | 101 ++ lib/buckets/fsbucket/methods.go | 107 ++ lib/buckets/fsbucket/queue.go | 44 + lib/buckets/fsbucket/treemethods.go | 261 ++++ lib/buckets/fsbucket/treemethods_test.go | 324 +++++ lib/buckets/init.go | 64 + lib/buckets/inmemory/bucket.go | 60 + lib/buckets/inmemory/methods.go | 107 ++ lib/container/alias.go | 15 + lib/container/storage.go | 134 ++ lib/container/storage_test.go | 83 ++ lib/core/storage.go | 94 ++ lib/core/storage_test.go | 65 + lib/core/validator.go | 22 + lib/core/verify.go | 69 + lib/fix/catch.go | 59 + lib/fix/config/config.go | 53 + lib/fix/fix.go | 112 ++ lib/fix/grace.go | 26 + lib/fix/logger/logger.go | 90 ++ lib/fix/module/module.go | 35 + lib/fix/services.go | 46 + lib/fix/web/http.go | 114 ++ lib/fix/web/metrics.go | 32 + lib/fix/web/pprof.go | 44 + lib/fix/web/server.go | 62 + lib/fix/worker/worker.go | 79 ++ lib/implementations/acl.go | 392 ++++++ lib/implementations/acl_test.go | 19 + lib/implementations/balance.go | 141 ++ lib/implementations/balance_test.go | 35 + lib/implementations/bootstrap.go | 311 +++++ lib/implementations/bootstrap_test.go | 30 + lib/implementations/epoch.go | 7 + lib/implementations/locator.go | 78 ++ lib/implementations/locator_test.go | 38 + lib/implementations/object.go | 131 ++ lib/implementations/peerstore.go | 74 + lib/implementations/placement.go | 152 +++ lib/implementations/reputation.go | 41 + lib/implementations/sg.go | 136 ++ lib/implementations/transport.go | 657 +++++++++ lib/implementations/validation.go | 405 ++++++ lib/implementations/validation_test.go | 273 ++++ lib/ir/info.go | 17 + lib/ir/info_test.go | 25 + lib/ir/node.go | 17 + lib/ir/node_test.go | 16 + lib/ir/storage.go | 94 ++ lib/ir/storage_test.go | 101 ++ lib/localstore/alias.go | 35 + lib/localstore/del.go | 38 + lib/localstore/filter.go | 306 +++++ lib/localstore/filter_funcs.go | 39 + lib/localstore/filter_test.go | 38 + lib/localstore/get.go | 30 + lib/localstore/has.go | 20 + lib/localstore/interface.go | 102 ++ lib/localstore/list.go | 41 + lib/localstore/localstore.pb.go | Bin 0 -> 11883 bytes lib/localstore/localstore.proto | 14 + lib/localstore/localstore_test.go | 501 +++++++ lib/localstore/meta.go | 52 + lib/localstore/put.go | 47 + lib/localstore/range.go | 36 + lib/meta/iterator.go | 15 + lib/metrics/meta.go | 33 + lib/metrics/metrics.go | 175 +++ lib/metrics/metrics_test.go | 275 ++++ lib/metrics/prometeus.go | 83 ++ lib/metrics/store.go | 122 ++ lib/metrics/store_test.go | 156 +++ lib/muxer/listener.go | 51 + lib/muxer/muxer.go | 247 ++++ lib/muxer/muxer_test.go | 415 ++++++ lib/muxer/muxer_test.pb.go | Bin 0 -> 15514 bytes lib/muxer/muxer_test.proto | 18 + lib/netmap/netmap.go | 392 ++++++ lib/netmap/netmap_test.go | 261 ++++ lib/netmap/storage.go | 27 + lib/netmap/storage_test.go | 23 + lib/objio/range.go | 459 +++++++ lib/objio/range_test.go | 386 ++++++ lib/objutil/verifier.go | 35 + lib/peers/metrics.go | 45 + lib/peers/peers.go | 455 +++++++ lib/peers/peers_test.go | 484 +++++++ lib/peers/peers_test.pb.go | Bin 0 -> 15514 bytes lib/peers/peers_test.proto | 18 + lib/peers/peerstore.go | 238 ++++ lib/peers/peerstore_test.go | 245 ++++ lib/peers/storage.go | 296 ++++ lib/peers/worker.go | 67 + lib/placement/graph.go | 178 +++ lib/placement/interface.go | 113 ++ lib/placement/neighbours.go | 69 + lib/placement/neighbours_test.go | 177 +++ lib/placement/placement.go | 257 ++++ lib/placement/placement_test.go | 407 ++++++ lib/placement/store.go | 66 + lib/rand/rand.go | 46 + lib/replication/common.go | 197 +++ lib/replication/garbage.go | 27 + lib/replication/implementations.go | 292 ++++ lib/replication/location_detector.go | 154 +++ lib/replication/manager.go | 347 +++++ lib/replication/object_replicator.go | 188 +++ lib/replication/object_restorer.go | 173 +++ lib/replication/placement_honorer.go | 198 +++ lib/replication/storage_validator.go | 194 +++ lib/storage/storage.go | 122 ++ lib/test/bucket.go | 144 ++ lib/test/keys.go | 142 ++ lib/test/logger.go | 30 + lib/transformer/alias.go | 25 + lib/transformer/put_test.go | 764 +++++++++++ lib/transformer/restore.go | 126 ++ lib/transformer/transformer.go | 528 +++++++ lib/transport/connection.go | 39 + lib/transport/object.go | 107 ++ lib/transport/transport.go | 76 ++ lib/transport/transport_test.go | 61 + misc/build.go | 18 + modules/bootstrap/healthy.go | 95 ++ modules/bootstrap/module.go | 10 + modules/grpc/billing.go | 141 ++ modules/grpc/module.go | 10 + modules/grpc/routing.go | 118 ++ modules/morph/balance.go | 67 + modules/morph/common.go | 140 ++ modules/morph/container.go | 122 ++ modules/morph/event.go | 28 + modules/morph/goclient.go | 32 + modules/morph/listener.go | 53 + modules/morph/module.go | 22 + modules/morph/netmap.go | 115 ++ modules/morph/reputation.go | 59 + modules/network/http.go | 49 + modules/network/module.go | 20 + modules/network/muxer.go | 57 + modules/network/peers.go | 41 + modules/network/placement.go | 79 ++ modules/node/audit.go | 63 + modules/node/container.go | 31 + modules/node/core.go | 29 + modules/node/localstore.go | 64 + modules/node/metrics.go | 52 + modules/node/module.go | 91 ++ modules/node/objectmanager.go | 219 +++ modules/node/peerstore.go | 28 + modules/node/placement.go | 33 + modules/node/replication.go | 394 ++++++ modules/node/services.go | 36 + modules/node/session.go | 26 + modules/settings/address.go | 109 ++ modules/settings/module.go | 10 + modules/settings/node.go | 149 ++ modules/workers/module.go | 10 + modules/workers/prepare.go | 132 ++ services/metrics/service.go | 60 + services/metrics/service.pb.go | Bin 0 -> 13467 bytes services/metrics/service.proto | 10 + services/public/accounting/service.go | 85 ++ services/public/accounting/service_test.go | 3 + services/public/container/acl.go | 64 + services/public/container/acl_test.go | 211 +++ services/public/container/alias.go | 15 + services/public/container/common_test.go | 19 + services/public/container/delete.go | 37 + services/public/container/delete_test.go | 118 ++ services/public/container/get.go | 38 + services/public/container/get_test.go | 123 ++ services/public/container/list.go | 39 + services/public/container/list_test.go | 124 ++ services/public/container/put.go | 54 + services/public/container/put_test.go | 132 ++ services/public/container/service.go | 78 ++ services/public/object/acl.go | 428 ++++++ services/public/object/acl_test.go | 512 +++++++ services/public/object/bearer.go | 72 + services/public/object/capacity.go | 19 + services/public/object/capacity_test.go | 75 + services/public/object/delete.go | 285 ++++ services/public/object/delete_test.go | 449 ++++++ services/public/object/execution.go | 471 +++++++ services/public/object/execution_test.go | 1207 ++++++++++++++++ services/public/object/filter.go | 251 ++++ services/public/object/filter_test.go | 400 ++++++ services/public/object/get.go | 111 ++ services/public/object/get_test.go | 225 +++ services/public/object/handler.go | 109 ++ services/public/object/handler_test.go | 442 ++++++ services/public/object/head.go | 640 +++++++++ services/public/object/head_test.go | 595 ++++++++ services/public/object/implementations.go | 32 + services/public/object/listing.go | 286 ++++ services/public/object/listing_test.go | 513 +++++++ services/public/object/postprocessor.go | 47 + services/public/object/postprocessor_test.go | 83 ++ services/public/object/preprocessor.go | 163 +++ services/public/object/preprocessor_test.go | 142 ++ services/public/object/put.go | 437 ++++++ services/public/object/put_test.go | 958 +++++++++++++ services/public/object/query.go | 234 ++++ services/public/object/query_test.go | 828 +++++++++++ services/public/object/ranges.go | 481 +++++++ services/public/object/ranges_test.go | 778 +++++++++++ services/public/object/response.go | 144 ++ services/public/object/response_test.go | 116 ++ services/public/object/search.go | 169 +++ services/public/object/search_test.go | 265 ++++ services/public/object/service.go | 680 +++++++++ services/public/object/status.go | 951 +++++++++++++ services/public/object/status_test.go | 1210 +++++++++++++++++ services/public/object/token.go | 107 ++ services/public/object/token_test.go | 156 +++ .../object/transport_implementations.go | 743 ++++++++++ services/public/object/transport_test.go | 76 ++ services/public/object/traverse.go | 186 +++ services/public/object/traverse_test.go | 378 +++++ services/public/object/ttl.go | 211 +++ services/public/object/ttl_test.go | 377 +++++ services/public/object/verb.go | 79 ++ services/public/object/verb_test.go | 124 ++ services/public/object/verification.go | 36 + services/public/object/verification_test.go | 63 + services/public/session/create.go | 53 + services/public/session/service.go | 66 + services/public/session/service_test.go | 3 + services/public/state/service.go | 324 +++++ services/public/state/service_test.go | 249 ++++ 276 files changed, 43489 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 cmd/neofs-node/defaults.go create mode 100644 cmd/neofs-node/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/error.go create mode 100644 lib/.gitkeep create mode 100644 lib/acl/action.go create mode 100644 lib/acl/action_test.go create mode 100644 lib/acl/basic.go create mode 100644 lib/acl/basic_test.go create mode 100644 lib/acl/binary.go create mode 100644 lib/acl/binary_test.go create mode 100644 lib/acl/extended.go create mode 100644 lib/acl/header.go create mode 100644 lib/acl/headers_test.go create mode 100644 lib/acl/match.go create mode 100644 lib/acl/match_test.go create mode 100644 lib/blockchain/event/event.go create mode 100644 lib/blockchain/event/handler.go create mode 100644 lib/blockchain/event/listener.go create mode 100644 lib/blockchain/event/netmap/epoch.go create mode 100644 lib/blockchain/event/netmap/epoch_test.go create mode 100644 lib/blockchain/event/parser.go create mode 100644 lib/blockchain/event/utils.go create mode 100644 lib/blockchain/goclient/client.go create mode 100644 lib/blockchain/goclient/client_test.go create mode 100644 lib/blockchain/goclient/util.go create mode 100644 lib/blockchain/goclient/util_test.go create mode 100644 lib/blockchain/subscriber/subscriber.go create mode 100644 lib/boot/bootstrap_test.go create mode 100644 lib/boot/bootstrapper.go create mode 100644 lib/boot/storage.go create mode 100644 lib/buckets/boltdb/boltdb.go create mode 100644 lib/buckets/boltdb/methods.go create mode 100644 lib/buckets/boltdb/methods_test.go create mode 100644 lib/buckets/boltdb/plugin/main.go create mode 100644 lib/buckets/fsbucket/bucket.go create mode 100644 lib/buckets/fsbucket/methods.go create mode 100644 lib/buckets/fsbucket/queue.go create mode 100644 lib/buckets/fsbucket/treemethods.go create mode 100644 lib/buckets/fsbucket/treemethods_test.go create mode 100644 lib/buckets/init.go create mode 100644 lib/buckets/inmemory/bucket.go create mode 100644 lib/buckets/inmemory/methods.go create mode 100644 lib/container/alias.go create mode 100644 lib/container/storage.go create mode 100644 lib/container/storage_test.go create mode 100644 lib/core/storage.go create mode 100644 lib/core/storage_test.go create mode 100644 lib/core/validator.go create mode 100644 lib/core/verify.go create mode 100644 lib/fix/catch.go create mode 100644 lib/fix/config/config.go create mode 100644 lib/fix/fix.go create mode 100644 lib/fix/grace.go create mode 100644 lib/fix/logger/logger.go create mode 100644 lib/fix/module/module.go create mode 100644 lib/fix/services.go create mode 100644 lib/fix/web/http.go create mode 100644 lib/fix/web/metrics.go create mode 100644 lib/fix/web/pprof.go create mode 100644 lib/fix/web/server.go create mode 100644 lib/fix/worker/worker.go create mode 100644 lib/implementations/acl.go create mode 100644 lib/implementations/acl_test.go create mode 100644 lib/implementations/balance.go create mode 100644 lib/implementations/balance_test.go create mode 100644 lib/implementations/bootstrap.go create mode 100644 lib/implementations/bootstrap_test.go create mode 100644 lib/implementations/epoch.go create mode 100644 lib/implementations/locator.go create mode 100644 lib/implementations/locator_test.go create mode 100644 lib/implementations/object.go create mode 100644 lib/implementations/peerstore.go create mode 100644 lib/implementations/placement.go create mode 100644 lib/implementations/reputation.go create mode 100644 lib/implementations/sg.go create mode 100644 lib/implementations/transport.go create mode 100644 lib/implementations/validation.go create mode 100644 lib/implementations/validation_test.go create mode 100644 lib/ir/info.go create mode 100644 lib/ir/info_test.go create mode 100644 lib/ir/node.go create mode 100644 lib/ir/node_test.go create mode 100644 lib/ir/storage.go create mode 100644 lib/ir/storage_test.go create mode 100644 lib/localstore/alias.go create mode 100644 lib/localstore/del.go create mode 100644 lib/localstore/filter.go create mode 100644 lib/localstore/filter_funcs.go create mode 100644 lib/localstore/filter_test.go create mode 100644 lib/localstore/get.go create mode 100644 lib/localstore/has.go create mode 100644 lib/localstore/interface.go create mode 100644 lib/localstore/list.go create mode 100644 lib/localstore/localstore.pb.go create mode 100644 lib/localstore/localstore.proto create mode 100644 lib/localstore/localstore_test.go create mode 100644 lib/localstore/meta.go create mode 100644 lib/localstore/put.go create mode 100644 lib/localstore/range.go create mode 100644 lib/meta/iterator.go create mode 100644 lib/metrics/meta.go create mode 100644 lib/metrics/metrics.go create mode 100644 lib/metrics/metrics_test.go create mode 100644 lib/metrics/prometeus.go create mode 100644 lib/metrics/store.go create mode 100644 lib/metrics/store_test.go create mode 100644 lib/muxer/listener.go create mode 100644 lib/muxer/muxer.go create mode 100644 lib/muxer/muxer_test.go create mode 100644 lib/muxer/muxer_test.pb.go create mode 100644 lib/muxer/muxer_test.proto create mode 100644 lib/netmap/netmap.go create mode 100644 lib/netmap/netmap_test.go create mode 100644 lib/netmap/storage.go create mode 100644 lib/netmap/storage_test.go create mode 100644 lib/objio/range.go create mode 100644 lib/objio/range_test.go create mode 100644 lib/objutil/verifier.go create mode 100644 lib/peers/metrics.go create mode 100644 lib/peers/peers.go create mode 100644 lib/peers/peers_test.go create mode 100644 lib/peers/peers_test.pb.go create mode 100644 lib/peers/peers_test.proto create mode 100644 lib/peers/peerstore.go create mode 100644 lib/peers/peerstore_test.go create mode 100644 lib/peers/storage.go create mode 100644 lib/peers/worker.go create mode 100644 lib/placement/graph.go create mode 100644 lib/placement/interface.go create mode 100644 lib/placement/neighbours.go create mode 100644 lib/placement/neighbours_test.go create mode 100644 lib/placement/placement.go create mode 100644 lib/placement/placement_test.go create mode 100644 lib/placement/store.go create mode 100644 lib/rand/rand.go create mode 100644 lib/replication/common.go create mode 100644 lib/replication/garbage.go create mode 100644 lib/replication/implementations.go create mode 100644 lib/replication/location_detector.go create mode 100644 lib/replication/manager.go create mode 100644 lib/replication/object_replicator.go create mode 100644 lib/replication/object_restorer.go create mode 100644 lib/replication/placement_honorer.go create mode 100644 lib/replication/storage_validator.go create mode 100644 lib/storage/storage.go create mode 100644 lib/test/bucket.go create mode 100644 lib/test/keys.go create mode 100644 lib/test/logger.go create mode 100644 lib/transformer/alias.go create mode 100644 lib/transformer/put_test.go create mode 100644 lib/transformer/restore.go create mode 100644 lib/transformer/transformer.go create mode 100644 lib/transport/connection.go create mode 100644 lib/transport/object.go create mode 100644 lib/transport/transport.go create mode 100644 lib/transport/transport_test.go create mode 100644 misc/build.go create mode 100644 modules/bootstrap/healthy.go create mode 100644 modules/bootstrap/module.go create mode 100644 modules/grpc/billing.go create mode 100644 modules/grpc/module.go create mode 100644 modules/grpc/routing.go create mode 100644 modules/morph/balance.go create mode 100644 modules/morph/common.go create mode 100644 modules/morph/container.go create mode 100644 modules/morph/event.go create mode 100644 modules/morph/goclient.go create mode 100644 modules/morph/listener.go create mode 100644 modules/morph/module.go create mode 100644 modules/morph/netmap.go create mode 100644 modules/morph/reputation.go create mode 100644 modules/network/http.go create mode 100644 modules/network/module.go create mode 100644 modules/network/muxer.go create mode 100644 modules/network/peers.go create mode 100644 modules/network/placement.go create mode 100644 modules/node/audit.go create mode 100644 modules/node/container.go create mode 100644 modules/node/core.go create mode 100644 modules/node/localstore.go create mode 100644 modules/node/metrics.go create mode 100644 modules/node/module.go create mode 100644 modules/node/objectmanager.go create mode 100644 modules/node/peerstore.go create mode 100644 modules/node/placement.go create mode 100644 modules/node/replication.go create mode 100644 modules/node/services.go create mode 100644 modules/node/session.go create mode 100644 modules/settings/address.go create mode 100644 modules/settings/module.go create mode 100644 modules/settings/node.go create mode 100644 modules/workers/module.go create mode 100644 modules/workers/prepare.go create mode 100644 services/metrics/service.go create mode 100644 services/metrics/service.pb.go create mode 100644 services/metrics/service.proto create mode 100644 services/public/accounting/service.go create mode 100644 services/public/accounting/service_test.go create mode 100644 services/public/container/acl.go create mode 100644 services/public/container/acl_test.go create mode 100644 services/public/container/alias.go create mode 100644 services/public/container/common_test.go create mode 100644 services/public/container/delete.go create mode 100644 services/public/container/delete_test.go create mode 100644 services/public/container/get.go create mode 100644 services/public/container/get_test.go create mode 100644 services/public/container/list.go create mode 100644 services/public/container/list_test.go create mode 100644 services/public/container/put.go create mode 100644 services/public/container/put_test.go create mode 100644 services/public/container/service.go create mode 100644 services/public/object/acl.go create mode 100644 services/public/object/acl_test.go create mode 100644 services/public/object/bearer.go create mode 100644 services/public/object/capacity.go create mode 100644 services/public/object/capacity_test.go create mode 100644 services/public/object/delete.go create mode 100644 services/public/object/delete_test.go create mode 100644 services/public/object/execution.go create mode 100644 services/public/object/execution_test.go create mode 100644 services/public/object/filter.go create mode 100644 services/public/object/filter_test.go create mode 100644 services/public/object/get.go create mode 100644 services/public/object/get_test.go create mode 100644 services/public/object/handler.go create mode 100644 services/public/object/handler_test.go create mode 100644 services/public/object/head.go create mode 100644 services/public/object/head_test.go create mode 100644 services/public/object/implementations.go create mode 100644 services/public/object/listing.go create mode 100644 services/public/object/listing_test.go create mode 100644 services/public/object/postprocessor.go create mode 100644 services/public/object/postprocessor_test.go create mode 100644 services/public/object/preprocessor.go create mode 100644 services/public/object/preprocessor_test.go create mode 100644 services/public/object/put.go create mode 100644 services/public/object/put_test.go create mode 100644 services/public/object/query.go create mode 100644 services/public/object/query_test.go create mode 100644 services/public/object/ranges.go create mode 100644 services/public/object/ranges_test.go create mode 100644 services/public/object/response.go create mode 100644 services/public/object/response_test.go create mode 100644 services/public/object/search.go create mode 100644 services/public/object/search_test.go create mode 100644 services/public/object/service.go create mode 100644 services/public/object/status.go create mode 100644 services/public/object/status_test.go create mode 100644 services/public/object/token.go create mode 100644 services/public/object/token_test.go create mode 100644 services/public/object/transport_implementations.go create mode 100644 services/public/object/transport_test.go create mode 100644 services/public/object/traverse.go create mode 100644 services/public/object/traverse_test.go create mode 100644 services/public/object/ttl.go create mode 100644 services/public/object/ttl_test.go create mode 100644 services/public/object/verb.go create mode 100644 services/public/object/verb_test.go create mode 100644 services/public/object/verification.go create mode 100644 services/public/object/verification_test.go create mode 100644 services/public/session/create.go create mode 100644 services/public/session/service.go create mode 100644 services/public/session/service_test.go create mode 100644 services/public/state/service.go create mode 100644 services/public/state/service_test.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..ea5f329353 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.idea +.vscode +.git +docker-compose.yml +Dockerfile +temp +.dockerignore +docker \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..b002a5dde7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/**/*.pb.go -diff binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..690f5d9eff --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +bin +temp +cmd/test +/plugins/ +/vendor/ + +testfile +.neofs-cli.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000..7fc9abf2cf --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,136 @@ +# https://habr.com/company/roistat/blog/413175/ +# https://github.com/golangci/golangci-lint +linters-settings: + govet: + check-shadowing: false + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.8 + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + gocyclo: + min-complexity: 30 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + gosimple: + gocritic: + # Which checks should be enabled; can't be combined with 'disabled-checks'; + # See https://go-critic.github.io/overview#checks-overview + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` + # By default list of stable checks is used. +# enabled-checks: +# - rangeValCopy + # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty + disabled-checks: + - regexpMust + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - performance + + settings: # settings passed to gocritic + captLocal: # must be valid enabled check name + paramsOnly: true + rangeValCopy: + sizeThreshold: 32 +# depguard: +# list-type: blacklist +# include-go-root: false +# packages: +# - github.com/davecgh/go-spew/spew + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 1 + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find funcs usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 + + +linters: + enable-all: true + fast: false + disable: + - gochecknoglobals +# - maligned +# - prealloc +# disable-all: false +# presets: +# - bugs +# - unused + +# options for analysis running +run: + # default concurrency is a available CPU number +# concurrency: 8 + + # timeout for analysis, e.g. 30s, 5m, default is 1m +# deadline: 1m + + # exit code when at least one issue was found, default is 1 +# issues-exit-code: 1 + + # include test files or not, default is true +# tests: true + + # list of build tags, all linters use it. Default is empty list. +# build-tags: +# - mytag + + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but next dirs are always skipped independently + # from this option's value: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ +# skip-dirs: +# - src/external_libs +# - autogenerated_by_my_lib + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. +# skip-files: +# - ".*\\.my\\.go$" +# - lib/bad.go + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. +# modules-download-mode: readonly|release|vendor + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + format: tab + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..7e4a00da04 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +Changelog for NeoFS Node + +## [0.10.0] - 2020-07-10 + +First public review release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..b11fe2a494 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +We do not accept any contributions. As yet. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..82e5681385 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.14-alpine as builder + +ARG BUILD=now +ARG VERSION=dev +ARG REPO=repository + +WORKDIR /src + +COPY . /src + +RUN apk add --update make bash +RUN make bin/neofs-node + +# Executable image +FROM scratch AS neofs-node + +WORKDIR / + +COPY --from=builder /src/bin/neofs-node /bin/neofs-node + +CMD ["neofs-node"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..48dcf385ee --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +#!/usr/bin/make -f +SHELL = bash + +REPO ?= $(shell go list -m) +VERSION ?= "$(shell git describe --tags --dirty --always)" + +HUB_IMAGE ?= nspccdev/neofs +HUB_TAG ?= "$(shell echo ${VERSION} | sed 's/^v//')" + +BIN = bin +DIRS= $(BIN) + +# List of binaries to build. May be automated. +CMDS = neofs-node +CMS = $(addprefix $(BIN)/, $(CMDS)) +BINS = $(addprefix $(BIN)/, $(CMDS)) + +.PHONY: help dep clean fmt + +# To build a specific binary, use it's name prfixed with bin/ as a target +# For example `make bin/neofs-node` will buils only Storage node binary +# Just `make` will +# Build all possible binaries +all: $(DIRS) $(BINS) + +$(BINS): $(DIRS) dep + @echo "⇒ Build $@" + GOGC=off \ + CGO_ENABLED=0 \ + go build -v -mod=vendor -trimpath \ + -ldflags "-X ${REPO}/misc.Version=$(VERSION) -X ${REPO}/misc.Build=${BUILD}" \ + -o $@ ./cmd/$(notdir $@) + +$(DIRS): + @echo "⇒ Ensure dir: $@" + @mkdir -p $@ + +# Pull go dependencies +dep: + @printf "⇒ Ensure vendor: " + @go mod tidy -v && echo OK || (echo fail && exit 2) + @printf "⇒ Download requirements: " + @go mod download && echo OK || (echo fail && exit 2) + @printf "⇒ Store vendor localy: " + @go mod vendor && echo OK || (echo fail && exit 2) + +# Regenerate proto files: +protoc: + @GOPRIVATE=github.com/nspcc-dev go mod tidy -v + @GOPRIVATE=github.com/nspcc-dev go mod vendor + # Install specific version for gogo-proto + @go list -f '{{.Path}}/...@{{.Version}}' -m github.com/gogo/protobuf | xargs go get -v + # Install specific version for protobuf lib + @go list -f '{{.Path}}/...@{{.Version}}' -m github.com/golang/protobuf | xargs go get -v + # Protoc generate + @for f in `find . -type f -name '*.proto' -not -path './vendor/*'`; do \ + echo "⇒ Processing $$f "; \ + protoc \ + --proto_path=.:./vendor:./vendor/github.com/nspcc-dev/neofs-api-go:/usr/local/include \ + --gofast_out=plugins=grpc,paths=source_relative:. $$f; \ + done + +# Build NeoFS Sorage Node docker image +image-storage: + @echo "⇒ Build NeoFS Sorage Node docker image " + @docker build \ + --build-arg REPO=$(REPO) \ + --build-arg VERSION=$(VERSION) \ + -f Dockerfile \ + -t $(HUB_IMAGE)-storage:$(HUB_TAG) . + +# Build all Docker images +images: image-storage + +# Reformat code +fmt: + @[ ! -z `which goimports` ] || (echo "Install goimports" && exit 2) + @for f in `find . -type f -name '*.go' -not -path './vendor/*' -not -name '*.pb.go' -prune`; do \ + echo "⇒ Processing $$f"; \ + goimports -w $$f; \ + done + +# Print version +version: + @echo $(VERSION) + +# Show this help prompt +help: + @echo ' Usage:' + @echo '' + @echo ' make ' + @echo '' + @echo ' Targets:' + @echo '' + @awk '/^#/{ comment = substr($$0,3) } comment && /^[a-zA-Z][a-zA-Z0-9_-]+ ?:/{ print " ", $$1, comment }' $(MAKEFILE_LIST) | column -t -s ':' | grep -v 'IGNORE' | sort | uniq + +clean: + rm -rf vendor + rm -rf $(BIN) diff --git a/cmd/neofs-node/defaults.go b/cmd/neofs-node/defaults.go new file mode 100644 index 0000000000..4fe24b50ee --- /dev/null +++ b/cmd/neofs-node/defaults.go @@ -0,0 +1,346 @@ +package main + +import ( + "time" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/modules/morph" + "github.com/spf13/viper" +) + +func setDefaults(v *viper.Viper) { + // Logger section + { + v.SetDefault("logger.level", "debug") + v.SetDefault("logger.format", "console") + v.SetDefault("logger.trace_level", "fatal") + v.SetDefault("logger.no_disclaimer", false) // to disable app_name and app_version + + v.SetDefault("logger.sampling.initial", 1000) // todo: add description + v.SetDefault("logger.sampling.thereafter", 1000) // todo: add description + } + + // Transport section + { + v.SetDefault("transport.attempts_count", 5) + v.SetDefault("transport.attempts_ttl", "30s") + } + + // Peers section + { + v.SetDefault("peers.metrics_timeout", "5s") + v.SetDefault("peers.connections_ttl", "30s") + v.SetDefault("peers.connections_idle", "30s") + v.SetDefault("peers.keep_alive.ttl", "30s") + v.SetDefault("peers.keep_alive.ping", "100ms") + } + + // Muxer session + { + v.SetDefault("muxer.http.read_buffer_size", 0) + v.SetDefault("muxer.http.write_buffer_size", 0) + v.SetDefault("muxer.http.read_timeout", 0) + v.SetDefault("muxer.http.write_timeout", 0) + } + + // Node section + { + v.SetDefault("node.proto", "tcp") // tcp or udp + v.SetDefault("node.address", ":8080") + v.SetDefault("node.shutdown_ttl", "30s") + v.SetDefault("node.private_key", "keys/node_00.key") + + v.SetDefault("node.grpc.logging", true) + v.SetDefault("node.grpc.metrics", true) + v.SetDefault("node.grpc.billing", true) + + // Contains public keys, which can send requests to state.DumpConfig + // for now, in the future, should be replaced with ACL or something else. + v.SetDefault("node.rpc.owners", []string{ + // By default we add user.key + // TODO should be removed before public release: + // or add into default Dockerfile `NEOFS_NODE_RPC_OWNERS_0=` + "031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a", + }) + } + + // Storage section + { + storageTypes := []string{ + core.BlobStore.String(), + core.MetaStore.String(), + core.SpaceMetricsStore.String(), + } + + for i := range storageTypes { + v.SetDefault("storage."+storageTypes[i]+".bucket", "boltdb") + v.SetDefault("storage."+storageTypes[i]+".path", "./temp/storage/"+storageTypes[i]) + v.SetDefault("storage."+storageTypes[i]+".perm", 0777) + // v.SetDefault("storage."+storageTypes[i]+".no_grow_sync", false) + // v.SetDefault("storage."+storageTypes[i]+".lock_timeout", "30s") + } + } + + // Object section + { + v.SetDefault("object.max_processing_size", 100) // size in MB, use 0 to remove restriction + v.SetDefault("object.workers_count", 5) + v.SetDefault("object.assembly", true) + v.SetDefault("object.window_size", 3) + + v.SetDefault("object.transformers.payload_limiter.max_payload_size", 5000) // size in KB + + // algorithm used for salt applying in range hash, for now only xor is available + v.SetDefault("object.salitor", "xor") + + // set true to check container ACL rules + v.SetDefault("object.check_acl", true) + + v.SetDefault("object.dial_timeout", "500ms") + rpcs := []string{"put", "get", "delete", "head", "search", "range", "range_hash"} + for i := range rpcs { + v.SetDefault("object."+rpcs[i]+".timeout", "5s") + v.SetDefault("object."+rpcs[i]+".log_errs", false) + } + } + + // Replication section + { + v.SetDefault("replication.manager.pool_size", 100) + v.SetDefault("replication.manager.pool_expansion_rate", 0.1) + v.SetDefault("replication.manager.read_pool_interval", "500ms") + v.SetDefault("replication.manager.push_task_timeout", "1s") + v.SetDefault("replication.manager.placement_honorer_enabled", true) + v.SetDefault("replication.manager.capacities.replicate", 1) + v.SetDefault("replication.manager.capacities.restore", 1) + v.SetDefault("replication.manager.capacities.garbage", 1) + + v.SetDefault("replication.placement_honorer.chan_capacity", 1) + v.SetDefault("replication.placement_honorer.result_timeout", "1s") + v.SetDefault("replication.placement_honorer.timeouts.put", "5s") + v.SetDefault("replication.placement_honorer.timeouts.get", "5s") + + v.SetDefault("replication.location_detector.chan_capacity", 1) + v.SetDefault("replication.location_detector.result_timeout", "1s") + v.SetDefault("replication.location_detector.timeouts.search", "5s") + + v.SetDefault("replication.storage_validator.chan_capacity", 1) + v.SetDefault("replication.storage_validator.result_timeout", "1s") + v.SetDefault("replication.storage_validator.salt_size", 64) // size in bytes + v.SetDefault("replication.storage_validator.max_payload_range_size", 64) // size in bytes + v.SetDefault("replication.storage_validator.payload_range_count", 3) + v.SetDefault("replication.storage_validator.salitor", "xor") + v.SetDefault("replication.storage_validator.timeouts.get", "5s") + v.SetDefault("replication.storage_validator.timeouts.head", "5s") + v.SetDefault("replication.storage_validator.timeouts.range_hash", "5s") + + v.SetDefault("replication.replicator.chan_capacity", 1) + v.SetDefault("replication.replicator.result_timeout", "1s") + v.SetDefault("replication.replicator.timeouts.put", "5s") + + v.SetDefault("replication.restorer.chan_capacity", 1) + v.SetDefault("replication.restorer.result_timeout", "1s") + v.SetDefault("replication.restorer.timeouts.get", "5s") + v.SetDefault("replication.restorer.timeouts.head", "5s") + } + + // PPROF section + { + v.SetDefault("pprof.enabled", true) + v.SetDefault("pprof.address", ":6060") + v.SetDefault("pprof.shutdown_ttl", "10s") + // v.SetDefault("pprof.read_timeout", "10s") + // v.SetDefault("pprof.read_header_timeout", "10s") + // v.SetDefault("pprof.write_timeout", "10s") + // v.SetDefault("pprof.idle_timeout", "10s") + // v.SetDefault("pprof.max_header_bytes", 1024) + } + + // Metrics section + { + v.SetDefault("metrics.enabled", true) + v.SetDefault("metrics.address", ":8090") + v.SetDefault("metrics.shutdown_ttl", "10s") + // v.SetDefault("metrics.read_header_timeout", "10s") + // v.SetDefault("metrics.write_timeout", "10s") + // v.SetDefault("metrics.idle_timeout", "10s") + // v.SetDefault("metrics.max_header_bytes", 1024) + } + + // Workers section + { + workers := []string{ + "peers", + "boot", + "replicator", + "metrics", + "event_listener", + } + + for i := range workers { + v.SetDefault("workers."+workers[i]+".immediately", true) + v.SetDefault("workers."+workers[i]+".disabled", false) + // v.SetDefault("workers."+workers[i]+".timer", "5s") // run worker every 5sec and reset timer after job + // v.SetDefault("workers."+workers[i]+".ticker", "5s") // run worker every 5sec + } + } + + // Morph section + { + + // Endpoint + v.SetDefault( + morph.EndpointOptPath(), + "http://morph_chain.localtest.nspcc.ru:30333", + ) + + // Dial timeout + v.SetDefault( + morph.DialTimeoutOptPath(), + 5*time.Second, + ) + + v.SetDefault( + morph.MagicNumberOptPath(), + uint32(netmode.PrivNet), + ) + + { // Event listener + // Endpoint + v.SetDefault( + morph.ListenerEndpointOptPath(), + "ws://morph_chain.localtest.nspcc.ru:30333/ws", + ) + + // Dial timeout + v.SetDefault( + morph.ListenerDialTimeoutOptPath(), + 5*time.Second, + ) + } + + { // Common parameters + for _, name := range morph.ContractNames { + // Script hash + v.SetDefault( + morph.ScriptHashOptPath(name), + "c77ecae9773ad0c619ad59f7f2dd6f585ddc2e70", // LE + ) + + // Invocation fee + v.SetDefault( + morph.InvocationFeeOptPath(name), + 0, + ) + } + } + + { // Container + // Set EACL method name + v.SetDefault( + morph.ContainerContractSetEACLOptPath(), + "SetEACL", + ) + + // Get EACL method name + v.SetDefault( + morph.ContainerContractEACLOptPath(), + "EACL", + ) + + // Put method name + v.SetDefault( + morph.ContainerContractPutOptPath(), + "Put", + ) + + // Get method name + v.SetDefault( + morph.ContainerContractGetOptPath(), + "Get", + ) + + // Delete method name + v.SetDefault( + morph.ContainerContractDelOptPath(), + "Delete", + ) + + // List method name + v.SetDefault( + morph.ContainerContractListOptPath(), + "List", + ) + } + + { // Reputation + // Put method name + v.SetDefault( + morph.ReputationContractPutOptPath(), + "Put", + ) + + // List method name + v.SetDefault( + morph.ReputationContractListOptPath(), + "List", + ) + } + + { // Netmap + // AddPeer method name + v.SetDefault( + morph.NetmapContractAddPeerOptPath(), + "AddPeer", + ) + + // New epoch method name + v.SetDefault( + morph.NetmapContractNewEpochOptPath(), + "NewEpoch", + ) + + // Netmap method name + v.SetDefault( + morph.NetmapContractNetmapOptPath(), + "Netmap", + ) + + // Update state method name + v.SetDefault( + morph.NetmapContractUpdateStateOptPath(), + "UpdateState", + ) + + // IR list method name + v.SetDefault( + morph.NetmapContractIRListOptPath(), + "InnerRingList", + ) + + // New epoch event type + v.SetDefault( + morph.ContractEventOptPath( + morph.NetmapContractName, + morph.NewEpochEventType, + ), + "NewEpoch", + ) + } + + { // Balance + // balanceOf method name + v.SetDefault( + morph.BalanceContractBalanceOfOptPath(), + "balanceOf", + ) + + // decimals method name + v.SetDefault( + morph.BalanceContractDecimalsOfOptPath(), + "decimals", + ) + } + } +} diff --git a/cmd/neofs-node/main.go b/cmd/neofs-node/main.go new file mode 100644 index 0000000000..851f800c6b --- /dev/null +++ b/cmd/neofs-node/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "flag" + "os" + "time" + + "github.com/nspcc-dev/neofs-api-go/service" + state2 "github.com/nspcc-dev/neofs-api-go/state" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/lib/fix" + "github.com/nspcc-dev/neofs-node/lib/fix/config" + "github.com/nspcc-dev/neofs-node/lib/fix/web" + "github.com/nspcc-dev/neofs-node/lib/fix/worker" + "github.com/nspcc-dev/neofs-node/lib/muxer" + "github.com/nspcc-dev/neofs-node/misc" + "github.com/nspcc-dev/neofs-node/modules/node" + "github.com/nspcc-dev/neofs-node/services/public/state" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +type params struct { + dig.In + + Debug web.Profiler `optional:"true"` + Metric web.Metrics `optional:"true"` + Worker worker.Workers `optional:"true"` + Muxer muxer.Mux + Logger *zap.Logger +} + +var ( + healthCheck bool + configFile string +) + +func runner(ctx context.Context, p params) error { + // create combined service, that would start/stop all + svc := fix.NewServices(p.Debug, p.Metric, p.Muxer, p.Worker) + + p.Logger.Info("start services") + svc.Start(ctx) + + <-ctx.Done() + + p.Logger.Info("stop services") + svc.Stop() + + return nil +} + +func check(err error) { + if err != nil { + panic(err) + } +} + +// FIXME: this is a copypaste from node settings constructor +func keyFromCfg(v *viper.Viper) (*ecdsa.PrivateKey, error) { + switch key := v.GetString("node.private_key"); key { + case "": + return nil, errors.New("`node.private_key` could not be empty") + case "generated": + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + default: + return crypto.LoadPrivateKey(key) + } +} + +func runHealthCheck() { + if !healthCheck { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + cfg, err := config.NewConfig(config.Params{ + File: configFile, + Prefix: misc.Prefix, + Name: misc.NodeName, + Version: misc.Version, + + AppDefaults: setDefaults, + }) + check(err) + + addr := cfg.GetString("node.address") + + key, err := keyFromCfg(cfg) + if err != nil { + check(err) + } + + con, err := grpc.DialContext(ctx, addr, + // TODO: we must provide grpc.WithInsecure() or set credentials + grpc.WithInsecure()) + check(err) + + req := new(state.HealthRequest) + req.SetTTL(service.NonForwardingTTL) + if err := service.SignRequestData(key, req); err != nil { + check(err) + } + + res, err := state2.NewStatusClient(con). + HealthCheck(ctx, req) + check(errors.Wrapf(err, "address: %q", addr)) + + var exitCode int + + if !res.Healthy { + exitCode = 2 + } + _, _ = os.Stdout.Write([]byte(res.Status + "\n")) + os.Exit(exitCode) +} + +func main() { + flag.BoolVar(&healthCheck, "health", healthCheck, "run health-check") + + // todo: if configFile is empty, we can check './config.yml' manually + flag.StringVar(&configFile, "config", configFile, "use config.yml file") + flag.Parse() + + runHealthCheck() + + fix.New(&fix.Settings{ + File: configFile, + Name: misc.NodeName, + Prefix: misc.Prefix, + Runner: runner, + Build: misc.Build, + Version: misc.Version, + + AppDefaults: setDefaults, + }, node.Module).RunAndCatch() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..be64ab5edd --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module github.com/nspcc-dev/neofs-node + +go 1.14 + +require ( + bou.ke/monkey v1.0.2 + github.com/cenk/backoff v2.2.1+incompatible // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/fasthttp/router v1.0.2 + github.com/gogo/protobuf v1.3.1 + github.com/golang/protobuf v1.4.2 + github.com/google/uuid v1.1.1 + github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/mr-tron/base58 v1.1.3 + github.com/multiformats/go-multiaddr v0.2.0 + github.com/multiformats/go-multiaddr-net v0.1.2 // v0.1.1 => v0.1.2 + github.com/multiformats/go-multihash v0.0.13 + github.com/nspcc-dev/hrw v1.0.9 + github.com/nspcc-dev/neo-go v0.90.0-pre.0.20200708064050-cf1e5243b90b + github.com/nspcc-dev/neofs-api-go v1.2.0 + github.com/nspcc-dev/neofs-crypto v0.3.0 + github.com/nspcc-dev/netmap v1.7.0 + github.com/panjf2000/ants/v2 v2.3.0 + github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea // indirect + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.6.0 + github.com/rubyist/circuitbreaker v2.2.1+incompatible + github.com/soheilhy/cmux v0.1.4 + github.com/spaolacci/murmur3 v1.1.0 + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.7.0 + github.com/stretchr/testify v1.5.1 + github.com/valyala/fasthttp v1.9.0 + go.etcd.io/bbolt v1.3.4 + go.uber.org/atomic v1.5.1 + go.uber.org/dig v1.8.0 + go.uber.org/multierr v1.4.0 // indirect + go.uber.org/zap v1.13.0 + golang.org/x/crypto v0.0.0-20200117160349-530e935923ad // indirect + golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect + golang.org/x/tools v0.0.0-20200123022218-593de606220b // indirect + google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a + google.golang.org/grpc v1.29.1 +) + +// Used for debug reasons +// replace github.com/nspcc-dev/neofs-api-go => ../neofs-api-go diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..a397a2427848d7243d89b49144dee2b296f7e784 GIT binary patch literal 66535 zcmdRXS+AqWv*-JKiu;aEvu`8K1Ku&%#(=^2<%)gZcRu~3eX6U$b*j4m-F-(Im1>tF zB7@AxSbh;{JokV8m!0IT^P8XNe&YUS=P97`UmXyN`VENk8QrAb+WV5J7_R%pu8>Xs*xnv0tnPUApT9s<+j z_==D@ANi6bY=wT=y4AN$zR)Q=SR!^yWK!;Q?goT69%_@q_B~n$CYxBERSZOt-=X|# z(vI1fb(2@_=K-2;RPk_xIAg&Dx~4td#ty!QiVdv_qW>t1gR%XZwij0J zciP~1&Q#JS+&bSM)S@fNim8VEk#L;jWA!M__;xMVi6E<_^5|D-{59>Owd&UT6nd@@ z8QB+ghu0(_Z;$Jd@PD>E7M?$aC;Eu1#V(+GBnNNE<4lIff`_#_8)5t!!D=wusT$yfi_8=J`@lX)Y@QO ziIs7o18{2munAi0#cb79S);Ud5W-EKzL?^_p--Ep-($l_D5Jv5m4^y6yIo}>k(x1b z3DcFgLhd(@)e?-kT>k>anW)1-U!V8Uip=%3HUfDiV`S6Vg&Nx(gfmJ zE{!lA^ST!d+7~$*4rot}Le5Y7;*lSmjM6)Aq=?9E)0L`?&D{W2;@@FAQ}pS$j`iCr z2-zZo*OhTWued}p^oR+r8B`ZZVOJYg-0Ur2Q2vZNjVu?9AuNg^2uglDMd7nc6Z*WI3l*Un%{=hNQMo<(- z9KwF8oJ`CrlEkYvO47DmYsmiM5y51w4RiHS`ul+}6E95KV+qXBf4t}~+0GVyY?6cL zmg3ky#JU%FVV7|QG*0eh5Su7u%f!11E9|m?fnE7i?&wVAPhDdyN}~iue|R*FyEeso z7DGH2?c3@-X3YyE1&7u-o~7N9JrHs#M9wr3h}w17d5f-k`SDl2^j~5-Tl7xkNsQtv zSwEz9eSy+Ly|yBXD2|VyzwT7=_-G-HkoFhg{md4<0hh`q_u;8-oTrHfe!z1vcs>Yc zytfXaRSpbGw~e&MMD=`FWu=5ys{Lc1 zp_Y5uN#5QRyQLofl=y4`4`_$UwuzMMS~E5ZGtm~ePZ#4bQB;AzzB{Ygg zv{a`;W!r5ZzOvg~=^IM&>Q=t(tJ_5$n8m*QJN#zKtX9D3tAsFQGz=Y`wZ%^dGI)gR z>z4M&Evrm73w}HX5?`KeGjZ)E$Wyb18#gTyvzafH0*fRO8hyGHCW6wW>*AJe+cU=E zr5)SUj;7t+x9##`c@8W_qs89Z-H}pNNw)gz5rQ4v_vv;qrgZe1NJ>iG> zmcr8G)Jyz|fmGKeN&h$H4Lt)6f`^dS_MdH>*p}Qi|14YZW z#g*&RXrg#KPG$eo5Btvbsa&vqF$^O=Y}|1P61OSbBmNbxq3v3^1xOWvTYW@zp3M~e z{G=psR7sNhAVDO9TPBUVF7;F}L0W@HbB|N6+hr_H8}8cr+O~I{u&&5VKB5&wi$>NV zOM7M}e~aa8vC2N%X;h%q#um8DYhnAKn$1~_OyzFeV(MDvyg*46*Vyk{_#5Xm%s_Mk z2q!I;!L`C*M&#kbuA{lam~P7p|NGj0$ziq> z=J1HN5i#Bnrw@!nJVKkn7$9nOisF^!l9%(2-=U|w=YddYzDFyN`GD=PvaQyeeK6o5 zV>E&gEJ1x(mSs8!OJgW8H?a5(1}Fc52W#u_U{nFbjvLI`sB14^PwZ$yJbNsx6@6ir zSB3e>GSBKZ`7n!c#ua$w&x449llb9Um`z!v<)RifTc}@g=U_)V`KZ_UjjJnMAb6^5 zxHP%ikBwXJ&fA0Z;HK!!%)YpSyDxP?Ol8kcKLdPKVCpa&$3Hv-M-T_43D$Yul)qt8-6k{^#)QS$;>`s7&b2a~a9FIAK^VZ5!q5-e za~#jc85~C!(XDrDAJBA&h=EWuQ6rA}wivNs(3PT=#~y@mK7U~C_2F%$y_qy_Rog+E zOwSMIS7s4(;WTHDA}9`)jNr_Ng)t22c;He)UoJocm4jT#O@9t=1_#{(EK=@4zQ_D3 zJFQQ`o!I$QGO%~_?L7VizB5(tw;I`}K4FyYxu-icj>r7Yx(y#Qqn6$yh>JIew%w|S zda;u&H?J}pF5D_!eq0T7ySjfnUH(zQEB=O(=AtJ*%E5|DmTEUU3lDGibjhPj)d^c;V5v~i@j9s$^IVuoOE^~DV+DCZ0cL7V~E9yY&VNx z%e3R%17;P31izsRNA-C*IFYkcX^-%CSkttVEJ)VrqWcds^4*$m_F}&$cj{G0(`AJ4 zXS8P@gx)o|Ybx0xe`M(c1bei7!byY86rFC)qZvA_pB+?lxA}oZ-PRvrRyS3ff{z2- zYj98Gg_{BH&;Kf_y!lK_FA|>~B}i#l<YAnT#fnA|+Se3fPhrB}_GL8*ZFAjF_ebvI1r#rDnJ)>> zG;^<%Qbx71)V+Nb6kJ0xW-I52t_lj8$+D08fyb4So-ObGC0)3~o?rKytfPACbXp65 z(;8++hzYCuR)%^MFriA-SLqR6C!UhseY^~8enxX9uH*B!)eSE;bq@QI|3LHl@<75X z2=#MG#uzmk64K#MqqaP+|L;k^%nNnOASV2*w%h|r>mY&)-vOftH zaTEuk4EX_W9L#bt!XcQs3zaGDo^(kmS+^{s7);%54#%N=)b_@2?g}NSDkv?HT<|P1 z&FtkjrVU7p&o6ZZ1Bou21b)SbiMl31lMY}zSKbv@cs3_e_;JcsXO|xv(%toaPM4wR zO}j@Lozo$T!)x+(Jd+R@0Txcf8rbM(?2Y_}|27XC%7}{i^%}p@fJ~?9IW36Xw8qDQ z+|%*cDVx}4k)n{srRldb+k;!DczRp2wLG(jAg_%vAZ?0KtOGPhF&^d3j>L&MQBVVN z{rmgl7=VDf_2PO!J&N9FsE=% zmyBNJC~~>3b`yWRuyMR4NmjSh3@YPo7z#^A+Ww^KHa19B;eaT*%uGCOS+vVs*@*Z~ zJc+?ZTu-gq-auXKc#GsBv5!|8)I&~Lutwf4lRu$7Q%bv|+t3k1ZiA~J-=^c}W9Ps} zJ-2a8ih3uIn&8d?Z&!XZ_&>0~7x&e_#{#(EpGx{=_U7nLlyJRaE4bEc%Q0OK-)@-m zbc&Nn+7k{b@RxueaYX&{GXN&Fn|5yX+){wiQ~UTa#TzlLea^6>HALJBIyhFll~h3) zU2&!1Dm|<`%&C2|NuR2DCpptdmbr;B(egTgNHaJ#k~BP$8-#)#DG?i@!ndxITsD|| z{&QjRh1c^hP#$ZL-vN8_d$NN?o*pp*5?Ry-OVTvsc$P&kLc1Lqn0LC1G;NQ>Hi~<^ z?v^(#JWJ#DICStO?Xp#l7^(Z^MlE80#t(#77z1K|6d^uvmUT_m#axn{ALT8+$8$7b zc#w0k?;SuO7iZD>?Z`Zx`H$4WQWNAHo__ekvMcF^ z%5*AIM-Lb&Zly-I5ZCadcFK}t+S^B-H0{#hDQ9^k9J6z6KX}pxHn0D@Gv)!>kv15^ z*0FOK)PAbvaN6Ms*C%itU(Q4*9wMdYA)7qlOPBmh!mn$yi`WQmQLD_l_0~IH`diQ> z7fvo~9IE1jVW4I)~T2gAvy3@Md_x*wO1m>M8U|8YjYXvf)$Q+90E!o9!TKKj`#DH#xaBhKR-b28DSaB zwO#iGqG_Duu`V94#BKt6TB}Mp_`HzzQb$3(L~iy`DH=Vg+`{F#{?ED1mZ+3BP`tcl z^)B+#yLIUk#m0s3aaoaf2+ws$wv1Dpwr;}gsn?(d z_A8uz2Ms(An5JZEZLG6ocQ8*F9QqeDpzipAqd~nYx32BrrhZ%!lkpdc&eU{hs>a&c z50t%;`!#*sl&O7FRt(LT^W;UcxQ6yh#3($z1hRRkWP5euy|r z*LY#%hAsLn0Ldisjp0&q*B&K=wAUwgX@_S_UeT=m0n!j39mu_c`q--)vP?mQN;=qM zRI2r{;4zsvo0~HY#b-?0-ZB9$>{XJwHCOH4tn<^|d702ZJkFQ3@s(pbR5ebEzFNv~ zJ|0(k9#b);bocGet#+!FG<``|g7|i2`>%~(g z2$A@0cZlL>T`l*&XFfCddF} zJC;5;4_w%#RE8{plJ%Y%3F{m{d5;zwyJUr$L7-hB9?l)^KHWP01H+z z(zYiWgrg{dnW(2RhI6m#8!r@io4@0l>h%|W6}OER!x(}Kh&DfE*y+TAHy9#bzFv+TpH>>%u( zaqUMPB+uyjyRS(iB=|C*&IIZ{xW_scOM4*S_Cmj`PItqQbyd6^g(QHI%c`NW+tt5w z-ippO{@RfCxpGV8E%^Q0OlJ$O9-hDB?mGpZ;AHDi;xYBugZ4nh*f+w%Ko*BUBR4Wi zEsvg8nOSU^8ChBr3B3{i{SVLGN&H4XPZ@u&p{tl?cO6^X_tt=z)6~J9zOALbK9sH}HEW4n zXBzvqJAXoRri-W#>WQ#eyoV)8uHf>F8e{6UOvBJ(wp*X_YuDsa5$%?jj34AqUBjdK z#DzV*?zwMFIIB&or+yh>2Q3t+cxQHIxMm;n#jUB+>3~(T;hwg;w@?@CAwPlr!~_2J z_g(Y^xUrxZh2ZRmOF1TD+77i$Y;-amOxJshO~;YtG|qRAWmozK_DZffC=}KY%pbQ} zF+7o`v(pRs0V4r}pHF^E^y;u0K$OW+&i$n8BXY`gtx5ZsV__6G%R$PzD%u$*4l|cW zZAo)^A^roJAamuV%`<@oetSy03>B{pD&-fF$;qZyI6LmqL$bSz0Bv)*lQ04>3G9BA zZOBAlI*>rxA1E(@!MJ?tm6G1I?jR9@nZ#~~yODIPA8K*GuPPSOh%#-QXub3l0OeP4 z6#iBsrJe@`Tcj19TU!yWvbw;0jAp@|BVpUx)~H0DZXo?~Er~El&fY%D!a`bJAb)}U zY?q%oe%qcd57-+#5RX*C=DL}f-UNW!l%u7*<08|xo_~kXd>c+2h>Wpl~lm~eKxTd5{WgA#Va?`6( zGamE~MFvK(u8BaKjnnkYFyjZZKrzof0@vguoF}A}cIyWjIF2tlIi?Djs#Ul1DMK!o zI|G}Klls27-^2v;W?}{f6m2o2sdaT{6k;!Q_asozAj%+LJ@=B2@f1J`6^g&S_kIu) z9L1-Yd*(u`ZgqDnh1gN`z+N;O&SdZCWOx7YXki7WcbIa9Zhs=?@@OlG;p^>{RHF4Z zq;!y2IIK-{>{i|_JJ_mKRgdN_2`0wA?}NA1f5Yx>%FWi+NYgdd)Y+|E>0B6ff$tJq zJHQyPXxfRrdW@tb?}qJ1dNXbQeKA4s-~*$d&(lWK69t6f0Jp?^U;wP*5iVt;#0zVs z3C&`r zT&hRkNTIui4rt3A3R~P|+mYLA8xf8P`si6HvUHu_h=L@;OhNw=QLsU0MC;WwZbUKa z@f&j#Rrk>;9TOc_P;(lh^sM{YVjI zpL?K!QEIDOQ%U4_Jf-~l4dU=k1#`mRj+C)uATL?n71F&h7^M=~R7%z01y?l)?g?T{ zUVD>TLRl@*hF?>h>EK@#^%dw)SuGh;P?E9i2Go^Lc|0adeYz0R*6J)?$LwD5WSAP> zPyY{EU6xM%RZ%}9m?%CLIp?hAVrU3e~V#hniyQ6PXxo`R;T4pabA5eN^^h-`cJ@+&uT- ztO=yo>)wGSir@PuQ^1Wv2+4@S^}@xshyF|RG~ZsoQwQ$#3$Oq>88DonFye!v9RdY8 zCmTbU*?s|acm3cZJJQWu{SE@bZF|=MC@T%1W4jUzBnc7oG~PM&%hUY@?|F4hOtupU z0=a;rt2MWG3E{NbbVnZIdXQ_q720{)atJOJ)t~O2Zz}`8eXi_TD#cNjAU`aI0woIE zLs(~SV)PqsGlCsoS#IY-#raQ zj#tht+ml!GbXWNCh?Y3h>q;IOYYU%9?MtivrmNXUh;rxF3B7I^=C(GSO-$~3m$YeG z49|jgVfjs0?lGa=+?OH$7yFj@M3X}gQrNG<$sa)v&ptqfN68_cEsAk()e%1_gsEX2 z-5>uF#hIx223$=i?!sKU^-%|LLyqkCd)}8RnaXY6uxh2Jak7&8WnlSD)H*YZ;sw_M z(b}^ai3UaJCzK9kgg;0UH!0Bk0x+lZW+eDNSfCToXOhA7gcHr**SNPjCom15tu~FB zYkAu0Rr-_5bk6eS9A5p3tpuHJH@CR5l=SL`n|g0DO0*x>mwnTk^-1r$0+ZgoxYuD` z(3@%I?~4iAK3jlAe}+BJqBgjD-V*Z9%_qPz#56X^aAqmSU^`qA{2D_W&}f>R+n@~y zk}g&|scGK#$e)s*En-Ei84{H4db6pIIe$Cz-de5;y1XDEmlpfog8&dlU9r~hx_S1W z!WJl?%zc(8Xc6EBL2C{60m5;_vQQ88kU3&1Idc%e2A)^=!(%jBa7l(rc79Z0&g0y6 zIi5&IW&a?L()*#Bb9~Py{6nfB+OpH)88j~)f&4ysr@RoDBNrtGJM4(=plBTEF&)M6 zPAgd>UP!Z)1!S2A`fSeydj6g4w*bJdGEgbA@^xpC-rc++{1H|5Y7N2^9bk9X`kL83 z(?MbM?y)qel?g!e?=k7B>S62d^Qo0lh9P{%IgNcxh2nd5;|H>H z|LR{O3yk=4OAj@tX(X-7!9JqmEgv$ndz`Mx;ZWKuW4%qdF3vk2U%_8)wm()6=*?E& zF^=kp)@hH$ybX89E`dCX)G*L^5#O!6-%^pU!v1+Ryhl~vR`0bF0KdL(DQA@+t3szb z4Te%fx)D;27wIh;VrqAl5~pc-CxK5CxqWxpUSw0VYQks1`N!COCOgX$b)F7}YVn7W zJRgOFoZ$UEqD((}VNM0jypx;q7PJOf%jg-NoaCd#|VBG)m zY|g07w5{@^^l*ujUv{2rY*sthIpzC(vF%~8ytw8?g|c9{LF3^)G=GW+(C;|h&>fwTPwaof*tUR-$Pw-(1z`CTeI$0Tl6dHeX=^h*{gaBBc8r@getU=1rjeKJL*uGUVUljiWkJ)l|;kZXJdcEIx zc$26>>W}M$X6^uKbuG7-<7&MzInUK6>CFKCmF0lT{J9*27ZMS4V;pKW+nAw1+R6cR z6x!D`GpV4i+cAx4R-JoTv8ezBG_SiY!Ch|d34iTYX6PZ z^RGTAn)l{wyu`BR(Vg8^8&<$h^)-?Ew+_?`69xxu-iF5NdsPuNXBAwoySEhiv!ONw zxXJ<)L7&Y+@T*zqY0u~0ftI#B48kBZTO;JwxawyRcv}wEXxAS`01*XA?L`Xy59)4J z^`HUgNB_e(K{cSq;ZC?gf-UVWl`4zxS_o>74x9Y(x1B?azFiqRW1f8`X z-#VDyy(na4J2SiS%=u33Y9qhklY5E+4PKKW>l0Ue)7#4VK}gguxByUQ@v5#o*D%l1 zn1Kj3q$?HWHjO+z6~eNU;tpV$S>a1;2>Tl(jVHKc9Qgy1uL*3m&MHv7KL zp}8i-zHTA=aXxc#ygtJc9^|*pAy^yNyN2IR2vzU*)$#x=7*u)Q*lzV~DScOuKuxMw z7ILz_@lo8@XW{_`q2f)7dNnC`#RKO_@cMIhSbFXY`p@gfbewN!g}M0jJ+vxzq&oYGHK>6f5-NxPgiu_o=Qt$7wmy%ajprW-8gLGY5#}k%5{P?l& zggxr)oURs&#J8I~nSQZBe|MXfYNwRdqq_vK!f!x@1j_+7Hz5@xhShFg_PZl&o#}zF z#!Iy{aSOHlL3(~-{|(Z=WzX9lh`g``K_tXm-Co~hAA(#9$D`|w&daOKv-iz9yG;iU z)VXXVW`DlbpBaabq-sBp>-T|99bLxTpdzt@r<#JgvMMX$-TeDW(^De_u@#w2c-yjfGdv&PpnbOvQ z%l8wz_y@h&aom!lC&mz6_hDLCmeSCfPMz!vx{V06X9%JX+aslb zkzg2HKoeFRFhaiydbE<_Es~zqiRpeObLys+TO|Ol`rHtL2onf%0Y z5xK`vDF{yM$5F!yX_fOya_iS;dKm-#aZ+lxdWDe8qR)W}E+SW$(Fkt-FZ;1a?ioX-p#7F(o#^o<{iO}8P4}Z=B+QCV0ZvNBQZf7R>3j1?J%0XRU#xbxV6S~Gevs|&H=HYtr~ z%VQVi8EDN(0Pz=Ct_Q<4K0n^eh0_ELdf2T*CHSbbPVSZyHH8n*m^5Z9?RtT7>xMu` zU`tE;_H(K;t$@7BM!@%|Y>K8nL~K))2n}JBj-ZW|W0>y@xc8NgwA6)9qT~nDr3f{Hb$?J0Jj(CaJRU8dHBAjJkKWoeafIOA_b6k z;6+8xNeKX~QsmV)yZ_2%KYJD^lpq*@J2E{4v{V7~8iLIubgIKF5)F9RI~@BHhUF9I z3nHNj8~11*g5xe|R!)!tyYr?oaBVsuf_@q*5L1Gy<_Gl0;DjNM`4;aVU|&4Y+-7v@ zBie2*+ev1|PNUbzsnuv&fBVe8COz9o*W-iU!6q01Ry|a?F^_28!-?qaJk40iz6h}f z0G1yv{qars_Sp^w8q7g(*}zZl^A9xTGF(ysl8m<_l)5&lc{prn(`cfoKJIBvT@UT; z78a*ch?$yL)@H+_6TH}V5_G)!3A%Xsvs zL}voaAe?Ph2TBAuCEeUaOm^#I>lPdt+6eJ>kNE9*lLy`wU6vAOw2N;kxNk6??ifNB zI6FDJ2I3&tajqi zm7oLo0i`e&7mc^7C-HQP*Th4Bx4r_~ZyWv9>-r5U2<*$VI0w4hK_3H-fPp%o%1XHS z6G;j3yEAqWk{6PRT_2BTzgz7dNfg3ztl2!ak29`@H}{*a7b83xZ=foC0*6bI_4^W#N6FOA}YN6$Llxq`gV5aOI_NIuqa^@%V`M?wtAjR2 z9lxot7=k;(HLfn|dS!DP;7|+96ec>);i~g;sj2CMq)ujrPc4Fh0-%cWdBB#qN~tKk zyVu^`(6V952&3rCOuUsx4P#G|)o<{7=~Vrc4$GDQg8b|JKGv9K)9_;#@6%davqE=? z+H?c$$>cs_a-#|(n8a|wR@bE?@+-PCt^9qlo91PNUU>hf)A|{I&OIHrYY#4SugUVB z0elKy-CIxDPl^7U z0*5uw^*1=gJc>I(iF{XA<=)+&9WhG4puu$>UXm3#IG4DTkIs2G=_>sQ-*?@&Pd6t# z_dowt!gCxEUlODZ-#G&B0*VFW6~eM~p6% z^NqAo^{U&mWR>!E>-r!8c!!*^5hg>pY(wVuMX8k#-w%>tA;EXrT{Fz1iZ(u!}9H6iN8yI@vjf43tb3cU$3O0%lLvT98WelbS@&gjNzCzXm zxX(*YPHO!@6?`B5Xe|CJpV>FqFb9_%uV;TUR$GnDVvSv!ljnk=4$gi80|)62KUZ%X225oG z4a*4_C4^ZYb-3t$2Ll{297J_sGB4;zB0U--Y%pc)bvGnN5m(m(X+|D$xnJy?wU4^T zFoaRN0+uFCltB#J+%utnf#FPf#fFhPeoVlaQARxzU~rNN+NrCv*#Qv6IiIwQ-86e1 zcf7X^{90%om>6iU`hA_ifp|4KgN}R9t_N!O03Y~4fnk4O90>6I=+{cYrV!9pZg!Sd zcAVeogXuk7l7Pr|HHb;OACpZe_)gF--Kgg{x*YybInI{4B3(3xP+0KKsa`2p0bva4 zfoc|6^nBd3PJ$dsCL9rAna~H5ut6Z0B%mQK0TbiiQ`NzlPa=?k;~C21?c!oNw2AEO zYEX5zN&4evZ`-b#$X&VgTK^ym{C_YF6Ck#}$g+bA(OlmO(1?p_iAV4^Biyi_bSF5n zKUQF5AwQvKv_y*PeRKaV^pg5AlKoexKU>j2EDR<#f#~Jg$@!y>0Ngiyg`*ia}_G@b$+wStaz zWH5<{w~rHkloSG6?a%Lr=6B_UxgGpIxnZ6WKlVnNpjo@3hs#tKB<4uPVdr_>WxZ*O zdXGHO5XxygD$BzCA0IU}3jj;~9CQ2WPrbq*F!KxG*Z`IWfD~*nZNi~#gu>is2jlMp z|8|;oz&*cjr}cqUnA>q5f?hi;KtSt%zu~ct0Kcm%rtuXG81DOz~E1 z`nXXS1-nrD`hJ3XyN(~5R=7^x8QpH&H=pSHS6D9UMfl$kJa^nGdje5_b8fd4K;_I+ zVIb{=f$6uPl@qk!Atug(fpHdr&0`hHIj)hElfWYi25)tvO?$v=*N={K9mR~{ef+p0 z*JZwXE5rggxS!sdvD+a9sc!0_PmrP_iiam}5N%{|>ee288q?9;CZ&IDbB*~y0MYt|(E z7WJwtNLfg?T;kA2TrryLubfEdvGm-(T_Io4eKjw1P#u%>y*1au{$A*fwz@OMaRP5F zoIq8KGoye|ua>jjIe&rRzai+e4DRI>@FYRv84S#M{{6G8{DQ?keRS{y2v+ZUTV1zT zi?UQNyYHw8X@EcvFpueb=IAU9Lfo*XGPO zkDI8Ub^i@B_tPcJ=+BmMt@Q@K3ost^pkDSk7AzK?uEJ@HmItpe(vgpLTCH3R>g|H~ zY8lUB-KQOxhhT;xwvGZxA#*-#>6~gTe8Y5AA%H8(Ew-C<&t$68f&tOXZ0VOP`GWA`f-AF7(>Z*3fGEo|M6^PrBp{LME-gf7&ht>HOIgPFL!^PTOSB>no~oP28iu047sM~HQeM{uMdEw_~l6+ar(W;)rLDCYV_$=1Uq zdb_aRv_99!JbU}M1@A#(78HMgSpe5R80qp2jq@#UOaZ(<2q#D|7X8DvafEdV6T`6i zw=QKpgvhMiu5|Do(V;78I#rH(wHEq-v4+4`X$N+B8ob-IP+3+Nw*BF_9qd@%+~aso z00dL02?}Mvdj+aj!JS zI^BMG?tjMfIfV9!m<3M?P#*-%fHon|0#m48vsKKf8~m-ymIl#vxsq1grgSy~9msC( z2x_Kc{2f3K%Ex|r*Zs%LW_uqtb^>(241XB#{yox` z1&&|v#GbF<5GbgkqAcrR4COkYmG^lb#w~zeOql{z(H4mk#1Y#F)CC_3n6z?JqaB1G zq@A7>A2XXR@Pd?0Mqx#DRmyOha??MgZ7bvxb3Mg3T$xt6;B31Te;a}I3!b1) z8O+CF!7OjmL>UaUvZks zBug9LAAz`27=x)M@@FUW`?BX*_kS+tA5TNkpj#MBPX2rv3Y>f}tra}j0~+Q&Jg{*r zFelLl%@s2pfuc_@J+QcjS}?6p7H{MIQi!2zZm!A}j9@yS@adpga+Ll|^?xvDpT>N~ z96UpXPz12CAGU|O;DIXKVv$#S{fXb6O3Mh~h$m!Y@bW+3p3*z54mG zz-t^Ha0@?S)l8Rn4ij`)t}8?tD<)M1<7p@Ex=3u!f?1KBA?J<q1u6W4N^Q58>=L?^(HV*%h2qd{%cP8s~cunTLdU@%DYX{ z3GZd-wy&iS_e|{!<_K}Dz-_tPRi>TO@JP?ga{RC8zg$7nHt{f`Hv6O z!GVtK_DXD9>VDF4UR8LZS-NSzBs^0L)1%aWd$bM|&7yImm4`EH$mauIv#ae^Fou1Q z?6Lx}H$S_kKNbInV!n0TO-Y3pASu;ORk~jfHmNhtUh_Csu5!i+cb%54d_*L|H+1mN z1z*nH_%{RxxR)39``h>~Gp2|E6#68`H^5l2x>S@M9dmoo4Yn=Z_PE{Q_kMLJME2)L zYesUW##53+qPb2puZIN+X=^AR6h^ZnL=RdjBIoo0-4%c zbRS1B`t_lnFOjRkO>T6TpP7!CxXZ)$&&6$~&k*c6((9#MS>t6W9WJW_==WJ~Sr*#d zn=Oul)H^9{5vxx&!1}ha0@s+SO{qA0rF9BjxsP?fmDXOlY7>FuE>GfJ<3~OP`{C zCrW=N@B}2XWq`H0o`7=(rUMkkso4ZD1fCW;R#D<4cgEw8XrqeD0@9DvW+L7l;o+~> zJMosEV0yhfYLdNY*646Wxg*jQoI4$>6@4_uHxa=j5zhb;lp|gbMtuA8x6620`?$qt z?&{!s-sblnqPQUK2)b5wN|T(yc%Q^XIZrVL>6TziD^QqO)gD+CHPuSgYY6~=7?bF*Ee(z^AXDSiFqd4a3PF>s`S<0Yu z#B}$WVCtq7VH>D962V^{B_qJbQv80f4HyryrLQeM{9m2lczp&KJP)kuw&I?n;5?kh&Fg?<>(1IQkN)6%z0*X&rb+r#x(iAceC$|HyG$;K!BWxQH|cVYfR zcC$sIntj+hc$E{^Q-1J5Fufw8k8zT*kZ^55&l(uB+8jtD{$@qY*cOk@YwB7=tRHg)L{X5SN~s z`?ckR7opFZLDcfbc)ZPI0DxmKL8y9Rj)4_<)svUlL^nk;@P@E&vmid~!Ru)0_9Vj( zk1h^!Z}UZ!6py+X4d|M^i1)vddASE5kRe^q*|p9Rt`xV%wWCo8l-8=Q)h+tA(6n7ri&Z? z=Nz8(x_NyZOw%1G)NN!HYxdwd7a7cnzepmv0~2k)=qX{uCFDr0JwE;G9KHdykJ+iX z+Eh{0XCHJ0N!)%%ZE}a8*{}h_6S|~6P1D&N#mis)w$GdAWj6kYNY6BXKi#MYc8W|{ z&|278qh(thc~%L}@y3`Yd~ofPujs&mU8bUcrFkIy|Mwh#@BIww3{A?N>%|sOQo4Y| zJSX!Vu>D#g+O5E(ev+Lk-2;;$s5d9(r|0(Ft$&D@J!{AK5u|HYbQzz#8Qvc_Jh1_wmEyZ204n6*jcqWJ}3&Vc9qzyq-hfZfSb_N@xW zSvuAo%wKcvlq9SAD1f;ZmAY5AY+VAF8g^vfx}$#l`tQtN0~>)40>&A9cn(f^Aa#d; z7Z09FRX}HcuLEcu$5blQ2s^pm-yT8BvB+iQSB12`$F|kqn74=4{yx{)b{tMTN^;0e zsyFpks845h1P=r7Aa_QoDYi!Ujj?yUebbbG_C4;exQWsf~PSp1a&@N z6~9hIy0-4%N$<>NGc;Ha2Yb)qj{)E2(CzTZ`E0kpfeOXjt@Wp0nFZ*auekotF8Ny$ zp)JV(aiD!Wgm>{Mo&m-h3>qwN`bG-H&Rq$g8#cho1briIn9~4 z>e-vb4RSft)nPgv-D4cOw~b_^yEPcgHNYc7$yYZ)t@!6QWJY>!z5i9x-|~7QgeK#j zV2nM)vH*bA*7`}`9KSXn;-Ax3>Ag!!|JR9e5 z3sR>dtYyGnf@fa}!NHpbqt<>aiuErn#$4S04@rLu5`@dCIb#g?IA)5&^h4Mh!XXgy z+N_WxG*UCWAFUAGTJZKE762?9hGNTfUE04NJ2J^dM?w{1c1-S+qW^ncDN zcR5tK)?7=hFvFN*m>gB|2mX6!;jb_8?7mi>wiJBvUh9V~eJ580fAq?U)GvpfJU1Fg{n9yuz=u!{*9!Yz~WEnqDl914ss%TBFzR% z%N6dE9qHs-&e$A{OW%`&Ko$y=j>j(n@!wnVC$nGy!^?|m5rpr-=f@i&G_z2gZ=Sv^ za_;g_tglR7r*3t21;TSiu}5ChO0(yiOmm{h-0iNLZ@2!xw%1yw8c0{ZhA1w(Fzl7u z)x41^FOD;>n6H^TI-=CmU`WftS^DuZi<0r|T$)?igt~7I z)a?1H`j3sZ7I*=*gKZ_{Aet;e6_DS@OZBk7j)+18lO9KpjvH^sxx~PFe)BT0b$xU* z+inYT@>m>=4~j4PVI#VwZOWuss*M8~mZ@B;ol=(|L%^{C8inevZjY?;x+K=3a`2?u zqKI2Ie9oz7eu_>Bw6~+Mhb_|_sn0McC{(vVgos;rby**j=G_Y*b0om@v@J0Lcu{OhwH^}jV@Yw`JfQCwsz z&oyyjF10L`^2%&^O+J|>F{nVmfH)GK?0{X-7Ho2l?IB5&dKwo!V&m#nbp^AkGfXF1vRS+adKC*m#_1)*`}@P zk=Wx+(iNC3(}y&gGiMK}6?4vN3L5526DzbZ4~G*ystDsnr7rts%(qO}0n8rt&-9bR z>CqQ+6}mN<^Ihj2R9T=P)>{*sVaL$qQCG;P_q8RRG8rk@!Lys1#jm5arWi1BmHW$G zWK$7mpm}>1&wGRAdf1mEsdx6hioZltvQG?N-qy#UCVa?GFRs~ak!$B&ARbmyG_voB zQr=TS3fL;B+%OrRZ?3zQx^AClmFE!X#|&G8wI-;PtXX=*4RYT5qu=+h`)$Xr_H`m9 z(OH%T1#9Q7H$Q9BmkamD1S2B_IHPeho>25N`aU~V*9E`3O%}tg*a0!5$ILiF1IH6* zUl-E$F68sU3u)tzi3WF2GT7c?(6plDq6JCb?3Bq#m4T`;HBVtg@0 zI_G+3)|U!#Y?8N-ubzF&Uyt1S9;Pw_8erPXm4kEKA38(pMqg*c+T9tPu|AtcFg1myN0qEz_f&2$uDJUFsP<*9hPsOO(%CB8aXW4#06Kjb`FcG( zKl_hArrRE^HO1la$S?Y5(y;0x8_?UM7p#mJQ;So^t<77KEQ5I{htiAa)=z^zVqm}O z>_*yZuqO46P>u4aYo#tVn6d>?ZK6K!obL2+hK{Af>zQx1eQS#C4X`i|jvP)mX$=x& z;E7iKxj`7}DUsB(-}4Zw-HD3sh183h&yOkIe6X7Q*!S(*>E!S2eWwPz{>o>m)feSx zSl6e6KGi`HGZ&R$=`XuO=n1~O4UeDZuR%YPgXL~k-ef*1Rx&GhG%-W}s;@Z<9hmG0 z3CP|)JD<~JM(kd%*k;}B(ON%^us^5knsQ7rD3jB2FqOMJHyJ=BDrOX6qR&SWI7Zkc zwqAa3xB=+qO}mj1d_FJjLgi!xjsudDN1gO;4V^{ryyI8-9UY9jI)z5;h8vH{&Rr@O zFNgH!VcRk8|9IF()3oz=zYz+yzJo5T!j0)0Oz3&UO5gN$onxK}{CQ?2#Cpxg=!fIZ z!?U(~`KkV#aBJjGRFdl724Oe0`wFieg(#9uP)3hpb-Aqxh0-`J9bGghF>K=HCjENI z)?^p#UalapO_8!BJK=b6Ka5S*IDz|Ynmdy#sqHlP6m~@E<>LQhu6xf{ zXEh2BS(G1r=Hf#O5AovVR2-LsgDoC5)7NY2>mgf{4S0KON5v$Wm4_W0YsE}r>YBGk zfv|Uw_YlZ6I!$boef8zP7#VcO&9f4&#-TD?@S~%&yxS1la=YizE{rldm2UY=4K}yc zep?&d z6@Aw|j+e=OKol&QN%p3zYx#CA9cBm7zj0!^sp4et43)^L(8^N`bOA}#!8Kw|@UHvXG8v6<~S>4BiEOQ6_s>>A=| z%&dHIks`LNZa1Cv@V1Y&ZnD2Hd!3#4 z;^Xnaobs9BKl_s~%yw2D7lN^z>evI=!F z4x*wYi&QpJ5&}*+Z4o-(W~$YXNdI={$Sj4ysou}L5kW+@n0mFwfmX}F$7|4YaEm2 zCt6GBdmgOxuG*)LPMV(w#)}f&FN~sPw;}BioBl2=8fX=;CRqu+zvv(mZss3DukV_5d-PC;B8Le`q!R89%kU?ZuZER!#!TIg$AJzj#a!q9m^M8 zVU0dBzbD&(8_Q9+n;znmug{j1K5JgGug7Vv@(t5^BxXFHG!B!+F1;uox4)OY3SGW~ zU3YtwFA2~!nRmBhoymK%#uNW?T5T$=F)J-p(5H))pq$42!6n_45XMc)syuCt@1?yrwM35PCN&WK z(G(~Ch*Gx;D(kC{H}$Ws8ndkBT_mY`tVqewQwmyf!rP&@V|o_^m)f8_@3hgKhLp}P zMr!?{?vR?;;4-H>)wol+^OKp@%2ZFT`#|i;S3V?!`vn(bcijXpUrvpEf^BBK{<6Q? z9V@?hHG&eH>9mXjt1{AOzHi0Z$d8iWp;-vDUy<`pE<4d;dPu2GO6c?`nM zaLJxa656tXGzIUzw^gVVN_jou~v}&A8%LGu%pU| zAW3Gm6{g-1T9sAv^0h{L&`1CZ-^}bYn>UH1>t>>?ciW^exH|wenZ1nFn%;fGo+Inp8!3b~6O@Z_4=&qfMVvUnoqEHmHfFP6BQi<1@zB1R z-e-`Qjn*MGo||+W4N%mJ<-^$P4{=Km7BV^wy7g9@AJU-PDH@OD`>|Tn+qt`xw{KTD z<%)GM@0V^vk~~`;szeg?vpi)Fp%eQvG|hZJ>tKGRYr}6l&C{7 zh?g40viIfe#1d@c(=SJ%{q-=`JEqrgY`2RIB3ufAqF9~~KPVIMOp3H@8se4MkHpn= zpxe!dj@22(H}tdCj69TdZF^P8Dyv*&z$eFu&&Ds`;NKc)E$B9tHV4}>40^F!u#PAN z5i#I|?!+4jg)YwrmK%h}jvK8SZ@BdnLRadsQ9oVv)ut5K&``lZQVW-$4sV0Sh6G>s zdNvB7Jd)}zNgSwNR=k|;);R5H-Tm%KM-fgl`{&A>WS4R%_Nm%uH1*((I3nY=`?wg6 z9_fcKruDf)!wt8LhJCl^aD0Z0qNg9AjutFBlv~F17JX~y$?$^u|L)~_{c~E+75)02 zj^^&dv^3Dx-u#1flqTeHw$xYsaCXQ_N?uz!p5*(F8?52Id|dnG58v+bH2oE>ijQDQ zpjjx-5vWWP=oAv$93|X#EV5dU8Y+%G3>|H~ux`4tCltO~r|Y3FhL>?foj1uzs60*ddq(d;Ea|rW@d4gWuY&5WjJ4B5dI(ft5t~Fe!nd zZ9sm-ClB2UmaZwKWEYxSy}{Wf{kh{$HCBo_Qqj)iy?`8HmdQltaVP)kOE%syf^Rs; z><0=u;9S9%eXf32zB|&yTQaGSz9eYZ+srogvxKTwxDV(cU_?~fMu0&)N*fP$)bDl) zt_j!!11L>@IHG8xEe&ioMmx@buQh~N0>gk$hBd*p(8zUC&VyBhyhm|=m+!277!owt zH!q2GP?IyS@%3667#)>K4NW{r(c>ij`V%&h+|OoO=l`vXYFO%^2+BYM*s@X46&#Dy zA8zZUkP%62(*0(eCdaxY6PXd0%hI!aYMmZ=x5Ud!PJ{Uar|=KI)YoQy)1t5kNkB|k z+#UjNnF0vrn=r9?KENiL4$rp1oTt`c&9Zzi*aQhD#_GgbM#VhepT>M3rt9UyIsN4? z^?%pQ|L(W@E!F@9YWR|%V!((WAhc}&pdv-<4^+X_6HMJFoJ5{MA~+lqn!Db!_t6eEbDH%Ob+Dn&%EPN@E$r1)!+p$BJk5XTq{VYA1%gStopF4hspeisJ_)ag z?SM>+q6~_q+A;hosN^|__%df154dbms?*i`4Aas_lCp4pm^}CVRL*CoXD7z#UM4^&Q_gIytX^V*NC=irN1aIai@w0 z#?apOJ6YdQ-R-pSSSw_Mrl9a^V@MYv*DBzxG98D4SHod98m*5wvgdcxb%TIL%|q)% zE7jPdqj)>rEJ*0p3sR8x8EtyHtRJCHYZqSr;_v%WT7dTi8Ebq=y!jjttENGdX4`wx zc2+nf$5#lUZkB_idfsSMH;{^pGogiJSR98c#7!{ z7G$}$#TvTJwV{!3Tvvk{WNDrPKOP=r0;L1A6tuq~TzXK7MKR%po!3^VMxXj zIMOf}y-kSlPFpmbcouyPC~-9&FUK0J^#~eSuQ(smTYtO}y7uyXV6T>D*J+QN`nq%) z=2JtI{0=$4^j}ZbPn!Z8{R<<%<>Mji%c63J+8tUHEo7T@#v#}TPIk&(%ySPSq+D{m ztWL{7B(C%0Qk7=N>Dz@#R=#}_60O$$=|p{Fq_rS6nIF&QfmsEbwiAp^G4#RcLg#T+ z8J)QnYnRP_UOL`Bef@Y>)C>L;Kl#j_qu~-X_CTJ0STs+Ij=-^w-p-!aok;YVF&MTi zpH4NBz3NJLRT6o5n~TX90ysqpEYdnba5o&?y(xMEDLV?1<#&4}n>iUz= zD3TipgVlk#Nir`H2Isg^hLODs&EfDk3Fn@zcV3@;bXPtVLRcJN0ZT#%X*irklf2Cn z?Vdg8UNYl;-w5Ta!CBXAHrA%pT+dT)(hU=@G)G0~PHsSJzaHREQ_opjLYY+QQJXJ` zE}dMU0j?%v!H{P(m=A@jUPP>YUIg^;@}QJxKNf!t_ouc18gSVLs!ZVm7YW<3F>};Q zTc1fufHS{UNrq~$J+Axp(sf6NFil3}09lBed$u<0D(68m^YSwNCBFJuhDNIaMKjHo zra7Lr8Nxsi)sEP%b8^|m?$VXXc?jV`Iz3JAIU0iKJMHAx`#npDzIN?xve&r&)sz5v zEL`BYdf<4}?3pGqfC!O{)@kUd4`;ak! z2LVL0BO=#>=Gyj4`z})r457(l!Y}_IZ34au+Sm=geXibTibBIM5(n`2P=2N0?3o-~ zh}IE)pDDV@PXI|mf1vy+K!k)x6w=-nxj2EOy)aoVOV(DYG!OZNv(-c%920F9&N4cO zqAaaIar&3H1#*B*o(vnF{Xkj=`6QNfD82n#z0VY?$apFkm=aBp5^->mkWFfRz;~KL z0}LfH+=qj#CJ*)jbltQ!`}<5a0)gnY!EJ$58G*Vn7>T*|bvxPConkAgox>>HY=n8a zVdI)!IdpoY!3x{oWlVWw7=3do-ZhG}+UjpN;`lW?_U~-8ex#ZmY<8-uUO{8X^J+bxkF^eA z1nU(sKNqXH&ve;wKHX>Ce_T#;P2dmzK^p{!s~boK`{1VC08|s{SzV?MIhM4&x*OKp z+3r3EM|ONakyUcI3gEt{e$<^>p7>A8`O?O3S&jfS6o>N!sDv&$rfo6?B|ZQIX9$vm zoOL~4y9QvUBg@+ivL28#lv8a_WbIIht#8V}mmkH6Uo7Wq8*f=oq!+nIwHz+E#YouW zd56PgQFha{#?XS%zFe1G!sZT_Yhfp|pf@g~709tNL79vCH^wvLV0q#J0R6s#k?uh(czxY zU$f|6SZjVMmS%8l2?uQnoM<{WSaVTgYMeKuG5>pO00}<9)*9Ag1Q{f5Bhumsn}O40 zK1I2ka^c!{b~i2S0wB6zs!D8O2XU~wtn98|gEx?vMiAf9UpL6BEjXI6L@=m)Fcgvm zyuhRM(T=F!Xsb!J00KpFA2eKX2Q(xmW3}^eZ?p9yE5WiNG6gmsdX4jHgLr_^S^YU}B?MMuzhE=v4XlpAl{jv9&ip#1O zQ}}2Q%0=%HBhmtEb2hT5?fl34Of_5!0>Q8h92lE49CX+kblZV>rzxl%z+nQbA_3$% zPay~Qe6Lk$D32^yf0zqCN8iC4MpLcecSkov;8Y6`L)lsPxgCEAqM6f?+MN7 z77a^Hfb~0$FcQvyAOK8@$_MyREgrcm?Dw`Qk6Z?Ov+35Dj}#mfA-%e{&v#&huE0)5#sWEDo~KbmLYyEzT#L?znT=PWU2{xNAUC(5x0#th zmayGi7NO`N?+%?cJM&Q~c^pTe@ZHg4llS|GzW^^aQ{m z4G$P?JBWh;`+ZI;y>C@)bI%eysNrnnuNagOIhJl;6Yn$C0LJlK(ZUBjB}!mH{AY{p zm;Q~Wz;%Z^36&0$1zy8oL=z6@Z`pwj79H9+Kxh+;gY=7~izr_H7E?l#>%nhCqAXxD za42{k8H*+?(2pzKSSh2J+XdX790`Q(II4DAiCTG`0);js+|23q`_%)E+KS~~&+gYY ze#dg)UBa1VQL*`88JfXnK-KaokbB#HK!O(TGWfZv4+EeVq~uJ{7(Alj?!i&Sn)eOC zmZk^Fv)qw-ns&oHC$jtU%tpt4jh;_+aQn4)iDb){Hh#~S`(HBE04SP6i!3dRFGq5= z0TpObXh#BX`jH-3jV8n0xO@UWyq9D`oNKJs?o)dyD)LaWU7OZN^X!k6; z90$i*Jh-Fsc5$f72^u%f`-HG%0 zewMg5y~A3)M?X&MIm-QO8$2UCGDj@lJORf?HyyzOu9R#$4f$reT>I5PA^dQ2JC?>$ z49@7LzuorT)nTi6>TnxLNIl6vee!2pKM=DGK7#*%z#TlH2DoFioyq@!trw(w)76C* zI!0g`ptEBM3=geBx_t$O{BlrOX1^Osb-5c~=f-e2?WQbBKk-ez)0hYtT%#69eTW2V{Ed{}bMoko#u_9SjPVrr!9i0R zsU(AAxpwIEK4VSOBMPG^W8*PIjR>(6Z|OqY*L$b2ADL%lCO(j2Mboxp^Q_R``Qq`y zmw`^4!i4rm9zBWVG_nq2v|Jo+mT52Ri?$K!VDXdd@^ecB(kn=+H>D{gPmzY>5%5~= zGU9wt@5W$J;L5`)I4g7%7UT#@S1GaGlkR{wXJ@h)3Fq-*wv=4PR9VPdTW-%lFG=Y@DcB_ueytC=f9A-7zlNr!fs-mc_^9tbk`J5rz!TAzk zjI&XlZE&u}6D;n%#sp-$(X))Z5yHF#E)n3MS{vk@#;_*1hb$nqoFIsZf}xc0gx)ne zl!O=m=pF8d(>)JQoYqVFG9`3Cl?ZL={7hJT>i{LvSIE`RpRuV=@F<7?bOpxscsU!__d`H88bO#2v|-yh$F~|oM-ncm#s@J?MtArIbltx7-)sy(xhDBz zh|QW%9VVah`ENCbPlsd)Hi!TpMCaj&@cKilG>^@02uSsi<8HlkmzpgRdv}L7MXsrA zb{)j~TQ$qqS7-50o(`hMFRlEhozXm+B5*$Jhpy9tBiJI3%);+7hU=a|mA1)8lj!9_ z`UH{J?=XhFj$}0as)nE(%+Zz&tH_2weylp*Ud+&3Na?nx)rWhyTcL#ky6RxSUQPOa z3QcT&bv!`c>NQq)epJ4-(pnNY6Ljl1@G5KS#di6|x zA1KLuO0Cw;_4ta7|H2&Z4R`?f3k6#Mm>5pCpmWmpJl<=rp`k(95$bB9VL7@10tMvN zn#f$@-Htex)r8=^7?hX=$H}{)T`u&qb7Lucexe44{@$OZuZf1Qmi4ur|NbjR?#2G2 z)ztva-zGwTZZQ_XKXWMpU0QcsG358lZ0HKgnlrWCQWiL8uw0miUQ7A?vFpAW6SQls zdG2#i3GKDzcGsVDC%ro_PUO+yv`nO1-y3%}dPgq_4?-yP1K;q?n9t&D5o7nVQ{<~1 z-x!MYCJ&^@+v}In3fg=nH`wWk?bn_Z{-AvLW=s&#wC4GqG0EqqU4L##cUH#HPDdm= zQwQYid?EbooD@sS9XX*Gi3d3|&b>#q=jVxnTmp)qpORwj=}IU<5xw0_puCcToTW8h zyPC{iyt>yN6GQQMzf2EvmYSZw`K+yxpEp4no6`+Q{L*e3N?}F}!NYpIb@st=U=@=r zJ*#|I>az#w_Snb2obxlC!#hXjo;cinf{RnRSQv+TnCRwm>g?E+rS6f@IQHUo!QSs( zbJ5@0#^XwPowI9)VBg7~d!z;2W5CkGM*!GU;1NNgS^m+Fy1cVY-Y0;}@s8aku~crG zy)Yaz$1|;Ps%RGC#vCcbf_$hzNB;wBt^GOmm;ADfdh_wsKRE?I=z-Su;JMH=gF=*( z+t-UsWczD*efY=!!f2C<0fe7zKwd23UR@AqU4KG5@9H`^O*i5AJn!EowyUIvpr5RZ zR8dj8pEV3lWtJus_oKDwUl|1+3Na1VF!I2e;dzITiJ~c#&iQuMv}xcD4&MQm3cX3EEiyej^EhNj0?}@`D1l9N>!fcP#M{4AD(z z0Lk+J0s-}Av+cTxY%DLMK|f6nw`dmIE&;dd!kg7eyGHtGc9id_6dS|Yy!Y^;{>@ow z^0M#AOL56$qf>a+Xf6q)dN&_yVJ2Z*ehTSrWXm6tpl^zvC4$*m!mB$2Ywy>AQgL^QShg|V0t#j^Y&Q*`uOkcbA!5m!Fsy5_a=TFG z;%Ww}E6!7H9h(%_v(EIi{PNm=vinI$l~}7;vFS>3FLm|(GO+W&Rl(M+_D-E`cYoNW zi8$GdwSRp;z`x%qSjuy=AHzdJoe6jPXi|lc+g|9G+F8-bVCOHd$%#7G{&_8%soHzw z%zioYXUUUU96_Jr^vU6Tyy$nq;;qg@u<0MU&WI4ll3#P%EL|pOC2ZpNe;TrNs-pYc3O@FZgn|EE_LZ*eL-#__4Mn_rInlBN^< z&s)9Yc-O-0WP9cK^tJw8&_5da9ScGWI-seIbO|_aNC(dYjbuCdaQ==P!36*RmQk%? zAfYl;W-fsO^`g5p>EUtZNxjRyQ%1(=aj?GsYTp_8A1?^`E5Pl4oQCI1!CPh)3EoE| zst2;q%T;S(8Js5PJuf(0#WJ)~4*II; z_Q;yN3a5!f?5{J;>n*BBrSNk|KlDFm#$vV2p6e;MbzIW(S{*KkUJ%Ng%WiZZjmCpc z7;mCDIF<8hz`0h~{)wK)X-z9$&K+T|`PEX&m7pltF?&<51gApqfBO6EFa1&!QdDe*8SeW<0e_e>Kr1IjwDKFDP^mVNpC+JqnYJ zR9^4!X6-x_{G!iE|9cy)?_=fNledN>)s~qHvn{0NuBzQX%N#qr6O*jcCWW-S97y)Z zBlc;k(yy&6yI~yXabW>m1z!h{Qa}`eGr)h@?KJDOcUcIF+tdc5PdU(-{>2w3p5}<1 zJU5QUytj4aI8+RefB0)Zn|c=V_GFPh^skBPXZ_Ja!;fkRqiZKf>7h%4w<@{E9kxgA z#y^0|Uk&#_!JG!uxC5VgrSgz_)pn4V=BRTlC;`VzBTlacsza-mXr0=(cIyi~>kqtP zS`Rv|uI*1#%`AfT&YsfcWxLn?rB4^z-E}zb&&YT<(I&l6kI43Mf0|Z1$bQ4L9ye%p z-IW2PDkitXP}n5v&9$CPvOc}L6{dgNp5@+cIvUFCif%v6Pt$5YV{e$&YJB?F^zKl`dK)kLy)4kVodX~|(*QYYubP;qeCrV_N_p{jK z>HpCfaGO8+!V!N96iWB#=53w#cN%LN2@%_%z0bxq23AQCP=w!Qj73}l{U`vgZ5?e$ zh@V(Aw!V91$QL1P1fji01qJesxsi`13x9CU>hM0^soLIf$I-15?~?H1kd`!6m%T%y zboSjZ^IL~(tzV}NKQA=mQh`8KR5hjCgnDKTqUrD~X;Lqda$;KFPF@c&`#(?iza2Tv z%m?+iAL?n9+U_Ua*YhKqs5iblSea*l07lhybJMLGN4X2@ROf4b>lN;5;L~zP7$V;N EKRppW`Tzg` literal 0 HcmV?d00001 diff --git a/internal/error.go b/internal/error.go new file mode 100644 index 0000000000..7df1603391 --- /dev/null +++ b/internal/error.go @@ -0,0 +1,7 @@ +package internal + +// Error is a custom error. +type Error string + +// Error is an implementation of error interface. +func (e Error) Error() string { return string(e) } diff --git a/lib/.gitkeep b/lib/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/acl/action.go b/lib/acl/action.go new file mode 100644 index 0000000000..a173d92e98 --- /dev/null +++ b/lib/acl/action.go @@ -0,0 +1,94 @@ +package acl + +import ( + "bytes" + + "github.com/nspcc-dev/neofs-api-go/acl" +) + +// RequestInfo is an interface of request information needed for extended ACL check. +type RequestInfo interface { + TypedHeaderSource + + // Must return the binary representation of request initiator's key. + Key() []byte + + // Must return true if request corresponds to operation type. + TypeOf(acl.OperationType) bool + + // Must return true if request has passed target. + TargetOf(acl.Target) bool +} + +// ExtendedACLChecker is an interface of extended ACL checking tool. +type ExtendedACLChecker interface { + // Must return an action according to the results of applying the ACL table rules to request. + // + // Must return ActionUndefined if it is unable to explicitly calculate the action. + Action(acl.ExtendedACLTable, RequestInfo) acl.ExtendedACLAction +} + +type extendedACLChecker struct{} + +// NewExtendedACLChecker creates a new extended ACL checking tool and returns ExtendedACLChecker interface. +func NewExtendedACLChecker() ExtendedACLChecker { + return new(extendedACLChecker) +} + +// Action returns an action for passed request based on information about it and ACL table. +// +// Returns action of the first suitable table record, or ActionUndefined in the absence thereof. +// +// If passed ExtendedACLTable is nil, ActionUndefined returns. +// If passed RequestInfo is nil, ActionUndefined returns. +func (s extendedACLChecker) Action(table acl.ExtendedACLTable, req RequestInfo) acl.ExtendedACLAction { + if table == nil { + return acl.ActionUndefined + } else if req == nil { + return acl.ActionUndefined + } + + for _, record := range table.Records() { + // check type of operation + if !req.TypeOf(record.OperationType()) { + continue + } + + // check target + if !targetMatches(req, record.TargetList()) { + continue + } + + // check headers + switch MatchFilters(req, record.HeaderFilters()) { + case mResUndefined: + // headers of some type could not be composed => allow + return acl.ActionAllow + case mResMatch: + return record.Action() + } + } + + return acl.ActionAllow +} + +// returns true if one of ExtendedACLTarget has suitable target OR suitable public key. +func targetMatches(req RequestInfo, list []acl.ExtendedACLTarget) bool { + rKey := req.Key() + + for _, target := range list { + // check public key match + for _, key := range target.KeyList() { + if bytes.Equal(key, rKey) { + return true + } + } + + // check target group match + if req.TargetOf(target.Target()) { + return true + } + } + + return false +} diff --git a/lib/acl/action_test.go b/lib/acl/action_test.go new file mode 100644 index 0000000000..49e30eea8f --- /dev/null +++ b/lib/acl/action_test.go @@ -0,0 +1,163 @@ +package acl + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/stretchr/testify/require" +) + +type testExtendedACLTable struct { + records []acl.ExtendedACLRecord +} + +type testRequestInfo struct { + headers []acl.TypedHeader + key []byte + opType acl.OperationType + target acl.Target +} + +type testEACLRecord struct { + opType acl.OperationType + filters []acl.HeaderFilter + targets []acl.ExtendedACLTarget + action acl.ExtendedACLAction +} + +type testEACLTarget struct { + target acl.Target + keys [][]byte +} + +func (s testEACLTarget) Target() acl.Target { + return s.target +} + +func (s testEACLTarget) KeyList() [][]byte { + return s.keys +} + +func (s testEACLRecord) OperationType() acl.OperationType { + return s.opType +} + +func (s testEACLRecord) HeaderFilters() []acl.HeaderFilter { + return s.filters +} + +func (s testEACLRecord) TargetList() []acl.ExtendedACLTarget { + return s.targets +} + +func (s testEACLRecord) Action() acl.ExtendedACLAction { + return s.action +} + +func (s testRequestInfo) HeadersOfType(typ acl.HeaderType) ([]acl.Header, bool) { + res := make([]acl.Header, 0, len(s.headers)) + + for i := range s.headers { + if s.headers[i].HeaderType() == typ { + res = append(res, s.headers[i]) + } + } + + return res, true +} + +func (s testRequestInfo) Key() []byte { + return s.key +} + +func (s testRequestInfo) TypeOf(t acl.OperationType) bool { + return s.opType == t +} + +func (s testRequestInfo) TargetOf(t acl.Target) bool { + return s.target == t +} + +func (s testExtendedACLTable) Records() []acl.ExtendedACLRecord { + return s.records +} + +func TestExtendedACLChecker_Action(t *testing.T) { + s := NewExtendedACLChecker() + + // nil ExtendedACLTable + require.Equal(t, acl.ActionUndefined, s.Action(nil, nil)) + + // create test ExtendedACLTable + table := new(testExtendedACLTable) + + // nil RequestInfo + require.Equal(t, acl.ActionUndefined, s.Action(table, nil)) + + // create test RequestInfo + req := new(testRequestInfo) + + // create test ExtendedACLRecord + record := new(testEACLRecord) + table.records = append(table.records, record) + + // set different OperationType + record.opType = acl.OperationType(3) + req.opType = record.opType + 1 + + require.Equal(t, acl.ActionAllow, s.Action(table, req)) + + // set equal OperationType + req.opType = record.opType + + // create test ExtendedACLTarget through group + target := new(testEACLTarget) + record.targets = append(record.targets, target) + + // set not matching ExtendedACLTarget + target.target = acl.Target(5) + req.target = target.target + 1 + + require.Equal(t, acl.ActionAllow, s.Action(table, req)) + + // set matching ExtendedACLTarget + req.target = target.target + + // create test HeaderFilter + fHeader := new(testTypedHeader) + hFilter := &testHeaderFilter{ + TypedHeader: fHeader, + } + record.filters = append(record.filters, hFilter) + + // create test TypedHeader + header := new(testTypedHeader) + req.headers = append(req.headers, header) + + // set not matching values + header.t = hFilter.HeaderType() + 1 + + require.Equal(t, acl.ActionAllow, s.Action(table, req)) + + // set matching values + header.k = "key" + header.v = "value" + + fHeader.t = header.HeaderType() + fHeader.k = header.Name() + fHeader.v = header.Value() + + hFilter.t = acl.StringEqual + + // set ExtendedACLAction + record.action = acl.ExtendedACLAction(7) + + require.Equal(t, record.action, s.Action(table, req)) + + // set matching ExtendedACLTarget through key + target.target = req.target + 1 + req.key = []byte{1, 2, 3} + target.keys = append(target.keys, req.key) + + require.Equal(t, record.action, s.Action(table, req)) +} diff --git a/lib/acl/basic.go b/lib/acl/basic.go new file mode 100644 index 0000000000..eae2d7fa9d --- /dev/null +++ b/lib/acl/basic.go @@ -0,0 +1,179 @@ +package acl + +import ( + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/internal" +) + +type ( + // BasicChecker is an interface of the basic ACL control tool. + BasicChecker interface { + // Action returns true if request is allowed for this target. + Action(uint32, object.RequestType, acl.Target) (bool, error) + + // Bearer returns true if bearer token is allowed for this request. + Bearer(uint32, object.RequestType) (bool, error) + + // Extended returns true if extended ACL is allowed for this. + Extended(uint32) bool + + // Sticky returns true if sticky bit is set. + Sticky(uint32) bool + } + + // BasicACLChecker performs basic ACL check. + BasicACLChecker struct{} + + // MaskedBasicACLChecker performs all basic ACL checks, but applying + // mask on ACL first. It is useful, when some bits must be always + // set or unset. + MaskedBasicACLChecker struct { + BasicACLChecker + + andMask uint32 + orMask uint32 + } + + nibble struct { + value uint32 + } +) + +const ( + errUnknownRequest = internal.Error("unknown request type") + errUnknownTarget = internal.Error("unknown target type") +) + +const ( + aclFinalBit = 0x10000000 // 29th bit + aclStickyBit = 0x20000000 // 30th bit + + nibbleBBit = 0x1 + nibbleOBit = 0x2 + nibbleSBit = 0x4 + nibbleUBit = 0x8 + + // DefaultAndFilter is a default AND mask of basic ACL value of container. + DefaultAndFilter = 0xFFFFFFFF +) + +var ( + nibbleOffset = map[object.RequestType]uint32{ + object.RequestGet: 0, + object.RequestHead: 1 * 4, + object.RequestPut: 2 * 4, + object.RequestDelete: 3 * 4, + object.RequestSearch: 4 * 4, + object.RequestRange: 5 * 4, + object.RequestRangeHash: 6 * 4, + } +) + +// Action returns true if request is allowed for target. +func (c *BasicACLChecker) Action(rule uint32, req object.RequestType, t acl.Target) (bool, error) { + n, err := fetchNibble(rule, req) + if err != nil { + return false, err + } + + switch t { + case acl.Target_User: + return n.U(), nil + case acl.Target_System: + return n.S(), nil + case acl.Target_Others: + return n.O(), nil + default: + return false, errUnknownTarget + } +} + +// Bearer returns true if bearer token is allowed to use for this request +// as source of extended ACL. +func (c *BasicACLChecker) Bearer(rule uint32, req object.RequestType) (bool, error) { + n, err := fetchNibble(rule, req) + if err != nil { + return false, err + } + + return n.B(), nil +} + +// Extended returns true if extended ACL stored in the container are allowed +// to use. +func (c *BasicACLChecker) Extended(rule uint32) bool { + return rule&aclFinalBit != aclFinalBit +} + +// Sticky returns true if container is not allowed to store objects with +// owners different from request owner. +func (c *BasicACLChecker) Sticky(rule uint32) bool { + return rule&aclStickyBit == aclStickyBit +} + +func fetchNibble(rule uint32, req object.RequestType) (*nibble, error) { + offset, ok := nibbleOffset[req] + if !ok { + return nil, errUnknownRequest + } + + return &nibble{value: (rule >> offset) & 0xf}, nil +} + +// B returns true if `Bearer` bit set in the nibble. +func (n *nibble) B() bool { return n.value&nibbleBBit == nibbleBBit } + +// O returns true if `Others` bit set in the nibble. +func (n *nibble) O() bool { return n.value&nibbleOBit == nibbleOBit } + +// S returns true if `System` bit set in the nibble. +func (n *nibble) S() bool { return n.value&nibbleSBit == nibbleSBit } + +// U returns true if `User` bit set in the nibble. +func (n *nibble) U() bool { return n.value&nibbleUBit == nibbleUBit } + +// NewMaskedBasicACLChecker returns BasicChecker that applies predefined +// bit mask on basic ACL value. +func NewMaskedBasicACLChecker(or, and uint32) BasicChecker { + return MaskedBasicACLChecker{ + BasicACLChecker: BasicACLChecker{}, + andMask: and, + orMask: or, + } +} + +// Action returns true if request is allowed for target. +func (c MaskedBasicACLChecker) Action(rule uint32, req object.RequestType, t acl.Target) (bool, error) { + rule |= c.orMask + rule &= c.andMask + + return c.BasicACLChecker.Action(rule, req, t) +} + +// Bearer returns true if bearer token is allowed to use for this request +// as source of extended ACL. +func (c MaskedBasicACLChecker) Bearer(rule uint32, req object.RequestType) (bool, error) { + rule |= c.orMask + rule &= c.andMask + + return c.BasicACLChecker.Bearer(rule, req) +} + +// Extended returns true if extended ACL stored in the container are allowed +// to use. +func (c MaskedBasicACLChecker) Extended(rule uint32) bool { + rule |= c.orMask + rule &= c.andMask + + return c.BasicACLChecker.Extended(rule) +} + +// Sticky returns true if container is not allowed to store objects with +// owners different from request owner. +func (c MaskedBasicACLChecker) Sticky(rule uint32) bool { + rule |= c.orMask + rule &= c.andMask + + return c.BasicACLChecker.Sticky(rule) +} diff --git a/lib/acl/basic_test.go b/lib/acl/basic_test.go new file mode 100644 index 0000000000..b379751f62 --- /dev/null +++ b/lib/acl/basic_test.go @@ -0,0 +1,116 @@ +package acl + +import ( + "math/bits" + "testing" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/stretchr/testify/require" +) + +func TestBasicACLChecker(t *testing.T) { + reqs := []object.RequestType{ + object.RequestGet, + object.RequestHead, + object.RequestPut, + object.RequestDelete, + object.RequestSearch, + object.RequestRange, + object.RequestRangeHash, + } + + targets := []acl.Target{ + acl.Target_Others, + acl.Target_System, + acl.Target_User, + } + + checker := new(BasicACLChecker) + + t.Run("verb permissions", func(t *testing.T) { + mask := uint32(1) + + for i := range reqs { + res, err := checker.Bearer(mask, reqs[i]) + require.NoError(t, err) + require.True(t, res) + + mask = bits.Reverse32(mask) + res, err = checker.Bearer(mask, reqs[i]) + require.NoError(t, err) + require.False(t, res) + + mask = bits.Reverse32(mask) + + for j := range targets { + mask <<= 1 + res, err = checker.Action(mask, reqs[i], targets[j]) + require.NoError(t, err) + require.True(t, res) + + mask = bits.Reverse32(mask) + res, err = checker.Action(mask, reqs[i], targets[j]) + require.NoError(t, err) + require.False(t, res) + + mask = bits.Reverse32(mask) + } + mask <<= 1 + } + }) + + t.Run("unknown verb", func(t *testing.T) { + mask := uint32(1) + _, err := checker.Bearer(mask, -1) + require.Error(t, err) + + mask = 2 + _, err = checker.Action(mask, -1, acl.Target_Others) + require.Error(t, err) + }) + + t.Run("unknown action", func(t *testing.T) { + mask := uint32(2) + _, err := checker.Action(mask, object.RequestGet, -1) + require.Error(t, err) + }) + + t.Run("extended acl permission", func(t *testing.T) { + // set F-bit + mask := uint32(0) | aclFinalBit + require.False(t, checker.Extended(mask)) + + // unset F-bit + mask = bits.Reverse32(mask) + require.True(t, checker.Extended(mask)) + }) + + t.Run("sticky bit permission", func(t *testing.T) { + mask := uint32(0x20000000) + require.True(t, checker.Sticky(mask)) + + mask = bits.Reverse32(mask) + require.False(t, checker.Sticky(mask)) + }) +} + +// todo: add tests like in basic acl checker +func TestNeoFSMaskedBasicACLChecker(t *testing.T) { + const orFilter = 0x04040444 // this OR filter will be used in neofs-node + checker := NewMaskedBasicACLChecker(orFilter, DefaultAndFilter) + + reqs := []object.RequestType{ + object.RequestGet, + object.RequestHead, + object.RequestPut, + object.RequestSearch, + object.RequestRangeHash, + } + + for i := range reqs { + res, err := checker.Action(0, reqs[i], acl.Target_System) + require.NoError(t, err) + require.True(t, res) + } +} diff --git a/lib/acl/binary.go b/lib/acl/binary.go new file mode 100644 index 0000000000..a1cf6e50b9 --- /dev/null +++ b/lib/acl/binary.go @@ -0,0 +1,129 @@ +package acl + +import ( + "context" + "encoding/binary" + "io" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/internal" +) + +// BinaryEACLKey is a binary EACL storage key. +type BinaryEACLKey struct { + cid refs.CID +} + +// BinaryEACLValue is a binary EACL storage value. +type BinaryEACLValue struct { + eacl []byte + + sig []byte +} + +// BinaryExtendedACLSource is an interface of storage of binary extended ACL tables with read access. +type BinaryExtendedACLSource interface { + // Must return binary extended ACL table by key. + GetBinaryEACL(context.Context, BinaryEACLKey) (BinaryEACLValue, error) +} + +// BinaryExtendedACLStore is an interface of storage of binary extended ACL tables. +type BinaryExtendedACLStore interface { + BinaryExtendedACLSource + + // Must store binary extended ACL table for key. + PutBinaryEACL(context.Context, BinaryEACLKey, BinaryEACLValue) error +} + +// ErrNilBinaryExtendedACLStore is returned by function that expect a non-nil +// BinaryExtendedACLStore, but received nil. +const ErrNilBinaryExtendedACLStore = internal.Error("binary extended ACL store is nil") + +const sliceLenSize = 4 + +var eaclEndianness = binary.BigEndian + +// CID is a container ID getter. +func (s BinaryEACLKey) CID() refs.CID { + return s.cid +} + +// SetCID is a container ID setter. +func (s *BinaryEACLKey) SetCID(v refs.CID) { + s.cid = v +} + +// EACL is a binary extended ACL table getter. +func (s BinaryEACLValue) EACL() []byte { + return s.eacl +} + +// SetEACL is a binary extended ACL table setter. +func (s *BinaryEACLValue) SetEACL(v []byte) { + s.eacl = v +} + +// Signature is an EACL signature getter. +func (s BinaryEACLValue) Signature() []byte { + return s.sig +} + +// SetSignature is an EACL signature setter. +func (s *BinaryEACLValue) SetSignature(v []byte) { + s.sig = v +} + +// MarshalBinary returns a binary representation of BinaryEACLValue. +func (s BinaryEACLValue) MarshalBinary() ([]byte, error) { + data := make([]byte, sliceLenSize+len(s.eacl)+sliceLenSize+len(s.sig)) + + off := 0 + + eaclEndianness.PutUint32(data[off:], uint32(len(s.eacl))) + off += sliceLenSize + + off += copy(data[off:], s.eacl) + + eaclEndianness.PutUint32(data[off:], uint32(len(s.sig))) + off += sliceLenSize + + copy(data[off:], s.sig) + + return data, nil +} + +// UnmarshalBinary unmarshals BinaryEACLValue from bytes. +func (s *BinaryEACLValue) UnmarshalBinary(data []byte) (err error) { + err = io.ErrUnexpectedEOF + off := 0 + + if len(data[off:]) < sliceLenSize { + return + } + + aclLn := eaclEndianness.Uint32(data[off:]) + off += 4 + + if uint32(len(data[off:])) < aclLn { + return + } + + s.eacl = make([]byte, aclLn) + off += copy(s.eacl, data[off:]) + + if len(data[off:]) < sliceLenSize { + return + } + + sigLn := eaclEndianness.Uint32(data[off:]) + off += 4 + + if uint32(len(data[off:])) < sigLn { + return + } + + s.sig = make([]byte, sigLn) + copy(s.sig, data[off:]) + + return nil +} diff --git a/lib/acl/binary_test.go b/lib/acl/binary_test.go new file mode 100644 index 0000000000..eefb59ab59 --- /dev/null +++ b/lib/acl/binary_test.go @@ -0,0 +1,27 @@ +package acl + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBinaryEACLValue(t *testing.T) { + s := BinaryEACLValue{} + + eacl := []byte{1, 2, 3} + s.SetEACL(eacl) + require.Equal(t, eacl, s.EACL()) + + sig := []byte{4, 5, 6} + s.SetSignature(sig) + require.Equal(t, sig, s.Signature()) + + data, err := s.MarshalBinary() + require.NoError(t, err) + + s2 := BinaryEACLValue{} + require.NoError(t, s2.UnmarshalBinary(data)) + + require.Equal(t, s, s2) +} diff --git a/lib/acl/extended.go b/lib/acl/extended.go new file mode 100644 index 0000000000..20695bc6e5 --- /dev/null +++ b/lib/acl/extended.go @@ -0,0 +1,29 @@ +package acl + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/refs" +) + +// TypedHeaderSource is a various types of header set interface. +type TypedHeaderSource interface { + // Must return list of Header of particular type. + // Must return false if there is no ability to compose header list. + HeadersOfType(acl.HeaderType) ([]acl.Header, bool) +} + +// ExtendedACLSource is an interface of storage of extended ACL tables with read access. +type ExtendedACLSource interface { + // Must return extended ACL table by container ID key. + GetExtendedACLTable(context.Context, refs.CID) (acl.ExtendedACLTable, error) +} + +// ExtendedACLStore is an interface of storage of extended ACL tables. +type ExtendedACLStore interface { + ExtendedACLSource + + // Must store extended ACL table for container ID key. + PutExtendedACLTable(context.Context, refs.CID, acl.ExtendedACLTable) error +} diff --git a/lib/acl/header.go b/lib/acl/header.go new file mode 100644 index 0000000000..8c779b3b6a --- /dev/null +++ b/lib/acl/header.go @@ -0,0 +1,234 @@ +package acl + +import ( + "strconv" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" +) + +type objectHeaderSource struct { + obj *object.Object +} + +type typedHeader struct { + n string + v string + t acl.HeaderType +} + +type extendedHeadersWrapper struct { + hdrSrc service.ExtendedHeadersSource +} + +type typedExtendedHeader struct { + hdr service.ExtendedHeader +} + +func newTypedObjSysHdr(name, value string) acl.TypedHeader { + return &typedHeader{ + n: name, + v: value, + t: acl.HdrTypeObjSys, + } +} + +// Name is a name field getter. +func (s typedHeader) Name() string { + return s.n +} + +// Value is a value field getter. +func (s typedHeader) Value() string { + return s.v +} + +// HeaderType is a type field getter. +func (s typedHeader) HeaderType() acl.HeaderType { + return s.t +} + +// TypedHeaderSourceFromObject wraps passed object and returns TypedHeaderSource interface. +func TypedHeaderSourceFromObject(obj *object.Object) TypedHeaderSource { + return &objectHeaderSource{ + obj: obj, + } +} + +// HeaderOfType gathers object headers of passed type and returns Header list. +// +// If value of some header can not be calculated (e.g. nil extended header), it does not appear in list. +// +// Always returns true. +func (s objectHeaderSource) HeadersOfType(typ acl.HeaderType) ([]acl.Header, bool) { + if s.obj == nil { + return nil, true + } + + var res []acl.Header + + switch typ { + case acl.HdrTypeObjUsr: + objHeaders := s.obj.GetHeaders() + + res = make([]acl.Header, 0, len(objHeaders)) // 7 system header fields + + for i := range objHeaders { + if h := newTypedObjectExtendedHeader(objHeaders[i]); h != nil { + res = append(res, h) + } + } + case acl.HdrTypeObjSys: + res = make([]acl.Header, 0, 7) + + sysHdr := s.obj.GetSystemHeader() + + created := sysHdr.GetCreatedAt() + + res = append(res, + // ID + newTypedObjSysHdr( + acl.HdrObjSysNameID, + sysHdr.ID.String(), + ), + + // CID + newTypedObjSysHdr( + acl.HdrObjSysNameCID, + sysHdr.CID.String(), + ), + + // OwnerID + newTypedObjSysHdr( + acl.HdrObjSysNameOwnerID, + sysHdr.OwnerID.String(), + ), + + // Version + newTypedObjSysHdr( + acl.HdrObjSysNameVersion, + strconv.FormatUint(sysHdr.GetVersion(), 10), + ), + + // PayloadLength + newTypedObjSysHdr( + acl.HdrObjSysNamePayloadLength, + strconv.FormatUint(sysHdr.GetPayloadLength(), 10), + ), + + // CreatedAt.UnitTime + newTypedObjSysHdr( + acl.HdrObjSysNameCreatedUnix, + strconv.FormatUint(uint64(created.GetUnixTime()), 10), + ), + + // CreatedAt.Epoch + newTypedObjSysHdr( + acl.HdrObjSysNameCreatedEpoch, + strconv.FormatUint(created.GetEpoch(), 10), + ), + ) + } + + return res, true +} + +func newTypedObjectExtendedHeader(h object.Header) acl.TypedHeader { + val := h.GetValue() + if val == nil { + return nil + } + + res := new(typedHeader) + res.t = acl.HdrTypeObjSys + + switch hdr := val.(type) { + case *object.Header_UserHeader: + if hdr.UserHeader == nil { + return nil + } + + res.t = acl.HdrTypeObjUsr + res.n = hdr.UserHeader.GetKey() + res.v = hdr.UserHeader.GetValue() + case *object.Header_Link: + if hdr.Link == nil { + return nil + } + + switch hdr.Link.GetType() { + case object.Link_Previous: + res.n = acl.HdrObjSysLinkPrev + case object.Link_Next: + res.n = acl.HdrObjSysLinkNext + case object.Link_Child: + res.n = acl.HdrObjSysLinkChild + case object.Link_Parent: + res.n = acl.HdrObjSysLinkPar + case object.Link_StorageGroup: + res.n = acl.HdrObjSysLinkSG + default: + return nil + } + + res.v = hdr.Link.ID.String() + default: + return nil + } + + return res +} + +// TypedHeaderSourceFromExtendedHeaders wraps passed ExtendedHeadersSource and returns TypedHeaderSource interface. +func TypedHeaderSourceFromExtendedHeaders(hdrSrc service.ExtendedHeadersSource) TypedHeaderSource { + return &extendedHeadersWrapper{ + hdrSrc: hdrSrc, + } +} + +// Name returns the result of Key method. +func (s typedExtendedHeader) Name() string { + return s.hdr.Key() +} + +// Value returns the result of Value method. +func (s typedExtendedHeader) Value() string { + return s.hdr.Value() +} + +// HeaderType always returns HdrTypeRequest. +func (s typedExtendedHeader) HeaderType() acl.HeaderType { + return acl.HdrTypeRequest +} + +// TypedHeaders gathers extended request headers and returns TypedHeader list. +// +// Nil headers are ignored. +// +// Always returns true. +func (s extendedHeadersWrapper) HeadersOfType(typ acl.HeaderType) ([]acl.Header, bool) { + if s.hdrSrc == nil { + return nil, true + } + + var res []acl.Header + + if typ == acl.HdrTypeRequest { + hs := s.hdrSrc.ExtendedHeaders() + + res = make([]acl.Header, 0, len(hs)) + + for i := range hs { + if hs[i] == nil { + continue + } + + res = append(res, &typedExtendedHeader{ + hdr: hs[i], + }) + } + } + + return res, true +} diff --git a/lib/acl/headers_test.go b/lib/acl/headers_test.go new file mode 100644 index 0000000000..236e084d29 --- /dev/null +++ b/lib/acl/headers_test.go @@ -0,0 +1,60 @@ +package acl + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/stretchr/testify/require" +) + +func TestNewTypedObjectExtendedHeader(t *testing.T) { + var res acl.TypedHeader + + hdr := object.Header{} + + // nil value + require.Nil(t, newTypedObjectExtendedHeader(hdr)) + + // UserHeader + { + key := "key" + val := "val" + hdr.Value = &object.Header_UserHeader{ + UserHeader: &object.UserHeader{ + Key: key, + Value: val, + }, + } + + res = newTypedObjectExtendedHeader(hdr) + require.Equal(t, acl.HdrTypeObjUsr, res.HeaderType()) + require.Equal(t, key, res.Name()) + require.Equal(t, val, res.Value()) + } + + { // Link + link := new(object.Link) + link.ID = object.ID{1, 2, 3} + + hdr.Value = &object.Header_Link{ + Link: link, + } + + check := func(lt object.Link_Type, name string) { + link.Type = lt + + res = newTypedObjectExtendedHeader(hdr) + + require.Equal(t, acl.HdrTypeObjSys, res.HeaderType()) + require.Equal(t, name, res.Name()) + require.Equal(t, link.ID.String(), res.Value()) + } + + check(object.Link_Previous, acl.HdrObjSysLinkPrev) + check(object.Link_Next, acl.HdrObjSysLinkNext) + check(object.Link_Parent, acl.HdrObjSysLinkPar) + check(object.Link_Child, acl.HdrObjSysLinkChild) + check(object.Link_StorageGroup, acl.HdrObjSysLinkSG) + } +} diff --git a/lib/acl/match.go b/lib/acl/match.go new file mode 100644 index 0000000000..7d4289cb40 --- /dev/null +++ b/lib/acl/match.go @@ -0,0 +1,94 @@ +package acl + +import ( + "github.com/nspcc-dev/neofs-api-go/acl" +) + +// Maps MatchType to corresponding function. +// 1st argument of function - header value, 2nd - header filter. +var mMatchFns = map[acl.MatchType]func(acl.Header, acl.Header) bool{ + acl.StringEqual: stringEqual, + + acl.StringNotEqual: stringNotEqual, +} + +const ( + mResUndefined = iota + mResMatch + mResMismatch +) + +// MatchFilters checks if passed source carry at least one header that satisfies passed filters. +// +// Nil header does not satisfy any filter. Any header does not satisfy nil filter. +// +// Returns mResMismatch if passed TypedHeaderSource is nil. +// Returns mResMatch if passed filters are empty. +// +// If headers for some of the HeaderType could not be composed, mResUndefined returns. +func MatchFilters(src TypedHeaderSource, filters []acl.HeaderFilter) int { + if src == nil { + return mResMismatch + } else if len(filters) == 0 { + return mResMatch + } + + matched := 0 + + for _, filter := range filters { + // prevent NPE + if filter == nil { + continue + } + + headers, ok := src.HeadersOfType(filter.HeaderType()) + if !ok { + return mResUndefined + } + + // get headers of filtering type + for _, header := range headers { + // prevent NPE + if header == nil { + continue + } + + // check header name + if header.Name() != filter.Name() { + continue + } + + // get match function + matchFn, ok := mMatchFns[filter.MatchType()] + if !ok { + continue + } + + // check match + if !matchFn(header, filter) { + continue + } + + // increment match counter + matched++ + + break + } + } + + res := mResMismatch + + if matched >= len(filters) { + res = mResMatch + } + + return res +} + +func stringEqual(header, filter acl.Header) bool { + return header.Value() == filter.Value() +} + +func stringNotEqual(header, filter acl.Header) bool { + return header.Value() != filter.Value() +} diff --git a/lib/acl/match_test.go b/lib/acl/match_test.go new file mode 100644 index 0000000000..123f852bc1 --- /dev/null +++ b/lib/acl/match_test.go @@ -0,0 +1,192 @@ +package acl + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/stretchr/testify/require" +) + +type testTypedHeader struct { + t acl.HeaderType + k string + v string +} + +type testHeaderSrc struct { + hs []acl.TypedHeader +} + +type testHeaderFilter struct { + acl.TypedHeader + t acl.MatchType +} + +func (s testHeaderFilter) MatchType() acl.MatchType { + return s.t +} + +func (s testHeaderSrc) HeadersOfType(typ acl.HeaderType) ([]acl.Header, bool) { + res := make([]acl.Header, 0, len(s.hs)) + + for i := range s.hs { + if s.hs[i].HeaderType() == typ { + res = append(res, s.hs[i]) + } + } + + return res, true +} + +func (s testTypedHeader) Name() string { + return s.k +} + +func (s testTypedHeader) Value() string { + return s.v +} + +func (s testTypedHeader) HeaderType() acl.HeaderType { + return s.t +} + +func TestMatchFilters(t *testing.T) { + // nil TypedHeaderSource + require.Equal(t, mResMismatch, MatchFilters(nil, nil)) + + // empty HeaderFilter list + require.Equal(t, mResMatch, MatchFilters(new(testHeaderSrc), nil)) + + k := "key" + v := "value" + ht := acl.HeaderType(1) + + items := []struct { + // list of Key-Value-HeaderType for headers construction + hs []interface{} + // list of Key-Value-HeaderType-MatchType for filters construction + fs []interface{} + exp int + }{ + { // different HeaderType + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k, v, ht + 1, acl.StringEqual, + }, + exp: mResMismatch, + }, + { // different keys + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k + "1", v, ht, acl.StringEqual, + }, + exp: mResMismatch, + }, + { // equal values, StringEqual + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k, v, ht, acl.StringEqual, + }, + exp: mResMatch, + }, + { // equal values, StringNotEqual + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k, v, ht, acl.StringNotEqual, + }, + exp: mResMismatch, + }, + { // not equal values, StringEqual + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k, v + "1", ht, acl.StringEqual, + }, + exp: mResMismatch, + }, + { // not equal values, StringNotEqual + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k, v + "1", ht, acl.StringNotEqual, + }, + exp: mResMatch, + }, + { // one header, two filters + hs: []interface{}{ + k, v, ht, + }, + fs: []interface{}{ + k, v + "1", ht, acl.StringNotEqual, + k, v, ht, acl.StringEqual, + }, + exp: mResMatch, + }, + { // two headers, one filter + hs: []interface{}{ + k, v + "1", ht, + k, v, ht, + }, + fs: []interface{}{ + k, v, ht, acl.StringEqual, + }, + exp: mResMatch, + }, + { + hs: []interface{}{ + k, v + "1", acl.HdrTypeRequest, + k, v, acl.HdrTypeObjUsr, + }, + fs: []interface{}{ + k, v, acl.HdrTypeRequest, acl.StringNotEqual, + k, v, acl.HdrTypeObjUsr, acl.StringEqual, + }, + exp: mResMatch, + }, + } + + for _, item := range items { + headers := make([]acl.TypedHeader, 0) + + for i := 0; i < len(item.hs); i += 3 { + headers = append(headers, &testTypedHeader{ + t: item.hs[i+2].(acl.HeaderType), + k: item.hs[i].(string), + v: item.hs[i+1].(string), + }) + } + + filters := make([]acl.HeaderFilter, 0) + + for i := 0; i < len(item.fs); i += 4 { + filters = append(filters, &testHeaderFilter{ + TypedHeader: &testTypedHeader{ + t: item.fs[i+2].(acl.HeaderType), + k: item.fs[i].(string), + v: item.fs[i+1].(string), + }, + t: item.fs[i+3].(acl.MatchType), + }) + } + + require.Equal(t, + item.exp, + MatchFilters( + &testHeaderSrc{ + hs: headers, + }, + filters, + ), + ) + } +} diff --git a/lib/blockchain/event/event.go b/lib/blockchain/event/event.go new file mode 100644 index 0000000000..d614844ce6 --- /dev/null +++ b/lib/blockchain/event/event.go @@ -0,0 +1,31 @@ +package event + +// Type is a notification event enumeration type. +type Type string + +// Event is an interface that is +// provided by Neo:Morph event structures. +type Event interface { + MorphEvent() +} + +// Equal compares two Type values and +// returns true if they are equal. +func (t Type) Equal(t2 Type) bool { + return string(t) == string(t2) +} + +// String returns casted to string Type. +func (t Type) String() string { + return string(t) +} + +// TypeFromBytes converts bytes slice to Type. +func TypeFromBytes(data []byte) Type { + return Type(data) +} + +// TypeFromString converts string to Type. +func TypeFromString(str string) Type { + return Type(str) +} diff --git a/lib/blockchain/event/handler.go b/lib/blockchain/event/handler.go new file mode 100644 index 0000000000..2d9c5b7744 --- /dev/null +++ b/lib/blockchain/event/handler.go @@ -0,0 +1,22 @@ +package event + +// Handler is an Event processing function. +type Handler func(Event) + +// HandlerInfo is a structure that groups +// the parameters of the handler of particular +// contract event. +type HandlerInfo struct { + scriptHashWithType + + h Handler +} + +// SetHandler is an event handler setter. +func (s *HandlerInfo) SetHandler(v Handler) { + s.h = v +} + +func (s HandlerInfo) handler() Handler { + return s.h +} diff --git a/lib/blockchain/event/listener.go b/lib/blockchain/event/listener.go new file mode 100644 index 0000000000..2dcfceb3c5 --- /dev/null +++ b/lib/blockchain/event/listener.go @@ -0,0 +1,309 @@ +package event + +import ( + "context" + "sync" + + "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/nspcc-dev/neofs-node/lib/blockchain/subscriber" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +// Listener is an interface of smart contract notification event listener. +type Listener interface { + // Must start the event listener. + // + // Must listen to events with the parser installed. + // + // Must return an error if event listening could not be started. + Listen(context.Context) + + // Must set the parser of particular contract event. + // + // Parser of each event must be set once. All parsers must be set before Listen call. + // + // Must ignore nil parsers and all calls after listener has been started. + SetParser(ParserInfo) + + // Must register the event handler for particular notification event of contract. + // + // The specified handler must be called after each capture and parsing of the event + // + // Must ignore nil handlers. + RegisterHandler(HandlerInfo) +} + +// ListenerParams is a group of parameters +// for Listener constructor. +type ListenerParams struct { + Logger *zap.Logger + + Subscriber subscriber.Subscriber +} + +type listener struct { + mtx *sync.RWMutex + + once *sync.Once + + started bool + + parsers map[scriptHashWithType]Parser + + handlers map[scriptHashWithType][]Handler + + log *zap.Logger + + subscriber subscriber.Subscriber +} + +const ( + newListenerFailMsg = "could not instantiate Listener" + + errNilLogger = internal.Error("nil logger") + + errNilSubscriber = internal.Error("nil event subscriber") +) + +// Listen starts the listening for events with registered handlers. +// +// Executes once, all subsequent calls do nothing. +// +// Returns an error if listener was already started. +func (s listener) Listen(ctx context.Context) { + s.once.Do(func() { + if err := s.listen(ctx); err != nil { + s.log.Error("could not start listen to events", + zap.String("error", err.Error()), + ) + } + }) +} + +func (s listener) listen(ctx context.Context) error { + // create the list of listening contract hashes + hashes := make([]util.Uint160, 0) + + // fill the list with the contracts with set event parsers. + s.mtx.RLock() + for hashType := range s.parsers { + scHash := hashType.scriptHash() + + // prevent repetitions + for _, hash := range hashes { + if hash.Equals(scHash) { + continue + } + } + + hashes = append(hashes, hashType.scriptHash()) + } + + // mark listener as started + s.started = true + + s.mtx.RUnlock() + + chEvent, err := s.subscriber.SubscribeForNotification(hashes...) + if err != nil { + return err + } + + s.listenLoop(ctx, chEvent) + + return nil +} + +func (s listener) listenLoop(ctx context.Context, chEvent <-chan *result.NotificationEvent) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn("stop event listener by context", + zap.String("error", ctx.Err().Error()), + ) + break loop + case notifyEvent, ok := <-chEvent: + if !ok { + s.log.Warn("stop event listener by channel") + break loop + } else if notifyEvent == nil { + s.log.Warn("nil notification event was caught") + continue loop + } + + s.parseAndHandle(notifyEvent) + } + } +} + +func (s listener) parseAndHandle(notifyEvent *result.NotificationEvent) { + log := s.log.With( + zap.String("script hash LE", notifyEvent.Contract.StringLE()), + ) + + // stack item must be an array of items + arr, err := goclient.ArrayFromStackParameter(notifyEvent.Item) + if err != nil { + log.Warn("stack item is not an array type", + zap.String("error", err.Error()), + ) + + return + } else if len(arr) == 0 { + log.Warn("stack item array is empty") + return + } + + // first item must be a byte array + typBytes, err := goclient.BytesFromStackParameter(arr[0]) + if err != nil { + log.Warn("first array item is not a byte array", + zap.String("error", err.Error()), + ) + + return + } + + // calculate event type from bytes + typEvent := TypeFromBytes(typBytes) + + log = log.With( + zap.Stringer("event type", typEvent), + ) + + // get the event parser + keyEvent := scriptHashWithType{} + keyEvent.SetScriptHash(notifyEvent.Contract) + keyEvent.SetType(typEvent) + + s.mtx.RLock() + parser, ok := s.parsers[keyEvent] + s.mtx.RUnlock() + + if !ok { + log.Warn("event parser not set") + + return + } + + // parse the notification event + event, err := parser(arr[1:]) + if err != nil { + log.Warn("could not parse notification event", + zap.String("error", err.Error()), + ) + + return + } + + // handler the event + s.mtx.RLock() + handlers := s.handlers[keyEvent] + s.mtx.RUnlock() + + if len(handlers) == 0 { + log.Info("handlers for parsed notification event were not registered", + zap.Any("event", event), + ) + + return + } + + for _, handler := range handlers { + handler(event) + } +} + +// SetParser sets the parser of particular contract event. +// +// Ignores nil and already set parsers. +// Ignores the parser if listener is started. +func (s listener) SetParser(p ParserInfo) { + log := s.log.With( + zap.String("script hash LE", p.scriptHash().StringLE()), + zap.Stringer("event type", p.getType()), + ) + + parser := p.parser() + if parser == nil { + log.Info("ignore nil event parser") + return + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + // check if the listener was started + if s.started { + log.Warn("listener has been already started, ignore parser") + return + } + + // add event parser + if _, ok := s.parsers[p.scriptHashWithType]; !ok { + s.parsers[p.scriptHashWithType] = p.parser() + } + + log.Info("registered new event parser") +} + +// RegisterHandler registers the handler for particular notification event of contract. +// +// Ignores nil handlers. +// Ignores handlers of event without parser. +func (s listener) RegisterHandler(p HandlerInfo) { + log := s.log.With( + zap.String("script hash LE", p.scriptHash().StringLE()), + zap.Stringer("event type", p.getType()), + ) + + handler := p.handler() + if handler == nil { + log.Warn("ignore nil event handler") + return + } + + // check if parser was set + s.mtx.RLock() + _, ok := s.parsers[p.scriptHashWithType] + s.mtx.RUnlock() + + if !ok { + log.Warn("ignore handler of event w/o parser") + return + } + + // add event handler + s.mtx.Lock() + s.handlers[p.scriptHashWithType] = append( + s.handlers[p.scriptHashWithType], + p.handler(), + ) + s.mtx.Unlock() + + log.Info("registered new event handler") +} + +// NewListener create the notification event listener instance and returns Listener interface. +func NewListener(p ListenerParams) (Listener, error) { + switch { + case p.Logger == nil: + return nil, errors.Wrap(errNilLogger, newListenerFailMsg) + case p.Subscriber == nil: + return nil, errors.Wrap(errNilSubscriber, newListenerFailMsg) + } + + return &listener{ + mtx: new(sync.RWMutex), + once: new(sync.Once), + parsers: make(map[scriptHashWithType]Parser), + handlers: make(map[scriptHashWithType][]Handler), + log: p.Logger, + subscriber: p.Subscriber, + }, nil +} diff --git a/lib/blockchain/event/netmap/epoch.go b/lib/blockchain/event/netmap/epoch.go new file mode 100644 index 0000000000..2445b85a16 --- /dev/null +++ b/lib/blockchain/event/netmap/epoch.go @@ -0,0 +1,39 @@ +package netmap + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/pkg/errors" +) + +// NewEpoch is a new epoch Neo:Morph event. +type NewEpoch struct { + num uint64 +} + +// MorphEvent implements Neo:Morph Event interface. +func (NewEpoch) MorphEvent() {} + +// EpochNumber returns new epoch number. +func (s NewEpoch) EpochNumber() uint64 { + return s.num +} + +// ParseNewEpoch is a parser of new epoch notification event. +// +// Result is type of NewEpoch. +func ParseNewEpoch(prms []smartcontract.Parameter) (event.Event, error) { + if ln := len(prms); ln != 1 { + return nil, event.WrongNumberOfParameters(1, ln) + } + + prmEpochNum, err := goclient.IntFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get integer epoch number") + } + + return NewEpoch{ + num: uint64(prmEpochNum), + }, nil +} diff --git a/lib/blockchain/event/netmap/epoch_test.go b/lib/blockchain/event/netmap/epoch_test.go new file mode 100644 index 0000000000..48342697b2 --- /dev/null +++ b/lib/blockchain/event/netmap/epoch_test.go @@ -0,0 +1,47 @@ +package netmap + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/stretchr/testify/require" +) + +func TestParseNewEpoch(t *testing.T) { + t.Run("wrong number of parameters", func(t *testing.T) { + prms := []smartcontract.Parameter{ + {}, + {}, + } + + _, err := ParseNewEpoch(prms) + require.EqualError(t, err, event.WrongNumberOfParameters(1, len(prms)).Error()) + }) + + t.Run("wrong first parameter type", func(t *testing.T) { + _, err := ParseNewEpoch([]smartcontract.Parameter{ + { + Type: smartcontract.ByteArrayType, + }, + }) + + require.Error(t, err) + }) + + t.Run("correct behavior", func(t *testing.T) { + epochNum := uint64(100) + + ev, err := ParseNewEpoch([]smartcontract.Parameter{ + { + Type: smartcontract.IntegerType, + Value: int64(epochNum), + }, + }) + + require.NoError(t, err) + require.Equal(t, NewEpoch{ + num: epochNum, + }, ev) + }) +} diff --git a/lib/blockchain/event/parser.go b/lib/blockchain/event/parser.go new file mode 100644 index 0000000000..f0fdbc0936 --- /dev/null +++ b/lib/blockchain/event/parser.go @@ -0,0 +1,53 @@ +package event + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/pkg/errors" +) + +// Parser is a function that constructs Event +// from the StackItem list. +type Parser func([]smartcontract.Parameter) (Event, error) + +// ParserInfo is a structure that groups +// the parameters of particular contract +// notification event parser. +type ParserInfo struct { + scriptHashWithType + + p Parser +} + +type wrongPrmNumber struct { + exp, act int +} + +// WrongNumberOfParameters returns an error about wrong number of smart contract parameters. +func WrongNumberOfParameters(exp, act int) error { + return &wrongPrmNumber{ + exp: exp, + act: act, + } +} + +func (s wrongPrmNumber) Error() string { + return errors.Errorf("wrong parameter count: expected %d, has %d", s.exp, s.act).Error() +} + +// SetParser is an event parser setter. +func (s *ParserInfo) SetParser(v Parser) { + s.p = v +} + +func (s ParserInfo) parser() Parser { + return s.p +} + +// SetType is an event type setter. +func (s *ParserInfo) SetType(v Type) { + s.typ = v +} + +func (s ParserInfo) getType() Type { + return s.typ +} diff --git a/lib/blockchain/event/utils.go b/lib/blockchain/event/utils.go new file mode 100644 index 0000000000..66ef187d03 --- /dev/null +++ b/lib/blockchain/event/utils.go @@ -0,0 +1,34 @@ +package event + +import "github.com/nspcc-dev/neo-go/pkg/util" + +type scriptHashValue struct { + hash util.Uint160 +} + +type typeValue struct { + typ Type +} + +type scriptHashWithType struct { + scriptHashValue + typeValue +} + +// SetScriptHash is a script hash setter. +func (s *scriptHashValue) SetScriptHash(v util.Uint160) { + s.hash = v +} + +func (s scriptHashValue) scriptHash() util.Uint160 { + return s.hash +} + +// SetType is an event type setter. +func (s *typeValue) SetType(v Type) { + s.typ = v +} + +func (s typeValue) getType() Type { + return s.typ +} diff --git a/lib/blockchain/goclient/client.go b/lib/blockchain/goclient/client.go new file mode 100644 index 0000000000..977c9b8005 --- /dev/null +++ b/lib/blockchain/goclient/client.go @@ -0,0 +1,190 @@ +package goclient + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "time" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // Params is a group of Client's constructor parameters. + Params struct { + Log *zap.Logger + Key *ecdsa.PrivateKey + Endpoint string + Magic netmode.Magic + DialTimeout time.Duration + } + + // Client is a neo-go wrapper that provides smart-contract invocation interface. + Client struct { + log *zap.Logger + cli *client.Client + acc *wallet.Account + } +) + +// ErrNilClient is returned by functions that expect +// a non-nil Client, but received nil. +const ErrNilClient = internal.Error("go client is nil") + +// HaltState returned if TestInvoke function processed without panic. +const HaltState = "HALT" + +// ErrMissingFee is returned by functions that expect +// a positive invocation fee, but received non-positive. +const ErrMissingFee = internal.Error("invocation fee must be positive") + +var ( + errNilParams = errors.New("chain/client: config was not provided to the constructor") + + errNilLogger = errors.New("chain/client: logger was not provided to the constructor") + + errNilKey = errors.New("chain/client: private key was not provided to the constructor") +) + +// Invoke invokes contract method by sending transaction into blockchain. +// Supported args types: int64, string, util.Uint160, []byte and bool. +// +// If passed fee is non-positive, ErrMissingFee returns. +func (c *Client) Invoke(contract util.Uint160, fee util.Fixed8, method string, args ...interface{}) error { + var params []sc.Parameter + for i := range args { + param, err := toStackParameter(args[i]) + if err != nil { + return err + } + + params = append(params, param) + } + + cosigner := []transaction.Cosigner{ + { + Account: c.acc.PrivateKey().PublicKey().GetScriptHash(), + Scopes: transaction.Global, + }, + } + + resp, err := c.cli.InvokeFunction(contract, method, params, cosigner) + if err != nil { + return err + } + + if len(resp.Script) == 0 { + return errors.New("chain/client: got empty invocation script from neo node") + } + + script, err := hex.DecodeString(resp.Script) + if err != nil { + return errors.New("chain/client: can't decode invocation script from neo node") + } + + txHash, err := c.cli.SignAndPushInvocationTx(script, c.acc, 0, fee, cosigner) + if err != nil { + return err + } + + c.log.Debug("neo client invoke", + zap.String("method", method), + zap.Stringer("tx_hash", txHash)) + + return nil +} + +// TestInvoke invokes contract method locally in neo-go node. This method should +// be used to read data from smart-contract. +func (c *Client) TestInvoke(contract util.Uint160, method string, args ...interface{}) ([]sc.Parameter, error) { + var params = make([]sc.Parameter, 0, len(args)) + + for i := range args { + p, err := toStackParameter(args[i]) + if err != nil { + return nil, err + } + + params = append(params, p) + } + + cosigner := []transaction.Cosigner{ + { + Account: c.acc.PrivateKey().PublicKey().GetScriptHash(), + Scopes: transaction.Global, + }, + } + + val, err := c.cli.InvokeFunction(contract, method, params, cosigner) + if err != nil { + return nil, err + } + + if val.State != HaltState { + return nil, errors.Errorf("chain/client: contract execution finished with state %s", val.State) + } + + return val.Stack, nil +} + +// New is a Client constructor. +func New(ctx context.Context, p *Params) (*Client, error) { + switch { + case p == nil: + return nil, errNilParams + case p.Log == nil: + return nil, errNilLogger + case p.Key == nil: + return nil, errNilKey + } + + privKeyBytes := crypto.MarshalPrivateKey(p.Key) + + wif, err := keys.WIFEncode(privKeyBytes, keys.WIFVersion, true) + if err != nil { + return nil, err + } + + account, err := wallet.NewAccountFromWIF(wif) + if err != nil { + return nil, err + } + + cli, err := client.New(ctx, p.Endpoint, client.Options{ + DialTimeout: p.DialTimeout, + Network: p.Magic, + }) + if err != nil { + return nil, err + } + + return &Client{log: p.Log, cli: cli, acc: account}, nil +} + +func toStackParameter(value interface{}) (sc.Parameter, error) { + var result = sc.Parameter{ + Value: value, + } + + // todo: add more types + switch value.(type) { + case []byte: + result.Type = sc.ByteArrayType + case int64: // TODO: add other numerical types + result.Type = sc.IntegerType + default: + return result, errors.Errorf("chain/client: unsupported parameter %v", value) + } + + return result, nil +} diff --git a/lib/blockchain/goclient/client_test.go b/lib/blockchain/goclient/client_test.go new file mode 100644 index 0000000000..90d2c271aa --- /dev/null +++ b/lib/blockchain/goclient/client_test.go @@ -0,0 +1,33 @@ +package goclient + +import ( + "testing" + + sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/stretchr/testify/require" +) + +func TestToStackParameter(t *testing.T) { + items := []struct { + value interface{} + expType sc.ParamType + }{ + { + value: []byte{1, 2, 3}, + expType: sc.ByteArrayType, + }, + { + value: int64(100), + expType: sc.IntegerType, + }, + } + + for _, item := range items { + t.Run(item.expType.String()+" to stack parameter", func(t *testing.T) { + res, err := toStackParameter(item.value) + require.NoError(t, err) + require.Equal(t, item.expType, res.Type) + require.Equal(t, item.value, res.Value) + }) + } +} diff --git a/lib/blockchain/goclient/util.go b/lib/blockchain/goclient/util.go new file mode 100644 index 0000000000..82e30f49b9 --- /dev/null +++ b/lib/blockchain/goclient/util.go @@ -0,0 +1,131 @@ +package goclient + +import ( + "encoding/binary" + + sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/pkg/errors" +) + +/* + Use these function to parse stack parameters obtained from `TestInvoke` + function to native go types. You should know upfront return types of invoked + method. +*/ + +// BoolFromStackParameter receives boolean value from the value of a smart contract parameter. +func BoolFromStackParameter(param sc.Parameter) (bool, error) { + switch param.Type { + case sc.BoolType: + val, ok := param.Value.(bool) + if !ok { + return false, errors.Errorf("chain/client: can't convert %T to boolean", param.Value) + } + + return val, nil + case sc.IntegerType: + val, ok := param.Value.(int64) + if !ok { + return false, errors.Errorf("chain/client: can't convert %T to boolean", param.Value) + } + + return val > 0, nil + case sc.ByteArrayType: + val, ok := param.Value.([]byte) + if !ok { + return false, errors.Errorf("chain/client: can't convert %T to boolean", param.Value) + } + + return len(val) != 0, nil + default: + return false, errors.Errorf("chain/client: %s is not a bool type", param.Type) + } +} + +// IntFromStackParameter receives numerical value from the value of a smart contract parameter. +func IntFromStackParameter(param sc.Parameter) (int64, error) { + switch param.Type { + case sc.IntegerType: + val, ok := param.Value.(int64) + if !ok { + return 0, errors.Errorf("chain/client: can't convert %T to integer", param.Value) + } + + return val, nil + case sc.ByteArrayType: + val, ok := param.Value.([]byte) + if !ok || len(val) > 8 { + return 0, errors.Errorf("chain/client: can't convert %T to integer", param.Value) + } + + res := make([]byte, 8) + copy(res[:len(val)], val) + + return int64(binary.LittleEndian.Uint64(res)), nil + default: + return 0, errors.Errorf("chain/client: %s is not an integer type", param.Type) + } +} + +// BytesFromStackParameter receives binary value from the value of a smart contract parameter. +func BytesFromStackParameter(param sc.Parameter) ([]byte, error) { + if param.Type != sc.ByteArrayType { + return nil, errors.Errorf("chain/client: %s is not a byte array type", param.Type) + } + + val, ok := param.Value.([]byte) + if !ok { + return nil, errors.Errorf("chain/client: can't convert %T to byte slice", param.Value) + } + + return val, nil +} + +// ArrayFromStackParameter returns the slice contract parameters from passed parameter. +// +// If passed parameter carries boolean false value, (nil, nil) returns. +func ArrayFromStackParameter(param sc.Parameter) ([]sc.Parameter, error) { + if param.Type == sc.BoolType && !param.Value.(bool) { + return nil, nil + } + + if param.Type != sc.ArrayType { + return nil, errors.Errorf("chain/client: %s is not an array type", param.Type) + } + + val, ok := param.Value.([]sc.Parameter) + if !ok { + return nil, errors.Errorf("chain/client: can't convert %T to parameter slice", param.Value) + } + + return val, nil +} + +// StringFromStackParameter receives string value from the value of a smart contract parameter. +func StringFromStackParameter(param sc.Parameter) (string, error) { + switch param.Type { + case sc.StringType: + val, ok := param.Value.(string) + if !ok { + return "", errors.Errorf("chain/client: can't convert %T to string", param.Value) + } + + return val, nil + case sc.ByteArrayType: + val, ok := param.Value.([]byte) + if !ok { + return "", errors.Errorf("chain/client: can't convert %T to string", param.Value) + } + + return string(val), nil + default: + return "", errors.Errorf("chain/client: %s is not a string type", param.Type) + } +} + +// ReadStorage of the contract directly. Use it for debug, try to obtain +// smart-contract data from contract method with TestInvoke function. +func ReadStorage(c *Client, contract util.Uint160, key []byte) ([]byte, error) { + return c.cli.GetStorageByHash(contract, key) +} diff --git a/lib/blockchain/goclient/util_test.go b/lib/blockchain/goclient/util_test.go new file mode 100644 index 0000000000..5752e2dda2 --- /dev/null +++ b/lib/blockchain/goclient/util_test.go @@ -0,0 +1,145 @@ +package goclient + +import ( + "testing" + + sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/stretchr/testify/require" +) + +var ( + stringParam = sc.Parameter{ + Type: sc.StringType, + Value: "Hello World", + } + + intParam = sc.Parameter{ + Type: sc.IntegerType, + Value: int64(1), + } + + byteWithIntParam = sc.Parameter{ + Type: sc.ByteArrayType, + Value: []byte{0x0a}, + } + + byteArrayParam = sc.Parameter{ + Type: sc.ByteArrayType, + Value: []byte("Hello World"), + } + + emptyByteArrayParam = sc.Parameter{ + Type: sc.ByteArrayType, + Value: []byte{}, + } + + trueBoolParam = sc.Parameter{ + Type: sc.BoolType, + Value: true, + } + + falseBoolParam = sc.Parameter{ + Type: sc.BoolType, + Value: false, + } + + arrayParam = sc.Parameter{ + Type: sc.ArrayType, + Value: []sc.Parameter{intParam, byteArrayParam}, + } +) + +func TestBoolFromStackParameter(t *testing.T) { + t.Run("true assert", func(t *testing.T) { + val, err := BoolFromStackParameter(trueBoolParam) + require.NoError(t, err) + require.True(t, val) + + val, err = BoolFromStackParameter(intParam) + require.NoError(t, err) + require.True(t, val) + }) + + t.Run("false assert", func(t *testing.T) { + val, err := BoolFromStackParameter(falseBoolParam) + require.NoError(t, err) + require.False(t, val) + + val, err = BoolFromStackParameter(emptyByteArrayParam) + require.NoError(t, err) + require.False(t, val) + }) + + t.Run("incorrect assert", func(t *testing.T) { + _, err := BoolFromStackParameter(stringParam) + require.Error(t, err) + }) +} + +func TestArrayFromStackParameter(t *testing.T) { + t.Run("correct assert", func(t *testing.T) { + val, err := ArrayFromStackParameter(arrayParam) + require.NoError(t, err) + require.Len(t, val, len(arrayParam.Value.([]sc.Parameter))) + }) + t.Run("incorrect assert", func(t *testing.T) { + _, err := ArrayFromStackParameter(byteArrayParam) + require.Error(t, err) + }) + t.Run("boolean false case", func(t *testing.T) { + val, err := ArrayFromStackParameter(falseBoolParam) + require.NoError(t, err) + require.Nil(t, val) + }) +} + +func TestBytesFromStackParameter(t *testing.T) { + t.Run("correct assert", func(t *testing.T) { + val, err := BytesFromStackParameter(byteArrayParam) + require.NoError(t, err) + require.Equal(t, byteArrayParam.Value.([]byte), val) + }) + + t.Run("incorrect assert", func(t *testing.T) { + _, err := BytesFromStackParameter(stringParam) + require.Error(t, err) + }) +} + +func TestIntFromStackParameter(t *testing.T) { + t.Run("correct assert", func(t *testing.T) { + val, err := IntFromStackParameter(intParam) + require.NoError(t, err) + require.Equal(t, intParam.Value.(int64), val) + + val, err = IntFromStackParameter(byteWithIntParam) + require.NoError(t, err) + require.Equal(t, int64(0x0a), val) + + val, err = IntFromStackParameter(emptyByteArrayParam) + require.NoError(t, err) + require.Equal(t, int64(0), val) + }) + + t.Run("incorrect assert", func(t *testing.T) { + _, err := IntFromStackParameter(byteArrayParam) + require.Error(t, err) + }) +} + +func TestStringFromStackParameter(t *testing.T) { + t.Run("correct assert", func(t *testing.T) { + val, err := StringFromStackParameter(stringParam) + require.NoError(t, err) + require.Equal(t, stringParam.Value.(string), val) + + val, err = StringFromStackParameter(byteArrayParam) + require.NoError(t, err) + require.Equal(t, string(byteArrayParam.Value.([]byte)), val) + }) + + t.Run("incorrect assert", func(t *testing.T) { + _, err := StringFromStackParameter(intParam) + require.Error(t, err) + }) +} diff --git a/lib/blockchain/subscriber/subscriber.go b/lib/blockchain/subscriber/subscriber.go new file mode 100644 index 0000000000..5d2528e97e --- /dev/null +++ b/lib/blockchain/subscriber/subscriber.go @@ -0,0 +1,151 @@ +package subscriber + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/rpc/response" + "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "go.uber.org/zap" +) + +type ( + // Subscriber is an interface of the NotificationEvent listener. + Subscriber interface { + SubscribeForNotification(...util.Uint160) (<-chan *result.NotificationEvent, error) + UnsubscribeForNotification() + } + + subscriber struct { + *sync.RWMutex + log *zap.Logger + client *client.WSClient + + notify chan *result.NotificationEvent + notifyIDs map[util.Uint160]string + } + + // Params is a group of Subscriber constructor parameters. + Params struct { + Log *zap.Logger + Endpoint string + DialTimeout time.Duration + } +) + +var ( + errNilParams = errors.New("chain/subscriber: config was not provided to the constructor") + + errNilLogger = errors.New("chain/subscriber: logger was not provided to the constructor") +) + +func (s *subscriber) SubscribeForNotification(contracts ...util.Uint160) (<-chan *result.NotificationEvent, error) { + s.Lock() + defer s.Unlock() + + notifyIDs := make(map[util.Uint160]string, len(contracts)) + + for i := range contracts { + // do not subscribe to already subscribed contracts + if _, ok := s.notifyIDs[contracts[i]]; ok { + continue + } + + // subscribe to contract notifications + id, err := s.client.SubscribeForExecutionNotifications(&contracts[i]) + if err != nil { + // if there is some error, undo all subscriptions and return error + for _, id := range notifyIDs { + _ = s.client.Unsubscribe(id) + } + + return nil, err + } + + // save notification id + notifyIDs[contracts[i]] = id + } + + // update global map of subscribed contracts + for contract, id := range notifyIDs { + s.notifyIDs[contract] = id + } + + return s.notify, nil +} + +func (s *subscriber) UnsubscribeForNotification() { + s.Lock() + defer s.Unlock() + + for i := range s.notifyIDs { + err := s.client.Unsubscribe(s.notifyIDs[i]) + if err != nil { + s.log.Error("unsubscribe for notification", + zap.String("event", s.notifyIDs[i]), + zap.Error(err)) + } + + delete(s.notifyIDs, i) + } +} + +func (s *subscriber) routeNotifications(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case notification := <-s.client.Notifications: + switch notification.Type { + case response.NotificationEventID: + notification, ok := notification.Value.(*result.NotificationEvent) + if !ok { + s.log.Error("can't cast notify event to the notify struct") + continue + } + + s.notify <- notification + default: + s.log.Debug("unsupported notification from the chain", + zap.Uint8("type", uint8(notification.Type)), + ) + } + } + } +} + +// New is a constructs Neo:Morph event listener and returns Subscriber interface. +func New(ctx context.Context, p *Params) (Subscriber, error) { + switch { + case p == nil: + return nil, errNilParams + case p.Log == nil: + return nil, errNilLogger + } + + wsClient, err := client.NewWS(ctx, p.Endpoint, client.Options{ + DialTimeout: p.DialTimeout, + }) + if err != nil { + return nil, err + } + + sub := &subscriber{ + RWMutex: new(sync.RWMutex), + log: p.Log, + client: wsClient, + notify: make(chan *result.NotificationEvent), + notifyIDs: make(map[util.Uint160]string), + } + + // Worker listens all events from neo-go websocket and puts them + // into corresponding channel. It may be notifications, transactions, + // new blocks. For now only notifications. + go sub.routeNotifications(ctx) + + return sub, nil +} diff --git a/lib/boot/bootstrap_test.go b/lib/boot/bootstrap_test.go new file mode 100644 index 0000000000..206e2562ee --- /dev/null +++ b/lib/boot/bootstrap_test.go @@ -0,0 +1,24 @@ +package boot + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/stretchr/testify/require" +) + +func TestBootstrapPeerParams(t *testing.T) { + s := BootstrapPeerParams{} + + nodeInfo := &bootstrap.NodeInfo{ + Address: "address", + PubKey: []byte{1, 2, 3}, + Options: []string{ + "opt1", + "opt2", + }, + } + s.SetNodeInfo(nodeInfo) + + require.Equal(t, nodeInfo, s.NodeInfo()) +} diff --git a/lib/boot/bootstrapper.go b/lib/boot/bootstrapper.go new file mode 100644 index 0000000000..f97e6a7894 --- /dev/null +++ b/lib/boot/bootstrapper.go @@ -0,0 +1,31 @@ +package boot + +import ( + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-node/internal" +) + +// BootstrapPeerParams is a group of parameters +// for storage node bootstrap. +type BootstrapPeerParams struct { + info *bootstrap.NodeInfo +} + +// PeerBootstrapper is an interface of the NeoFS node bootstrap tool. +type PeerBootstrapper interface { + AddPeer(BootstrapPeerParams) error +} + +// ErrNilPeerBootstrapper is returned by functions that expect +// a non-nil PeerBootstrapper, but received nil. +const ErrNilPeerBootstrapper = internal.Error("peer bootstrapper is nil") + +// SetNodeInfo is a node info setter. +func (s *BootstrapPeerParams) SetNodeInfo(v *bootstrap.NodeInfo) { + s.info = v +} + +// NodeInfo is a node info getter. +func (s BootstrapPeerParams) NodeInfo() *bootstrap.NodeInfo { + return s.info +} diff --git a/lib/boot/storage.go b/lib/boot/storage.go new file mode 100644 index 0000000000..9043576ce2 --- /dev/null +++ b/lib/boot/storage.go @@ -0,0 +1,46 @@ +package boot + +import ( + "context" + + "go.uber.org/zap" +) + +// StorageBootParams is a group of parameters +// for storage node bootstrap operation. +type StorageBootParams struct { + BootstrapPeerParams +} + +// StorageBootController is an entity that performs +// registration of a storage node in NeoFS network. +type StorageBootController struct { + peerBoot PeerBootstrapper + + bootPrm StorageBootParams + + log *zap.Logger +} + +// SetPeerBootstrapper is a PeerBootstrapper setter. +func (s *StorageBootController) SetPeerBootstrapper(v PeerBootstrapper) { + s.peerBoot = v +} + +// SetBootParams is a storage node bootstrap parameters setter. +func (s *StorageBootController) SetBootParams(v StorageBootParams) { + s.bootPrm = v +} + +// SetLogger is a logging component setter. +func (s *StorageBootController) SetLogger(v *zap.Logger) { + s.log = v +} + +// Bootstrap registers storage node in NeoFS system. +func (s StorageBootController) Bootstrap(context.Context) { + // register peer in NeoFS network + if err := s.peerBoot.AddPeer(s.bootPrm.BootstrapPeerParams); err != nil && s.log != nil { + s.log.Error("could not register storage node in network") + } +} diff --git a/lib/buckets/boltdb/boltdb.go b/lib/buckets/boltdb/boltdb.go new file mode 100644 index 0000000000..4310151b10 --- /dev/null +++ b/lib/buckets/boltdb/boltdb.go @@ -0,0 +1,109 @@ +package boltdb + +import ( + "io/ioutil" + "log" + "os" + "path" + + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.etcd.io/bbolt" +) + +type ( + bucket struct { + db *bbolt.DB + name []byte + } + + // Options groups the BoltDB bucket's options. + Options struct { + bbolt.Options + Name []byte + Path string + Perm os.FileMode + } +) + +const ( + defaultFilePermission = 0777 + + errEmptyPath = internal.Error("database empty path") +) + +var _ core.Bucket = (*bucket)(nil) + +func makeCopy(val []byte) []byte { + tmp := make([]byte, len(val)) + copy(tmp, val) + + return tmp +} + +// NewOptions prepares options for badger instance. +func NewOptions(name core.BucketType, v *viper.Viper) (opts Options, err error) { + key := string(name) + opts = Options{ + Options: bbolt.Options{ + // set defaults: + Timeout: bbolt.DefaultOptions.Timeout, + FreelistType: bbolt.DefaultOptions.FreelistType, + + // set config options: + NoSync: v.GetBool(key + ".no_sync"), + ReadOnly: v.GetBool(key + ".read_only"), + NoGrowSync: v.GetBool(key + ".no_grow_sync"), + NoFreelistSync: v.GetBool(key + ".no_freelist_sync"), + + PageSize: v.GetInt(key + ".page_size"), + MmapFlags: v.GetInt(key + ".mmap_flags"), + InitialMmapSize: v.GetInt(key + ".initial_mmap_size"), + }, + + Name: []byte(name), + Perm: defaultFilePermission, + Path: v.GetString(key + ".path"), + } + + if opts.Path == "" { + return opts, errEmptyPath + } + + if tmp := v.GetDuration(key + ".lock_timeout"); tmp > 0 { + opts.Timeout = tmp + } + + if perm := v.GetUint32(key + ".perm"); perm != 0 { + opts.Perm = os.FileMode(perm) + } + + base := path.Dir(opts.Path) + if err := os.MkdirAll(base, opts.Perm); err != nil { + return opts, errors.Wrapf(err, "could not use `%s` dir", base) + } + + return opts, nil +} + +// NewBucket creates badger-bucket instance. +func NewBucket(opts *Options) (core.Bucket, error) { + log.SetOutput(ioutil.Discard) // disable default logger + + db, err := bbolt.Open(opts.Path, opts.Perm, &opts.Options) + if err != nil { + return nil, err + } + + err = db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(opts.Name) + return err + }) + if err != nil { + return nil, err + } + + return &bucket{db: db, name: opts.Name}, nil +} diff --git a/lib/buckets/boltdb/methods.go b/lib/buckets/boltdb/methods.go new file mode 100644 index 0000000000..b302a7dbd6 --- /dev/null +++ b/lib/buckets/boltdb/methods.go @@ -0,0 +1,94 @@ +package boltdb + +import ( + "os" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "go.etcd.io/bbolt" +) + +// Get value by key or return error. +func (b *bucket) Get(key []byte) (data []byte, err error) { + err = b.db.View(func(txn *bbolt.Tx) error { + txn.Bucket(b.name).Cursor().Seek(key) + val := txn.Bucket(b.name).Get(key) + if val == nil { + return errors.Wrapf(core.ErrNotFound, "key=%s", base58.Encode(key)) + } + + data = makeCopy(val) + return nil + }) + + return +} + +// Set value for key. +func (b *bucket) Set(key, value []byte) error { + return b.db.Update(func(txn *bbolt.Tx) error { + k, v := makeCopy(key), makeCopy(value) + return txn.Bucket(b.name).Put(k, v) + }) +} + +// Del removes item from bucket by key. +func (b *bucket) Del(key []byte) error { + return b.db.Update(func(txn *bbolt.Tx) error { + return txn.Bucket(b.name).Delete(key) + }) +} + +// Has checks key exists. +func (b *bucket) Has(key []byte) bool { + _, err := b.Get(key) + return !errors.Is(errors.Cause(err), core.ErrNotFound) +} + +// Size returns size of database. +func (b *bucket) Size() int64 { + info, err := os.Stat(b.db.Path()) + if err != nil { + return 0 + } + + return info.Size() +} + +// List all items in bucket. +func (b *bucket) List() ([][]byte, error) { + var items [][]byte + + if err := b.db.View(func(txn *bbolt.Tx) error { + return txn.Bucket(b.name).ForEach(func(k, _ []byte) error { + items = append(items, makeCopy(k)) + return nil + }) + }); err != nil { + return nil, err + } + + return items, nil +} + +// Filter elements by filter closure. +func (b *bucket) Iterate(handler core.FilterHandler) error { + if handler == nil { + return core.ErrNilFilterHandler + } + + return b.db.View(func(txn *bbolt.Tx) error { + return txn.Bucket(b.name).ForEach(func(k, v []byte) error { + if !handler(makeCopy(k), makeCopy(v)) { + return core.ErrIteratingAborted + } + return nil + }) + }) +} + +// Close bucket database. +func (b *bucket) Close() error { + return b.db.Close() +} diff --git a/lib/buckets/boltdb/methods_test.go b/lib/buckets/boltdb/methods_test.go new file mode 100644 index 0000000000..dc9517d739 --- /dev/null +++ b/lib/buckets/boltdb/methods_test.go @@ -0,0 +1,95 @@ +package boltdb + +import ( + "encoding/binary" + "io/ioutil" + "os" + "strings" + "testing" + "time" + + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +var config = strings.NewReader(` +storage: + test_bucket: + bucket: boltdb + path: ./temp/storage/test_bucket + perm: 0777 +`) + +func TestBucket(t *testing.T) { + file, err := ioutil.TempFile("", "test_bolt_db") + require.NoError(t, err) + require.NoError(t, file.Close()) + + v := viper.New() + require.NoError(t, v.ReadConfig(config)) + + // -- // + _, err = NewOptions("storage.test_bucket", v) + require.EqualError(t, err, errEmptyPath.Error()) + + v.SetDefault("storage.test_bucket.path", file.Name()) + v.SetDefault("storage.test_bucket.timeout", time.Millisecond*100) + // -- // + + opts, err := NewOptions("storage.test_bucket", v) + require.NoError(t, err) + + db, err := NewBucket(&opts) + require.NoError(t, err) + + require.NotPanics(t, func() { db.Size() }) + + var ( + count = uint64(10) + expected = []byte("test") + ) + + for i := uint64(0); i < count; i++ { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, i) + + require.False(t, db.Has(key)) + + val, err := db.Get(key) + require.EqualError(t, errors.Cause(err), core.ErrNotFound.Error()) + require.Empty(t, val) + + require.NoError(t, db.Set(key, expected)) + + require.True(t, db.Has(key)) + + val, err = db.Get(key) + require.NoError(t, err) + require.Equal(t, expected, val) + + keys, err := db.List() + require.NoError(t, err) + require.Len(t, keys, 1) + require.Equal(t, key, keys[0]) + + require.EqualError(t, db.Iterate(nil), core.ErrNilFilterHandler.Error()) + + items, err := core.ListBucketItems(db, func(_, _ []byte) bool { return true }) + require.NoError(t, err) + require.Len(t, items, 1) + require.Equal(t, key, items[0].Key) + require.Equal(t, val, items[0].Val) + + require.NoError(t, db.Del(key)) + require.False(t, db.Has(key)) + + val, err = db.Get(key) + require.EqualError(t, errors.Cause(err), core.ErrNotFound.Error()) + require.Empty(t, val) + } + + require.NoError(t, db.Close()) + require.NoError(t, os.RemoveAll(file.Name())) +} diff --git a/lib/buckets/boltdb/plugin/main.go b/lib/buckets/boltdb/plugin/main.go new file mode 100644 index 0000000000..04a8f9f220 --- /dev/null +++ b/lib/buckets/boltdb/plugin/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "github.com/nspcc-dev/neofs-node/lib/buckets/boltdb" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +var _ = PrepareBucket + +// PrepareBucket is interface method for bucket. +func PrepareBucket(name core.BucketType, v *viper.Viper) (db core.Bucket, err error) { + var opts boltdb.Options + + if opts, err = boltdb.NewOptions("storage."+name, v); err != nil { + err = errors.Wrapf(err, "%q: could not prepare options", name) + return + } else if db, err = boltdb.NewBucket(&opts); err != nil { + err = errors.Wrapf(err, "%q: could not prepare bucket", name) + return + } + + return +} diff --git a/lib/buckets/fsbucket/bucket.go b/lib/buckets/fsbucket/bucket.go new file mode 100644 index 0000000000..029d509c95 --- /dev/null +++ b/lib/buckets/fsbucket/bucket.go @@ -0,0 +1,101 @@ +package fsbucket + +import ( + "os" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/atomic" +) + +type ( + bucket struct { + dir string + perm os.FileMode + } + + treeBucket struct { + dir string + perm os.FileMode + + depth int + prefixLength int + sz *atomic.Int64 + } +) + +const ( + defaultDirectory = "fsbucket" + defaultPermissions = 0755 + defaultDepth = 2 + defaultPrefixLen = 2 +) + +const errShortKey = internal.Error("key is too short for tree fs bucket") + +var _ core.Bucket = (*bucket)(nil) + +func stringifyKey(key []byte) string { + return base58.Encode(key) +} + +func decodeKey(key string) []byte { + k, err := base58.Decode(key) + if err != nil { + panic(err) // it can fail only for not base58 strings + } + + return k +} + +// NewBucket creates new in-memory bucket instance. +func NewBucket(name core.BucketType, v *viper.Viper) (core.Bucket, error) { + var ( + key = "storage." + string(name) + dir string + perm os.FileMode + + prefixLen int + depth int + ) + + if dir = v.GetString(key + ".directory"); dir == "" { + dir = defaultDirectory + } + + if perm = os.FileMode(v.GetInt(key + ".permissions")); perm == 0 { + perm = defaultPermissions + } + + if depth = v.GetInt(key + ".depth"); depth <= 0 { + depth = defaultDepth + } + + if prefixLen = v.GetInt(key + ".prefix_len"); prefixLen <= 0 { + prefixLen = defaultPrefixLen + } + + if err := os.MkdirAll(dir, perm); err != nil { + return nil, errors.Wrapf(err, "could not create bucket %s", string(name)) + } + + if v.GetBool(key + ".tree_enabled") { + b := &treeBucket{ + dir: dir, + perm: perm, + depth: depth, + prefixLength: prefixLen, + } + b.sz = atomic.NewInt64(b.size()) + + return b, nil + } + + return &bucket{ + dir: dir, + perm: perm, + }, nil +} diff --git a/lib/buckets/fsbucket/methods.go b/lib/buckets/fsbucket/methods.go new file mode 100644 index 0000000000..9aeaf45f25 --- /dev/null +++ b/lib/buckets/fsbucket/methods.go @@ -0,0 +1,107 @@ +package fsbucket + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/nspcc-dev/neofs-node/lib/core" +) + +// Get value by key. +func (b *bucket) Get(key []byte) ([]byte, error) { + p := path.Join(b.dir, stringifyKey(key)) + if _, err := os.Stat(p); os.IsNotExist(err) { + return nil, core.ErrNotFound + } + + return ioutil.ReadFile(p) +} + +// Set value by key. +func (b *bucket) Set(key, value []byte) error { + p := path.Join(b.dir, stringifyKey(key)) + + return ioutil.WriteFile(p, value, b.perm) +} + +// Del value by key. +func (b *bucket) Del(key []byte) error { + p := path.Join(b.dir, stringifyKey(key)) + if _, err := os.Stat(p); os.IsNotExist(err) { + return core.ErrNotFound + } + + return os.Remove(p) +} + +// Has checks key exists. +func (b *bucket) Has(key []byte) bool { + p := path.Join(b.dir, stringifyKey(key)) + _, err := os.Stat(p) + + return err == nil +} + +func listing(root string, fn func(path string, info os.FileInfo) error) error { + return filepath.Walk(root, func(p string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + + if fn == nil { + return nil + } + + return fn(p, info) + }) +} + +// Size of bucket. +func (b *bucket) Size() (size int64) { + err := listing(b.dir, func(_ string, info os.FileInfo) error { + size += info.Size() + return nil + }) + + if err != nil { + size = 0 + } + + return +} + +// List all bucket items. +func (b *bucket) List() ([][]byte, error) { + buckets := make([][]byte, 0) + + err := listing(b.dir, func(p string, info os.FileInfo) error { + buckets = append(buckets, decodeKey(info.Name())) + return nil + }) + + return buckets, err +} + +// Filter bucket items by closure. +func (b *bucket) Iterate(handler core.FilterHandler) error { + return listing(b.dir, func(p string, info os.FileInfo) error { + key := decodeKey(info.Name()) + val, err := ioutil.ReadFile(p) + if err != nil { + return err + } + + if !handler(key, val) { + return core.ErrIteratingAborted + } + + return nil + }) +} + +// Close bucket (just empty). +func (b *bucket) Close() error { + return os.RemoveAll(b.dir) +} diff --git a/lib/buckets/fsbucket/queue.go b/lib/buckets/fsbucket/queue.go new file mode 100644 index 0000000000..e2b0361629 --- /dev/null +++ b/lib/buckets/fsbucket/queue.go @@ -0,0 +1,44 @@ +package fsbucket + +import "sync" + +type ( + queue struct { + *sync.RWMutex + buf []elem + } + + elem struct { + depth int + prefix string + path string + } +) + +func newQueue(n int) *queue { + return &queue{ + RWMutex: new(sync.RWMutex), + buf: make([]elem, 0, n), + } +} + +func (q *queue) Len() int { + return len(q.buf) +} + +func (q *queue) Push(s elem) { + q.Lock() + q.buf = append(q.buf, s) + q.Unlock() +} + +func (q *queue) Pop() (s elem) { + q.Lock() + if len(q.buf) > 0 { + s = q.buf[0] + q.buf = q.buf[1:] + } + q.Unlock() + + return +} diff --git a/lib/buckets/fsbucket/treemethods.go b/lib/buckets/fsbucket/treemethods.go new file mode 100644 index 0000000000..1a1927a826 --- /dev/null +++ b/lib/buckets/fsbucket/treemethods.go @@ -0,0 +1,261 @@ +package fsbucket + +import ( + "encoding/hex" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/nspcc-dev/neofs-node/lib/core" +) + +const queueCap = 1000 + +func stringifyHexKey(key []byte) string { + return hex.EncodeToString(key) +} + +func decodeHexKey(key string) ([]byte, error) { + k, err := hex.DecodeString(key) + if err != nil { + return nil, err + } + + return k, nil +} + +// treePath returns slice of the dir names that contain the path +// and filename, e.g. 0xabcdef => []string{"ab", "cd"}, "abcdef". +// In case of errors - return nil slice. +func (b *treeBucket) treePath(key []byte) ([]string, string) { + filename := stringifyHexKey(key) + if len(filename) <= b.prefixLength*b.depth { + return nil, filename + } + + filepath := filename + dirs := make([]string, 0, b.depth) + + for i := 0; i < b.depth; i++ { + dirs = append(dirs, filepath[:b.prefixLength]) + filepath = filepath[b.prefixLength:] + } + + return dirs, filename +} + +// Get value by key. +func (b *treeBucket) Get(key []byte) ([]byte, error) { + dirPaths, filename := b.treePath(key) + if dirPaths == nil { + return nil, errShortKey + } + + p := path.Join(b.dir, path.Join(dirPaths...), filename) + + if _, err := os.Stat(p); os.IsNotExist(err) { + return nil, core.ErrNotFound + } + + return ioutil.ReadFile(p) +} + +// Set value by key. +func (b *treeBucket) Set(key, value []byte) error { + dirPaths, filename := b.treePath(key) + if dirPaths == nil { + return errShortKey + } + + var ( + dirPath = path.Join(dirPaths...) + p = path.Join(b.dir, dirPath, filename) + ) + + if err := os.MkdirAll(path.Join(b.dir, dirPath), b.perm); err != nil { + return err + } + + err := ioutil.WriteFile(p, value, b.perm) + if err == nil { + b.sz.Add(int64(len(value))) + } + + return err +} + +// Del value by key. +func (b *treeBucket) Del(key []byte) error { + dirPaths, filename := b.treePath(key) + if dirPaths == nil { + return errShortKey + } + + var ( + err error + fi os.FileInfo + p = path.Join(b.dir, path.Join(dirPaths...), filename) + ) + + if fi, err = os.Stat(p); os.IsNotExist(err) { + return core.ErrNotFound + } else if err = os.Remove(p); err == nil { + b.sz.Sub(fi.Size()) + } + + return err +} + +// Has checks if key exists. +func (b *treeBucket) Has(key []byte) bool { + dirPaths, filename := b.treePath(key) + if dirPaths == nil { + return false + } + + p := path.Join(b.dir, path.Join(dirPaths...), filename) + + _, err := os.Stat(p) + + return err == nil +} + +// There might be two implementation of listing method: simple with `filepath.Walk()` +// or more complex implementation with path checks, BFS etc. `filepath.Walk()` might +// be slow in large dirs due to sorting operations and non controllable depth. +func (b *treeBucket) listing(root string, fn func(path string, info os.FileInfo) error) error { + // todo: DFS might be better since it won't store many files in queue. + // todo: queue length can be specified as a parameter + q := newQueue(queueCap) + q.Push(elem{path: root}) + + for q.Len() > 0 { + e := q.Pop() + + s, err := os.Lstat(e.path) + if err != nil { + // might be better to log and ignore + return err + } + + // check if it is correct file + if !s.IsDir() { + // we accept files that located in excepted depth and have correct prefix + // e.g. file 'abcdef0123' => /ab/cd/abcdef0123 + if e.depth == b.depth+1 && strings.HasPrefix(s.Name(), e.prefix) { + err = fn(e.path, s) + if err != nil { + // might be better to log and ignore + return err + } + } + + continue + } + + // ignore dirs with inappropriate length or depth + if e.depth > b.depth || (e.depth > 0 && len(s.Name()) > b.prefixLength) { + continue + } + + files, err := readDirNames(e.path) + if err != nil { + // might be better to log and ignore + return err + } + + for i := range files { + // add prefix of all dirs in path except root dir + var prefix string + if e.depth > 0 { + prefix = e.prefix + s.Name() + } + + q.Push(elem{ + depth: e.depth + 1, + prefix: prefix, + path: path.Join(e.path, files[i]), + }) + } + } + + return nil +} + +// Size returns the size of the bucket in bytes. +func (b *treeBucket) Size() int64 { + return b.sz.Load() +} + +func (b *treeBucket) size() (size int64) { + err := b.listing(b.dir, func(_ string, info os.FileInfo) error { + size += info.Size() + return nil + }) + + if err != nil { + size = 0 + } + + return +} + +// List all bucket items. +func (b *treeBucket) List() ([][]byte, error) { + buckets := make([][]byte, 0) + + err := b.listing(b.dir, func(p string, info os.FileInfo) error { + key, err := decodeHexKey(info.Name()) + if err != nil { + return err + } + buckets = append(buckets, key) + return nil + }) + + return buckets, err +} + +// Filter bucket items by closure. +func (b *treeBucket) Iterate(handler core.FilterHandler) error { + return b.listing(b.dir, func(p string, info os.FileInfo) error { + val, err := ioutil.ReadFile(path.Join(b.dir, p)) + if err != nil { + return err + } + + key, err := decodeHexKey(info.Name()) + if err != nil { + return err + } + + if !handler(key, val) { + return core.ErrIteratingAborted + } + + return nil + }) +} + +// Close bucket (remove all available data). +func (b *treeBucket) Close() error { + return os.RemoveAll(b.dir) +} + +// readDirNames copies `filepath.readDirNames()` without sorting the output. +func readDirNames(dirname string) ([]string, error) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + + names, err := f.Readdirnames(-1) + if err != nil { + return nil, err + } + + f.Close() + + return names, nil +} diff --git a/lib/buckets/fsbucket/treemethods_test.go b/lib/buckets/fsbucket/treemethods_test.go new file mode 100644 index 0000000000..f0e88e554a --- /dev/null +++ b/lib/buckets/fsbucket/treemethods_test.go @@ -0,0 +1,324 @@ +package fsbucket + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/atomic" + + "github.com/nspcc-dev/neofs-node/lib/core" +) + +func prepareTree(badFiles bool) (string, error) { + name := make([]byte, 32) + root, err := ioutil.TempDir("", "treeBucket_test") + if err != nil { + return "", err + } + + // paths must contain strings with hex ascii symbols + paths := [][]string{ + {root, "abcd"}, + {root, "abcd", "cdef"}, + {root, "abcd", "cd01"}, + {root, "0123", "2345"}, + {root, "0123", "2345", "4567"}, + } + + dirs := make([]string, len(paths)) + + for i := range paths { + dirs[i] = path.Join(paths[i]...) + + err = os.MkdirAll(dirs[i], 0700) + if err != nil { + return "", err + } + + // create couple correct files + for j := 0; j < 2; j++ { + _, err := rand.Read(name) + if err != nil { + return "", err + } + + filePrefix := new(strings.Builder) + for k := 1; k < len(paths[i]); k++ { + filePrefix.WriteString(paths[i][k]) + } + filePrefix.WriteString(hex.EncodeToString(name)) + + file, err := os.OpenFile(path.Join(dirs[i], filePrefix.String()), os.O_CREATE, 0700) + if err != nil { + return "", err + } + file.Close() + } + + if !badFiles { + continue + } + + // create one bad file + _, err := rand.Read(name) + if err != nil { + return "", err + } + + file, err := os.OpenFile(path.Join(dirs[i], "fff"+hex.EncodeToString(name)), os.O_CREATE, 0700) + if err != nil { + return "", err + } + file.Close() + } + + return root, nil +} + +func TestTreebucket_List(t *testing.T) { + root, err := prepareTree(true) + require.NoError(t, err) + defer os.RemoveAll(root) + + b := treeBucket{ + dir: root, + perm: 0700, + depth: 1, + prefixLength: 4, + } + results, err := b.List() + require.NoError(t, err) + require.Len(t, results, 2) + + b.depth = 2 + results, err = b.List() + require.NoError(t, err) + require.Len(t, results, 6) + + b.depth = 3 + results, err = b.List() + require.NoError(t, err) + require.Len(t, results, 2) + + b.depth = 4 + results, err = b.List() + require.NoError(t, err) + require.Len(t, results, 0) +} + +func TestTreebucket(t *testing.T) { + root, err := prepareTree(true) + require.NoError(t, err) + defer os.RemoveAll(root) + + b := treeBucket{ + dir: root, + perm: 0700, + depth: 2, + prefixLength: 4, + sz: atomic.NewInt64(0), + } + + results, err := b.List() + require.NoError(t, err) + require.Len(t, results, 6) + + t.Run("Get", func(t *testing.T) { + for i := range results { + _, err = b.Get(results[i]) + require.NoError(t, err) + } + _, err = b.Get([]byte("Hello world!")) + require.Error(t, err) + }) + + t.Run("Has", func(t *testing.T) { + for i := range results { + require.True(t, b.Has(results[i])) + } + require.False(t, b.Has([]byte("Unknown key"))) + }) + + t.Run("Set", func(t *testing.T) { + keyHash := sha256.Sum256([]byte("Set this key")) + key := keyHash[:] + value := make([]byte, 32) + rand.Read(value) + + // set sha256 key + err := b.Set(key, value) + require.NoError(t, err) + + require.True(t, b.Has(key)) + data, err := b.Get(key) + require.NoError(t, err) + require.Equal(t, data, value) + + filename := hex.EncodeToString(key) + _, err = os.Lstat(path.Join(root, filename[:4], filename[4:8], filename)) + require.NoError(t, err) + + // set key that cannot be placed in the required dir depth + key, err = hex.DecodeString("abcdef") + require.NoError(t, err) + + err = b.Set(key, value) + require.Error(t, err) + }) + + t.Run("Delete", func(t *testing.T) { + keyHash := sha256.Sum256([]byte("Delete this key")) + key := keyHash[:] + value := make([]byte, 32) + rand.Read(value) + + err := b.Set(key, value) + require.NoError(t, err) + + // delete sha256 key + err = b.Del(key) + require.NoError(t, err) + + _, err = b.Get(key) + require.Error(t, err) + filename := hex.EncodeToString(key) + _, err = os.Lstat(path.Join(root, filename[:4], filename[4:8], filename)) + require.Error(t, err) + }) +} + +func TestTreebucket_Close(t *testing.T) { + root, err := prepareTree(true) + require.NoError(t, err) + defer os.RemoveAll(root) + + b := treeBucket{ + dir: root, + perm: 0700, + depth: 2, + prefixLength: 4, + } + err = b.Close() + require.NoError(t, err) + + _, err = os.Lstat(root) + require.Error(t, err) +} + +func TestTreebucket_Size(t *testing.T) { + root, err := prepareTree(true) + require.NoError(t, err) + defer os.RemoveAll(root) + + var size int64 = 1024 + key := []byte("Set this key") + value := make([]byte, size) + rand.Read(value) + + b := treeBucket{ + dir: root, + perm: 0700, + depth: 2, + prefixLength: 4, + sz: atomic.NewInt64(0), + } + + err = b.Set(key, value) + require.NoError(t, err) + require.Equal(t, size, b.Size()) +} + +func BenchmarkTreebucket_List(b *testing.B) { + root, err := prepareTree(false) + defer os.RemoveAll(root) + if err != nil { + b.Error(err) + } + + treeFSBucket := &treeBucket{ + dir: root, + perm: 0755, + depth: 2, + prefixLength: 4, + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := treeFSBucket.List() + if err != nil { + b.Error(err) + } + } +} + +func BenchmarkFilewalkBucket_List(b *testing.B) { + root, err := prepareTree(false) + defer os.RemoveAll(root) + if err != nil { + b.Error(err) + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + buckets := make([]core.BucketItem, 0) + + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + val, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + key, err := decodeHexKey(info.Name()) + if err != nil { + return err + } + + buckets = append(buckets, core.BucketItem{ + Key: key, + Val: val, + }) + + return nil + }) + } +} + +func BenchmarkTreeBucket_Size(b *testing.B) { + root, err := prepareTree(false) + defer os.RemoveAll(root) + if err != nil { + b.Error(err) + } + + treeFSBucket := &treeBucket{ + dir: root, + perm: 0755, + depth: 2, + prefixLength: 4, + } + + treeFSBucket.sz = atomic.NewInt64(treeFSBucket.size()) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = treeFSBucket.Size() + } +} diff --git a/lib/buckets/init.go b/lib/buckets/init.go new file mode 100644 index 0000000000..ea4c5756d5 --- /dev/null +++ b/lib/buckets/init.go @@ -0,0 +1,64 @@ +package buckets + +import ( + "plugin" + "strings" + + "github.com/nspcc-dev/neofs-node/lib/buckets/boltdb" + "github.com/nspcc-dev/neofs-node/lib/buckets/fsbucket" + "github.com/nspcc-dev/neofs-node/lib/buckets/inmemory" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +const ( + // BoltDBBucket is a name of BoltDB bucket. + BoltDBBucket = "boltdb" + + // InMemoryBucket is a name RAM bucket. + InMemoryBucket = "in-memory" + + // FileSystemBucket is a name of file system bucket. + FileSystemBucket = "fsbucket" + + bucketSymbol = "PrepareBucket" +) + +// NewBucket is a bucket's constructor. +func NewBucket(name core.BucketType, l *zap.Logger, v *viper.Viper) (core.Bucket, error) { + bucket := v.GetString("storage." + string(name) + ".bucket") + + l.Info("initialize bucket", + zap.String("name", string(name)), + zap.String("bucket", bucket)) + + switch strings.ToLower(bucket) { + case FileSystemBucket: + return fsbucket.NewBucket(name, v) + + case InMemoryBucket: + return inmemory.NewBucket(name, v), nil + + case BoltDBBucket: + opts, err := boltdb.NewOptions("storage."+name, v) + if err != nil { + return nil, err + } + + return boltdb.NewBucket(&opts) + default: + instance, err := plugin.Open(bucket) + if err != nil { + return nil, errors.Wrapf(err, "could not load bucket: `%s`", bucket) + } + + sym, err := instance.Lookup(bucketSymbol) + if err != nil { + return nil, errors.Wrapf(err, "could not find bucket signature: `%s`", bucket) + } + + return sym.(func(core.BucketType, *viper.Viper) (core.Bucket, error))(name, v) + } +} diff --git a/lib/buckets/inmemory/bucket.go b/lib/buckets/inmemory/bucket.go new file mode 100644 index 0000000000..b5f48316c7 --- /dev/null +++ b/lib/buckets/inmemory/bucket.go @@ -0,0 +1,60 @@ +package inmemory + +import ( + "sync" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/spf13/viper" +) + +type ( + bucket struct { + *sync.RWMutex + items map[string][]byte + } +) + +const ( + defaultCapacity = 100 +) + +var ( + _ core.Bucket = (*bucket)(nil) + + // for in usage + _ = NewBucket +) + +func stringifyKey(key []byte) string { + return base58.Encode(key) +} + +func decodeKey(key string) []byte { + k, err := base58.Decode(key) + if err != nil { + panic(err) // it can fail only for not base58 strings + } + + return k +} + +func makeCopy(val []byte) []byte { + tmp := make([]byte, len(val)) + copy(tmp, val) + + return tmp +} + +// NewBucket creates new in-memory bucket instance. +func NewBucket(name core.BucketType, v *viper.Viper) core.Bucket { + var capacity int + if capacity = v.GetInt("storage." + string(name) + ".capacity"); capacity <= 0 { + capacity = defaultCapacity + } + + return &bucket{ + RWMutex: new(sync.RWMutex), + items: make(map[string][]byte, capacity), + } +} diff --git a/lib/buckets/inmemory/methods.go b/lib/buckets/inmemory/methods.go new file mode 100644 index 0000000000..7e1685c70e --- /dev/null +++ b/lib/buckets/inmemory/methods.go @@ -0,0 +1,107 @@ +package inmemory + +import ( + "unsafe" + + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" +) + +// Get value by key. +func (b *bucket) Get(key []byte) ([]byte, error) { + k := stringifyKey(key) + + b.RLock() + val, ok := b.items[k] + result := makeCopy(val) + b.RUnlock() + + if !ok { + return nil, errors.Wrapf(core.ErrNotFound, "key=`%s`", k) + } + + return result, nil +} + +// Set value by key. +func (b *bucket) Set(key, value []byte) error { + k := stringifyKey(key) + + b.Lock() + b.items[k] = makeCopy(value) + b.Unlock() + + return nil +} + +// Del value by key. +func (b *bucket) Del(key []byte) error { + k := stringifyKey(key) + + b.Lock() + delete(b.items, k) + b.Unlock() + + return nil +} + +// Has checks key exists. +func (b *bucket) Has(key []byte) bool { + k := stringifyKey(key) + + b.RLock() + _, ok := b.items[k] + b.RUnlock() + + return ok +} + +// Size size of bucket. +func (b *bucket) Size() int64 { + b.RLock() + // TODO we must replace in future + size := unsafe.Sizeof(b.items) + b.RUnlock() + + return int64(size) +} + +func (b *bucket) List() ([][]byte, error) { + var result = make([][]byte, 0) + + b.RLock() + for key := range b.items { + result = append(result, decodeKey(key)) + } + b.RUnlock() + + return result, nil +} + +// Filter items by closure. +func (b *bucket) Iterate(handler core.FilterHandler) error { + if handler == nil { + return core.ErrNilFilterHandler + } + + b.RLock() + for key, val := range b.items { + k, v := decodeKey(key), makeCopy(val) + + if !handler(k, v) { + return core.ErrIteratingAborted + } + } + b.RUnlock() + + return nil +} + +// Close bucket (just empty). +func (b *bucket) Close() error { + b.Lock() + b.items = make(map[string][]byte) + b.Unlock() + + return nil +} diff --git a/lib/container/alias.go b/lib/container/alias.go new file mode 100644 index 0000000000..cb2cdf3c68 --- /dev/null +++ b/lib/container/alias.go @@ -0,0 +1,15 @@ +package container + +import ( + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/refs" +) + +// Container is a type alias of Container. +type Container = container.Container + +// CID is a type alias of CID. +type CID = refs.CID + +// OwnerID is a type alias of OwnerID. +type OwnerID = refs.OwnerID diff --git a/lib/container/storage.go b/lib/container/storage.go new file mode 100644 index 0000000000..5192a3b2eb --- /dev/null +++ b/lib/container/storage.go @@ -0,0 +1,134 @@ +package container + +import ( + "context" +) + +// GetParams is a group of parameters for container receiving operation. +type GetParams struct { + ctxValue + + cidValue +} + +// GetResult is a group of values returned by container receiving operation. +type GetResult struct { + cnrValue +} + +// PutParams is a group of parameters for container storing operation. +type PutParams struct { + ctxValue + + cnrValue +} + +// PutResult is a group of values returned by container storing operation. +type PutResult struct { + cidValue +} + +// DeleteParams is a group of parameters for container removal operation. +type DeleteParams struct { + ctxValue + + cidValue + + ownerID OwnerID +} + +// DeleteResult is a group of values returned by container removal operation. +type DeleteResult struct{} + +// ListParams is a group of parameters for container listing operation. +type ListParams struct { + ctxValue + + ownerIDList []OwnerID +} + +// ListResult is a group of values returned by container listing operation. +type ListResult struct { + cidList []CID +} + +type cnrValue struct { + cnr *Container +} + +type cidValue struct { + cid CID +} + +type ctxValue struct { + ctx context.Context +} + +// Storage is an interface of the storage of NeoFS containers. +type Storage interface { + GetContainer(GetParams) (*GetResult, error) + PutContainer(PutParams) (*PutResult, error) + DeleteContainer(DeleteParams) (*DeleteResult, error) + ListContainers(ListParams) (*ListResult, error) + // TODO: add EACL methods +} + +// Context is a context getter. +func (s ctxValue) Context() context.Context { + return s.ctx +} + +// SetContext is a context setter. +func (s *ctxValue) SetContext(v context.Context) { + s.ctx = v +} + +// CID is a container ID getter. +func (s cidValue) CID() CID { + return s.cid +} + +// SetCID is a container ID getter. +func (s *cidValue) SetCID(v CID) { + s.cid = v +} + +// Container is a container getter. +func (s cnrValue) Container() *Container { + return s.cnr +} + +// SetContainer is a container setter. +func (s *cnrValue) SetContainer(v *Container) { + s.cnr = v +} + +// OwnerID is an owner ID getter. +func (s DeleteParams) OwnerID() OwnerID { + return s.ownerID +} + +// SetOwnerID is an owner ID setter. +func (s *DeleteParams) SetOwnerID(v OwnerID) { + s.ownerID = v +} + +// OwnerIDList is an owner ID list getter. +func (s ListParams) OwnerIDList() []OwnerID { + return s.ownerIDList +} + +// SetOwnerIDList is an owner ID list setter. +func (s *ListParams) SetOwnerIDList(v ...OwnerID) { + s.ownerIDList = v +} + +// CIDList is a container ID list getter. +func (s ListResult) CIDList() []CID { + return s.cidList +} + +// SetCIDList is a container ID list setter. +func (s *ListResult) SetCIDList(v []CID) { + s.cidList = v +} diff --git a/lib/container/storage_test.go b/lib/container/storage_test.go new file mode 100644 index 0000000000..77f3865143 --- /dev/null +++ b/lib/container/storage_test.go @@ -0,0 +1,83 @@ +package container + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetParams(t *testing.T) { + p := new(GetParams) + + cid := CID{1, 2, 3} + p.SetCID(cid) + + require.Equal(t, cid, p.CID()) +} + +func TestGetResult(t *testing.T) { + r := new(GetResult) + + cnr := &Container{ + OwnerID: OwnerID{1, 2, 3}, + } + r.SetContainer(cnr) + + require.Equal(t, cnr, r.Container()) +} + +func TestPutParams(t *testing.T) { + p := new(PutParams) + + cnr := &Container{ + OwnerID: OwnerID{1, 2, 3}, + } + p.SetContainer(cnr) + + require.Equal(t, cnr, p.Container()) +} + +func TestPutResult(t *testing.T) { + r := new(PutResult) + + cid := CID{1, 2, 3} + r.SetCID(cid) + + require.Equal(t, cid, r.CID()) +} + +func TestDeleteParams(t *testing.T) { + p := new(DeleteParams) + + ownerID := OwnerID{1, 2, 3} + p.SetOwnerID(ownerID) + require.Equal(t, ownerID, p.OwnerID()) + + cid := CID{4, 5, 6} + p.SetCID(cid) + require.Equal(t, cid, p.CID()) +} + +func TestListParams(t *testing.T) { + p := new(ListParams) + + ownerIDList := []OwnerID{ + {1, 2, 3}, + {4, 5, 6}, + } + p.SetOwnerIDList(ownerIDList...) + + require.Equal(t, ownerIDList, p.OwnerIDList()) +} + +func TestListResult(t *testing.T) { + r := new(ListResult) + + cidList := []CID{ + {1, 2, 3}, + {4, 5, 6}, + } + r.SetCIDList(cidList) + + require.Equal(t, cidList, r.CIDList()) +} diff --git a/lib/core/storage.go b/lib/core/storage.go new file mode 100644 index 0000000000..27e22f6d71 --- /dev/null +++ b/lib/core/storage.go @@ -0,0 +1,94 @@ +package core + +import ( + "github.com/nspcc-dev/neofs-node/internal" + "github.com/pkg/errors" +) + +type ( + // BucketType is name of bucket + BucketType string + + // FilterHandler where you receive key/val in your closure + FilterHandler func(key, val []byte) bool + + // BucketItem used in filter + BucketItem struct { + Key []byte + Val []byte + } + + // Bucket is sub-store interface + Bucket interface { + Get(key []byte) ([]byte, error) + Set(key, value []byte) error + Del(key []byte) error + Has(key []byte) bool + Size() int64 + List() ([][]byte, error) + Iterate(FilterHandler) error + // Steam can be implemented by badger.Stream, but not for now + // Stream(ctx context.Context, key []byte, cb func(io.ReadWriter) error) error + Close() error + } + + // Storage component interface + Storage interface { + GetBucket(name BucketType) (Bucket, error) + Size() int64 + Close() error + } +) + +const ( + // BlobStore is a blob bucket name. + BlobStore BucketType = "blob" + + // MetaStore is a meta bucket name. + MetaStore BucketType = "meta" + + // SpaceMetricsStore is a space metrics bucket name. + SpaceMetricsStore BucketType = "space-metrics" +) + +var ( + // ErrNilFilterHandler when FilterHandler is empty + ErrNilFilterHandler = errors.New("handler can't be nil") + + // ErrNotFound is returned by key-value storage methods + // that could not find element by key. + ErrNotFound = internal.Error("key not found") +) + +// ErrIteratingAborted is returned by storage iterator +// after iteration has been interrupted. +var ErrIteratingAborted = errors.New("iteration aborted") + +var errEmptyBucket = errors.New("empty bucket") + +func (t BucketType) String() string { return string(t) } + +// ListBucketItems performs iteration over Bucket and returns the full list of its items. +func ListBucketItems(b Bucket, h FilterHandler) ([]BucketItem, error) { + if b == nil { + return nil, errEmptyBucket + } else if h == nil { + return nil, ErrNilFilterHandler + } + + items := make([]BucketItem, 0) + + if err := b.Iterate(func(key, val []byte) bool { + if h(key, val) { + items = append(items, BucketItem{ + Key: key, + Val: val, + }) + } + return true + }); err != nil { + return nil, err + } + + return items, nil +} diff --git a/lib/core/storage_test.go b/lib/core/storage_test.go new file mode 100644 index 0000000000..a4b4511176 --- /dev/null +++ b/lib/core/storage_test.go @@ -0,0 +1,65 @@ +package core + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +type testBucket struct { + Bucket + + items []BucketItem +} + +func (s *testBucket) Iterate(f FilterHandler) error { + for i := range s.items { + if !f(s.items[i].Key, s.items[i].Val) { + return ErrIteratingAborted + } + } + + return nil +} + +func TestListBucketItems(t *testing.T) { + _, err := ListBucketItems(nil, nil) + require.EqualError(t, err, errEmptyBucket.Error()) + + b := new(testBucket) + + _, err = ListBucketItems(b, nil) + require.EqualError(t, err, ErrNilFilterHandler.Error()) + + var ( + count = 10 + ln = 10 + items = make([]BucketItem, 0, count) + ) + + for i := 0; i < count; i++ { + items = append(items, BucketItem{ + Key: testData(t, ln), + Val: testData(t, ln), + }) + } + + b.items = items + + res, err := ListBucketItems(b, func(key, val []byte) bool { return true }) + require.NoError(t, err) + require.Equal(t, items, res) + + res, err = ListBucketItems(b, func(key, val []byte) bool { return false }) + require.NoError(t, err) + require.Empty(t, res) +} + +func testData(t *testing.T, sz int) []byte { + d := make([]byte, sz) + _, err := rand.Read(d) + require.NoError(t, err) + + return d +} diff --git a/lib/core/validator.go b/lib/core/validator.go new file mode 100644 index 0000000000..ca66a93a12 --- /dev/null +++ b/lib/core/validator.go @@ -0,0 +1,22 @@ +package core + +import ( + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" +) + +// ErrMissingKeySignPairs is returned by functions that expect +// a non-empty SignKeyPair slice, but received empty. +const ErrMissingKeySignPairs = internal.Error("missing key-signature pairs") + +// VerifyRequestWithSignatures checks if request has signatures and all of them are valid. +// +// Returns ErrMissingKeySignPairs if request does not have signatures. +// Otherwise, behaves like service.VerifyRequestData. +func VerifyRequestWithSignatures(req service.RequestVerifyData) error { + if len(req.GetSignKeyPairs()) == 0 { + return ErrMissingKeySignPairs + } + + return service.VerifyRequestData(req) +} diff --git a/lib/core/verify.go b/lib/core/verify.go new file mode 100644 index 0000000000..57b80663fd --- /dev/null +++ b/lib/core/verify.go @@ -0,0 +1,69 @@ +package core + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" +) + +// OwnerKeyContainer is an interface of the container of owner's ID and key pair with read access. +type OwnerKeyContainer interface { + GetOwnerID() refs.OwnerID + GetOwnerKey() []byte +} + +// OwnerKeyVerifier is an interface of OwnerKeyContainer validator. +type OwnerKeyVerifier interface { + // Must check if OwnerKeyContainer satisfies a certain criterion. + // Nil error is equivalent to matching the criterion. + VerifyKey(context.Context, OwnerKeyContainer) error +} + +type neoKeyVerifier struct{} + +// ErrNilOwnerKeyContainer is returned by functions that expect a non-nil +// OwnerKeyContainer, but received nil. +const ErrNilOwnerKeyContainer = internal.Error("owner-key container is nil") + +// ErrNilOwnerKeyVerifier is returned by functions that expect a non-nil +// OwnerKeyVerifier, but received nil. +const ErrNilOwnerKeyVerifier = internal.Error("owner-key verifier is nil") + +// NewNeoKeyVerifier creates a new Neo owner key verifier and return a OwnerKeyVerifier interface. +func NewNeoKeyVerifier() OwnerKeyVerifier { + return new(neoKeyVerifier) +} + +// VerifyKey checks if the public key converts to owner ID. +// +// If passed OwnerKeyContainer is nil, ErrNilOwnerKeyContainer returns. +// If public key cannot be unmarshaled, service.ErrInvalidPublicKeyBytes returns. +// If public key is not converted to owner ID, service.ErrWrongOwner returns. +// With neo:morph adoption public key can be unrelated to owner ID. In this +// case VerifyKey should call NeoFS.ID smart-contract to check whether public +// key is bounded with owner ID. If there is no bound, then return +// service.ErrWrongOwner. +func (s neoKeyVerifier) VerifyKey(_ context.Context, src OwnerKeyContainer) error { + if src == nil { + return ErrNilOwnerKeyContainer + } + + pubKey := crypto.UnmarshalPublicKey(src.GetOwnerKey()) + if pubKey == nil { + return service.ErrInvalidPublicKeyBytes + } + + ownerFromKey, err := refs.NewOwnerID(pubKey) + if err != nil { + return err + } + + if !ownerFromKey.Equal(src.GetOwnerID()) { + return service.ErrWrongOwner + } + + return nil +} diff --git a/lib/fix/catch.go b/lib/fix/catch.go new file mode 100644 index 0000000000..c0bb5a6535 --- /dev/null +++ b/lib/fix/catch.go @@ -0,0 +1,59 @@ +package fix + +import ( + "fmt" + "reflect" + + "go.uber.org/zap" +) + +func (a *app) Catch(err error) { + if err == nil { + return + } + + if a.log == nil { + panic(err) + } + + a.log.Fatal("Can't run app", + zap.Error(err)) +} + +// CatchTrace catch errors for debugging +// use that function just for debug your application. +func (a *app) CatchTrace(err error) { + if err == nil { + return + } + + // digging into the root of the problem + for { + var ( + ok bool + v = reflect.ValueOf(err) + fn reflect.Value + ) + + if v.Type().Kind() != reflect.Struct { + break + } + + if !v.FieldByName("Reason").IsValid() { + break + } + + if v.FieldByName("Func").IsValid() { + fn = v.FieldByName("Func") + } + + fmt.Printf("Place: %#v\nReason: %s\n\n", fn, err) + + if err, ok = v.FieldByName("Reason").Interface().(error); !ok { + err = v.Interface().(error) + break + } + } + + panic(err) +} diff --git a/lib/fix/config/config.go b/lib/fix/config/config.go new file mode 100644 index 0000000000..fa9e860c46 --- /dev/null +++ b/lib/fix/config/config.go @@ -0,0 +1,53 @@ +package config + +import ( + "strings" + + "github.com/spf13/viper" +) + +// Params groups the parameters of configuration. +type Params struct { + File string + Type string + Prefix string + Name string + Version string + + AppDefaults func(v *viper.Viper) +} + +// NewConfig is a configuration tool's constructor. +func NewConfig(p Params) (v *viper.Viper, err error) { + v = viper.New() + v.SetEnvPrefix(p.Prefix) + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + v.SetDefault("app.name", p.Name) + v.SetDefault("app.version", p.Version) + + if p.AppDefaults != nil { + p.AppDefaults(v) + } + + if p.fromFile() { + v.SetConfigFile(p.File) + v.SetConfigType(p.safeType()) + + err = v.ReadInConfig() + } + + return v, err +} + +func (p Params) fromFile() bool { + return p.File != "" +} + +func (p Params) safeType() string { + if p.Type == "" { + p.Type = "yaml" + } + return strings.ToLower(p.Type) +} diff --git a/lib/fix/fix.go b/lib/fix/fix.go new file mode 100644 index 0000000000..7fd4e9df3c --- /dev/null +++ b/lib/fix/fix.go @@ -0,0 +1,112 @@ +package fix + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/nspcc-dev/neofs-node/lib/fix/config" + "github.com/nspcc-dev/neofs-node/lib/fix/logger" + "github.com/nspcc-dev/neofs-node/lib/fix/module" + "github.com/nspcc-dev/neofs-node/misc" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + // App is an interface of executable application. + App interface { + Run() error + RunAndCatch() + } + + app struct { + err error + log *zap.Logger + di *dig.Container + runner interface{} + } + + // Settings groups the application parameters. + Settings struct { + File string + Type string + Name string + Prefix string + Build string + Version string + Runner interface{} + + AppDefaults func(v *viper.Viper) + } +) + +func (a *app) RunAndCatch() { + err := a.Run() + + if errors.Is(err, context.Canceled) { + return + } + + if ok, _ := strconv.ParseBool(misc.Debug); ok { + a.CatchTrace(err) + } + + a.Catch(err) +} + +func (a *app) Run() error { + if a.err != nil { + return a.err + } + + // setup app logger: + if err := a.di.Invoke(func(l *zap.Logger) { + a.log = l + }); err != nil { + return err + } + + return a.di.Invoke(a.runner) +} + +// New is an application constructor. +func New(s *Settings, mod module.Module) App { + var ( + a app + err error + ) + + a.di = dig.New(dig.DeferAcyclicVerification()) + a.runner = s.Runner + + if s.Prefix == "" { + s.Prefix = s.Name + } + + mod = mod.Append( + module.Module{ + {Constructor: logger.NewLogger}, + {Constructor: NewGracefulContext}, + {Constructor: func() (*viper.Viper, error) { + return config.NewConfig(config.Params{ + File: s.File, + Type: s.Type, + Prefix: strings.ToUpper(s.Prefix), + Name: s.Name, + Version: fmt.Sprintf("%s(%s)", s.Version, s.Build), + + AppDefaults: s.AppDefaults, + }) + }}, + }) + + if err = module.Provide(a.di, mod); err != nil { + a.err = err + } + + return &a +} diff --git a/lib/fix/grace.go b/lib/fix/grace.go new file mode 100644 index 0000000000..3343b8ea43 --- /dev/null +++ b/lib/fix/grace.go @@ -0,0 +1,26 @@ +package fix + +import ( + "context" + "os" + "os/signal" + "syscall" + + "go.uber.org/zap" +) + +// NewGracefulContext returns graceful context. +func NewGracefulContext(l *zap.Logger) context.Context { + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + sig := <-ch + l.Info("received signal", + zap.String("signal", sig.String())) + cancel() + }() + + return ctx +} diff --git a/lib/fix/logger/logger.go b/lib/fix/logger/logger.go new file mode 100644 index 0000000000..4f10ee11c2 --- /dev/null +++ b/lib/fix/logger/logger.go @@ -0,0 +1,90 @@ +package logger + +import ( + "strings" + + "github.com/spf13/viper" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +const ( + formatJSON = "json" + formatConsole = "console" + + defaultSamplingInitial = 100 + defaultSamplingThereafter = 100 +) + +func safeLevel(lvl string) zap.AtomicLevel { + switch strings.ToLower(lvl) { + case "debug": + return zap.NewAtomicLevelAt(zap.DebugLevel) + case "warn": + return zap.NewAtomicLevelAt(zap.WarnLevel) + case "error": + return zap.NewAtomicLevelAt(zap.ErrorLevel) + case "fatal": + return zap.NewAtomicLevelAt(zap.FatalLevel) + case "panic": + return zap.NewAtomicLevelAt(zap.PanicLevel) + default: + return zap.NewAtomicLevelAt(zap.InfoLevel) + } +} + +// NewLogger is a logger's constructor. +func NewLogger(v *viper.Viper) (*zap.Logger, error) { + c := zap.NewProductionConfig() + + c.OutputPaths = []string{"stdout"} + c.ErrorOutputPaths = []string{"stdout"} + + if v.IsSet("logger.sampling") { + c.Sampling = &zap.SamplingConfig{ + Initial: defaultSamplingInitial, + Thereafter: defaultSamplingThereafter, + } + + if val := v.GetInt("logger.sampling.initial"); val > 0 { + c.Sampling.Initial = val + } + + if val := v.GetInt("logger.sampling.thereafter"); val > 0 { + c.Sampling.Thereafter = val + } + } + + // logger level + c.Level = safeLevel(v.GetString("logger.level")) + traceLvl := safeLevel(v.GetString("logger.trace_level")) + + // logger format + switch f := v.GetString("logger.format"); strings.ToLower(f) { + case formatConsole: + c.Encoding = formatConsole + default: + c.Encoding = formatJSON + } + + // logger time + c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + + l, err := c.Build( + // enable trace only for current log-level + zap.AddStacktrace(traceLvl)) + if err != nil { + return nil, err + } + + if v.GetBool("logger.no_disclaimer") { + return l, nil + } + + name := v.GetString("app.name") + version := v.GetString("app.version") + + return l.With( + zap.String("app_name", name), + zap.String("app_version", version)), nil +} diff --git a/lib/fix/module/module.go b/lib/fix/module/module.go new file mode 100644 index 0000000000..9e33f48e47 --- /dev/null +++ b/lib/fix/module/module.go @@ -0,0 +1,35 @@ +package module + +import ( + "go.uber.org/dig" +) + +type ( + // Module type + Module []*Provider + + // Provider struct + Provider struct { + Constructor interface{} + Options []dig.ProvideOption + } +) + +// Append module to target module and return new module +func (m Module) Append(mods ...Module) Module { + var result = m + for _, mod := range mods { + result = append(result, mod...) + } + return result +} + +// Provide set providers functions to DI container +func Provide(dic *dig.Container, providers Module) error { + for _, p := range providers { + if err := dic.Provide(p.Constructor, p.Options...); err != nil { + return err + } + } + return nil +} diff --git a/lib/fix/services.go b/lib/fix/services.go new file mode 100644 index 0000000000..59a1a169e6 --- /dev/null +++ b/lib/fix/services.go @@ -0,0 +1,46 @@ +package fix + +import ( + "context" +) + +type ( + // Service interface + Service interface { + Start(context.Context) + Stop() + } + + combiner []Service +) + +var _ Service = (combiner)(nil) + +// NewServices creates single runner. +func NewServices(items ...Service) Service { + var svc = make(combiner, 0, len(items)) + + for _, item := range items { + if item == nil { + continue + } + + svc = append(svc, item) + } + + return svc +} + +// Start all services. +func (c combiner) Start(ctx context.Context) { + for _, svc := range c { + svc.Start(ctx) + } +} + +// Stop all services. +func (c combiner) Stop() { + for _, svc := range c { + svc.Stop() + } +} diff --git a/lib/fix/web/http.go b/lib/fix/web/http.go new file mode 100644 index 0000000000..19941eb6e1 --- /dev/null +++ b/lib/fix/web/http.go @@ -0,0 +1,114 @@ +package web + +import ( + "context" + "net/http" + "sync/atomic" + "time" + + "github.com/spf13/viper" + "go.uber.org/zap" +) + +type ( + httpParams struct { + Key string + Viper *viper.Viper + Logger *zap.Logger + Handler http.Handler + } + + httpServer struct { + name string + started *int32 + logger *zap.Logger + shutdownTTL time.Duration + server server + } +) + +func (h *httpServer) Start(ctx context.Context) { + if h == nil { + return + } + + if !atomic.CompareAndSwapInt32(h.started, 0, 1) { + h.logger.Info("http: already started", + zap.String("server", h.name)) + return + } + + go func() { + if err := h.server.serve(ctx); err != nil { + if err != http.ErrServerClosed { + h.logger.Error("http: could not start server", + zap.Error(err)) + } + } + }() +} + +func (h *httpServer) Stop() { + if h == nil { + return + } + + if !atomic.CompareAndSwapInt32(h.started, 1, 0) { + h.logger.Info("http: already stopped", + zap.String("server", h.name)) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), h.shutdownTTL) + defer cancel() + + h.logger.Debug("http: try to stop server", + zap.String("server", h.name)) + + if err := h.server.shutdown(ctx); err != nil { + h.logger.Error("http: could not stop server", + zap.Error(err)) + } +} + +const defaultShutdownTTL = 30 * time.Second + +func newHTTPServer(p httpParams) *httpServer { + var ( + address string + shutdown time.Duration + ) + + if address = p.Viper.GetString(p.Key + ".address"); address == "" { + p.Logger.Info("Empty bind address, skip", + zap.String("server", p.Key)) + return nil + } + if p.Handler == nil { + p.Logger.Info("Empty handler, skip", + zap.String("server", p.Key)) + return nil + } + + p.Logger.Info("Create http.Server", + zap.String("server", p.Key), + zap.String("address", address)) + + if shutdown = p.Viper.GetDuration(p.Key + ".shutdown_ttl"); shutdown <= 0 { + shutdown = defaultShutdownTTL + } + + return &httpServer{ + name: p.Key, + started: new(int32), + logger: p.Logger, + shutdownTTL: shutdown, + server: newServer(params{ + Address: address, + Name: p.Key, + Config: p.Viper, + Logger: p.Logger, + Handler: p.Handler, + }), + } +} diff --git a/lib/fix/web/metrics.go b/lib/fix/web/metrics.go new file mode 100644 index 0000000000..951b17f2a2 --- /dev/null +++ b/lib/fix/web/metrics.go @@ -0,0 +1,32 @@ +package web + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +// Metrics is an interface of metric tool. +type Metrics interface { + Start(ctx context.Context) + Stop() +} + +const metricsKey = "metrics" + +// NewMetrics is a metric tool's constructor. +func NewMetrics(l *zap.Logger, v *viper.Viper) Metrics { + if !v.GetBool(metricsKey + ".enabled") { + l.Debug("metrics server disabled") + return nil + } + + return newHTTPServer(httpParams{ + Key: metricsKey, + Viper: v, + Logger: l, + Handler: promhttp.Handler(), + }) +} diff --git a/lib/fix/web/pprof.go b/lib/fix/web/pprof.go new file mode 100644 index 0000000000..da5a331b8c --- /dev/null +++ b/lib/fix/web/pprof.go @@ -0,0 +1,44 @@ +package web + +import ( + "context" + "expvar" + "net/http" + "net/http/pprof" + + "github.com/spf13/viper" + "go.uber.org/zap" +) + +// Profiler is an interface of profiler. +type Profiler interface { + Start(ctx context.Context) + Stop() +} + +const profilerKey = "pprof" + +// NewProfiler is a profiler's constructor. +func NewProfiler(l *zap.Logger, v *viper.Viper) Profiler { + if !v.GetBool(profilerKey + ".enabled") { + l.Debug("pprof server disabled") + return nil + } + + mux := http.NewServeMux() + + mux.Handle("/debug/vars", expvar.Handler()) + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + return newHTTPServer(httpParams{ + Key: profilerKey, + Viper: v, + Logger: l, + Handler: mux, + }) +} diff --git a/lib/fix/web/server.go b/lib/fix/web/server.go new file mode 100644 index 0000000000..e4fcb845cd --- /dev/null +++ b/lib/fix/web/server.go @@ -0,0 +1,62 @@ +package web + +import ( + "context" + "net/http" + + "github.com/spf13/viper" + "go.uber.org/zap" +) + +type ( + // Server is an interface of server. + server interface { + serve(ctx context.Context) error + shutdown(ctx context.Context) error + } + + contextServer struct { + logger *zap.Logger + server *http.Server + } + + params struct { + Address string + Name string + Config *viper.Viper + Logger *zap.Logger + Handler http.Handler + } +) + +func newServer(p params) server { + return &contextServer{ + logger: p.Logger, + server: &http.Server{ + Addr: p.Address, + Handler: p.Handler, + ReadTimeout: p.Config.GetDuration(p.Name + ".read_timeout"), + ReadHeaderTimeout: p.Config.GetDuration(p.Name + ".read_header_timeout"), + WriteTimeout: p.Config.GetDuration(p.Name + ".write_timeout"), + IdleTimeout: p.Config.GetDuration(p.Name + ".idle_timeout"), + MaxHeaderBytes: p.Config.GetInt(p.Name + ".max_header_bytes"), + }, + } +} + +func (cs *contextServer) serve(ctx context.Context) error { + go func() { + <-ctx.Done() + + if err := cs.server.Close(); err != nil { + cs.logger.Info("something went wrong", + zap.Error(err)) + } + }() + + return cs.server.ListenAndServe() +} + +func (cs *contextServer) shutdown(ctx context.Context) error { + return cs.server.Shutdown(ctx) +} diff --git a/lib/fix/worker/worker.go b/lib/fix/worker/worker.go new file mode 100644 index 0000000000..c6cbd13b4a --- /dev/null +++ b/lib/fix/worker/worker.go @@ -0,0 +1,79 @@ +package worker + +import ( + "context" + "sync" + "sync/atomic" + "time" +) + +type ( + // Workers is an interface of worker tool. + Workers interface { + Start(context.Context) + Stop() + + Add(Job Handler) + } + + workers struct { + cancel context.CancelFunc + started *int32 + wg *sync.WaitGroup + jobs []Handler + } + + // Handler is a worker's handling function. + Handler func(ctx context.Context) + + // Jobs is a map of worker names to handlers. + Jobs map[string]Handler + + // Job groups the parameters of worker's job. + Job struct { + Disabled bool + Immediately bool + Timer time.Duration + Ticker time.Duration + Handler Handler + } +) + +// New is a constructor of workers. +func New() Workers { + return &workers{ + started: new(int32), + wg: new(sync.WaitGroup), + } +} + +func (w *workers) Add(job Handler) { + w.jobs = append(w.jobs, job) +} + +func (w *workers) Stop() { + if !atomic.CompareAndSwapInt32(w.started, 1, 0) { + // already stopped + return + } + + w.cancel() + w.wg.Wait() +} + +func (w *workers) Start(ctx context.Context) { + if !atomic.CompareAndSwapInt32(w.started, 0, 1) { + // already started + return + } + + ctx, w.cancel = context.WithCancel(ctx) + for _, job := range w.jobs { + w.wg.Add(1) + + go func(handler Handler) { + defer w.wg.Done() + handler(ctx) + }(job) + } +} diff --git a/lib/implementations/acl.go b/lib/implementations/acl.go new file mode 100644 index 0000000000..ce3fd58adb --- /dev/null +++ b/lib/implementations/acl.go @@ -0,0 +1,392 @@ +package implementations + +import ( + "context" + + sc "github.com/nspcc-dev/neo-go/pkg/smartcontract" + libacl "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/acl" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/nspcc-dev/neofs-node/lib/container" + + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/pkg/errors" +) + +// Consider moving ACLHelper implementation to the ACL library. + +type ( + // ACLHelper is an interface, that provides useful functions + // for ACL object pre-processor. + ACLHelper interface { + BasicACLGetter + ContainerOwnerChecker + } + + // BasicACLGetter helper provides function to return basic ACL value. + BasicACLGetter interface { + GetBasicACL(context.Context, CID) (uint32, error) + } + + // ContainerOwnerChecker checks owner of the container. + ContainerOwnerChecker interface { + IsContainerOwner(context.Context, CID, refs.OwnerID) (bool, error) + } + + aclHelper struct { + cnr container.Storage + } +) + +type binaryEACLSource struct { + binaryStore acl.BinaryExtendedACLSource +} + +// StaticContractClient is a wrapper over Neo:Morph client +// that invokes single smart contract methods with fixed fee. +type StaticContractClient struct { + // neo-go client instance + client *goclient.Client + + // contract script-hash + scScriptHash util.Uint160 + + // invocation fee + fee util.Fixed8 +} + +// MorphContainerContract is a wrapper over StaticContractClient +// for Container contract calls. +type MorphContainerContract struct { + // NeoFS Container smart-contract + containerContract StaticContractClient + + // set EACL method name of container contract + eaclSetMethodName string + + // get EACL method name of container contract + eaclGetMethodName string + + // get container method name of container contract + cnrGetMethodName string + + // put container method name of container contract + cnrPutMethodName string + + // delete container method name of container contract + cnrDelMethodName string + + // list containers method name of container contract + cnrListMethodName string +} + +const ( + errNewACLHelper = internal.Error("cannot create ACLHelper instance") +) + +// GetBasicACL returns basic ACL of the container. +func (h aclHelper) GetBasicACL(ctx context.Context, cid CID) (uint32, error) { + gp := container.GetParams{} + gp.SetContext(ctx) + gp.SetCID(cid) + + gResp, err := h.cnr.GetContainer(gp) + if err != nil { + return 0, err + } + + return gResp.Container().BasicACL, nil +} + +// IsContainerOwner returns true if provided id is an owner container. +func (h aclHelper) IsContainerOwner(ctx context.Context, cid CID, id refs.OwnerID) (bool, error) { + gp := container.GetParams{} + gp.SetContext(ctx) + gp.SetCID(cid) + + gResp, err := h.cnr.GetContainer(gp) + if err != nil { + return false, err + } + + return gResp.Container().OwnerID.Equal(id), nil +} + +// NewACLHelper returns implementation of the ACLHelper interface. +func NewACLHelper(cnr container.Storage) (ACLHelper, error) { + if cnr == nil { + return nil, errNewACLHelper + } + + return aclHelper{cnr}, nil +} + +// ExtendedACLSourceFromBinary wraps BinaryExtendedACLSource and returns ExtendedACLSource. +// +// If passed BinaryExtendedACLSource is nil, acl.ErrNilBinaryExtendedACLStore returns. +func ExtendedACLSourceFromBinary(v acl.BinaryExtendedACLSource) (acl.ExtendedACLSource, error) { + if v == nil { + return nil, acl.ErrNilBinaryExtendedACLStore + } + + return &binaryEACLSource{ + binaryStore: v, + }, nil +} + +// GetExtendedACLTable receives eACL table in a binary representation from storage, +// unmarshals it and returns ExtendedACLTable interface. +func (s binaryEACLSource) GetExtendedACLTable(ctx context.Context, cid refs.CID) (libacl.ExtendedACLTable, error) { + key := acl.BinaryEACLKey{} + key.SetCID(cid) + + val, err := s.binaryStore.GetBinaryEACL(ctx, key) + if err != nil { + return nil, err + } + + eacl := val.EACL() + + // TODO: verify signature + + res := libacl.WrapEACLTable(nil) + + return res, res.UnmarshalBinary(eacl) +} + +// NewStaticContractClient initializes a new StaticContractClient. +// +// If passed Client is nil, goclient.ErrNilClient returns. +func NewStaticContractClient(client *goclient.Client, scHash util.Uint160, fee util.Fixed8) (StaticContractClient, error) { + res := StaticContractClient{ + client: client, + scScriptHash: scHash, + fee: fee, + } + + var err error + if client == nil { + err = goclient.ErrNilClient + } + + return res, err +} + +// Invoke calls Invoke method of goclient with predefined script hash and fee. +// Supported args types are the same as in goclient. +// +// If Client is not initialized, goclient.ErrNilClient returns. +func (s StaticContractClient) Invoke(method string, args ...interface{}) error { + if s.client == nil { + return goclient.ErrNilClient + } + + return s.client.Invoke( + s.scScriptHash, + s.fee, + method, + args..., + ) +} + +// TestInvoke calls TestInvoke method of goclient with predefined script hash. +// +// If Client is not initialized, goclient.ErrNilClient returns. +func (s StaticContractClient) TestInvoke(method string, args ...interface{}) ([]sc.Parameter, error) { + if s.client == nil { + return nil, goclient.ErrNilClient + } + + return s.client.TestInvoke( + s.scScriptHash, + method, + args..., + ) +} + +// SetContainerContractClient is a container contract client setter. +func (s *MorphContainerContract) SetContainerContractClient(v StaticContractClient) { + s.containerContract = v +} + +// SetEACLGetMethodName is a container contract Get EACL method name setter. +func (s *MorphContainerContract) SetEACLGetMethodName(v string) { + s.eaclGetMethodName = v +} + +// SetEACLSetMethodName is a container contract Set EACL method name setter. +func (s *MorphContainerContract) SetEACLSetMethodName(v string) { + s.eaclSetMethodName = v +} + +// SetContainerGetMethodName is a container contract Get method name setter. +func (s *MorphContainerContract) SetContainerGetMethodName(v string) { + s.cnrGetMethodName = v +} + +// SetContainerPutMethodName is a container contract Put method name setter. +func (s *MorphContainerContract) SetContainerPutMethodName(v string) { + s.cnrPutMethodName = v +} + +// SetContainerDeleteMethodName is a container contract Delete method name setter. +func (s *MorphContainerContract) SetContainerDeleteMethodName(v string) { + s.cnrDelMethodName = v +} + +// SetContainerListMethodName is a container contract List method name setter. +func (s *MorphContainerContract) SetContainerListMethodName(v string) { + s.cnrListMethodName = v +} + +// GetBinaryEACL performs the test invocation call of GetEACL method of NeoFS Container contract. +func (s *MorphContainerContract) GetBinaryEACL(_ context.Context, key acl.BinaryEACLKey) (acl.BinaryEACLValue, error) { + res := acl.BinaryEACLValue{} + + prms, err := s.containerContract.TestInvoke( + s.eaclGetMethodName, + key.CID().Bytes(), + ) + if err != nil { + return res, err + } else if ln := len(prms); ln != 1 { + return res, errors.Errorf("unexpected stack parameter count: %d", ln) + } + + eacl, err := goclient.BytesFromStackParameter(prms[0]) + if err == nil { + res.SetEACL(eacl) + } + + return res, err +} + +// PutBinaryEACL invokes the call of SetEACL method of NeoFS Container contract. +func (s *MorphContainerContract) PutBinaryEACL(_ context.Context, key acl.BinaryEACLKey, val acl.BinaryEACLValue) error { + return s.containerContract.Invoke( + s.eaclSetMethodName, + key.CID().Bytes(), + val.EACL(), + val.Signature(), + ) +} + +// GetContainer performs the test invocation call of Get method of NeoFS Container contract. +func (s *MorphContainerContract) GetContainer(p container.GetParams) (*container.GetResult, error) { + prms, err := s.containerContract.TestInvoke( + s.cnrGetMethodName, + p.CID().Bytes(), + ) + if err != nil { + return nil, errors.Wrap(err, "could not perform test invocation") + } else if ln := len(prms); ln != 1 { + return nil, errors.Errorf("unexpected stack item count: %d", ln) + } + + cnrBytes, err := goclient.BytesFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get byte array from stack item") + } + + cnr := new(container.Container) + if err := cnr.Unmarshal(cnrBytes); err != nil { + return nil, errors.Wrap(err, "could not unmarshal container from bytes") + } + + res := new(container.GetResult) + res.SetContainer(cnr) + + return res, nil +} + +// PutContainer invokes the call of Put method of NeoFS Container contract. +func (s *MorphContainerContract) PutContainer(p container.PutParams) (*container.PutResult, error) { + cnr := p.Container() + + cid, err := cnr.ID() + if err != nil { + return nil, errors.Wrap(err, "could not calculate container ID") + } + + cnrBytes, err := cnr.Marshal() + if err != nil { + return nil, errors.Wrap(err, "could not marshal container") + } + + if err := s.containerContract.Invoke( + s.cnrPutMethodName, + cnr.OwnerID.Bytes(), + cnrBytes, + []byte{}, + ); err != nil { + return nil, errors.Wrap(err, "could not invoke contract method") + } + + res := new(container.PutResult) + res.SetCID(cid) + + return res, nil +} + +// DeleteContainer invokes the call of Delete method of NeoFS Container contract. +func (s *MorphContainerContract) DeleteContainer(p container.DeleteParams) (*container.DeleteResult, error) { + if err := s.containerContract.Invoke( + s.cnrDelMethodName, + p.CID().Bytes(), + p.OwnerID().Bytes(), + []byte{}, + ); err != nil { + return nil, errors.Wrap(err, "could not invoke contract method") + } + + return new(container.DeleteResult), nil +} + +// ListContainers performs the test invocation call of Get method of NeoFS Container contract. +// +// If owner ID list in parameters is non-empty, bytes of first owner are attached to call. +func (s *MorphContainerContract) ListContainers(p container.ListParams) (*container.ListResult, error) { + args := make([]interface{}, 0, 1) + + if ownerIDList := p.OwnerIDList(); len(ownerIDList) > 0 { + args = append(args, ownerIDList[0].Bytes()) + } + + prms, err := s.containerContract.TestInvoke( + s.cnrListMethodName, + args..., + ) + if err != nil { + return nil, errors.Wrap(err, "could not perform test invocation") + } else if ln := len(prms); ln != 1 { + return nil, errors.Errorf("unexpected stack item count: %d", ln) + } + + prms, err = goclient.ArrayFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get stack item array from stack item") + } + + cidList := make([]CID, 0, len(prms)) + + for i := range prms { + cidBytes, err := goclient.BytesFromStackParameter(prms[i]) + if err != nil { + return nil, errors.Wrap(err, "could not get byte array from stack item") + } + + cid, err := refs.CIDFromBytes(cidBytes) + if err != nil { + return nil, errors.Wrap(err, "could not get container ID from bytes") + } + + cidList = append(cidList, cid) + } + + res := new(container.ListResult) + res.SetCIDList(cidList) + + return res, nil +} diff --git a/lib/implementations/acl_test.go b/lib/implementations/acl_test.go new file mode 100644 index 0000000000..cb462de746 --- /dev/null +++ b/lib/implementations/acl_test.go @@ -0,0 +1,19 @@ +package implementations + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStaticContractClient(t *testing.T) { + s := new(StaticContractClient) + + require.NotPanics(t, func() { + _, _ = s.TestInvoke("") + }) + + require.NotPanics(t, func() { + _ = s.Invoke("") + }) +} diff --git a/lib/implementations/balance.go b/lib/implementations/balance.go new file mode 100644 index 0000000000..d535c0eafe --- /dev/null +++ b/lib/implementations/balance.go @@ -0,0 +1,141 @@ +package implementations + +import ( + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/pkg/errors" +) + +// MorphBalanceContract is a wrapper over NeoFS Balance contract client +// that provides an interface of manipulations with user funds. +type MorphBalanceContract struct { + // NeoFS Balance smart-contract + balanceContract StaticContractClient + + // "balance of" method name of balance contract + balanceOfMethodName string + + // decimals method name of balance contract + decimalsMethodName string +} + +// BalanceOfParams is a structure that groups the parameters +// for NeoFS user balance receiving operation. +type BalanceOfParams struct { + owner refs.OwnerID +} + +// BalanceOfResult is a structure that groups the values +// of the result of NeoFS user balance receiving operation. +type BalanceOfResult struct { + amount int64 +} + +// DecimalsParams is a structure that groups the parameters +// for NeoFS token decimals receiving operation. +type DecimalsParams struct { +} + +// DecimalsResult is a structure that groups the values +// of the result of NeoFS token decimals receiving operation. +type DecimalsResult struct { + dec int64 +} + +// SetBalanceContractClient is a Balance contract client setter. +func (s *MorphBalanceContract) SetBalanceContractClient(v StaticContractClient) { + s.balanceContract = v +} + +// SetBalanceOfMethodName is a Balance contract balanceOf method name setter. +func (s *MorphBalanceContract) SetBalanceOfMethodName(v string) { + s.balanceOfMethodName = v +} + +// SetDecimalsMethodName is a Balance contract decimals method name setter. +func (s *MorphBalanceContract) SetDecimalsMethodName(v string) { + s.decimalsMethodName = v +} + +// BalanceOf performs the test invocation call of balanceOf method of NeoFS Balance contract. +func (s MorphBalanceContract) BalanceOf(p BalanceOfParams) (*BalanceOfResult, error) { + owner := p.OwnerID() + + u160, err := address.StringToUint160(owner.String()) + if err != nil { + return nil, errors.Wrap(err, "could not convert wallet address to Uint160") + } + + prms, err := s.balanceContract.TestInvoke( + s.balanceOfMethodName, + u160.BytesBE(), + ) + if err != nil { + return nil, errors.Wrap(err, "could not perform test invocation") + } else if ln := len(prms); ln != 1 { + return nil, errors.Errorf("unexpected stack item count (balanceOf): %d", ln) + } + + amount, err := goclient.IntFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get integer stack item from stack item (amount)") + } + + res := new(BalanceOfResult) + res.SetAmount(amount) + + return res, nil +} + +// Decimals performs the test invocation call of decimals method of NeoFS Balance contract. +func (s MorphBalanceContract) Decimals(DecimalsParams) (*DecimalsResult, error) { + prms, err := s.balanceContract.TestInvoke( + s.decimalsMethodName, + ) + if err != nil { + return nil, errors.Wrap(err, "could not perform test invocation") + } else if ln := len(prms); ln != 1 { + return nil, errors.Errorf("unexpected stack item count (decimals): %d", ln) + } + + dec, err := goclient.IntFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get integer stack item from stack item (decimal)") + } + + res := new(DecimalsResult) + res.SetDecimals(dec) + + return res, nil +} + +// SetOwnerID is an owner ID setter. +func (s *BalanceOfParams) SetOwnerID(v refs.OwnerID) { + s.owner = v +} + +// OwnerID is an owner ID getter. +func (s BalanceOfParams) OwnerID() refs.OwnerID { + return s.owner +} + +// SetAmount is an funds amount setter. +func (s *BalanceOfResult) SetAmount(v int64) { + s.amount = v +} + +// Amount is an funds amount getter. +func (s BalanceOfResult) Amount() int64 { + return s.amount +} + +// SetDecimals is a decimals setter. +func (s *DecimalsResult) SetDecimals(v int64) { + s.dec = v +} + +// Decimals is a decimals getter. +func (s DecimalsResult) Decimals() int64 { + return s.dec +} diff --git a/lib/implementations/balance_test.go b/lib/implementations/balance_test.go new file mode 100644 index 0000000000..c9b571c8a7 --- /dev/null +++ b/lib/implementations/balance_test.go @@ -0,0 +1,35 @@ +package implementations + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/stretchr/testify/require" +) + +func TestBalanceOfParams(t *testing.T) { + s := BalanceOfParams{} + + owner := refs.OwnerID{1, 2, 3} + s.SetOwnerID(owner) + + require.Equal(t, owner, s.OwnerID()) +} + +func TestBalanceOfResult(t *testing.T) { + s := BalanceOfResult{} + + amount := int64(100) + s.SetAmount(amount) + + require.Equal(t, amount, s.Amount()) +} + +func TestDecimalsResult(t *testing.T) { + s := DecimalsResult{} + + dec := int64(100) + s.SetDecimals(dec) + + require.Equal(t, dec, s.Decimals()) +} diff --git a/lib/implementations/bootstrap.go b/lib/implementations/bootstrap.go new file mode 100644 index 0000000000..458967521f --- /dev/null +++ b/lib/implementations/bootstrap.go @@ -0,0 +1,311 @@ +package implementations + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/nspcc-dev/neofs-node/lib/boot" + "github.com/nspcc-dev/neofs-node/lib/ir" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/pkg/errors" +) + +// MorphNetmapContract is a wrapper over NeoFS Netmap contract client +// that provides an interface of network map manipulations. +type MorphNetmapContract struct { + // NeoFS Netmap smart-contract + netmapContract StaticContractClient + + // add peer method name of netmap contract + addPeerMethodName string + + // new epoch method name of netmap contract + newEpochMethodName string + + // get netmap method name of netmap contract + getNetMapMethodName string + + // update state method name of netmap contract + updStateMethodName string + + // IR list method name of netmap contract + irListMethodName string +} + +// UpdateEpochParams is a structure that groups the parameters +// for NeoFS epoch number updating. +type UpdateEpochParams struct { + epoch uint64 +} + +// UpdateStateParams is a structure that groups the parameters +// for NeoFS node state updating. +type UpdateStateParams struct { + st NodeState + + key []byte +} + +// NodeState is a type of node states enumeration. +type NodeState int64 + +const ( + _ NodeState = iota + + // StateOffline is an offline node state value. + StateOffline +) + +const addPeerFixedArgNumber = 2 + +const nodeInfoFixedPrmNumber = 3 + +// SetNetmapContractClient is a Netmap contract client setter. +func (s *MorphNetmapContract) SetNetmapContractClient(v StaticContractClient) { + s.netmapContract = v +} + +// SetAddPeerMethodName is a Netmap contract AddPeer method name setter. +func (s *MorphNetmapContract) SetAddPeerMethodName(v string) { + s.addPeerMethodName = v +} + +// SetNewEpochMethodName is a Netmap contract NewEpoch method name setter. +func (s *MorphNetmapContract) SetNewEpochMethodName(v string) { + s.newEpochMethodName = v +} + +// SetNetMapMethodName is a Netmap contract Netmap method name setter. +func (s *MorphNetmapContract) SetNetMapMethodName(v string) { + s.getNetMapMethodName = v +} + +// SetUpdateStateMethodName is a Netmap contract UpdateState method name setter. +func (s *MorphNetmapContract) SetUpdateStateMethodName(v string) { + s.updStateMethodName = v +} + +// SetIRListMethodName is a Netmap contract InnerRingList method name setter. +func (s *MorphNetmapContract) SetIRListMethodName(v string) { + s.irListMethodName = v +} + +// AddPeer invokes the call of AddPeer method of NeoFS Netmap contract. +func (s *MorphNetmapContract) AddPeer(p boot.BootstrapPeerParams) error { + info := p.NodeInfo() + opts := info.GetOptions() + + args := make([]interface{}, 0, addPeerFixedArgNumber+len(opts)) + + args = append(args, + // Address + []byte(info.GetAddress()), + + // Public key + info.GetPubKey(), + ) + + // Options + for i := range opts { + args = append(args, []byte(opts[i])) + } + + return s.netmapContract.Invoke( + s.addPeerMethodName, + args..., + ) +} + +// UpdateEpoch invokes the call of NewEpoch method of NeoFS Netmap contract. +func (s *MorphNetmapContract) UpdateEpoch(p UpdateEpochParams) error { + return s.netmapContract.Invoke( + s.newEpochMethodName, + int64(p.Number()), // TODO: do not cast after uint64 type will become supported in client + ) +} + +// GetNetMap performs the test invocation call of Netmap method of NeoFS Netmap contract. +func (s *MorphNetmapContract) GetNetMap(p netmap.GetParams) (*netmap.GetResult, error) { + prms, err := s.netmapContract.TestInvoke( + s.getNetMapMethodName, + ) + if err != nil { + return nil, errors.Wrap(err, "could not perform test invocation") + } else if ln := len(prms); ln != 1 { + return nil, errors.Errorf("unexpected stack item count (Nodes): %d", ln) + } + + prms, err = goclient.ArrayFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get stack item array from stack item (Nodes)") + } + + nm := netmap.NewNetmap() + + for i := range prms { + nodeInfo, err := nodeInfoFromStackItem(prms[i]) + if err != nil { + return nil, errors.Wrapf(err, "could not parse stack item (Node #%d)", i) + } + + if err := nm.AddNode(nodeInfo); err != nil { + return nil, errors.Wrapf(err, "could not add node #%d to network map", i) + } + } + + res := new(netmap.GetResult) + res.SetNetMap(nm) + + return res, nil +} + +func nodeInfoFromStackItem(prm smartcontract.Parameter) (*bootstrap.NodeInfo, error) { + prms, err := goclient.ArrayFromStackParameter(prm) + if err != nil { + return nil, errors.Wrapf(err, "could not get stack item array (NodeInfo)") + } else if ln := len(prms); ln != nodeInfoFixedPrmNumber { + return nil, errors.Errorf("unexpected stack item count (NodeInfo): expected %d, has %d", 3, ln) + } + + res := new(bootstrap.NodeInfo) + + // Address + addrBytes, err := goclient.BytesFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get byte array from stack item (Address)") + } + + res.Address = string(addrBytes) + + // Public key + res.PubKey, err = goclient.BytesFromStackParameter(prms[1]) + if err != nil { + return nil, errors.Wrap(err, "could not get byte array from stack item (Public key)") + } + + // Options + prms, err = goclient.ArrayFromStackParameter(prms[2]) + if err != nil { + return nil, errors.Wrapf(err, "could not get stack item array (Options)") + } + + res.Options = make([]string, 0, len(prms)) + + for i := range prms { + optBytes, err := goclient.BytesFromStackParameter(prms[i]) + if err != nil { + return nil, errors.Wrapf(err, "could not get byte array from stack item (Option #%d)", i) + } + + res.Options = append(res.Options, string(optBytes)) + } + + return res, nil +} + +// UpdateState invokes the call of UpdateState method of NeoFS Netmap contract. +func (s *MorphNetmapContract) UpdateState(p UpdateStateParams) error { + return s.netmapContract.Invoke( + s.updStateMethodName, + p.State().Int64(), + p.Key(), + ) +} + +// GetIRInfo performs the test invocation call of InnerRingList method of NeoFS Netmap contract. +func (s *MorphNetmapContract) GetIRInfo(ir.GetInfoParams) (*ir.GetInfoResult, error) { + prms, err := s.netmapContract.TestInvoke( + s.irListMethodName, + ) + if err != nil { + return nil, errors.Wrap(err, "could not perform test invocation") + } else if ln := len(prms); ln != 1 { + return nil, errors.Errorf("unexpected stack item count (Nodes): %d", ln) + } + + irInfo, err := irInfoFromStackItem(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get IR info from stack item") + } + + res := new(ir.GetInfoResult) + res.SetInfo(*irInfo) + + return res, nil +} + +func irInfoFromStackItem(prm smartcontract.Parameter) (*ir.Info, error) { + prms, err := goclient.ArrayFromStackParameter(prm) + if err != nil { + return nil, errors.Wrap(err, "could not get stack item array") + } + + nodes := make([]ir.Node, 0, len(prms)) + + for i := range prms { + node, err := irNodeFromStackItem(prms[i]) + if err != nil { + return nil, errors.Wrapf(err, "could not get node info from stack item (IRNode #%d)", i) + } + + nodes = append(nodes, *node) + } + + info := new(ir.Info) + info.SetNodes(nodes) + + return info, nil +} + +func irNodeFromStackItem(prm smartcontract.Parameter) (*ir.Node, error) { + prms, err := goclient.ArrayFromStackParameter(prm) + if err != nil { + return nil, errors.Wrap(err, "could not get stack item array (IRNode)") + } + + // Public key + keyBytes, err := goclient.BytesFromStackParameter(prms[0]) + if err != nil { + return nil, errors.Wrap(err, "could not get byte array from stack item (Key)") + } + + node := new(ir.Node) + node.SetKey(keyBytes) + + return node, nil +} + +// SetNumber is an epoch number setter. +func (s *UpdateEpochParams) SetNumber(v uint64) { + s.epoch = v +} + +// Number is an epoch number getter. +func (s UpdateEpochParams) Number() uint64 { + return s.epoch +} + +// SetState is a state setter. +func (s *UpdateStateParams) SetState(v NodeState) { + s.st = v +} + +// State is a state getter. +func (s UpdateStateParams) State() NodeState { + return s.st +} + +// SetKey is a public key setter. +func (s *UpdateStateParams) SetKey(v []byte) { + s.key = v +} + +// Key is a public key getter. +func (s UpdateStateParams) Key() []byte { + return s.key +} + +// Int64 converts NodeState to int64. +func (s NodeState) Int64() int64 { + return int64(s) +} diff --git a/lib/implementations/bootstrap_test.go b/lib/implementations/bootstrap_test.go new file mode 100644 index 0000000000..a9968ae98e --- /dev/null +++ b/lib/implementations/bootstrap_test.go @@ -0,0 +1,30 @@ +package implementations + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUpdateEpochParams(t *testing.T) { + s := UpdateEpochParams{} + + e := uint64(100) + s.SetNumber(e) + + require.Equal(t, e, s.Number()) +} + +func TestUpdateStateParams(t *testing.T) { + s := UpdateStateParams{} + + st := NodeState(1) + s.SetState(st) + + require.Equal(t, st, s.State()) + + key := []byte{1, 2, 3} + s.SetKey(key) + + require.Equal(t, key, s.Key()) +} diff --git a/lib/implementations/epoch.go b/lib/implementations/epoch.go new file mode 100644 index 0000000000..16d9a5c373 --- /dev/null +++ b/lib/implementations/epoch.go @@ -0,0 +1,7 @@ +package implementations + +// EpochReceiver is an interface of the container +// of NeoFS epoch number with read access. +type EpochReceiver interface { + Epoch() uint64 +} diff --git a/lib/implementations/locator.go b/lib/implementations/locator.go new file mode 100644 index 0000000000..6cf19ce0e5 --- /dev/null +++ b/lib/implementations/locator.go @@ -0,0 +1,78 @@ +package implementations + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/lib/replication" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + locator struct { + executor SelectiveContainerExecutor + log *zap.Logger + } + + // LocatorParams groups the parameters of ObjectLocator constructor. + LocatorParams struct { + SelectiveContainerExecutor SelectiveContainerExecutor + Logger *zap.Logger + } +) + +const locatorInstanceFailMsg = "could not create object locator" + +var errEmptyObjectsContainerHandler = errors.New("empty container objects container handler") + +func (s *locator) LocateObject(ctx context.Context, addr Address) (res []multiaddr.Multiaddr, err error) { + queryBytes, err := (&query.Query{ + Filters: []query.Filter{ + { + Type: query.Filter_Exact, + Name: transport.KeyID, + Value: addr.ObjectID.String(), + }, + }, + }).Marshal() + if err != nil { + return nil, errors.Wrap(err, "locate object failed on query marshal") + } + + err = s.executor.Search(ctx, &SearchParams{ + SelectiveParams: SelectiveParams{ + CID: addr.CID, + TTL: service.NonForwardingTTL, + IDList: make([]ObjectID, 1), + }, + SearchCID: addr.CID, + SearchQuery: queryBytes, + Handler: func(node multiaddr.Multiaddr, addrList []refs.Address) { + if len(addrList) > 0 { + res = append(res, node) + } + }, + }) + + return +} + +// NewObjectLocator constructs replication.ObjectLocator from SelectiveContainerExecutor. +func NewObjectLocator(p LocatorParams) (replication.ObjectLocator, error) { + switch { + case p.SelectiveContainerExecutor == nil: + return nil, errors.Wrap(errEmptyObjectsContainerHandler, locatorInstanceFailMsg) + case p.Logger == nil: + return nil, errors.Wrap(errEmptyLogger, locatorInstanceFailMsg) + } + + return &locator{ + executor: p.SelectiveContainerExecutor, + log: p.Logger, + }, nil +} diff --git a/lib/implementations/locator_test.go b/lib/implementations/locator_test.go new file mode 100644 index 0000000000..892b388391 --- /dev/null +++ b/lib/implementations/locator_test.go @@ -0,0 +1,38 @@ +package implementations + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type testExecutor struct { + SelectiveContainerExecutor +} + +func TestNewObjectLocator(t *testing.T) { + validParams := LocatorParams{ + SelectiveContainerExecutor: new(testExecutor), + Logger: zap.L(), + } + + t.Run("valid params", func(t *testing.T) { + s, err := NewObjectLocator(validParams) + require.NoError(t, err) + require.NotNil(t, s) + }) + t.Run("empty logger", func(t *testing.T) { + p := validParams + p.Logger = nil + _, err := NewObjectLocator(p) + require.EqualError(t, err, errors.Wrap(errEmptyLogger, locatorInstanceFailMsg).Error()) + }) + t.Run("empty container handler", func(t *testing.T) { + p := validParams + p.SelectiveContainerExecutor = nil + _, err := NewObjectLocator(p) + require.EqualError(t, err, errors.Wrap(errEmptyObjectsContainerHandler, locatorInstanceFailMsg).Error()) + }) +} diff --git a/lib/implementations/object.go b/lib/implementations/object.go new file mode 100644 index 0000000000..ed260af136 --- /dev/null +++ b/lib/implementations/object.go @@ -0,0 +1,131 @@ +package implementations + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/replication" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // ObjectStorage is an interface of encapsulated ObjectReceptacle and ObjectSource pair. + ObjectStorage interface { + replication.ObjectReceptacle + replication.ObjectSource + } + + objectStorage struct { + ls localstore.Localstore + executor SelectiveContainerExecutor + log *zap.Logger + } + + // ObjectStorageParams groups the parameters of ObjectStorage constructor. + ObjectStorageParams struct { + Localstore localstore.Localstore + SelectiveContainerExecutor SelectiveContainerExecutor + Logger *zap.Logger + } +) + +const objectSourceInstanceFailMsg = "could not create object source" + +var errNilObject = errors.New("object is nil") + +var errCouldNotGetObject = errors.New("could not get object from any node") + +func (s *objectStorage) Put(ctx context.Context, params replication.ObjectStoreParams) error { + if params.Object == nil { + return errNilObject + } else if len(params.Nodes) == 0 { + if s.ls == nil { + return errEmptyLocalstore + } + return s.ls.Put(ctx, params.Object) + } + + nodes := make([]multiaddr.Multiaddr, len(params.Nodes)) + for i := range params.Nodes { + nodes[i] = params.Nodes[i].Node + } + + return s.executor.Put(ctx, &PutParams{ + SelectiveParams: SelectiveParams{ + CID: params.Object.SystemHeader.CID, + Nodes: nodes, + TTL: service.NonForwardingTTL, + IDList: make([]ObjectID, 1), + }, + Object: params.Object, + Handler: func(node multiaddr.Multiaddr, valid bool) { + if params.Handler == nil { + return + } + for i := range params.Nodes { + if params.Nodes[i].Node.Equal(node) { + params.Handler(params.Nodes[i], valid) + return + } + } + }, + }) +} + +func (s *objectStorage) Get(ctx context.Context, addr Address) (res *Object, err error) { + if s.ls != nil { + if has, err := s.ls.Has(addr); err == nil && has { + if res, err = s.ls.Get(addr); err == nil { + return res, err + } + } + } + + if err = s.executor.Get(ctx, &GetParams{ + SelectiveParams: SelectiveParams{ + CID: addr.CID, + TTL: service.NonForwardingTTL, + IDList: []ObjectID{addr.ObjectID}, + Breaker: func(refs.Address) (cFlag ProgressControlFlag) { + if res != nil { + cFlag = BreakProgress + } + return + }, + }, + Handler: func(node multiaddr.Multiaddr, obj *object.Object) { res = obj }, + }); err != nil { + return + } else if res == nil { + return nil, errCouldNotGetObject + } + + return +} + +// NewObjectStorage encapsulates Localstore and SelectiveContainerExecutor +// and returns ObjectStorage interface. +func NewObjectStorage(p ObjectStorageParams) (ObjectStorage, error) { + if p.Logger == nil { + return nil, errors.Wrap(errEmptyLogger, objectSourceInstanceFailMsg) + } + + if p.Localstore == nil { + p.Logger.Warn("local storage not provided") + } + + if p.SelectiveContainerExecutor == nil { + p.Logger.Warn("object container handler not provided") + } + + return &objectStorage{ + ls: p.Localstore, + executor: p.SelectiveContainerExecutor, + log: p.Logger, + }, nil +} diff --git a/lib/implementations/peerstore.go b/lib/implementations/peerstore.go new file mode 100644 index 0000000000..6a7070f1bd --- /dev/null +++ b/lib/implementations/peerstore.go @@ -0,0 +1,74 @@ +package implementations + +import ( + "crypto/ecdsa" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // AddressStoreComponent is an interface of encapsulated AddressStore and NodePublicKeyReceiver pair. + AddressStoreComponent interface { + AddressStore + NodePublicKeyReceiver + } + + // AddressStore is an interface of the container of local Multiaddr. + AddressStore interface { + SelfAddr() (multiaddr.Multiaddr, error) + } + + // NodePublicKeyReceiver is an interface of Multiaddr to PublicKey converter. + NodePublicKeyReceiver interface { + PublicKey(multiaddr.Multiaddr) *ecdsa.PublicKey + } + + addressStore struct { + ps peers.Store + + log *zap.Logger + } +) + +const ( + addressStoreInstanceFailMsg = "could not create address store" + errEmptyPeerStore = internal.Error("empty peer store") + + errEmptyAddressStore = internal.Error("empty address store") +) + +func (s addressStore) SelfAddr() (multiaddr.Multiaddr, error) { return s.ps.GetAddr(s.ps.SelfID()) } + +func (s addressStore) PublicKey(mAddr multiaddr.Multiaddr) (res *ecdsa.PublicKey) { + if peerID, err := s.ps.AddressID(mAddr); err != nil { + s.log.Error("could not peer ID", + zap.Stringer("node", mAddr), + zap.Error(err), + ) + } else if res, err = s.ps.GetPublicKey(peerID); err != nil { + s.log.Error("could not receive public key", + zap.Stringer("peer", peerID), + zap.Error(err), + ) + } + + return res +} + +// NewAddressStore wraps peer store and returns AddressStoreComponent. +func NewAddressStore(ps peers.Store, log *zap.Logger) (AddressStoreComponent, error) { + if ps == nil { + return nil, errors.Wrap(errEmptyPeerStore, addressStoreInstanceFailMsg) + } else if log == nil { + return nil, errors.Wrap(errEmptyLogger, addressStoreInstanceFailMsg) + } + + return &addressStore{ + ps: ps, + log: log, + }, nil +} diff --git a/lib/implementations/placement.go b/lib/implementations/placement.go new file mode 100644 index 0000000000..4c7d95cf1f --- /dev/null +++ b/lib/implementations/placement.go @@ -0,0 +1,152 @@ +package implementations + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +/* + File source code includes implementations of placement-related solutions. + Highly specialized interfaces give the opportunity to hide placement implementation in a black box for the reasons: + * placement is implementation-tied entity working with graphs, filters, etc.; + * NeoFS components are mostly needed in a small part of the solutions provided by placement; + * direct dependency from placement avoidance helps other components do not touch crucial changes in placement. +*/ + +type ( + // CID is a type alias of + // CID from refs package of neofs-api-go. + CID = refs.CID + + // SGID is a type alias of + // SGID from refs package of neofs-api-go. + SGID = refs.SGID + + // ObjectID is a type alias of + // ObjectID from refs package of neofs-api-go. + ObjectID = refs.ObjectID + + // Object is a type alias of + // Object from object package of neofs-api-go. + Object = object.Object + + // Address is a type alias of + // Address from refs package of neofs-api-go. + Address = refs.Address + + // Netmap is a type alias of + // NetMap from netmap package. + Netmap = netmap.NetMap + + // ObjectPlacer is an interface of placement utility. + ObjectPlacer interface { + ContainerNodesLister + ContainerInvolvementChecker + GetNodes(ctx context.Context, addr Address, usePreviousNetMap bool, excl ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) + Epoch() uint64 + } + + // ContainerNodesLister is an interface of container placement vector builder. + ContainerNodesLister interface { + ContainerNodes(ctx context.Context, cid CID) ([]multiaddr.Multiaddr, error) + ContainerNodesInfo(ctx context.Context, cid CID, prev int) ([]bootstrap.NodeInfo, error) + } + + // ContainerInvolvementChecker is an interface of container affiliation checker. + ContainerInvolvementChecker interface { + IsContainerNode(ctx context.Context, addr multiaddr.Multiaddr, cid CID, previousNetMap bool) (bool, error) + } + + objectPlacer struct { + pl placement.Component + } +) + +const errEmptyPlacement = internal.Error("could not create storage lister: empty placement component") + +// NewObjectPlacer wraps placement.Component and returns ObjectPlacer interface. +func NewObjectPlacer(pl placement.Component) (ObjectPlacer, error) { + if pl == nil { + return nil, errEmptyPlacement + } + + return &objectPlacer{pl}, nil +} + +func (v objectPlacer) ContainerNodes(ctx context.Context, cid CID) ([]multiaddr.Multiaddr, error) { + graph, err := v.pl.Query(ctx, placement.ContainerID(cid)) + if err != nil { + return nil, errors.Wrap(err, "objectPlacer.ContainerNodes failed on graph query") + } + + return graph.NodeList() +} + +func (v objectPlacer) ContainerNodesInfo(ctx context.Context, cid CID, prev int) ([]bootstrap.NodeInfo, error) { + graph, err := v.pl.Query(ctx, placement.ContainerID(cid), placement.UsePreviousNetmap(prev)) + if err != nil { + return nil, errors.Wrap(err, "objectPlacer.ContainerNodesInfo failed on graph query") + } + + return graph.NodeInfo() +} + +func (v objectPlacer) GetNodes(ctx context.Context, addr Address, usePreviousNetMap bool, excl ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + queryOptions := make([]placement.QueryOption, 1, 2) + queryOptions[0] = placement.ContainerID(addr.CID) + + if usePreviousNetMap { + queryOptions = append(queryOptions, placement.UsePreviousNetmap(1)) + } + + graph, err := v.pl.Query(ctx, queryOptions...) + if err != nil { + if st, ok := status.FromError(errors.Cause(err)); ok && st.Code() == codes.NotFound { + return nil, container.ErrNotFound + } + + return nil, errors.Wrap(err, "placer.GetNodes failed on graph query") + } + + filter := func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { + return bucket + } + + if !addr.ObjectID.Empty() { + filter = func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { + return bucket.GetSelection(group.Selectors, addr.ObjectID.Bytes()) + } + } + + return graph.Exclude(excl).Filter(filter).NodeList() +} + +func (v objectPlacer) IsContainerNode(ctx context.Context, addr multiaddr.Multiaddr, cid CID, previousNetMap bool) (bool, error) { + nodes, err := v.GetNodes(ctx, Address{ + CID: cid, + }, previousNetMap) + if err != nil { + return false, errors.Wrap(err, "placer.FromContainer failed on placer.GetNodes") + } + + for i := range nodes { + if nodes[i].Equal(addr) { + return true, nil + } + } + + return false, nil +} + +func (v objectPlacer) Epoch() uint64 { return v.pl.NetworkState().Epoch } diff --git a/lib/implementations/reputation.go b/lib/implementations/reputation.go new file mode 100644 index 0000000000..2fb4865e28 --- /dev/null +++ b/lib/implementations/reputation.go @@ -0,0 +1,41 @@ +package implementations + +import ( + "github.com/nspcc-dev/neofs-node/lib/peers" +) + +// MorphReputationContract is a wrapper over NeoFS Reputation contract client +// that provides an interface of the storage of global trust values. +type MorphReputationContract struct { + // NeoFS Reputation smart-contract + repContract StaticContractClient + + // put method name of reputation contract + putMethodName string + + // list method name of reputation contract + listMethodName string + + // public key storage + pkStore peers.PublicKeyStore +} + +// SetReputationContractClient is a Reputation contract client setter. +func (s *MorphReputationContract) SetReputationContractClient(v StaticContractClient) { + s.repContract = v +} + +// SetPublicKeyStore is a public key store setter. +func (s *MorphReputationContract) SetPublicKeyStore(v peers.PublicKeyStore) { + s.pkStore = v +} + +// SetPutMethodName is a Reputation contract Put method name setter. +func (s *MorphReputationContract) SetPutMethodName(v string) { + s.putMethodName = v +} + +// SetListMethodName is a Reputation contract List method name setter. +func (s *MorphReputationContract) SetListMethodName(v string) { + s.listMethodName = v +} diff --git a/lib/implementations/sg.go b/lib/implementations/sg.go new file mode 100644 index 0000000000..ef0f95e8ac --- /dev/null +++ b/lib/implementations/sg.go @@ -0,0 +1,136 @@ +package implementations + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // StorageGroupInfoReceiverParams groups the parameters of + // storage group information receiver. + StorageGroupInfoReceiverParams struct { + SelectiveContainerExecutor SelectiveContainerExecutor + Logger *zap.Logger + } + + sgInfoRecv struct { + executor SelectiveContainerExecutor + log *zap.Logger + } +) + +const locationFinderInstanceFailMsg = "could not create object location finder" + +// ErrIncompleteSGInfo is returned by storage group information receiver +// that could not receive full information. +const ErrIncompleteSGInfo = internal.Error("could not receive full storage group info") + +// PublicSessionToken is a context key for SessionToken. +// FIXME: temp solution for cycle import fix. +// Unify with same const from transformer pkg. +const PublicSessionToken = "public token" + +// BearerToken is a context key for BearerToken. +const BearerToken = "bearer token" + +// ExtendedHeaders is a context key for X-headers. +const ExtendedHeaders = "extended headers" + +func (s *sgInfoRecv) GetSGInfo(ctx context.Context, cid CID, group []ObjectID) (*storagegroup.StorageGroup, error) { + var ( + err error + res = new(storagegroup.StorageGroup) + hashList = make([]hash.Hash, 0, len(group)) + ) + + m := make(map[string]struct{}, len(group)) + for i := range group { + m[group[i].String()] = struct{}{} + } + + // FIXME: hardcoded for simplicity. + // Function is called in next cases: + // - SG transformation on trusted node side (only in this case session token is needed); + // - SG info check on container nodes (token is not needed since system group has extra access); + // - data audit on inner ring nodes (same as previous). + var token service.SessionToken + if v, ok := ctx.Value(PublicSessionToken).(service.SessionToken); ok { + token = v + } + + var bearer service.BearerToken + if v, ok := ctx.Value(BearerToken).(service.BearerToken); ok { + bearer = v + } + + var extHdrs []service.ExtendedHeader + if v, ok := ctx.Value(ExtendedHeaders).([]service.ExtendedHeader); ok { + extHdrs = v + } + + if err = s.executor.Head(ctx, &HeadParams{ + GetParams: GetParams{ + SelectiveParams: SelectiveParams{ + CID: cid, + TTL: service.SingleForwardingTTL, + IDList: group, + Breaker: func(addr refs.Address) (cFlag ProgressControlFlag) { + if len(m) == 0 { + cFlag = BreakProgress + } else if _, ok := m[addr.ObjectID.String()]; !ok { + cFlag = NextAddress + } + return + }, + Token: token, + + Bearer: bearer, + + ExtendedHeaders: extHdrs, + }, + Handler: func(_ multiaddr.Multiaddr, obj *object.Object) { + _, hashHeader := obj.LastHeader(object.HeaderType(object.HomoHashHdr)) + if hashHeader == nil { + return + } + + hashList = append(hashList, hashHeader.Value.(*object.Header_HomoHash).HomoHash) + res.ValidationDataSize += obj.SystemHeader.PayloadLength + delete(m, obj.SystemHeader.ID.String()) + }, + }, + FullHeaders: true, + }); err != nil { + return nil, err + } else if len(m) > 0 { + return nil, ErrIncompleteSGInfo + } + + res.ValidationHash, err = hash.Concat(hashList) + + return res, err +} + +// NewStorageGroupInfoReceiver constructs storagegroup.InfoReceiver from SelectiveContainerExecutor. +func NewStorageGroupInfoReceiver(p StorageGroupInfoReceiverParams) (storagegroup.InfoReceiver, error) { + switch { + case p.Logger == nil: + return nil, errors.Wrap(errEmptyLogger, locationFinderInstanceFailMsg) + case p.SelectiveContainerExecutor == nil: + return nil, errors.Wrap(errEmptyObjectsContainerHandler, locationFinderInstanceFailMsg) + } + + return &sgInfoRecv{ + executor: p.SelectiveContainerExecutor, + log: p.Logger, + }, nil +} diff --git a/lib/implementations/transport.go b/lib/implementations/transport.go new file mode 100644 index 0000000000..b409be83dc --- /dev/null +++ b/lib/implementations/transport.go @@ -0,0 +1,657 @@ +package implementations + +import ( + "context" + "io" + "sync" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +/* + File source code includes implementation of unified objects container handler. + Implementation provides the opportunity to perform any logic over object container distributed in network. + Implementation holds placement and object transport implementations in a black box. + Any special logic could be tuned through passing handle parameters. + NOTE: Although the implementation of the other interfaces via OCH is the same, they are still separated in order to avoid mess. +*/ + +type ( + // SelectiveContainerExecutor is an interface the tool that performs + // object operations in container with preconditions. + SelectiveContainerExecutor interface { + Put(context.Context, *PutParams) error + Get(context.Context, *GetParams) error + Head(context.Context, *HeadParams) error + Search(context.Context, *SearchParams) error + RangeHash(context.Context, *RangeHashParams) error + } + + // PutParams groups the parameters + // of selective object Put. + PutParams struct { + SelectiveParams + Object *object.Object + Handler func(multiaddr.Multiaddr, bool) + + CopiesNumber uint32 + } + + // GetParams groups the parameters + // of selective object Get. + GetParams struct { + SelectiveParams + Handler func(multiaddr.Multiaddr, *object.Object) + } + + // HeadParams groups the parameters + // of selective object Head. + HeadParams struct { + GetParams + FullHeaders bool + } + + // SearchParams groups the parameters + // of selective object Search. + SearchParams struct { + SelectiveParams + SearchCID refs.CID + SearchQuery []byte + Handler func(multiaddr.Multiaddr, []refs.Address) + } + + // RangeHashParams groups the parameters + // of selective object GetRangeHash. + RangeHashParams struct { + SelectiveParams + Ranges []object.Range + Salt []byte + Handler func(multiaddr.Multiaddr, []hash.Hash) + } + + // SelectiveParams groups the parameters of + // the execution of selective container operation. + SelectiveParams struct { + /* Should be set to true only if service under object transport implementations is served on localhost. */ + ServeLocal bool + + /* Raw option of the request */ + Raw bool + + /* TTL for object transport. All transport operations inherit same value. */ + TTL uint32 + + /* Required ID of processing container. If empty or not set, an error is returned. */ + CID + + /* List of nodes selected for processing. If not specified => nodes will be selected during. */ + Nodes []multiaddr.Multiaddr + + /* + Next two parameters provide the opportunity to process selective objects in container. + At least on of non-empty IDList or Query is required, an error is returned otherwise. + */ + + /* List of objects to process (overlaps query). */ + IDList []refs.ObjectID + /* If no objects is indicated, query is used for selection. */ + Query []byte + + /* + If function provided, it is called after every successful operation. + True result breaks operation performing. + */ + Breaker func(refs.Address) ProgressControlFlag + + /* Public session token */ + Token service.SessionToken + + /* Bearer token */ + Bearer service.BearerToken + + /* Extended headers */ + ExtendedHeaders []service.ExtendedHeader + } + + // ProgressControlFlag is an enumeration of progress control flags. + ProgressControlFlag int + + // ObjectContainerHandlerParams grops the parameters of SelectiveContainerExecutor constructor. + ObjectContainerHandlerParams struct { + NodeLister ContainerNodesLister + Executor ContainerTraverseExecutor + *zap.Logger + } + + simpleTraverser struct { + *sync.Once + list []multiaddr.Multiaddr + } + + selectiveCnrExec struct { + cnl ContainerNodesLister + Executor ContainerTraverseExecutor + log *zap.Logger + } + + metaInfo struct { + ttl uint32 + raw bool + rt object.RequestType + + token service.SessionToken + + bearer service.BearerToken + + extHdrs []service.ExtendedHeader + } + + putInfo struct { + metaInfo + obj *object.Object + cn uint32 + } + + getInfo struct { + metaInfo + addr Address + raw bool + } + + headInfo struct { + getInfo + fullHdr bool + } + + searchInfo struct { + metaInfo + cid CID + query []byte + } + + rangeHashInfo struct { + metaInfo + addr Address + ranges []object.Range + salt []byte + } + + execItems struct { + params SelectiveParams + metaConstructor func(addr Address) transport.MetaInfo + handler transport.ResultHandler + } + + searchTarget struct { + list []refs.Address + } + + // ContainerTraverseExecutor is an interface of + // object operation executor with container traversing. + ContainerTraverseExecutor interface { + Execute(context.Context, TraverseParams) + } + + // TraverseParams groups the parameters of container traversing. + TraverseParams struct { + TransportInfo transport.MetaInfo + Handler transport.ResultHandler + Traverser Traverser + WorkerPool WorkerPool + ExecutionInterceptor func(context.Context, multiaddr.Multiaddr) bool + } + + // WorkerPool is an interface of go-routine pool + WorkerPool interface { + Submit(func()) error + } + + // Traverser is an interface of container traverser. + Traverser interface { + Next(context.Context) []multiaddr.Multiaddr + } + + cnrTraverseExec struct { + transport transport.ObjectTransport + } + + singleRoutinePool struct{} + + emptyReader struct{} +) + +const ( + _ ProgressControlFlag = iota + + // NextAddress is a ProgressControlFlag of to go to the next address of the object. + NextAddress + + // NextNode is a ProgressControlFlag of to go to the next node. + NextNode + + // BreakProgress is a ProgressControlFlag to interrupt the execution. + BreakProgress +) + +const ( + instanceFailMsg = "could not create container objects collector" + errEmptyLogger = internal.Error("empty logger") + errEmptyNodeLister = internal.Error("empty container node lister") + errEmptyTraverseExecutor = internal.Error("empty container traverse executor") + + errSelectiveParams = internal.Error("neither ID list nor query provided") +) + +var errNilObjectTransport = errors.New("object transport is nil") + +func (s *selectiveCnrExec) Put(ctx context.Context, p *PutParams) error { + meta := &putInfo{ + metaInfo: metaInfo{ + ttl: p.TTL, + rt: object.RequestPut, + raw: p.Raw, + + token: p.Token, + + bearer: p.Bearer, + + extHdrs: p.ExtendedHeaders, + }, + obj: p.Object, + cn: p.CopiesNumber, + } + + return s.exec(ctx, &execItems{ + params: p.SelectiveParams, + metaConstructor: func(Address) transport.MetaInfo { return meta }, + handler: p, + }) +} + +func (s *selectiveCnrExec) Get(ctx context.Context, p *GetParams) error { + return s.exec(ctx, &execItems{ + params: p.SelectiveParams, + metaConstructor: func(addr Address) transport.MetaInfo { + return &getInfo{ + metaInfo: metaInfo{ + ttl: p.TTL, + rt: object.RequestGet, + raw: p.Raw, + + token: p.Token, + + bearer: p.Bearer, + + extHdrs: p.ExtendedHeaders, + }, + addr: addr, + raw: p.Raw, + } + }, + handler: p, + }) +} + +func (s *selectiveCnrExec) Head(ctx context.Context, p *HeadParams) error { + return s.exec(ctx, &execItems{ + params: p.SelectiveParams, + metaConstructor: func(addr Address) transport.MetaInfo { + return &headInfo{ + getInfo: getInfo{ + metaInfo: metaInfo{ + ttl: p.TTL, + rt: object.RequestHead, + raw: p.Raw, + + token: p.Token, + + bearer: p.Bearer, + + extHdrs: p.ExtendedHeaders, + }, + addr: addr, + raw: p.Raw, + }, + fullHdr: p.FullHeaders, + } + }, + handler: p, + }) +} + +func (s *selectiveCnrExec) Search(ctx context.Context, p *SearchParams) error { + return s.exec(ctx, &execItems{ + params: p.SelectiveParams, + metaConstructor: func(Address) transport.MetaInfo { + return &searchInfo{ + metaInfo: metaInfo{ + ttl: p.TTL, + rt: object.RequestSearch, + raw: p.Raw, + + token: p.Token, + + bearer: p.Bearer, + + extHdrs: p.ExtendedHeaders, + }, + cid: p.SearchCID, + query: p.SearchQuery, + } + }, + handler: p, + }) +} + +func (s *selectiveCnrExec) RangeHash(ctx context.Context, p *RangeHashParams) error { + return s.exec(ctx, &execItems{ + params: p.SelectiveParams, + metaConstructor: func(addr Address) transport.MetaInfo { + return &rangeHashInfo{ + metaInfo: metaInfo{ + ttl: p.TTL, + rt: object.RequestRangeHash, + raw: p.Raw, + + token: p.Token, + + bearer: p.Bearer, + + extHdrs: p.ExtendedHeaders, + }, + addr: addr, + ranges: p.Ranges, + salt: p.Salt, + } + }, + handler: p, + }) +} + +func (s *selectiveCnrExec) exec(ctx context.Context, p *execItems) error { + if err := p.params.validate(); err != nil { + return err + } + + nodes, err := s.prepareNodes(ctx, &p.params) + if err != nil { + return err + } + +loop: + for i := range nodes { + addrList := s.prepareAddrList(ctx, &p.params, nodes[i]) + if len(addrList) == 0 { + continue + } + + for j := range addrList { + if p.params.Breaker != nil { + switch cFlag := p.params.Breaker(addrList[j]); cFlag { + case NextAddress: + continue + case NextNode: + continue loop + case BreakProgress: + break loop + } + } + + s.Executor.Execute(ctx, TraverseParams{ + TransportInfo: p.metaConstructor(addrList[j]), + Handler: p.handler, + Traverser: newSimpleTraverser(nodes[i]), + }) + } + } + + return nil +} + +func (s *SelectiveParams) validate() error { + switch { + case len(s.IDList) == 0 && len(s.Query) == 0: + return errSelectiveParams + default: + return nil + } +} + +func (s *selectiveCnrExec) prepareNodes(ctx context.Context, p *SelectiveParams) ([]multiaddr.Multiaddr, error) { + if len(p.Nodes) > 0 { + return p.Nodes, nil + } + + // If node serves Object transport service on localhost => pass single empty node + if p.ServeLocal { + // all transport implementations will use localhost by default + return []multiaddr.Multiaddr{nil}, nil + } + + // Otherwise use container nodes + return s.cnl.ContainerNodes(ctx, p.CID) +} + +func (s *selectiveCnrExec) prepareAddrList(ctx context.Context, p *SelectiveParams, node multiaddr.Multiaddr) []refs.Address { + var ( + addrList []Address + l = len(p.IDList) + ) + + if l > 0 { + addrList = make([]Address, 0, l) + for i := range p.IDList { + addrList = append(addrList, Address{CID: p.CID, ObjectID: p.IDList[i]}) + } + + return addrList + } + + handler := new(searchTarget) + + s.Executor.Execute(ctx, TraverseParams{ + TransportInfo: &searchInfo{ + metaInfo: metaInfo{ + ttl: p.TTL, + rt: object.RequestSearch, + raw: p.Raw, + + token: p.Token, + + bearer: p.Bearer, + + extHdrs: p.ExtendedHeaders, + }, + cid: p.CID, + query: p.Query, + }, + Handler: handler, + Traverser: newSimpleTraverser(node), + }) + + return handler.list +} + +func newSimpleTraverser(list ...multiaddr.Multiaddr) Traverser { + return &simpleTraverser{ + Once: new(sync.Once), + list: list, + } +} + +func (s *simpleTraverser) Next(context.Context) (res []multiaddr.Multiaddr) { + s.Do(func() { + res = s.list + }) + + return +} + +func (s metaInfo) GetTTL() uint32 { return s.ttl } + +func (s metaInfo) GetTimeout() time.Duration { return 0 } + +func (s metaInfo) GetRaw() bool { return s.raw } + +func (s metaInfo) Type() object.RequestType { return s.rt } + +func (s metaInfo) GetSessionToken() service.SessionToken { return s.token } + +func (s metaInfo) GetBearerToken() service.BearerToken { return s.bearer } + +func (s metaInfo) ExtendedHeaders() []service.ExtendedHeader { return s.extHdrs } + +func (s *putInfo) GetHead() *object.Object { return s.obj } + +func (s *putInfo) Payload() io.Reader { return new(emptyReader) } + +func (*emptyReader) Read(p []byte) (int, error) { return 0, io.EOF } + +func (s *putInfo) CopiesNumber() uint32 { + return s.cn +} + +func (s *getInfo) GetAddress() refs.Address { return s.addr } + +func (s *getInfo) Raw() bool { return s.raw } + +func (s *headInfo) GetFullHeaders() bool { return s.fullHdr } + +func (s *searchInfo) GetCID() refs.CID { return s.cid } + +func (s *searchInfo) GetQuery() []byte { return s.query } + +func (s *rangeHashInfo) GetAddress() refs.Address { return s.addr } + +func (s *rangeHashInfo) GetRanges() []object.Range { return s.ranges } + +func (s *rangeHashInfo) GetSalt() []byte { return s.salt } + +func (s *searchTarget) HandleResult(_ context.Context, _ multiaddr.Multiaddr, r interface{}, e error) { + if e == nil { + s.list = append(s.list, r.([]refs.Address)...) + } +} + +// HandleResult calls Handler with: +// - Multiaddr with argument value; +// - error equality to nil. +func (s *PutParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, _ interface{}, e error) { + s.Handler(node, e == nil) +} + +// HandleResult calls Handler if error argument is nil with: +// - Multiaddr with argument value; +// - result casted to an Object pointer. +func (s *GetParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, r interface{}, e error) { + if e == nil { + s.Handler(node, r.(*object.Object)) + } +} + +// HandleResult calls Handler if error argument is nil with: +// - Multiaddr with argument value; +// - result casted to Address slice. +func (s *SearchParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, r interface{}, e error) { + if e == nil { + s.Handler(node, r.([]refs.Address)) + } +} + +// HandleResult calls Handler if error argument is nil with: +// - Multiaddr with argument value; +// - result casted to Hash slice. +func (s *RangeHashParams) HandleResult(_ context.Context, node multiaddr.Multiaddr, r interface{}, e error) { + if e == nil { + s.Handler(node, r.([]hash.Hash)) + } +} + +func (s *cnrTraverseExec) Execute(ctx context.Context, p TraverseParams) { + if p.WorkerPool == nil { + p.WorkerPool = new(singleRoutinePool) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + wg := new(sync.WaitGroup) + + for { + select { + case <-ctx.Done(): + return + default: + } + + nodes := p.Traverser.Next(ctx) + if len(nodes) == 0 { + break + } + + for i := range nodes { + node := nodes[i] + + wg.Add(1) + + if err := p.WorkerPool.Submit(func() { + defer wg.Done() + + if p.ExecutionInterceptor != nil && p.ExecutionInterceptor(ctx, node) { + return + } + + s.transport.Transport(ctx, transport.ObjectTransportParams{ + TransportInfo: p.TransportInfo, + TargetNode: node, + ResultHandler: p.Handler, + }) + }); err != nil { + wg.Done() + } + } + + wg.Wait() + } +} + +func (*singleRoutinePool) Submit(fn func()) error { + fn() + return nil +} + +// NewObjectContainerHandler is a SelectiveContainerExecutor constructor. +func NewObjectContainerHandler(p ObjectContainerHandlerParams) (SelectiveContainerExecutor, error) { + switch { + case p.Executor == nil: + return nil, errors.Wrap(errEmptyTraverseExecutor, instanceFailMsg) + case p.Logger == nil: + return nil, errors.Wrap(errEmptyLogger, instanceFailMsg) + case p.NodeLister == nil: + return nil, errors.Wrap(errEmptyNodeLister, instanceFailMsg) + } + + return &selectiveCnrExec{ + cnl: p.NodeLister, + Executor: p.Executor, + log: p.Logger, + }, nil +} + +// NewContainerTraverseExecutor is a ContainerTraverseExecutor executor. +func NewContainerTraverseExecutor(t transport.ObjectTransport) (ContainerTraverseExecutor, error) { + if t == nil { + return nil, errNilObjectTransport + } + + return &cnrTraverseExec{transport: t}, nil +} diff --git a/lib/implementations/validation.go b/lib/implementations/validation.go new file mode 100644 index 0000000000..4ab858a3df --- /dev/null +++ b/lib/implementations/validation.go @@ -0,0 +1,405 @@ +package implementations + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/sha256" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/nspcc-dev/neofs-node/lib/rand" + "github.com/nspcc-dev/neofs-node/lib/replication" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + objectValidator struct { + as AddressStore + ls localstore.Localstore + executor SelectiveContainerExecutor + log *zap.Logger + + saltSize int + maxRngSize uint64 + rangeCount int + sltr Salitor + verifier objutil.Verifier + } + + // Salitor is a salting data function. + Salitor func(data, salt []byte) []byte + + // ObjectValidatorParams groups th + ObjectValidatorParams struct { + AddressStore AddressStore + Localstore localstore.Localstore + SelectiveContainerExecutor SelectiveContainerExecutor + Logger *zap.Logger + + Salitor Salitor + SaltSize int + MaxPayloadRangeSize uint64 + PayloadRangeCount int + + Verifier objutil.Verifier + } + + localHeadIntegrityVerifier struct { + keyVerifier core.OwnerKeyVerifier + } + + payloadVerifier struct { + } + + localIntegrityVerifier struct { + headVerifier objutil.Verifier + payloadVerifier objutil.Verifier + } +) + +const ( + objectValidatorInstanceFailMsg = "could not create object validator" + errEmptyLocalstore = internal.Error("empty local storage") + errEmptyObjectVerifier = internal.Error("empty object verifier") + + defaultSaltSize = 64 // bytes + defaultPayloadRangeCount = 3 + defaultMaxPayloadRangeSize = 64 +) + +const ( + errBrokenHeaderStructure = internal.Error("broken header structure") + + errMissingPayloadChecksumHeader = internal.Error("missing payload checksum header") + errWrongPayloadChecksum = internal.Error("wrong payload checksum") +) + +func (s *objectValidator) Verify(ctx context.Context, params *replication.ObjectVerificationParams) bool { + selfAddr, err := s.as.SelfAddr() + if err != nil { + s.log.Debug("receive self address failure", zap.Error(err)) + return false + } + + if params.Node == nil || params.Node.Equal(selfAddr) { + return s.verifyLocal(ctx, params.Address) + } + + return s.verifyRemote(ctx, params) +} + +func (s *objectValidator) verifyLocal(ctx context.Context, addr Address) bool { + var ( + err error + obj *Object + ) + + if obj, err = s.ls.Get(addr); err != nil { + s.log.Debug("get local meta information failure", zap.Error(err)) + return false + } else if err = s.verifier.Verify(ctx, obj); err != nil { + s.log.Debug("integrity check failure", zap.Error(err)) + } + + return err == nil +} + +func (s *objectValidator) verifyRemote(ctx context.Context, params *replication.ObjectVerificationParams) bool { + var ( + receivedObj *Object + valid bool + ) + + defer func() { + if params.Handler != nil && receivedObj != nil { + params.Handler(valid, receivedObj) + } + }() + + p := &HeadParams{ + GetParams: GetParams{ + SelectiveParams: SelectiveParams{ + CID: params.CID, + Nodes: []multiaddr.Multiaddr{params.Node}, + TTL: service.NonForwardingTTL, + IDList: []ObjectID{params.ObjectID}, + Raw: true, + }, + Handler: func(_ multiaddr.Multiaddr, obj *object.Object) { + receivedObj = obj + valid = s.verifier.Verify(ctx, obj) == nil + }, + }, + FullHeaders: true, + } + + if err := s.executor.Head(ctx, p); err != nil || !valid { + return false + } else if receivedObj.SystemHeader.PayloadLength <= 0 || receivedObj.IsLinking() { + return true + } + + if !params.LocalInvalid { + has, err := s.ls.Has(params.Address) + if err == nil && has { + obj, err := s.ls.Get(params.Address) + if err == nil { + return s.verifyThroughHashes(ctx, obj, params.Node) + } + } + } + + valid = false + _ = s.executor.Get(ctx, &p.GetParams) + + return valid +} + +func (s *objectValidator) verifyThroughHashes(ctx context.Context, obj *Object, node multiaddr.Multiaddr) (valid bool) { + var ( + salt = generateSalt(s.saltSize) + rngs = generateRanges(obj.SystemHeader.PayloadLength, s.maxRngSize, s.rangeCount) + ) + + _ = s.executor.RangeHash(ctx, &RangeHashParams{ + SelectiveParams: SelectiveParams{ + CID: obj.SystemHeader.CID, + Nodes: []multiaddr.Multiaddr{node}, + TTL: service.NonForwardingTTL, + IDList: []ObjectID{obj.SystemHeader.ID}, + }, + Ranges: rngs, + Salt: salt, + Handler: func(node multiaddr.Multiaddr, hashes []hash.Hash) { + valid = compareHashes(s.sltr, obj.Payload, salt, rngs, hashes) + }, + }) + + return +} + +func compareHashes(sltr Salitor, payload, salt []byte, rngs []object.Range, hashes []hash.Hash) bool { + if len(rngs) != len(hashes) { + return false + } + + for i := range rngs { + saltPayloadPart := sltr(payload[rngs[i].Offset:rngs[i].Offset+rngs[i].Length], salt) + if !hashes[i].Equal(hash.Sum(saltPayloadPart)) { + return false + } + } + + return true +} + +func generateRanges(payloadSize, maxRangeSize uint64, count int) []object.Range { + res := make([]object.Range, count) + + l := min(payloadSize, maxRangeSize) + + for i := 0; i < count; i++ { + res[i].Length = l + res[i].Offset = rand.Uint64(rand.New(), int64(payloadSize-l)) + } + + return res +} + +func min(a, b uint64) uint64 { + if a < b { + return a + } + + return b +} + +func generateSalt(saltSize int) []byte { + salt := make([]byte, saltSize) + if _, err := rand.Read(salt); err != nil { + return nil + } + + return salt +} + +// NewObjectValidator constructs universal replication.ObjectVerifier. +func NewObjectValidator(p *ObjectValidatorParams) (replication.ObjectVerifier, error) { + switch { + case p.Logger == nil: + return nil, errors.Wrap(errEmptyLogger, objectValidatorInstanceFailMsg) + case p.AddressStore == nil: + return nil, errors.Wrap(errEmptyAddressStore, objectValidatorInstanceFailMsg) + case p.Localstore == nil: + return nil, errors.Wrap(errEmptyLocalstore, objectValidatorInstanceFailMsg) + case p.Verifier == nil: + return nil, errors.Wrap(errEmptyObjectVerifier, objectValidatorInstanceFailMsg) + } + + if p.SaltSize <= 0 { + p.SaltSize = defaultSaltSize + } + + if p.PayloadRangeCount <= 0 { + p.PayloadRangeCount = defaultPayloadRangeCount + } + + if p.MaxPayloadRangeSize <= 0 { + p.MaxPayloadRangeSize = defaultMaxPayloadRangeSize + } + + return &objectValidator{ + as: p.AddressStore, + ls: p.Localstore, + executor: p.SelectiveContainerExecutor, + log: p.Logger, + saltSize: p.SaltSize, + maxRngSize: p.MaxPayloadRangeSize, + rangeCount: p.PayloadRangeCount, + sltr: p.Salitor, + verifier: p.Verifier, + }, nil +} + +// NewLocalHeadIntegrityVerifier constructs local object head verifier and returns objutil.Verifier interface. +func NewLocalHeadIntegrityVerifier(keyVerifier core.OwnerKeyVerifier) (objutil.Verifier, error) { + if keyVerifier == nil { + return nil, core.ErrNilOwnerKeyVerifier + } + + return &localHeadIntegrityVerifier{ + keyVerifier: keyVerifier, + }, nil +} + +// NewLocalIntegrityVerifier constructs local object verifier and returns objutil.Verifier interface. +func NewLocalIntegrityVerifier(keyVerifier core.OwnerKeyVerifier) (objutil.Verifier, error) { + if keyVerifier == nil { + return nil, core.ErrNilOwnerKeyVerifier + } + + return &localIntegrityVerifier{ + headVerifier: &localHeadIntegrityVerifier{ + keyVerifier: keyVerifier, + }, + payloadVerifier: new(payloadVerifier), + }, nil +} + +// NewPayloadVerifier constructs object payload verifier and returns objutil.Verifier. +func NewPayloadVerifier() objutil.Verifier { + return new(payloadVerifier) +} + +type hdrOwnerKeyContainer struct { + owner refs.OwnerID + key []byte +} + +func (s hdrOwnerKeyContainer) GetOwnerID() refs.OwnerID { + return s.owner +} + +func (s hdrOwnerKeyContainer) GetOwnerKey() []byte { + return s.key +} + +func (s *localHeadIntegrityVerifier) Verify(ctx context.Context, obj *Object) error { + var ( + checkKey *ecdsa.PublicKey + ownerKeyCnr core.OwnerKeyContainer + ) + + if _, h := obj.LastHeader(object.HeaderType(object.TokenHdr)); h != nil { + token := h.GetValue().(*object.Header_Token).Token + + if err := service.VerifySignatureWithKey( + crypto.UnmarshalPublicKey(token.GetOwnerKey()), + service.NewVerifiedSessionToken(token), + ); err != nil { + return err + } + + ownerKeyCnr = token + + checkKey = crypto.UnmarshalPublicKey(token.GetSessionKey()) + } else if _, h := obj.LastHeader(object.HeaderType(object.PublicKeyHdr)); h != nil { + pkHdr := h.GetValue().(*object.Header_PublicKey) + if pkHdr != nil && pkHdr.PublicKey != nil { + val := pkHdr.PublicKey.GetValue() + + ownerKeyCnr = &hdrOwnerKeyContainer{ + owner: obj.GetSystemHeader().OwnerID, + key: val, + } + + checkKey = crypto.UnmarshalPublicKey(val) + } + } + + if ownerKeyCnr == nil { + return core.ErrNilOwnerKeyContainer + } else if err := s.keyVerifier.VerifyKey(ctx, ownerKeyCnr); err != nil { + return err + } + + return verifyObjectIntegrity(obj, checkKey) +} + +// verifyObjectIntegrity verifies integrity of object header. +// Returns error if object +// - does not contains integrity header; +// - integrity header is not a last header in object; +// - integrity header signature is broken. +func verifyObjectIntegrity(obj *Object, key *ecdsa.PublicKey) error { + n, h := obj.LastHeader(object.HeaderType(object.IntegrityHdr)) + + if l := len(obj.Headers); l <= 0 || n != l-1 { + return errBrokenHeaderStructure + } + + integrityHdr := h.Value.(*object.Header_Integrity).Integrity + if integrityHdr == nil { + return errBrokenHeaderStructure + } + + data, err := objutil.MarshalHeaders(obj, n) + if err != nil { + return err + } + + hdrChecksum := sha256.Sum256(data) + + return crypto.Verify(key, hdrChecksum[:], integrityHdr.ChecksumSignature) +} + +func (s *payloadVerifier) Verify(_ context.Context, obj *Object) error { + if _, h := obj.LastHeader(object.HeaderType(object.PayloadChecksumHdr)); h == nil { + return errMissingPayloadChecksumHeader + } else if checksum := sha256.Sum256(obj.Payload); !bytes.Equal( + checksum[:], + h.Value.(*object.Header_PayloadChecksum).PayloadChecksum, + ) { + return errWrongPayloadChecksum + } + + return nil +} + +func (s *localIntegrityVerifier) Verify(ctx context.Context, obj *Object) error { + if err := s.headVerifier.Verify(ctx, obj); err != nil { + return err + } + + return s.payloadVerifier.Verify(ctx, obj) +} diff --git a/lib/implementations/validation_test.go b/lib/implementations/validation_test.go new file mode 100644 index 0000000000..f795ebd4b4 --- /dev/null +++ b/lib/implementations/validation_test.go @@ -0,0 +1,273 @@ +package implementations + +import ( + "context" + "crypto/ecdsa" + "crypto/sha256" + "math/rand" + "testing" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type testEntity struct { + err error +} + +func (s *testEntity) Verify(context.Context, *object.Object) error { return s.err } + +func (s *testEntity) SelfAddr() (multiaddr.Multiaddr, error) { panic("implement me") } +func (s *testEntity) Put(context.Context, *localstore.Object) error { panic("implement me") } +func (s *testEntity) Get(localstore.Address) (*localstore.Object, error) { panic("implement me") } +func (s *testEntity) Del(localstore.Address) error { panic("implement me") } +func (s *testEntity) Meta(localstore.Address) (*localstore.ObjectMeta, error) { panic("implement me") } +func (s *testEntity) Has(localstore.Address) (bool, error) { panic("implement me") } +func (s *testEntity) ObjectsCount() (uint64, error) { panic("implement me") } +func (s *testEntity) Size() int64 { panic("implement me") } +func (s *testEntity) Iterate(localstore.FilterPipeline, localstore.MetaHandler) error { + panic("implement me") +} + +func (s *testEntity) PRead(ctx context.Context, addr refs.Address, rng object.Range) ([]byte, error) { + panic("implement me") +} + +func (s *testEntity) VerifyKey(context.Context, core.OwnerKeyContainer) error { + return s.err +} + +func TestNewObjectValidator(t *testing.T) { + validParams := ObjectValidatorParams{ + Logger: zap.L(), + AddressStore: new(testEntity), + Localstore: new(testEntity), + Verifier: new(testEntity), + } + + t.Run("valid params", func(t *testing.T) { + s, err := NewObjectValidator(&validParams) + require.NoError(t, err) + require.NotNil(t, s) + }) + t.Run("fail on empty local storage", func(t *testing.T) { + p := validParams + p.Localstore = nil + _, err := NewObjectValidator(&p) + require.EqualError(t, err, errors.Wrap(errEmptyLocalstore, objectValidatorInstanceFailMsg).Error()) + }) + t.Run("fail on empty logger", func(t *testing.T) { + p := validParams + p.Logger = nil + _, err := NewObjectValidator(&p) + require.EqualError(t, err, errors.Wrap(errEmptyLogger, objectValidatorInstanceFailMsg).Error()) + }) +} + +func TestNewLocalIntegrityVerifier(t *testing.T) { + var ( + err error + verifier objutil.Verifier + keyVerifier = new(testEntity) + ) + + _, err = NewLocalHeadIntegrityVerifier(nil) + require.EqualError(t, err, core.ErrNilOwnerKeyVerifier.Error()) + + _, err = NewLocalIntegrityVerifier(nil) + require.EqualError(t, err, core.ErrNilOwnerKeyVerifier.Error()) + + verifier, err = NewLocalHeadIntegrityVerifier(keyVerifier) + require.NoError(t, err) + require.NotNil(t, verifier) + + verifier, err = NewLocalIntegrityVerifier(keyVerifier) + require.NoError(t, err) + require.NotNil(t, verifier) +} + +func TestLocalHeadIntegrityVerifier_Verify(t *testing.T) { + var ( + ctx = context.TODO() + ownerPrivateKey = test.DecodeKey(0) + ownerPublicKey = &ownerPrivateKey.PublicKey + sessionPrivateKey = test.DecodeKey(1) + sessionPublicKey = &sessionPrivateKey.PublicKey + ) + + ownerID, err := refs.NewOwnerID(ownerPublicKey) + require.NoError(t, err) + + s, err := NewLocalIntegrityVerifier(core.NewNeoKeyVerifier()) + require.NoError(t, err) + + okItems := []func() *Object{ + // correct object w/ session token + func() *Object { + token := new(service.Token) + token.SetOwnerID(ownerID) + token.SetSessionKey(crypto.MarshalPublicKey(sessionPublicKey)) + + require.NoError(t, + service.AddSignatureWithKey( + ownerPrivateKey, + service.NewSignedSessionToken(token), + ), + ) + + obj := new(Object) + obj.AddHeader(&object.Header{ + Value: &object.Header_Token{ + Token: token, + }, + }) + + obj.SetPayload([]byte{1, 2, 3}) + addPayloadChecksum(obj) + + addHeadersChecksum(t, obj, sessionPrivateKey) + + return obj + }, + // correct object w/o session token + func() *Object { + obj := new(Object) + obj.SystemHeader.OwnerID = ownerID + obj.SetPayload([]byte{1, 2, 3}) + + addPayloadChecksum(obj) + + obj.AddHeader(&object.Header{ + Value: &object.Header_PublicKey{ + PublicKey: &object.PublicKey{ + Value: crypto.MarshalPublicKey(ownerPublicKey), + }, + }, + }) + + addHeadersChecksum(t, obj, ownerPrivateKey) + + return obj + }, + } + + failItems := []func() *Object{} + + for _, item := range okItems { + require.NoError(t, s.Verify(ctx, item())) + } + + for _, item := range failItems { + require.Error(t, s.Verify(ctx, item())) + } +} + +func addPayloadChecksum(obj *Object) { + payloadChecksum := sha256.Sum256(obj.GetPayload()) + + obj.AddHeader(&object.Header{ + Value: &object.Header_PayloadChecksum{ + PayloadChecksum: payloadChecksum[:], + }, + }) +} + +func addHeadersChecksum(t *testing.T, obj *Object, key *ecdsa.PrivateKey) { + headersData, err := objutil.MarshalHeaders(obj, len(obj.Headers)) + require.NoError(t, err) + + headersChecksum := sha256.Sum256(headersData) + + integrityHdr := new(object.IntegrityHeader) + integrityHdr.SetHeadersChecksum(headersChecksum[:]) + + require.NoError(t, service.AddSignatureWithKey(key, integrityHdr)) + + obj.AddHeader(&object.Header{ + Value: &object.Header_Integrity{ + Integrity: integrityHdr, + }, + }) +} + +func TestPayloadVerifier_Verify(t *testing.T) { + ctx := context.TODO() + verifier := new(payloadVerifier) + + t.Run("missing header", func(t *testing.T) { + obj := new(Object) + require.EqualError(t, verifier.Verify(ctx, obj), errMissingPayloadChecksumHeader.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + payload := testData(t, 10) + + cs := sha256.Sum256(payload) + hdr := &object.Header_PayloadChecksum{PayloadChecksum: cs[:]} + + obj := &Object{ + Headers: []object.Header{{Value: hdr}}, + Payload: payload, + } + + require.NoError(t, verifier.Verify(ctx, obj)) + + hdr.PayloadChecksum[0]++ + require.EqualError(t, verifier.Verify(ctx, obj), errWrongPayloadChecksum.Error()) + + hdr.PayloadChecksum[0]-- + obj.Payload[0]++ + require.EqualError(t, verifier.Verify(ctx, obj), errWrongPayloadChecksum.Error()) + }) +} + +func TestLocalIntegrityVerifier_Verify(t *testing.T) { + ctx := context.TODO() + obj := new(Object) + + t.Run("head verification failure", func(t *testing.T) { + hErr := internal.Error("test error for head verifier") + + s := &localIntegrityVerifier{ + headVerifier: &testEntity{ + err: hErr, // force head verifier to return hErr + }, + } + + require.EqualError(t, s.Verify(ctx, obj), hErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + pErr := internal.Error("test error for payload verifier") + + s := &localIntegrityVerifier{ + headVerifier: new(testEntity), + payloadVerifier: &testEntity{ + err: pErr, // force payload verifier to return hErr + }, + } + + require.EqualError(t, s.Verify(ctx, obj), pErr.Error()) + }) +} + +// testData returns size bytes of random data. +func testData(t *testing.T, size int) []byte { + res := make([]byte, size) + _, err := rand.Read(res) + require.NoError(t, err) + return res +} + +// TODO: write functionality tests diff --git a/lib/ir/info.go b/lib/ir/info.go new file mode 100644 index 0000000000..991a1efad6 --- /dev/null +++ b/lib/ir/info.go @@ -0,0 +1,17 @@ +package ir + +// Info is a structure that groups the information +// about inner ring. +type Info struct { + nodes []Node +} + +// SetNodes is an IR node list setter. +func (s *Info) SetNodes(v []Node) { + s.nodes = v +} + +// Nodes is an IR node list getter. +func (s Info) Nodes() []Node { + return s.nodes +} diff --git a/lib/ir/info_test.go b/lib/ir/info_test.go new file mode 100644 index 0000000000..6b1f3df4bd --- /dev/null +++ b/lib/ir/info_test.go @@ -0,0 +1,25 @@ +package ir + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInfo(t *testing.T) { + s := Info{} + + n1 := Node{} + n1.SetKey([]byte{1, 2, 3}) + + n2 := Node{} + n2.SetKey([]byte{4, 5, 6}) + + nodes := []Node{ + n1, + n2, + } + s.SetNodes(nodes) + + require.Equal(t, nodes, s.Nodes()) +} diff --git a/lib/ir/node.go b/lib/ir/node.go new file mode 100644 index 0000000000..c1a765b5d5 --- /dev/null +++ b/lib/ir/node.go @@ -0,0 +1,17 @@ +package ir + +// Node is a structure that groups +// the information about IR node. +type Node struct { + key []byte +} + +// SetKey is an IR node public key setter. +func (s *Node) SetKey(v []byte) { + s.key = v +} + +// Key is an IR node public key getter. +func (s Node) Key() []byte { + return s.key +} diff --git a/lib/ir/node_test.go b/lib/ir/node_test.go new file mode 100644 index 0000000000..9663caf9c0 --- /dev/null +++ b/lib/ir/node_test.go @@ -0,0 +1,16 @@ +package ir + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNode(t *testing.T) { + s := Node{} + + key := []byte{1, 2, 3} + s.SetKey(key) + + require.Equal(t, key, s.Key()) +} diff --git a/lib/ir/storage.go b/lib/ir/storage.go new file mode 100644 index 0000000000..8df21933de --- /dev/null +++ b/lib/ir/storage.go @@ -0,0 +1,94 @@ +package ir + +import ( + "bytes" + + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/pkg/errors" +) + +// Storage is an interface of the storage of info about NeoFS IR. +type Storage interface { + GetIRInfo(GetInfoParams) (*GetInfoResult, error) +} + +// GetInfoParams is a structure that groups the parameters +// for IR info receiving operation. +type GetInfoParams struct { +} + +// GetInfoResult is a structure that groups +// values returned by IR info receiving operation. +type GetInfoResult struct { + info Info +} + +// ErrNilStorage is returned by functions that expect +// a non-nil Storage, but received nil. +const ErrNilStorage = internal.Error("inner ring storage is nil") + +// SetInfo is an IR info setter. +func (s *GetInfoResult) SetInfo(v Info) { + s.info = v +} + +// Info is an IR info getter. +func (s GetInfoResult) Info() Info { + return s.info +} + +// BinaryKeyList returns the list of binary public key of IR nodes. +// +// If passed Storage is nil, ErrNilStorage returns. +func BinaryKeyList(storage Storage) ([][]byte, error) { + if storage == nil { + return nil, ErrNilStorage + } + + // get IR info + getRes, err := storage.GetIRInfo(GetInfoParams{}) + if err != nil { + return nil, errors.Wrap( + err, + "could not get information about IR", + ) + } + + nodes := getRes.Info().Nodes() + + keys := make([][]byte, 0, len(nodes)) + + for i := range nodes { + keys = append(keys, nodes[i].Key()) + } + + return keys, nil +} + +// IsInnerRingKey checks if the passed argument is the +// key of one of IR nodes. +// +// Uses BinaryKeyList function to receive the key list of IR nodes internally. +// +// If passed key slice is empty, crypto.ErrEmptyPublicKey returns immediately. +func IsInnerRingKey(storage Storage, key []byte) (bool, error) { + // check key emptiness + // TODO: summarize the void check to a full IR key-format check. + if len(key) == 0 { + return false, crypto.ErrEmptyPublicKey + } + + irKeys, err := BinaryKeyList(storage) + if err != nil { + return false, err + } + + for i := range irKeys { + if bytes.Equal(irKeys[i], key) { + return true, nil + } + } + + return false, nil +} diff --git a/lib/ir/storage_test.go b/lib/ir/storage_test.go new file mode 100644 index 0000000000..71a654847f --- /dev/null +++ b/lib/ir/storage_test.go @@ -0,0 +1,101 @@ +package ir + +import ( + "testing" + + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type testInfoReceiver struct { + keys [][]byte + + err error +} + +func (s testInfoReceiver) GetIRInfo(GetInfoParams) (*GetInfoResult, error) { + if s.err != nil { + return nil, s.err + } + + nodes := make([]Node, 0, len(s.keys)) + + for i := range s.keys { + node := Node{} + node.SetKey(s.keys[i]) + + nodes = append(nodes, node) + } + + info := Info{} + info.SetNodes(nodes) + + res := new(GetInfoResult) + res.SetInfo(info) + + return res, nil +} + +func (s *testInfoReceiver) addKey(key []byte) { + s.keys = append(s.keys, key) +} + +func TestGetInfoResult(t *testing.T) { + s := GetInfoResult{} + + info := Info{} + + n := Node{} + n.SetKey([]byte{1, 2, 3}) + + info.SetNodes([]Node{ + n, + }) + + s.SetInfo(info) + + require.Equal(t, info, s.Info()) +} + +func TestIsInnerRingKey(t *testing.T) { + var ( + res bool + err error + s = new(testInfoReceiver) + ) + + // empty public key + res, err = IsInnerRingKey(nil, nil) + require.EqualError(t, err, crypto.ErrEmptyPublicKey.Error()) + + key := []byte{1, 2, 3} + + // nil Storage + res, err = IsInnerRingKey(nil, key) + require.EqualError(t, err, ErrNilStorage.Error()) + + // force Storage to return an error + s.err = errors.New("some error") + + // Storage error + res, err = IsInnerRingKey(s, key) + require.EqualError(t, errors.Cause(err), s.err.Error()) + + // reset Storage error + s.err = nil + + // IR keys don't contain key + s.addKey(append(key, 1)) + + res, err = IsInnerRingKey(s, key) + require.NoError(t, err) + require.False(t, res) + + // IR keys contain key + s.addKey(key) + + res, err = IsInnerRingKey(s, key) + require.NoError(t, err) + require.True(t, res) +} diff --git a/lib/localstore/alias.go b/lib/localstore/alias.go new file mode 100644 index 0000000000..03053f48b2 --- /dev/null +++ b/lib/localstore/alias.go @@ -0,0 +1,35 @@ +package localstore + +import ( + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" +) + +// CID is a type alias of +// CID from refs package of neofs-api-go. +type CID = refs.CID + +// SGID is a type alias of +// SGID from refs package of neofs-api-go. +type SGID = refs.ObjectID + +// Header is a type alias of +// Header from object package of neofs-api-go. +type Header = object.Header + +// Object is a type alias of +// Object from object package of neofs-api-go. +type Object = object.Object + +// ObjectID is a type alias of +// ObjectID from refs package of neofs-api-go. +type ObjectID = refs.ObjectID + +// Address is a type alias of +// Address from refs package of neofs-api-go. +type Address = refs.Address + +// Hash is a type alias of +// Hash from hash package of neofs-api-go. +type Hash = hash.Hash diff --git a/lib/localstore/del.go b/lib/localstore/del.go new file mode 100644 index 0000000000..f09f40868c --- /dev/null +++ b/lib/localstore/del.go @@ -0,0 +1,38 @@ +package localstore + +import ( + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/metrics" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func (l *localstore) Del(key refs.Address) error { + k, err := key.Hash() + if err != nil { + return errors.Wrap(err, "Localstore Del failed on key.Marshal") + } + + // try to fetch object for metrics + obj, err := l.Get(key) + if err != nil { + l.log.Warn("localstore Del failed on localstore.Get", zap.Error(err)) + } + + if err := l.blobBucket.Del(k); err != nil { + l.log.Warn("Localstore Del failed on BlobBucket.Del", zap.Error(err)) + } + + if err := l.metaBucket.Del(k); err != nil { + return errors.Wrap(err, "Localstore Del failed on MetaBucket.Del") + } + + if obj != nil { + l.col.UpdateContainer( + key.CID, + obj.SystemHeader.PayloadLength, + metrics.RemSpace) + } + + return nil +} diff --git a/lib/localstore/filter.go b/lib/localstore/filter.go new file mode 100644 index 0000000000..a568e7d9be --- /dev/null +++ b/lib/localstore/filter.go @@ -0,0 +1,306 @@ +package localstore + +import ( + "context" + "math" + "sort" + "sync" + + "github.com/nspcc-dev/neofs-node/internal" + "github.com/pkg/errors" +) + +type ( + // FilterCode is an enumeration of filter return codes. + FilterCode int + + // PriorityFlag is an enumeration of priority flags. + PriorityFlag int + + filterPipelineSet []FilterPipeline + + // FilterFunc is a function that checks whether an ObjectMeta matches a specific criterion. + FilterFunc func(ctx context.Context, meta *ObjectMeta) *FilterResult + + // FilterResult groups of ObjectMeta filter result values. + FilterResult struct { + c FilterCode + + e error + } + + // FilterPipeline is an interface of ObjectMeta filtering tool with sub-filters and priorities. + FilterPipeline interface { + Pass(ctx context.Context, meta *ObjectMeta) *FilterResult + PutSubFilter(params SubFilterParams) error + GetPriority() uint64 + SetPriority(uint64) + GetName() string + } + + // FilterParams groups the parameters of FilterPipeline constructor. + FilterParams struct { + Name string + Priority uint64 + FilterFunc FilterFunc + } + + // SubFilterParams groups the parameters of sub-filter registration. + SubFilterParams struct { + PriorityFlag + FilterPipeline + OnIgnore FilterCode + OnPass FilterCode + OnFail FilterCode + } + + filterPipeline struct { + *sync.RWMutex + + name string + pri uint64 + filterFn FilterFunc + + maxSubPri uint64 + mSubResult map[string]map[FilterCode]FilterCode + subFilters []FilterPipeline + } +) + +const ( + // PriorityValue is a PriorityFlag of the sub-filter registration with GetPriority() value. + PriorityValue PriorityFlag = iota + + // PriorityMax is a PriorityFlag of the sub-filter registration with maximum priority. + PriorityMax + + // PriorityMin is a PriorityFlag of the sub-filter registration with minimum priority. + PriorityMin +) + +const ( + // CodeUndefined is a undefined FilterCode. + CodeUndefined FilterCode = iota + + // CodePass is a FilterCode of filter passage. + CodePass + + // CodeFail is a FilterCode of filter failure. + CodeFail + + // CodeIgnore is a FilterCode of filter ignoring. + CodeIgnore +) + +var ( + rPass = &FilterResult{ + c: CodePass, + } + + rFail = &FilterResult{ + c: CodeFail, + } + + rIgnore = &FilterResult{ + c: CodeIgnore, + } + + rUndefined = &FilterResult{ + c: CodeUndefined, + } +) + +// ResultPass returns the FilterResult with CodePass code and nil error. +func ResultPass() *FilterResult { + return rPass +} + +// ResultFail returns the FilterResult with CodeFail code and nil error. +func ResultFail() *FilterResult { + return rFail +} + +// ResultIgnore returns the FilterResult with CodeIgnore code and nil error. +func ResultIgnore() *FilterResult { + return rIgnore +} + +// ResultUndefined returns the FilterResult with CodeUndefined code and nil error. +func ResultUndefined() *FilterResult { + return rUndefined +} + +// ResultWithError returns the FilterResult with passed code and error. +func ResultWithError(c FilterCode, e error) *FilterResult { + return &FilterResult{ + e: e, + c: c, + } +} + +// Code returns the filter result code. +func (s *FilterResult) Code() FilterCode { + return s.c +} + +// Err returns the filter result error. +func (s *FilterResult) Err() error { + return s.e +} + +func (f filterPipelineSet) Len() int { return len(f) } +func (f filterPipelineSet) Less(i, j int) bool { return f[i].GetPriority() > f[j].GetPriority() } +func (f filterPipelineSet) Swap(i, j int) { f[i], f[j] = f[j], f[i] } + +func (r FilterCode) String() string { + switch r { + case CodePass: + return "PASSED" + case CodeFail: + return "FAILED" + case CodeIgnore: + return "IGNORED" + default: + return "UNDEFINED" + } +} + +// NewFilter is a FilterPipeline constructor. +func NewFilter(p *FilterParams) FilterPipeline { + return &filterPipeline{ + RWMutex: new(sync.RWMutex), + name: p.Name, + pri: p.Priority, + filterFn: p.FilterFunc, + mSubResult: make(map[string]map[FilterCode]FilterCode), + } +} + +// AllPassIncludingFilter returns FilterPipeline with sub-filters composed from parameters. +// Result filter fails with CodeFail code if any of the sub-filters returns not a CodePass code. +func AllPassIncludingFilter(name string, params ...*FilterParams) (FilterPipeline, error) { + res := NewFilter(&FilterParams{ + Name: name, + FilterFunc: SkippingFilterFunc, + }) + + for i := range params { + if err := res.PutSubFilter(SubFilterParams{ + FilterPipeline: NewFilter(params[i]), + OnIgnore: CodeFail, + OnFail: CodeFail, + }); err != nil { + return nil, errors.Wrap(err, "could not create all pass including filter") + } + } + + return res, nil +} + +func (p *filterPipeline) Pass(ctx context.Context, meta *ObjectMeta) *FilterResult { + p.RLock() + defer p.RUnlock() + + for i := range p.subFilters { + subResult := p.subFilters[i].Pass(ctx, meta) + subName := p.subFilters[i].GetName() + + cSub := subResult.Code() + + if cSub <= CodeUndefined { + return ResultUndefined() + } + + if cFin := p.mSubResult[subName][cSub]; cFin != CodeIgnore { + return ResultWithError(cFin, subResult.Err()) + } + } + + if p.filterFn == nil { + return ResultUndefined() + } + + return p.filterFn(ctx, meta) +} + +func (p *filterPipeline) PutSubFilter(params SubFilterParams) error { + p.Lock() + defer p.Unlock() + + if params.FilterPipeline == nil { + return internal.Error("could not put sub filter: empty filter pipeline") + } + + name := params.FilterPipeline.GetName() + if _, ok := p.mSubResult[name]; ok { + return errors.Errorf("filter %s is already in pipeline %s", name, p.GetName()) + } + + if params.PriorityFlag != PriorityMin { + if pri := params.FilterPipeline.GetPriority(); pri < math.MaxUint64 { + params.FilterPipeline.SetPriority(pri + 1) + } + } else { + params.FilterPipeline.SetPriority(0) + } + + switch pri := params.FilterPipeline.GetPriority(); params.PriorityFlag { + case PriorityMax: + if p.maxSubPri < math.MaxUint64 { + p.maxSubPri++ + } + + params.FilterPipeline.SetPriority(p.maxSubPri) + case PriorityValue: + if pri > p.maxSubPri { + p.maxSubPri = pri + } + } + + if params.OnFail <= 0 { + params.OnFail = CodeIgnore + } + + if params.OnIgnore <= 0 { + params.OnIgnore = CodeIgnore + } + + if params.OnPass <= 0 { + params.OnPass = CodeIgnore + } + + p.mSubResult[name] = map[FilterCode]FilterCode{ + CodePass: params.OnPass, + CodeIgnore: params.OnIgnore, + CodeFail: params.OnFail, + } + + p.subFilters = append(p.subFilters, params.FilterPipeline) + + sort.Sort(filterPipelineSet(p.subFilters)) + + return nil +} + +func (p *filterPipeline) GetPriority() uint64 { + p.RLock() + defer p.RUnlock() + + return p.pri +} +func (p *filterPipeline) SetPriority(pri uint64) { + p.Lock() + p.pri = pri + p.Unlock() +} + +func (p *filterPipeline) GetName() string { + p.RLock() + defer p.RUnlock() + + if p.name == "" { + return "FILTER_UNNAMED" + } + + return p.name +} diff --git a/lib/localstore/filter_funcs.go b/lib/localstore/filter_funcs.go new file mode 100644 index 0000000000..c92610c20f --- /dev/null +++ b/lib/localstore/filter_funcs.go @@ -0,0 +1,39 @@ +package localstore + +import ( + "context" +) + +// SkippingFilterFunc is a FilterFunc that always returns result with +// CodePass code and nil error. +func SkippingFilterFunc(_ context.Context, _ *ObjectMeta) *FilterResult { + return ResultPass() +} + +// ContainerFilterFunc returns a FilterFunc that returns: +// - result with CodePass code and nil error if CID of ObjectMeta if from the CID list; +// - result with CodeFail code an nil error otherwise. +func ContainerFilterFunc(cidList []CID) FilterFunc { + return func(_ context.Context, meta *ObjectMeta) *FilterResult { + for i := range cidList { + if meta.Object.SystemHeader.CID.Equal(cidList[i]) { + return ResultPass() + } + } + + return ResultFail() + } +} + +// StoredEarlierThanFilterFunc returns a FilterFunc that returns: +// - result with CodePass code and nil error if StoreEpoch is less that argument; +// - result with CodeFail code and nil error otherwise. +func StoredEarlierThanFilterFunc(epoch uint64) FilterFunc { + return func(_ context.Context, meta *ObjectMeta) *FilterResult { + if meta.StoreEpoch < epoch { + return ResultPass() + } + + return ResultFail() + } +} diff --git a/lib/localstore/filter_test.go b/lib/localstore/filter_test.go new file mode 100644 index 0000000000..c07b9fe0c7 --- /dev/null +++ b/lib/localstore/filter_test.go @@ -0,0 +1,38 @@ +package localstore + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-node/internal" + "github.com/stretchr/testify/require" +) + +func TestSkippingFilterFunc(t *testing.T) { + res := SkippingFilterFunc(context.TODO(), &ObjectMeta{}) + require.Equal(t, CodePass, res.Code()) +} + +func TestFilterResult(t *testing.T) { + var ( + r *FilterResult + c = CodePass + e = internal.Error("test error") + ) + + r = ResultPass() + require.Equal(t, CodePass, r.Code()) + require.NoError(t, r.Err()) + + r = ResultFail() + require.Equal(t, CodeFail, r.Code()) + require.NoError(t, r.Err()) + + r = ResultIgnore() + require.Equal(t, CodeIgnore, r.Code()) + require.NoError(t, r.Err()) + + r = ResultWithError(c, e) + require.Equal(t, c, r.Code()) + require.EqualError(t, r.Err(), e.Error()) +} diff --git a/lib/localstore/get.go b/lib/localstore/get.go new file mode 100644 index 0000000000..4e4090f48b --- /dev/null +++ b/lib/localstore/get.go @@ -0,0 +1,30 @@ +package localstore + +import ( + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/pkg/errors" +) + +func (l *localstore) Get(key refs.Address) (*Object, error) { + var ( + err error + k, v []byte + o = new(Object) + ) + + k, err = key.Hash() + if err != nil { + return nil, errors.Wrap(err, "Localstore Get failed on key.Marshal") + } + + v, err = l.blobBucket.Get(k) + if err != nil { + return nil, errors.Wrap(err, "Localstore Get failed on blobBucket.Get") + } + + if err = o.Unmarshal(v); err != nil { + return nil, errors.Wrap(err, "Localstore Get failed on Object.Unmarshal") + } + + return o, nil +} diff --git a/lib/localstore/has.go b/lib/localstore/has.go new file mode 100644 index 0000000000..831e77def6 --- /dev/null +++ b/lib/localstore/has.go @@ -0,0 +1,20 @@ +package localstore + +import ( + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/pkg/errors" +) + +func (l *localstore) Has(key refs.Address) (bool, error) { + var ( + err error + k []byte + ) + + k, err = key.Hash() + if err != nil { + return false, errors.Wrap(err, "localstore.Has failed on key.Marshal") + } + + return l.metaBucket.Has(k) && l.blobBucket.Has(k), nil +} diff --git a/lib/localstore/interface.go b/lib/localstore/interface.go new file mode 100644 index 0000000000..b1b14b4d05 --- /dev/null +++ b/lib/localstore/interface.go @@ -0,0 +1,102 @@ +package localstore + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/metrics" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // Localstore is an interface of local object storage. + Localstore interface { + Put(context.Context, *Object) error + Get(Address) (*Object, error) + Del(Address) error + Meta(Address) (*ObjectMeta, error) + Iterator + Has(Address) (bool, error) + ObjectsCount() (uint64, error) + + object.PositionReader + Size() int64 + } + + // MetaHandler is a function that handles ObjectMeta. + MetaHandler func(*ObjectMeta) bool + + // Iterator is an interface of the iterator over local object storage. + Iterator interface { + Iterate(FilterPipeline, MetaHandler) error + } + + // ListItem is an ObjectMeta wrapper. + ListItem struct { + ObjectMeta + } + + // Params groups the parameters of + // local object storage constructor. + Params struct { + BlobBucket core.Bucket + MetaBucket core.Bucket + Logger *zap.Logger + Collector metrics.Collector + } + + localstore struct { + metaBucket core.Bucket + blobBucket core.Bucket + + log *zap.Logger + col metrics.Collector + } +) + +// ErrOutOfRange is returned when requested object payload range is +// out of object payload bounds. +var ErrOutOfRange = errors.New("range is out of payload bounds") + +// ErrEmptyMetaHandler is returned by functions that expect +// a non-nil MetaHandler, but received nil. +var ErrEmptyMetaHandler = errors.New("meta handler is nil") + +var errNilLogger = errors.New("logger is nil") + +var errNilCollector = errors.New("metrics collector is nil") + +// New is a local object storage constructor. +func New(p Params) (Localstore, error) { + switch { + case p.MetaBucket == nil: + return nil, errors.Errorf("%s bucket is nil", core.MetaStore) + case p.BlobBucket == nil: + return nil, errors.Errorf("%s bucket is nil", core.BlobStore) + case p.Logger == nil: + return nil, errNilLogger + case p.Collector == nil: + return nil, errNilCollector + } + + return &localstore{ + metaBucket: p.MetaBucket, + blobBucket: p.BlobBucket, + log: p.Logger, + col: p.Collector, + }, nil +} + +func (l localstore) Size() int64 { return l.blobBucket.Size() } + +// TODO: implement less costly method of counting. +func (l localstore) ObjectsCount() (uint64, error) { + items, err := l.metaBucket.List() + if err != nil { + return 0, err + } + + return uint64(len(items)), nil +} diff --git a/lib/localstore/list.go b/lib/localstore/list.go new file mode 100644 index 0000000000..c4e1ec62ce --- /dev/null +++ b/lib/localstore/list.go @@ -0,0 +1,41 @@ +package localstore + +import ( + "context" + + "go.uber.org/zap" +) + +func (l *localstore) Iterate(filter FilterPipeline, handler MetaHandler) error { + if handler == nil { + return ErrEmptyMetaHandler + } else if filter == nil { + filter = NewFilter(&FilterParams{ + Name: "SKIPPING_FILTER", + FilterFunc: SkippingFilterFunc, + }) + } + + return l.metaBucket.Iterate(func(_, v []byte) bool { + meta := new(ObjectMeta) + if err := meta.Unmarshal(v); err != nil { + l.log.Error("unmarshal meta bucket item failure", zap.Error(err)) + } else if filter.Pass(context.TODO(), meta).Code() == CodePass { + return !handler(meta) + } + return true + }) +} + +// ListItems iterates over Iterator with FilterPipeline and returns all passed items. +func ListItems(it Iterator, f FilterPipeline) ([]ListItem, error) { + res := make([]ListItem, 0) + err := it.Iterate(f, func(meta *ObjectMeta) (stop bool) { + res = append(res, ListItem{ + ObjectMeta: *meta, + }) + return + }) + + return res, err +} diff --git a/lib/localstore/localstore.pb.go b/lib/localstore/localstore.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..e6c13b373ecc2993589ee6867f0a780b993e3e95 GIT binary patch literal 11883 zcmds7dvDuD68~HJ6ng?%NVP2MWm|UR0&U~=0<=ldxgIvlj`yh7cm3sL*vd#@;O zy`)}+eg>C>D5{r;vB9$}VivA$V~{A7r=Yn-oan?c~ihAoI`;YnFD#udT&I?m>nTBy9a7iVR~j`5{g1{zRi`IV)a z#2FkRex;KKm1-|5)dSoJ8x{{LXB812Vqi-!(>i3G`cT#)BnKU|gB;`>jr~CG`h$4d zv+@^RlpsD)ge=h>sd+IkJUID=K<#$*u+ZuSyUO>v@>E$BOL)zjc1sr3>fdMLD+BZL zH|7>TdfxFLDqZ$R{d`gNGcVN_0@7!QNk{!&=Gl42Eaquk>2y&&bbf2Me()ZW+zbEd zmA4JC)hD$0vxLo9cBddCp=Cq;NAXp>myjY?5>#x7wJkNj5Ug-xNeh0l5USiDr z=B<(QH?htkX@%gIS)qe`oyeH*Q;!IuCGya4fH{-CJI*gh!b;D6h0N6Rcw z-Bf)h-avlWLnbM8)$P@L-l)$~3xDs;6hGKr1aF~M#uhWHZ(il4Bxgbl{KA^DSpmUNr+t^6vzralUEu->A->N?Zal0YmKX~ zLJf~d<18+#I8c6`Cyq#*z*r>Y@Ks1qw{qU!-`}L7H6-Qyrm>k#=3T$9(luJWcI&Ou z3DOgx@?mTL<$2&Ox_<8(3t6AUROc5ewOFolil^|aWrRZqV1NQ!1oe*#mBk4ovP|Kb zF#zZ-{uz^iM0VXLU{K$yC|$q@$|5jMWW0iPu}@y!22rtduOUbhI1fC=ahP-<%ljC6 z8kd1rgox)Xf1LpZw6}+$HSm;};&XKO9wYIy433d`iIJQvyCZbJ)s>hW+^iiCU=Aiq zT>}|Qq~;7EJ6P+GS$5?I5OIP6CldX$@@hNjUgX z&k<_6xWeJo?Ye2D3-Nn>)oE%)qasR>9X1?iPK(jl$s+s)*{0ihq$2I~)gxJnQX@Oa zBXEdUNk@YrreP8NHnbbPdOSBCzs;fu@>X2Mjdk4P#l6dx7y7(J@_5n1JS$ z5wDDbkecN<6KRBBzW4Zk%J-)X33U;Hh?ji6Mh*&D7j$wjZ%^1Zn&lJ8ple7o zVXh})Ik88;1cnUiv&dt99Wa?Q=IHE%H#5vBBN7D)$r=J=v=27#Dsc^yQgn-8c#F$f&faNCr9={D(Q68&N)`gfv z#+YHmPI3*H0Cd1cW>pLs$OIAF;Dkd)ta8e#6EB*v&4vtPqK65KB_wUkc;##&Dq`h` z-lt62AYzgu5!Oi+6P9cuBpH#*^@LZ%8~u?;z=)6$9ymD9@vCIDw56eLheHcootqMlQtYgk)URo#H}an4x*1?$P#Fl;Dooyb;>5) zOxKd4zJJ_-)CGvNZg%`qG>_QXMw+DRm%HESeIk#lWZ@ zy;r&}(FqD=tHN@^q+ z;yY3|POs#`*FI9~wD(T$Re+IeYSIxy5B`Ip$p?i| zU)>q+tQ$k~lB}KIIo^B+J@_=*LtqL_yA|aH5PWf=Mo*rcayYQk*yFnFUQxHzTn)lTi?6ICNF>5` zB$7?9Ng#!79Ghs!)B%K62S`Q-YkHCueHI!i>9jt}_ zzAW|_swVT8T@lmGO`=|HE8~t_f)srU*c>iVdH=R*LtOxH^CzBrjEUop?Vlx~g zpWd9JC2Pl9{5-t8r!{yd1(iHMfqPNb!l#XAS*RI+-%tqQBzzs?3xlwW71~$c#!*Fh zXtg^;?)}`X!CxgpDSHbTLJ(tTPK(wWmqqa`t6tpcB1-btjrxGzWVguhhRtUBW%tH8 zeKh$w)AtK}1=8W=i*M>}KJmoi&HP`}3%;YsZ?#yEKZR>(*r$B)O^@`?oHBHzeD#Dg zh+)|;wD-zVfJ3#SSSR80WlFRl{$$)`yfD^;k`6u42*kXWm!DFtOK+i-8URVd=*2i( z^FgTF;hD<0GbKLfjr>(ZXQ7%G`EsGi7+H>wdGv%yiaF#W%t8#4PizHY_4f#oI7#%} zOH}2})hD46_L=G@VXs3&7D3!+b}YpbpOrB9w=Y?B0cG*gIe1!;8YRtm)f={O^t#9~ z?rtm4SQbm zcD<$Aw5A581~KPbeE+q2!y68Z@m~=Z&gs81HfWLw^ZuQJ!;18AqQheQ5cjRk%(-aGv}1?N^jUnxnc-zD9@YuWODeGIs@ zw%T9FyyJnu*=YJ#SgEPycNnPw_9z zxVod0=2UD9E14nczVeN8g*m4#fa)f@IbLJ6En!cg&xu#3Dj5IvE8^=E! zU9Wb)+ZqYCxsyWYLSe}3R<}I+;!fh@R-69?-E4EUm4*!cB|0QJf@xZVU+ir4wRb#& z3Ctke$dOjq{e4-Tl?08ogH&If3E0r9hrMJ7G)rsgT}V0umMnY`eMdd~$I7)_FHo`;)(mZQ^DavsCdNemE<>6y-~db;2e8&~Isf8;4*0 u(O73!TD24uPPJ_+#V?)ax=^{9v>8 uint64(len(obj.Payload)) { + return nil, ErrOutOfRange + } + + return obj.Payload[rng.Offset : rng.Offset+rng.Length], nil +} diff --git a/lib/meta/iterator.go b/lib/meta/iterator.go new file mode 100644 index 0000000000..f5d3642ffa --- /dev/null +++ b/lib/meta/iterator.go @@ -0,0 +1,15 @@ +package meta + +import ( + "github.com/nspcc-dev/neofs-api-go/object" +) + +type ( + // Iterator is an interface of the iterator over object storage. + Iterator interface { + Iterate(IterateFunc) error + } + + // IterateFunc is a function that checks whether an object matches a specific criterion. + IterateFunc func(*object.Object) error +) diff --git a/lib/metrics/meta.go b/lib/metrics/meta.go new file mode 100644 index 0000000000..d11685a530 --- /dev/null +++ b/lib/metrics/meta.go @@ -0,0 +1,33 @@ +package metrics + +import ( + "sync" + + "github.com/nspcc-dev/neofs-node/lib/meta" +) + +type metaWrapper struct { + sync.Mutex + iter meta.Iterator +} + +func newMetaWrapper() *metaWrapper { + return &metaWrapper{} +} + +func (m *metaWrapper) changeIter(iter meta.Iterator) { + m.Lock() + m.iter = iter + m.Unlock() +} + +func (m *metaWrapper) Iterate(h meta.IterateFunc) error { + m.Lock() + defer m.Unlock() + + if m.iter == nil { + return errEmptyMetaStore + } + + return m.iter.Iterate(h) +} diff --git a/lib/metrics/metrics.go b/lib/metrics/metrics.go new file mode 100644 index 0000000000..143c66ac85 --- /dev/null +++ b/lib/metrics/metrics.go @@ -0,0 +1,175 @@ +package metrics + +import ( + "context" + "sync" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/meta" + "go.uber.org/zap" +) + +type ( + // Collector is an interface of the metrics collector. + Collector interface { + Start(ctx context.Context) + UpdateSpaceUsage() + + SetCounter(ObjectCounter) + SetIterator(iter meta.Iterator) + UpdateContainer(cid refs.CID, size uint64, op SpaceOp) + } + + collector struct { + log *zap.Logger + interval time.Duration + counter *counterWrapper + + sizes *syncStore + metas *metaWrapper + + updateSpaceSize func() + updateObjectCount func() + } + + // Params groups the parameters of metrics collector's constructor. + Params struct { + Options []string + Logger *zap.Logger + Interval time.Duration + MetricsStore core.Bucket + } + + // ObjectCounter is an interface of object number storage. + ObjectCounter interface { + ObjectsCount() (uint64, error) + } + + // CounterSetter is an interface of ObjectCounter container. + CounterSetter interface { + SetCounter(ObjectCounter) + } + + counterWrapper struct { + sync.Mutex + counter ObjectCounter + } +) + +const ( + errEmptyCounter = internal.Error("empty object counter") + errEmptyLogger = internal.Error("empty logger") + errEmptyMetaStore = internal.Error("empty meta store") + errEmptyMetricsStore = internal.Error("empty metrics store") + + defaultMetricsInterval = 5 * time.Second +) + +// New constructs metrics collector and returns Collector interface. +func New(p Params) (Collector, error) { + switch { + case p.Logger == nil: + return nil, errEmptyLogger + case p.MetricsStore == nil: + return nil, errEmptyMetricsStore + } + + if p.Interval <= 0 { + p.Interval = defaultMetricsInterval + } + + metas := newMetaWrapper() + sizes := newSyncStore(p.Logger, p.MetricsStore) + + sizes.Load() + + return &collector{ + log: p.Logger, + interval: p.Interval, + counter: new(counterWrapper), + + metas: metas, + sizes: sizes, + + updateSpaceSize: spaceUpdater(sizes), + updateObjectCount: metricsUpdater(p.Options), + }, nil +} + +func (c *counterWrapper) SetCounter(counter ObjectCounter) { + c.Lock() + defer c.Unlock() + + c.counter = counter +} + +func (c *counterWrapper) ObjectsCount() (uint64, error) { + c.Lock() + defer c.Unlock() + + if c.counter == nil { + return 0, errEmptyCounter + } + + return c.counter.ObjectsCount() +} + +func (c *collector) SetCounter(counter ObjectCounter) { + c.counter.SetCounter(counter) +} + +func (c *collector) SetIterator(iter meta.Iterator) { + c.metas.changeIter(iter) +} + +func (c *collector) UpdateContainer(cid refs.CID, size uint64, op SpaceOp) { + c.sizes.Update(cid, size, op) + c.updateSpaceSize() +} + +func (c *collector) UpdateSpaceUsage() { + sizes := make(map[refs.CID]uint64) + + err := c.metas.Iterate(func(obj *object.Object) error { + if !obj.IsTombstone() { + cid := obj.SystemHeader.CID + sizes[cid] += obj.SystemHeader.PayloadLength + } + + return nil + }) + + if err != nil { + c.log.Error("could not update space metrics", zap.Error(err)) + } + + c.sizes.Reset(sizes) + c.updateSpaceSize() +} + +func (c *collector) Start(ctx context.Context) { + t := time.NewTicker(c.interval) + +loop: + for { + select { + case <-ctx.Done(): + c.log.Warn("stop collecting metrics", zap.Error(ctx.Err())) + break loop + case <-t.C: + count, err := c.counter.ObjectsCount() + if err != nil { + c.log.Warn("get object count failure", zap.Error(err)) + continue loop + } + counter.Store(float64(count)) + c.updateObjectCount() + } + } + + t.Stop() +} diff --git a/lib/metrics/metrics_test.go b/lib/metrics/metrics_test.go new file mode 100644 index 0000000000..7e2b585d59 --- /dev/null +++ b/lib/metrics/metrics_test.go @@ -0,0 +1,275 @@ +package metrics + +import ( + "context" + "encoding/binary" + "sync" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/meta" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + fakeCounter int + fakeIterator string + fakeMetaStore []*object.Object +) + +var ( + _ ObjectCounter = (*fakeCounter)(nil) + _ meta.Iterator = (*fakeIterator)(nil) +) + +func (f fakeCounter) ObjectsCount() (uint64, error) { + return uint64(f), nil +} + +func (f fakeIterator) Iterate(_ meta.IterateFunc) error { + if f == "" { + return nil + } + + return errors.New(string(f)) +} + +func (f fakeMetaStore) Iterate(cb meta.IterateFunc) error { + if cb == nil { + return nil + } + + for i := range f { + if err := cb(f[i]); err != nil { + return err + } + } + + return nil +} + +func TestCollector(t *testing.T) { + buck := &fakeBucket{items: make(map[uint64]int)} + + t.Run("check errors", func(t *testing.T) { + t.Run("empty logger", func(t *testing.T) { + svc, err := New(Params{MetricsStore: buck}) + require.Nil(t, svc) + require.EqualError(t, err, errEmptyLogger.Error()) + }) + + t.Run("empty metrics store", func(t *testing.T) { + svc, err := New(Params{Logger: zap.L()}) + require.Nil(t, svc) + require.EqualError(t, err, errEmptyMetricsStore.Error()) + }) + }) + + svc, err := New(Params{ + Logger: zap.L(), + MetricsStore: buck, + Options: []string{ + "/Location:Europe/Country:Russia/City:Moscow", + "/Some:Another/Key:Value", + }, + }) + + require.NoError(t, err) + require.NotNil(t, svc) + + coll, ok := svc.(*collector) + require.True(t, ok) + require.NotNil(t, coll) + + t.Run("check start", func(t *testing.T) { + coll.interval = time.Second + + t.Run("stop by context", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + wg := new(sync.WaitGroup) + wg.Add(1) + + counter.Store(-1) + + go func() { + svc.Start(ctx) + wg.Done() + }() + + cancel() + wg.Wait() + + require.Equal(t, float64(-1), counter.Load()) + }) + + t.Run("should fail on empty counter", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + wg := new(sync.WaitGroup) + wg.Add(1) + + counter.Store(0) + + go func() { + svc.Start(ctx) + wg.Done() + }() + + time.Sleep(2 * time.Second) + cancel() + wg.Wait() + + require.Equal(t, float64(0), counter.Load()) + }) + + t.Run("should success on fakeCounter", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + wg := new(sync.WaitGroup) + wg.Add(1) + + coll.SetCounter(fakeCounter(8)) + counter.Store(0) + + go func() { + svc.Start(ctx) + wg.Done() + }() + + time.Sleep(2 * time.Second) + cancel() + wg.Wait() + + require.Equal(t, float64(8), counter.Load()) + }) + }) + + t.Run("iterator", func(t *testing.T) { + { + coll.SetIterator(nil) + require.Nil(t, coll.metas.iter) + require.EqualError(t, coll.metas.Iterate(nil), errEmptyMetaStore.Error()) + } + + { + iter := fakeIterator("") + coll.SetIterator(iter) + require.Equal(t, iter, coll.metas.iter) + require.NoError(t, coll.metas.Iterate(nil)) + } + + { + iter := fakeIterator("test") + coll.SetIterator(iter) + require.Equal(t, iter, coll.metas.iter) + require.EqualError(t, coll.metas.Iterate(nil), string(iter)) + } + }) + + t.Run("add-rem space", func(t *testing.T) { + cid := refs.CID{1, 2, 3, 4, 5} + buf := make([]byte, 8) + key := keyFromBytes(cid.Bytes()) + + zero := make([]byte, 8) + size := uint64(100) + + binary.BigEndian.PutUint64(buf, size) + + { + coll.UpdateContainer(cid, size, AddSpace) + require.Len(t, coll.sizes.items, 1) + require.Len(t, buck.items, 1) + require.Contains(t, buck.items, key) + require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: buf}) + } + + { + coll.UpdateContainer(cid, size, RemSpace) + require.Len(t, coll.sizes.items, 1) + require.Len(t, buck.items, 1) + require.Contains(t, buck.items, key) + require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: zero}) + } + + { + coll.UpdateContainer(cid, size, RemSpace) + require.Len(t, coll.sizes.items, 1) + require.Len(t, buck.items, 1) + require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: zero}) + } + }) + + t.Run("add-rem multi thread", func(t *testing.T) { + wg := new(sync.WaitGroup) + wg.Add(10) + + size := uint64(100) + zero := make([]byte, 8) + + // reset + coll.UpdateSpaceUsage() + + for i := 0; i < 10; i++ { + cid := refs.CID{1, 2, 3, 4, byte(i)} + coll.UpdateContainer(cid, size, AddSpace) + + go func() { + coll.UpdateContainer(cid, size, RemSpace) + wg.Done() + }() + } + + wg.Wait() + + require.Len(t, coll.sizes.items, 10) + require.Len(t, buck.items, 10) + + for i := 0; i < 10; i++ { + cid := refs.CID{1, 2, 3, 4, byte(i)} + require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: zero}) + } + }) + + t.Run("reset buckets", func(t *testing.T) { + coll.UpdateSpaceUsage() + require.Len(t, coll.sizes.items, 0) + require.Len(t, buck.items, 0) + }) + + t.Run("reset from metaStore", func(t *testing.T) { + cid := refs.CID{1, 2, 3, 4, 5} + buf := make([]byte, 8) + key := keyFromBytes(cid.Bytes()) + size := uint64(100) + binary.BigEndian.PutUint64(buf, size) + + iter := fakeMetaStore{ + { + SystemHeader: object.SystemHeader{ + PayloadLength: size, + CID: cid, + }, + }, + + { + Headers: []object.Header{ + { + Value: &object.Header_Tombstone{Tombstone: &object.Tombstone{}}, + }, + }, + }, + } + + coll.SetIterator(iter) + + coll.UpdateSpaceUsage() + require.Len(t, coll.sizes.items, 1) + require.Len(t, buck.items, 1) + + require.Contains(t, buck.items, key) + require.Contains(t, buck.kv, fakeKV{key: cid.Bytes(), val: buf}) + }) +} diff --git a/lib/metrics/prometeus.go b/lib/metrics/prometeus.go new file mode 100644 index 0000000000..438e85f561 --- /dev/null +++ b/lib/metrics/prometeus.go @@ -0,0 +1,83 @@ +package metrics + +import ( + "strings" + + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/atomic" +) + +const ( + locationLabel = "location" + countryLabel = "country" + cityLabel = "city" + + containerLabel = "cid" +) + +var ( + objectsCount = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "neofs", + Name: "count_objects_on_node", + Help: "Number of objects stored on this node", + }, []string{locationLabel, countryLabel, cityLabel}) + + spaceCounter = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "neofs", + Name: "container_space_sizes", + Help: "Space allocated by ContainerID", + }, []string{containerLabel}) + + counter = atomic.NewFloat64(0) +) + +func init() { + prometheus.MustRegister( + objectsCount, + spaceCounter, + ) +} + +func spaceUpdater(m *syncStore) func() { + return func() { + m.mutex.RLock() + for cid := range m.items { + spaceCounter. + With(prometheus.Labels{ + containerLabel: cid.String(), + }). + Set(float64(m.items[cid])) + } + m.mutex.RUnlock() + } +} + +func metricsUpdater(opts []string) func() { + var ( + locationCode string + countryCode string + cityCode string + ) + + for i := range opts { + ss := strings.Split(opts[i], "/") + for j := range ss { + switch s := strings.SplitN(ss[j], ":", 2); strings.ToLower(s[0]) { + case locationLabel: + locationCode = s[1] + case countryLabel: + countryCode = s[1] + case cityLabel: + cityCode = s[1] + } + } + } + + return func() { + objectsCount.With(prometheus.Labels{ + locationLabel: locationCode, + countryLabel: countryCode, + cityLabel: cityCode, + }).Set(counter.Load()) + } +} diff --git a/lib/metrics/store.go b/lib/metrics/store.go new file mode 100644 index 0000000000..85f72434ca --- /dev/null +++ b/lib/metrics/store.go @@ -0,0 +1,122 @@ +package metrics + +import ( + "encoding/binary" + "encoding/hex" + "sync" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/core" + "go.uber.org/zap" +) + +type ( + syncStore struct { + log *zap.Logger + store core.Bucket + mutex sync.RWMutex + items map[refs.CID]uint64 + } + + // SpaceOp is an enumeration of space size operations. + SpaceOp int +) + +const ( + _ SpaceOp = iota + + // AddSpace is a SpaceOp of space size increasing. + AddSpace + + // RemSpace is a SpaceOp of space size decreasing. + RemSpace +) + +func newSyncStore(log *zap.Logger, store core.Bucket) *syncStore { + return &syncStore{ + log: log, + store: store, + items: make(map[refs.CID]uint64), + } +} + +func (m *syncStore) Load() { + m.mutex.Lock() + defer m.mutex.Unlock() + + _ = m.store.Iterate(func(key, val []byte) bool { + cid, err := refs.CIDFromBytes(key) + if err != nil { + m.log.Error("could not load space value", zap.Error(err)) + return true + } + + m.items[cid] += binary.BigEndian.Uint64(val) + return true + }) +} + +func (m *syncStore) Reset(items map[refs.CID]uint64) { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.items = items + if items == nil { + m.items = make(map[refs.CID]uint64) + } + + keys, err := m.store.List() + if err != nil { + m.log.Error("could not fetch keys space metrics", zap.Error(err)) + return + } + + // cleanup metrics store + for i := range keys { + if err := m.store.Del(keys[i]); err != nil { + cid := hex.EncodeToString(keys[i]) + m.log.Error("could not remove key", + zap.String("cid", cid), + zap.Error(err)) + } + } + + buf := make([]byte, 8) + + for cid := range items { + binary.BigEndian.PutUint64(buf, items[cid]) + + if err := m.store.Set(cid.Bytes(), buf); err != nil { + m.log.Error("could not store space value", + zap.Stringer("cid", cid), + zap.Error(err)) + } + } +} + +func (m *syncStore) Update(cid refs.CID, size uint64, op SpaceOp) { + m.mutex.Lock() + defer m.mutex.Unlock() + + switch op { + case RemSpace: + if m.items[cid] < size { + m.log.Error("space could not be negative") + return + } + + m.items[cid] -= size + case AddSpace: + m.items[cid] += size + default: + m.log.Error("unknown space operation", zap.Int("op", int(op))) + return + } + + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, m.items[cid]) + + if err := m.store.Set(cid.Bytes(), buf); err != nil { + m.log.Error("could not update space size", zap.Int("op", int(op))) + } +} diff --git a/lib/metrics/store_test.go b/lib/metrics/store_test.go new file mode 100644 index 0000000000..2827308ecd --- /dev/null +++ b/lib/metrics/store_test.go @@ -0,0 +1,156 @@ +package metrics + +import ( + "sync" + "testing" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/spaolacci/murmur3" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + fakeKV struct { + key []byte + val []byte + } + + fakeBucket struct { + sync.RWMutex + kv []fakeKV + items map[uint64]int + } +) + +var _ core.Bucket = (*fakeBucket)(nil) + +func keyFromBytes(b []byte) uint64 { + return murmur3.Sum64(b) +} + +func (f *fakeBucket) Set(key, value []byte) error { + f.Lock() + defer f.Unlock() + + var ( + id int + ok bool + uid = keyFromBytes(key) + ) + + if id, ok = f.items[uid]; !ok || id >= len(f.kv) { + id = len(f.kv) + f.items[uid] = id + f.kv = append(f.kv, fakeKV{ + key: key, + val: value, + }) + + return nil + } + + f.kv[id] = fakeKV{ + key: key, + val: value, + } + + return nil +} + +func (f *fakeBucket) Del(key []byte) error { + f.Lock() + defer f.Unlock() + + delete(f.items, keyFromBytes(key)) + + return nil +} + +func (f *fakeBucket) List() ([][]byte, error) { + f.RLock() + defer f.RUnlock() + + items := make([][]byte, 0, len(f.items)) + for _, id := range f.items { + // ignore unknown KV + if id >= len(f.kv) { + continue + } + + items = append(items, f.kv[id].key) + } + + return items, nil +} + +func (f *fakeBucket) Iterate(handler core.FilterHandler) error { + f.Lock() + defer f.Unlock() + + for _, id := range f.items { + // ignore unknown KV + if id >= len(f.kv) { + continue + } + + kv := f.kv[id] + + if !handler(kv.key, kv.val) { + break + } + } + + return nil +} + +func (f *fakeBucket) Get(_ []byte) ([]byte, error) { panic("implement me") } +func (f *fakeBucket) Has(_ []byte) bool { panic("implement me") } +func (f *fakeBucket) Size() int64 { panic("implement me") } +func (f *fakeBucket) Close() error { panic("implement me") } + +func TestSyncStore(t *testing.T) { + buck := &fakeBucket{items: make(map[uint64]int)} + sizes := newSyncStore(zap.L(), buck) + + for i := 0; i < 10; i++ { + cid := refs.CID{0, 0, 0, byte(i)} + require.NoError(t, buck.Set(cid.Bytes(), []byte{1, 2, 3, 4, 5, 6, 7, byte(i)})) + } + + t.Run("load", func(t *testing.T) { + sizes.Load() + require.Len(t, sizes.items, len(buck.items)) + }) + + t.Run("reset", func(t *testing.T) { + sizes.Reset(nil) + require.Len(t, sizes.items, 0) + }) + + t.Run("update", func(t *testing.T) { + cid := refs.CID{1, 2, 3, 4, 5} + + { // add space + sizes.Update(cid, 8, AddSpace) + val, ok := sizes.items[cid] + require.True(t, ok) + require.Equal(t, uint64(8), val) + } + + { // rem space + sizes.Update(cid, 8, RemSpace) + val, ok := sizes.items[cid] + require.True(t, ok) + require.Zero(t, val) + } + + { // rem space (zero - val) + sizes.Update(cid, 8, RemSpace) + val, ok := sizes.items[cid] + require.True(t, ok) + require.Zero(t, val) + } + }) +} diff --git a/lib/muxer/listener.go b/lib/muxer/listener.go new file mode 100644 index 0000000000..9ba6699513 --- /dev/null +++ b/lib/muxer/listener.go @@ -0,0 +1,51 @@ +package muxer + +import ( + "net" + + manet "github.com/multiformats/go-multiaddr-net" + "github.com/pkg/errors" +) + +type netListenerAdapter struct { + manet.Listener +} + +var errNothingAccept = errors.New("nothing to accept") + +// Accept waits for and returns the next connection to the listener. +func (l *netListenerAdapter) Accept() (net.Conn, error) { + if l.Listener == nil { + return nil, errNothingAccept + } + + return l.Listener.Accept() +} + +// Close closes the listener. +// Any blocked Accept operations will be unblocked and return errors. +func (l *netListenerAdapter) Close() error { + if l.Listener == nil { + return nil + } + + return l.Listener.Close() +} + +// Addr returns the net.Listener's network address. +func (l *netListenerAdapter) Addr() net.Addr { + if l.Listener == nil { + return (*net.TCPAddr)(nil) + } + + return l.Listener.Addr() +} + +// NetListener turns this Listener into a net.Listener. +// +// * Connections returned from Accept implement multiaddr-net Conn. +// * Calling WrapNetListener on the net.Listener returned by this function will +// return the original (underlying) multiaddr-net Listener. +func NetListener(l manet.Listener) net.Listener { + return &netListenerAdapter{Listener: l} +} diff --git a/lib/muxer/muxer.go b/lib/muxer/muxer.go new file mode 100644 index 0000000000..9aff7cbb01 --- /dev/null +++ b/lib/muxer/muxer.go @@ -0,0 +1,247 @@ +package muxer + +import ( + "context" + "net" + "strings" + "sync/atomic" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/soheilhy/cmux" + "github.com/valyala/fasthttp" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +type ( + // StoreParams groups the parameters of network connections muxer constructor. + Params struct { + Logger *zap.Logger + API *fasthttp.Server + Address multiaddr.Multiaddr + ShutdownTTL time.Duration + P2P *grpc.Server + Peers peers.Interface + } + + // Mux is an interface of network connections muxer. + Mux interface { + Start(ctx context.Context) + Stop() + } + + muxer struct { + peers peers.Interface + maddr multiaddr.Multiaddr + run *int32 + lis net.Listener + log *zap.Logger + ttl time.Duration + + p2p *grpc.Server + api *fasthttp.Server + + done chan struct{} + } +) + +const ( + // we close listener, that's why we ignore this errors + errClosedConnection = "use of closed network connection" + errMuxListenerClose = "mux: listener closed" + errHTTPServerClosed = "http: Server closed" +) + +var ( + ignoredErrors = []string{ + errClosedConnection, + errMuxListenerClose, + errHTTPServerClosed, + } +) + +// New constructs network connections muxer and returns Mux interface. +func New(p Params) Mux { + return &muxer{ + maddr: p.Address, + ttl: p.ShutdownTTL, + run: new(int32), + api: p.API, + p2p: p.P2P, + log: p.Logger, + peers: p.Peers, + done: make(chan struct{}), + } +} + +func needCatch(err error) bool { + if err == nil || containsErr(err) { + return false + } + + return true +} + +func containsErr(err error) bool { + for _, msg := range ignoredErrors { + if strings.Contains(err.Error(), msg) { + return true + } + } + + return false +} + +func (m *muxer) Start(ctx context.Context) { + var err error + + // if already started - ignore + if !atomic.CompareAndSwapInt32(m.run, 0, 1) { + m.log.Warn("already started") + return + } else if m.lis != nil { + m.log.Info("try close old listener") + if err = m.lis.Close(); err != nil { + m.log.Fatal("could not close old listener", + zap.Error(err)) + } + } + + lis, err := m.peers.Listen(m.maddr) + if err != nil { + m.log.Fatal("could not close old listener", + zap.Error(err)) + } + + m.lis = NetListener(lis) + + m.log.Info("create mux-listener", + zap.String("bind-address", m.lis.Addr().String())) + + mux := cmux.New(m.lis) + mux.HandleError(func(e error) bool { + if needCatch(e) { + m.log.Error("error-handler: something went wrong", + zap.Error(e)) + } + return true + }) + + // trpcL := mux.Match(cmux.Any()) // Any means anything that is not yet matched. + hLis := mux.Match(cmux.HTTP1Fast()) + gLis := mux.Match(cmux.HTTP2()) + pLis := mux.Match(cmux.Any()) + + m.log.Debug("delay context worker") + + go func() { + <-ctx.Done() + m.Stop() + }() + + m.log.Debug("delay tcp") + + go func() { + m.log.Debug("tcp: serve") + loop: + for { + select { + case <-ctx.Done(): + break loop + default: + } + + con, err := pLis.Accept() + if err != nil { + break loop + } + + _ = con.Close() + } + + m.log.Debug("tcp: stopped") + }() + + m.log.Debug("delay p2p") + + go func() { + if m.p2p == nil { + m.log.Info("p2p: service is empty") + return + } + + m.log.Debug("p2p: serve") + + if err := m.p2p.Serve(gLis); needCatch(err) { + m.log.Error("p2p: something went wrong", + zap.Error(err)) + } + + m.log.Debug("p2p: stopped") + }() + + m.log.Debug("delay api") + + go func() { + if m.api == nil { + m.log.Info("api: service is empty") + return + } + + m.log.Debug("api: serve") + + if err := m.api.Serve(hLis); needCatch(err) { + m.log.Error("rpc: something went wrong", + zap.Error(err)) + } + + m.log.Debug("rpc: stopped") + }() + + m.log.Debug("delay serve") + + go func() { + defer func() { close(m.done) }() + + m.log.Debug("mux: serve") + + if err := mux.Serve(); needCatch(err) { + m.log.Fatal("mux: something went wrong", + zap.Error(err)) + } + + m.log.Debug("mux: stopped") + }() +} + +func (m *muxer) Stop() { + if !atomic.CompareAndSwapInt32(m.run, 1, 0) { + m.log.Warn("already stopped") + return + } + + if err := m.lis.Close(); err != nil { + m.log.Error("could not close connection", + zap.Error(err)) + } + + m.log.Debug("lis: close ok") + + <-m.done // muxer stopped + + if m.api != nil { + if err := m.api.Shutdown(); needCatch(err) { + m.log.Error("api: could not shutdown", + zap.Error(err)) + } + + m.log.Debug("api: shutdown ok") + } + + if m.p2p != nil { + m.p2p.GracefulStop() + m.log.Debug("p2p: shutdown ok") + } +} diff --git a/lib/muxer/muxer_test.go b/lib/muxer/muxer_test.go new file mode 100644 index 0000000000..fc728d3c01 --- /dev/null +++ b/lib/muxer/muxer_test.go @@ -0,0 +1,415 @@ +package muxer + +import ( + "context" + "net" + "net/http" + "os" + "reflect" + "strings" + "sync" + "testing" + "time" + + "bou.ke/monkey" + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "github.com/soheilhy/cmux" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" + "go.uber.org/atomic" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" +) + +type ( + errListener struct { + net.TCPListener + } + + syncListener struct { + sync.Mutex + net.Listener + } + + errMuxer struct { + handleError func(error) bool + } + + testWriter struct{} + + // service is used to implement GreaterServer. + service struct{} +) + +const MIMEApplicationJSON = "application/json" + +// Hello is simple handler +func (*service) Hello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) { + return &HelloResponse{ + Message: "Hello " + req.Name, + }, nil +} + +func (testWriter) Sync() error { return nil } +func (testWriter) Write(p []byte) (n int, err error) { return len(p), nil } + +func (errMuxer) Match(...cmux.Matcher) net.Listener { + return &errListener{} +} + +func (errMuxer) MatchWithWriters(...cmux.MatchWriter) net.Listener { + return &errListener{} +} + +func (errMuxer) Serve() error { + return errors.New("cmux.Serve error") +} + +func (e *errMuxer) HandleError(h cmux.ErrorHandler) { + e.handleError = h +} + +func (errMuxer) SetReadTimeout(time.Duration) { + panic("implement me") +} + +func (l *syncListener) Close() error { + l.Lock() + err := l.Listener.Close() + l.Unlock() + return err +} + +func (errListener) Close() error { return errors.New("close error") } + +func testMultiAddr(is *require.Assertions) multiaddr.Multiaddr { + mAddr, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/0") + is.NoError(err) + return mAddr +} + +func testPeers(is *require.Assertions, a multiaddr.Multiaddr) peers.Interface { + s, err := peers.New(peers.Params{ + Address: a, + Transport: transport.New(5, time.Second), + Logger: test.NewTestLogger(false), + }) + is.NoError(err) + return s +} + +func testLogger() *zap.Logger { + encoderCfg := zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + NameKey: "logger", + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + } + core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), testWriter{}, zap.DPanicLevel) + return zap.New(core).WithOptions() +} + +func testHTTPServer() *fasthttp.Server { + return &fasthttp.Server{Handler: func(ctx *fasthttp.RequestCtx) {}} +} + +func TestSuite(t *testing.T) { + t.Run("it should run, stop and not panic", func(t *testing.T) { + var ( + is = require.New(t) + v = viper.New() + g = grpc.NewServer() + l = testLogger() + a = testMultiAddr(is) + s = time.Second + err error + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + v.SetDefault("api.address", "/ip4/0.0.0.0/tcp/0") + v.SetDefault("api.shutdown_timeout", time.Second) + + m := New(Params{ + Logger: l, + Address: a, + ShutdownTTL: s, + API: testHTTPServer(), + P2P: g, + Peers: testPeers(is, a), + }) + + is.NotPanics(func() { + m.Start(ctx) + }) + + res, err := http.Post("http://"+m.(*muxer).lis.Addr().String(), MIMEApplicationJSON, strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 1 + "method": "get_version", + "params": [], + }`)) + is.NoError(err) + defer res.Body.Close() + + time.Sleep(100 * time.Millisecond) + + is.NotPanics(m.Stop) + }) + + t.Run("it should work with gRPC", func(t *testing.T) { + var ( + is = require.New(t) + g = grpc.NewServer() + l = testLogger() + s = time.Second + err error + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + addr, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/63090") + is.NoError(err) + + ps := testPeers(is, addr) + + RegisterGreeterServer(g, &service{}) + + m := New(Params{ + Logger: l, + Address: addr, + ShutdownTTL: s, + P2P: g, + Peers: ps, + }) + + is.NotPanics(func() { + m.Start(ctx) + }) + + con, err := ps.GRPCConnection(ctx, addr, false) + is.NoError(err) + + res, err := NewGreeterClient(con).Hello(ctx, &HelloRequest{Name: "test"}) + is.NoError(err) + is.Contains(res.Message, "test") + + time.Sleep(100 * time.Millisecond) + + is.NotPanics(m.Stop) + }) + + t.Run("it should not start if already started", func(t *testing.T) { + var ( + is = require.New(t) + g = grpc.NewServer() + l = testLogger() + a = testMultiAddr(is) + s = time.Second + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + m := New(Params{ + Logger: l, + Address: a, + ShutdownTTL: s, + API: testHTTPServer(), + P2P: g, + Peers: testPeers(is, a), + }) + is.NotNil(m) + + mux, ok := m.(*muxer) + is.True(ok) + is.NotNil(mux) + + *mux.run = 1 + + is.NotPanics(func() { + mux.Start(ctx) + }) + + *mux.run = 0 + + is.NotPanics(mux.Stop) + }) + + t.Run("it should fail on close listener", func(t *testing.T) { + var ( + is = require.New(t) + g = grpc.NewServer() + l = testLogger() + a = testMultiAddr(is) + s = time.Second + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + m := New(Params{ + Logger: l, + Address: a, + ShutdownTTL: s, + API: testHTTPServer(), + P2P: g, + Peers: testPeers(is, a), + }) + is.NotNil(m) + + mux, ok := m.(*muxer) + is.True(ok) + is.NotNil(mux) + + mux.lis = &errListener{} + + exit := atomic.NewInt32(0) + + monkey.Patch(os.Exit, func(v int) { exit.Store(int32(v)) }) + + is.NotPanics(func() { + mux.Start(ctx) + }) + is.Equal(int32(1), exit.Load()) + }) + + t.Run("it should fail on create/close Listener without handlers", func(t *testing.T) { + var ( + is = require.New(t) + l = testLogger() + a = testMultiAddr(is) + err error + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mux := new(muxer) + mux.log = l + mux.peers = testPeers(is, a) + mux.run = new(int32) + mux.done = make(chan struct{}) + mux.maddr, err = multiaddr.NewMultiaddr("/ip4/1.1.1.1/tcp/2") + is.NoError(err) + + mux.lis, err = net.ListenTCP("tcp", nil) + is.NoError(err) + + exit := atomic.NewInt32(0) + monkey.Patch(os.Exit, func(v int) { + exit.Store(int32(v)) + }) + + m := &errMuxer{handleError: func(e error) bool { return true }} + monkey.Patch(cmux.New, func(net.Listener) cmux.CMux { + // prevent panic: + mux.lis, err = net.ListenTCP("tcp", nil) + return m + }) + + mux.Start(ctx) + // c.So(mux.Start, ShouldNotPanic) + + m.handleError(errors.New("test")) + + is.Equal(int32(1), exit.Load()) + + mux.lis = &errListener{} + *mux.run = 1 + + is.NotPanics(mux.Stop) + }) + + t.Run("it should fail on create/close Listener with handlers", func(t *testing.T) { + var ( + is = require.New(t) + g = grpc.NewServer() + l = testLogger() + a = testMultiAddr(is) + err error + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + mux := new(muxer) + mux.api = testHTTPServer() + mux.p2p = g + mux.log = l + mux.peers = testPeers(is, a) + mux.run = new(int32) + mux.done = make(chan struct{}) + mux.maddr, err = multiaddr.NewMultiaddr("/ip4/1.1.1.1/tcp/2") + is.NoError(err) + + mu := new(sync.Mutex) + + exit := atomic.NewInt32(0) + monkey.Patch(os.Exit, func(v int) { + exit.Store(int32(v)) + + mu.Lock() + if l, ok := mux.lis.(*syncListener); ok { + l.Lock() + l.Listener, _ = net.ListenTCP("tcp", nil) + l.Unlock() + } + mu.Unlock() + }) + + m := &errMuxer{handleError: func(e error) bool { return true }} + monkey.Patch(cmux.New, func(net.Listener) cmux.CMux { + // prevent panic: + return m + }) + + is.NotPanics(func() { + mux.Start(ctx) + }) + + m.handleError(errors.New("test")) + + is.Equal(int32(1), exit.Load()) + + mu.Lock() + mux.lis = &syncListener{Listener: &errListener{}} + mu.Unlock() + *mux.run = 1 + + monkey.PatchInstanceMethod(reflect.TypeOf(&http.Server{}), "Shutdown", func(*http.Server, context.Context) error { + return errors.New("http.Shutdown error") + }) + + is.NotPanics(mux.Stop) + }) + + t.Run("should not panic when work with nil listener", func(t *testing.T) { + var ( + is = require.New(t) + err error + ) + + lis := NetListener(nil) + is.NotPanics(func() { + is.NoError(lis.Close()) + }) + is.NotPanics(func() { + lis.Addr() + }) + is.NotPanics(func() { + _, err = lis.Accept() + is.EqualError(err, errNothingAccept.Error()) + }) + }) +} diff --git a/lib/muxer/muxer_test.pb.go b/lib/muxer/muxer_test.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..f998ce85b1cf0abc53c9b56b086b8905989fa417 GIT binary patch literal 15514 zcmeHOdvDuD68~HJ6ni=}pb}Y@CC3jn36RUH2HGS@oT5PBb18Bu^Fom-N!fB;fA{;% z%q~Srlx+9XqUj}Q32JwC-ZQ&1OK)$hXNj-XSjRebbM33qrJANmo_Jdr+Zre1q^q94 zQLo<|s~69Yj=Rn6ZIvan)YC&1g`@4sY_3yzp5{8syS%#DoVwm8cdQl9H=E&PnxwgM znhh_Bb3M;h%e=IjjbMVGc;J;+tJXNo&t{{pmrSmQ*}C~{o&{8vyZLNAm`npMOjIjO@G^1pGu5J}`Efc5b0DCRZF+4r z+fCBZJ00j$$DURqdZzM3WwYrt)mf%=nkH$c!a(I``ck>6R&kQ61i#XYFw<(5X}{Y% zcT;t$9*ae~Ke%aj=0?RRG}e7Vh=O8>*ytX`0kd#?7J@{kTm?N&!$@!C;Y2Gp%XFHD zNlYB+IGcfZe&*(Q0C?dE0alPC7^L-X9*!cdE?^mAT6j{{%wbM&BOS(LOU+mA*bQU2 z1YvnXYww zn{cTtFKO&8dGcHF#G~v-on|0DP=qYfE~$AsPF?u_ia^GOzs%s$;3><h4vE}flDGR-?N7Coj%hvx?^ z^`9(>hbD+1C#p+7k zpTKX+Kw$5B-SYRWAFMZux>BjmXK8F4{~5WTmmec-7|2PxK(@pQoBRhcm@sr@$u2=Y z*O`}w(>zH5=ZI!H&=4eVlO+uc6b1{>Q$IM^ALxTa_fU@pdpo{aan-wSS~@G*A)|eW zClbES$XKCmqF_O4(k(%?H{&zOpupBqzJ@nU!Z^(G&{Lx%iArK|z_D1!IaNr|x3bRX z^HbyONFvE;*?uYMmEq0oJr6U_O?^aloLs~}0xfQ2YiYrRSmQll(<&qT z5w_nnb|a$$vh&)oex-BDXW+;NCbq&rP0B_x)|hk&k}P5E(`pe{{(vBlnpnfPs7>LiW~E%EW1kr zwfrH2nk09-Bx->523Sif5+IotMB>Z)f`X+HcN7QpMLt%Ggxi4C)f3AxfgaD|NdzRT$aBO7 z-z)jjh`l0^#daCokrpFSBJ+L5j+1H4b)QME&a{`Pv(|kUg*whL5&sE2Chqasj?d;6 z)1vjmY`#aQ4GqWT-p?*G{VLT|D~l-9x1#os$AYP+Z;w=vq{_f3^vcddPg4(zl>8D? znVz^YYHW1Y(qtC<%FE}}uv0F*M@fcW&$f^R7|CHm_@uSzw4+F4llCa8_Gwj2JB}`XY;}o7YlLM3RC*{~lfp zhcm8DK8;^%U7d}(w6eylBsth5(W$!AMz292eTdN)kMsn|MEg}*o*Sc+tn_50eII8x z3KRESQ&?W0O~Q?2(~T~0a9cSu0kHvh;hcrL++Pp9(Q{J$%Pl1)4=GzrZWT=H-r=hV zH2w-vG&YphMCWITFP_U+1EuNG>DwN+D++Wn6Lw;CYF?e&NRlBgA+n-;ph4s~=iaHo zb4nh95jrvR^2K?-0AYtc{SG-VLI;mXp!Fr0=jyt{IdLWD_}Wuc$T%fX(*`Ba$8t|o zLsoIt_z=74r7+A*E1831l~&taL^?)Bs|F$W<`)5kNicxb?QfSmjb-y7V!8Z6*p*p& zyy#0BXvo}kDw=Ogoh1qJBcpdacd$k^4qnDD_tR~31LDE4Sv%3`i%EFsU z4u^t{HAQMMJ_s$DU({rTN~EYYx^F>a#t*3w9%8?$T16^ZYB!QbvmpvbrXat`$;k}# zzX35+Oire8gP)C5FkJ}>SvC^uKJ+AY>`Y08^rQ5O#581#Z(poRKb9IqZn!Bs7 z+ok`byRQ$gB9A9QfE#s6AsuyY0dFK>dc8J?K#^V+|JV<4r&xJm65SUyKDN#)&+3t>vJW zLxLioqdTLRYaH&{gOfyE=3A=#c`}f7`8zN(3T>$0;j z;JyKkIz3b+->n^=mLgl+}mXW#Rm{C()1|K-=LHaqU54bAF%Uh60}xW!&>t} zg^klNq4M)Xtmjkhp#XpJ=4G*LM54(qPJ*7$1mmF`&}gt6ze(bNibs?=Ube{q%qT+# zYNQ@MGIh%`o>ox0@MA*Z@1|BOEl17hAhnt-`9C(#UzvTD5viX!Bs_a93J~sho}+# zB2CbHTxg3}A>U)_iq3ps#ZD6w;yUE8XqBLEiI`n@?XBIm-fyk(RDB%rIDt@o1KCNYn0-!41~z2+Ku&b?na>x z9OL|KHI%1GmRGY1GH4m8agGJt?;rv`7wezgC1psxfz4S2H-!pik70q4YDQ>_b0@xN zwE|(#*MU2Wa&d4wZ?Y(@$v)u@_fl$GD42d08Y+`A6jnbYv!0`=v$IH!f|nB2UCjYa zIU)%T8q6IM;O9YeN4+l}PhbNV7v8at8gO|IAMH^E&x{WxqYLvV(r32T!R{mDP!?Pq z^>Qg5ja!&3SR0F2W2U9XTRsii1Jc)h_y1$x6?^~j8?UAFCG5QBME`a3tv^A2-F&NC zQf;0!I~jVO=I;}}?!UCX+s)_ZQ&rtI{&(GfX-BV~noO_B*6ho72b-#1AGdU8bl9z} zfd80k7vH>Ws2k)czP{ka>P<0JS2f59sZ#Sgrr|JZD=tRwUiaEk%T@~XTX50w*i?T7 z5cLzd`s?xG__j?0%ATuT0~;5c6=}*SV2}fDsdrF9utt}5F&*NiTR^NVPMupjC)K_{ zT}4$ccextCdGx?4qR)!p&LEdcR0L7be<`Y|bF@l%8i`n_O{s^4zl^E%v|I0wd>@38 zaMEgFYC6UKo8T3-T#Cd* zrsPm&9DJX|xE~5`>khksD_4!C+yH@^7Kl9#7E$$$zjW*(iiN`$b=(5!rk%vBU6$F_ zN5rxD8zmA9|1Hqg1rdrZa&G*c>{_DsuVrxMU;F`R(dvrPbIL<}EMxpA;1AUN8UOv^ z%>E4;`a0C*T9Z#L0fl0+h6LPFkMa3XVj7PI(?+}2W11EHN8C92^ue}mz_pwI1uq`T AQ~&?~ literal 0 HcmV?d00001 diff --git a/lib/muxer/muxer_test.proto b/lib/muxer/muxer_test.proto new file mode 100644 index 0000000000..b3a723f983 --- /dev/null +++ b/lib/muxer/muxer_test.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +option go_package = "github.com/nspcc-dev/neofs-node/lib/muxer"; + +package muxer; + +// The Greater service definition. +service Greeter { + rpc Hello(HelloRequest) returns (HelloResponse); +} + +// Request message example +message HelloRequest { + string name = 1; +} + +message HelloResponse { + string message = 1; +} diff --git a/lib/netmap/netmap.go b/lib/netmap/netmap.go new file mode 100644 index 0000000000..e339d0f9b2 --- /dev/null +++ b/lib/netmap/netmap.go @@ -0,0 +1,392 @@ +package netmap + +import ( + "crypto/sha256" + "encoding/json" + "reflect" + "sort" + "sync" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/netmap" + "github.com/pkg/errors" + "github.com/spaolacci/murmur3" +) + +type ( + // Bucket is an alias for github.com/nspcc-dev/netmap.Bucket + Bucket = netmap.Bucket + // SFGroup is an alias for github.com/nspcc-dev/netmap.SFGroup + SFGroup = netmap.SFGroup + // Select is an alias for github.com/nspcc-dev/netmap.Select + Select = netmap.Select + // Filter is an alias for github.com/nspcc-dev/netmap.Filter + Filter = netmap.Filter + // SimpleFilter is an alias for github.com/nspcc-dev/netmap.Filter + SimpleFilter = netmap.SimpleFilter + // PlacementRule is an alias for github.com/nspcc-dev/netmap.Filter + PlacementRule = netmap.PlacementRule + + // NetMap is a general network map structure for NeoFS + NetMap struct { + mu *sync.RWMutex + root Bucket + items Nodes + } + + // Nodes is an alias for slice of NodeInfo which is structure that describes every host + Nodes []bootstrap.NodeInfo +) + +const ( + // Separator separates key:value pairs in string representation of options. + Separator = netmap.Separator + + // NodesBucket is the name for optionless bucket containing only nodes. + NodesBucket = netmap.NodesBucket +) + +var ( + // FilterIn returns filter, which checks if value is in specified list. + FilterIn = netmap.FilterIn + // FilterNotIn returns filter, which checks if value is not in specified list. + FilterNotIn = netmap.FilterNotIn + // FilterOR returns OR combination of filters. + FilterOR = netmap.FilterOR + // FilterAND returns AND combination of filters. + FilterAND = netmap.FilterAND + // FilterEQ returns filter, which checks if value is equal to v. + FilterEQ = netmap.FilterEQ + // FilterNE returns filter, which checks if value is not equal to v. + FilterNE = netmap.FilterNE + // FilterGT returns filter, which checks if value is greater than v. + FilterGT = netmap.FilterGT + // FilterGE returns filter, which checks if value is greater or equal than v. + FilterGE = netmap.FilterGE + // FilterLT returns filter, which checks if value is less than v. + FilterLT = netmap.FilterLT + // FilterLE returns filter, which checks if value is less or equal than v. + FilterLE = netmap.FilterLE +) + +var errNetMapsConflict = errors.New("netmaps are in conflict") + +// Copy creates new slice of copied nodes. +func (n Nodes) Copy() Nodes { + res := make(Nodes, len(n)) + for i := range n { + res[i].Address = n[i].Address + res[i].Status = n[i].Status + + if n[i].PubKey != nil { + res[i].PubKey = make([]byte, len(n[i].PubKey)) + copy(res[i].PubKey, n[i].PubKey) + } + + if n[i].Options != nil { + res[i].Options = make([]string, len(n[i].Options)) + copy(res[i].Options, n[i].Options) + } + } + + return res +} + +// NewNetmap is an constructor. +func NewNetmap() *NetMap { + return &NetMap{ + items: make([]bootstrap.NodeInfo, 0), + mu: new(sync.RWMutex), + } +} + +// Equals return whether two netmap are identical. +func (n *NetMap) Equals(nm *NetMap) bool { + n.mu.RLock() + defer n.mu.RUnlock() + + return len(n.items) == len(nm.items) && + n.root.Equals(nm.root) && + reflect.DeepEqual(n.items, nm.items) +} + +// Root returns netmap root-bucket. +func (n *NetMap) Root() *Bucket { + n.mu.RLock() + cp := n.root.Copy() + n.mu.RUnlock() + + return &cp +} + +// Copy creates and returns full copy of target netmap. +func (n *NetMap) Copy() *NetMap { + n.mu.RLock() + defer n.mu.RUnlock() + + nm := NewNetmap() + nm.items = n.items.Copy() + nm.root = n.root.Copy() + + return nm +} + +type hashedItem struct { + h uint32 + info *bootstrap.NodeInfo +} + +// Normalise reorders netmap items into some canonical order. +func (n *NetMap) Normalise() *NetMap { + nm := NewNetmap() + items := n.items.Copy() + + if len(items) == 0 { + return nm + } + + itemsH := make([]hashedItem, len(n.items)) + for i := range itemsH { + itemsH[i].h = murmur3.Sum32(n.items[i].PubKey) + itemsH[i].info = &items[i] + } + + sort.Slice(itemsH, func(i, j int) bool { + if itemsH[i].h == itemsH[j].h { + return itemsH[i].info.Address < itemsH[j].info.Address + } + return itemsH[i].h < itemsH[j].h + }) + + lastHash := ^itemsH[0].h + lastAddr := "" + + for i := range itemsH { + if itemsH[i].h != lastHash || itemsH[i].info.Address != lastAddr { + _ = nm.AddNode(itemsH[i].info) + lastHash = itemsH[i].h + } + } + + return nm +} + +// Hash returns hash of n. +func (n *NetMap) Hash() (sum [32]byte) { + items := n.Normalise().Items() + w := sha256.New() + + for i := range items { + data, _ := items[i].Marshal() + _, _ = w.Write(data) + } + + s := w.Sum(nil) + copy(sum[:], s) + + return +} + +// InheritWeights calculates average capacity and minimal price, then provides buckets with IQR weight. +func (n *NetMap) InheritWeights() *NetMap { + nm := n.Copy() + + // find average capacity in the network map + meanCap := nm.root.Traverse(netmap.NewMeanAgg(), netmap.CapWeightFunc).Compute() + capNorm := netmap.NewSigmoidNorm(meanCap) + + // find minimal price in the network map + minPrice := nm.root.Traverse(netmap.NewMinAgg(), netmap.PriceWeightFunc).Compute() + priceNorm := netmap.NewReverseMinNorm(minPrice) + + // provide all buckets with + wf := netmap.NewWeightFunc(capNorm, priceNorm) + meanAF := netmap.AggregatorFactory{New: netmap.NewMeanIQRAgg} + nm.root.TraverseTree(meanAF, wf) + + return nm +} + +// Merge checks if merge is possible and then add new elements from given netmap. +func (n *NetMap) Merge(n1 *NetMap) error { + n.mu.Lock() + defer n.mu.Unlock() + + var ( + tr = make(map[uint32]netmap.Node, len(n1.items)) + items = n.items + ) + +loop: + for j := range n1.items { + for i := range n.items { + if n.items[i].Equals(n1.items[j]) { + tr[uint32(j)] = netmap.Node{ + N: uint32(i), + C: n.items[i].Capacity(), + P: n.items[i].Price(), + } + continue loop + } + } + tr[uint32(j)] = netmap.Node{ + N: uint32(len(items)), + C: n1.items[j].Capacity(), + P: n1.items[j].Price(), + } + items = append(items, n1.items[j]) + } + + root := n1.root.UpdateIndices(tr) + if n.root.CheckConflicts(root) { + return errNetMapsConflict + } + + n.items = items + n.root.Merge(root) + + return nil +} + +// FindGraph finds sub-graph filtered by given SFGroup. +func (n *NetMap) FindGraph(pivot []byte, ss ...SFGroup) (c *Bucket) { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.root.FindGraph(pivot, ss...) +} + +// FindNodes finds sub-graph filtered by given SFGroup and returns all sub-graph items. +func (n *NetMap) FindNodes(pivot []byte, ss ...SFGroup) (nodes []uint32) { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.root.FindNodes(pivot, ss...).Nodes() +} + +// Items return slice of all NodeInfo in netmap. +func (n *NetMap) Items() []bootstrap.NodeInfo { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.items +} + +// ItemsCopy return copied slice of all NodeInfo in netmap (is it useful?). +func (n *NetMap) ItemsCopy() Nodes { + n.mu.RLock() + defer n.mu.RUnlock() + + return n.items.Copy() +} + +// Add adds node with given address and given options. +func (n *NetMap) Add(addr string, pk []byte, st bootstrap.NodeStatus, opts ...string) error { + return n.AddNode(&bootstrap.NodeInfo{Address: addr, PubKey: pk, Status: st, Options: opts}) +} + +// Update replaces netmap with given netmap. +func (n *NetMap) Update(nxt *NetMap) { + n.mu.Lock() + defer n.mu.Unlock() + + n.root = nxt.root + n.items = nxt.items +} + +// GetMaxSelection returns 'maximal container' -- subgraph which contains +// any other subgraph satisfying specified selects and filters. +func (n *NetMap) GetMaxSelection(ss []Select, fs []Filter) (r *Bucket) { + return n.root.GetMaxSelection(netmap.SFGroup{Selectors: ss, Filters: fs}) +} + +// AddNode adds to exited or new node slice of given options. +func (n *NetMap) AddNode(nodeInfo *bootstrap.NodeInfo, opts ...string) error { + n.mu.Lock() + defer n.mu.Unlock() + + info := *nodeInfo + + info.Options = append(info.Options, opts...) + + num := -1 + + // looking for existed node info item + for i := range n.items { + if n.items[i].Equals(info) { + num = i + break + } + } + // if item is not existed - add it + if num < 0 { + num = len(n.items) + n.items = append(n.items, info) + } + + return n.root.AddStrawNode(netmap.Node{ + N: uint32(num), + C: n.items[num].Capacity(), + P: n.items[num].Price(), + }, info.Options...) +} + +// GetNodesByOption returns slice of NodeInfo that has given option. +func (n *NetMap) GetNodesByOption(opts ...string) []bootstrap.NodeInfo { + n.mu.RLock() + defer n.mu.RUnlock() + + ns := n.root.GetNodesByOption(opts...) + nodes := make([]bootstrap.NodeInfo, 0, len(ns)) + + for _, info := range ns { + nodes = append(nodes, n.items[info.N]) + } + + return nodes +} + +// MarshalJSON custom marshaller. +func (n *NetMap) MarshalJSON() ([]byte, error) { + n.mu.RLock() + defer n.mu.RUnlock() + + return json.Marshal(n.items) +} + +// UnmarshalJSON custom unmarshaller. +func (n *NetMap) UnmarshalJSON(data []byte) error { + var ( + nm = NewNetmap() + items []bootstrap.NodeInfo + ) + + if err := json.Unmarshal(data, &items); err != nil { + return err + } + + for i := range items { + if err := nm.Add(items[i].Address, items[i].PubKey, items[i].Status, items[i].Options...); err != nil { + return err + } + } + + if n.mu == nil { + n.mu = new(sync.RWMutex) + } + + n.mu.Lock() + n.root = nm.root + n.items = nm.items + n.mu.Unlock() + + return nil +} + +// Size returns number of nodes in network map. +func (n *NetMap) Size() int { + n.mu.RLock() + defer n.mu.RUnlock() + + return len(n.items) +} diff --git a/lib/netmap/netmap_test.go b/lib/netmap/netmap_test.go new file mode 100644 index 0000000000..1cd579b61d --- /dev/null +++ b/lib/netmap/netmap_test.go @@ -0,0 +1,261 @@ +package netmap + +import ( + "bytes" + "encoding/json" + "math/rand" + "sync" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/netmap" + "github.com/stretchr/testify/require" +) + +func TestNetMap_DataRace(t *testing.T) { + var ( + nm = NewNetmap() + wg = new(sync.WaitGroup) + nodes = []bootstrap.NodeInfo{ + {Address: "SPB1", Options: []string{"/Location:Europe/Country:USA"}}, + {Address: "SPB2", Options: []string{"/Location:Europe/Country:Italy"}}, + {Address: "MSK1", Options: []string{"/Location:Europe/Country:Germany"}}, + {Address: "MSK2", Options: []string{"/Location:Europe/Country:Russia"}}, + } + ) + + wg.Add(10) + for i := 0; i < 10; i++ { + go func(n int) { + for _, node := range nodes { + require.NoError(t, nm.Add(node.Address, node.PubKey, 0, node.Options...)) + // t.Logf("%02d: add node %q", n, node.Address) + } + + wg.Done() + }(i) + } + + wg.Add(3 * 10) + for i := 0; i < 10; i++ { + go func(n int) { + nm.Copy() + // t.Logf("%02d: Copy", n) + wg.Done() + }(i) + go func(n int) { + nm.Items() + // t.Logf("%02d: Items", n) + wg.Done() + }(i) + go func(n int) { + nm.Root() + // t.Logf("%02d: Root", n) + wg.Done() + }(i) + } + + wg.Wait() +} + +func TestNetMapSuite(t *testing.T) { + var ( + err error + nm1 = NewNetmap() + nodes = []bootstrap.NodeInfo{ + {Address: "SPB1", Options: []string{"/Location:Europe/Country:USA"}, Status: 1}, + {Address: "SPB2", Options: []string{"/Location:Europe/Country:Italy"}, Status: 2}, + {Address: "MSK1", Options: []string{"/Location:Europe/Country:Germany"}, Status: 3}, + {Address: "MSK2", Options: []string{"/Location:Europe/Country:Russia"}, Status: 4}, + } + ) + + for _, node := range nodes { + err = nm1.Add(node.Address, nil, node.Status, node.Options...) + require.NoError(t, err) + } + + t.Run("copy should work like expected", func(t *testing.T) { + nm2 := nm1.Copy() + require.Equal(t, nm1.root, nm2.root) + require.Equal(t, nm1.items, nm2.items) + }) + + t.Run("add node should not ignore options", func(t *testing.T) { + items := nm1.ItemsCopy() + + nm2 := NewNetmap() + err = nm2.AddNode(&items[0], "/New/Option") + require.NoError(t, err) + require.Len(t, nm2.items, 1) + require.Equal(t, append(items[0].Options, "/New/Option"), nm2.items[0].Options) + }) + + t.Run("copyItems should work like expected", func(t *testing.T) { + require.Equal(t, nm1.items, nm1.ItemsCopy()) + }) + + t.Run("marshal / unmarshal should be identical on same data", func(t *testing.T) { + var nm2 *NetMap + want, err := json.Marshal(nodes) + require.NoError(t, err) + + actual, err := json.Marshal(nm1) + require.NoError(t, err) + + require.Equal(t, want, actual) + + err = json.Unmarshal(actual, &nm2) + require.NoError(t, err) + require.Equal(t, nm1.root, nm2.root) + require.Equal(t, nm1.items, nm2.items) + }) + + t.Run("unmarshal should override existing data", func(t *testing.T) { + var nm2 *NetMap + + want, err := json.Marshal(nodes) + require.NoError(t, err) + + actual, err := json.Marshal(nm1) + require.NoError(t, err) + + require.Equal(t, want, actual) + + nm2 = nm1.Copy() + err = nm2.Add("SOMEADDR", nil, 0, "/Location:Europe/Country:USA") + require.NoError(t, err) + + err = json.Unmarshal(actual, &nm2) + require.NoError(t, err) + require.Equal(t, nm1.root, nm2.root) + require.Equal(t, nm1.items, nm2.items) + }) + + t.Run("unmarshal should fail on bad data", func(t *testing.T) { + var nm2 *NetMap + require.Error(t, json.Unmarshal([]byte(`"some bad data"`), &nm2)) + }) + + t.Run("unmarshal should fail on add nodes", func(t *testing.T) { + var nm2 *NetMap + require.Error(t, json.Unmarshal([]byte(`[{"address": "SPB1","options":["1-2-3-4"]}]`), &nm2)) + }) + + t.Run("merge two netmaps", func(t *testing.T) { + newNodes := []bootstrap.NodeInfo{ + {Address: "SPB3", Options: []string{"/Location:Europe/Country:France"}}, + } + nm2 := NewNetmap() + for _, node := range newNodes { + err = nm2.Add(node.Address, nil, 0, node.Options...) + require.NoError(t, err) + } + + err = nm2.Merge(nm1) + require.NoError(t, err) + require.Len(t, nm2.items, len(nodes)+len(newNodes)) + + ns := nm2.FindNodes([]byte("pivot"), netmap.SFGroup{ + Filters: []Filter{{Key: "Country", F: FilterEQ("Germany")}}, + Selectors: []Select{{Count: 1, Key: NodesBucket}}, + }) + require.Len(t, ns, 1) + }) + + t.Run("weighted netmaps", func(t *testing.T) { + strawNodes := []bootstrap.NodeInfo{ + {Address: "SPB2", Options: []string{"/Location:Europe/Country:Italy", "/Capacity:10", "/Price:100"}}, + {Address: "MSK1", Options: []string{"/Location:Europe/Country:Germany", "/Capacity:10", "/Price:1"}}, + {Address: "MSK2", Options: []string{"/Location:Europe/Country:Russia", "/Capacity:5", "/Price:10"}}, + {Address: "SPB1", Options: []string{"/Location:Europe/Country:France", "/Capacity:20", "/Price:2"}}, + } + nm2 := NewNetmap() + for _, node := range strawNodes { + err = nm2.Add(node.Address, nil, 0, node.Options...) + require.NoError(t, err) + } + + ns1 := nm1.FindNodes([]byte("pivot"), netmap.SFGroup{ + Selectors: []Select{{Count: 2, Key: NodesBucket}}, + }) + require.Len(t, ns1, 2) + + ns2 := nm2.FindNodes([]byte("pivot"), netmap.SFGroup{ + Selectors: []Select{{Count: 2, Key: NodesBucket}}, + }) + require.Len(t, ns2, 2) + require.NotEqual(t, ns1, ns2) + require.Equal(t, []uint32{1, 3}, ns2) + }) +} + +func TestNetMap_Normalise(t *testing.T) { + const testCount = 5 + + nodes := []bootstrap.NodeInfo{ + {Address: "SPB2", PubKey: []byte{4}, Options: []string{"/Location:Europe/Country:Italy", "/Capacity:10", "/Price:100"}}, + {Address: "MSK1", PubKey: []byte{2}, Options: []string{"/Location:Europe/Country:Germany", "/Capacity:10", "/Price:1"}}, + {Address: "MSK2", PubKey: []byte{3}, Options: []string{"/Location:Europe/Country:Russia", "/Capacity:5", "/Price:10"}}, + {Address: "SPB1", PubKey: []byte{1}, Options: []string{"/Location:Europe/Country:France", "/Capacity:20", "/Price:2"}}, + } + + add := func(nm *NetMap, indices ...int) { + for _, i := range indices { + err := nm.Add(nodes[i].Address, nodes[i].PubKey, 0, nodes[i].Options...) + require.NoError(t, err) + } + } + + indices := []int{0, 1, 2, 3} + + nm1 := NewNetmap() + add(nm1, indices...) + norm := nm1.Normalise() + + for i := 0; i < testCount; i++ { + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(indices), func(i, j int) { indices[i], indices[j] = indices[j], indices[i] }) + + nm := NewNetmap() + add(nm, indices...) + require.Equal(t, norm, nm.Normalise()) + } + + t.Run("normalise removes duplicates", func(t *testing.T) { + before := NewNetmap() + add(before, indices...) + before.items = append(before.items, before.items...) + + nm := before.Normalise() + require.Len(t, nm.items, len(indices)) + + loop: + for i := range nodes { + for j := range nm.items { + if bytes.Equal(nm.items[j].PubKey, nodes[i].PubKey) { + continue loop + } + } + require.Fail(t, "normalized netmap does not contain '%s' node", nodes[i].Address) + } + }) +} + +func TestNodeInfo_Price(t *testing.T) { + var info bootstrap.NodeInfo + + // too small value + info = bootstrap.NodeInfo{Options: []string{"/Price:0.01048575"}} + require.Equal(t, uint64(0), info.Price()) + + // min value + info = bootstrap.NodeInfo{Options: []string{"/Price:0.01048576"}} + require.Equal(t, uint64(1), info.Price()) + + // big value + info = bootstrap.NodeInfo{Options: []string{"/Price:1000000000.666"}} + require.Equal(t, uint64(1000000000.666*1e8/object.UnitsMB), info.Price()) +} diff --git a/lib/netmap/storage.go b/lib/netmap/storage.go new file mode 100644 index 0000000000..fc26bb555e --- /dev/null +++ b/lib/netmap/storage.go @@ -0,0 +1,27 @@ +package netmap + +// GetParams is a group of parameters +// for network map receiving operation. +type GetParams struct { +} + +// GetResult is a group of values +// returned by container receiving operation. +type GetResult struct { + nm *NetMap +} + +// Storage is an interface of the storage of NeoFS network map. +type Storage interface { + GetNetMap(GetParams) (*GetResult, error) +} + +// NetMap is a network map getter. +func (s GetResult) NetMap() *NetMap { + return s.nm +} + +// SetNetMap is a network map setter. +func (s *GetResult) SetNetMap(v *NetMap) { + s.nm = v +} diff --git a/lib/netmap/storage_test.go b/lib/netmap/storage_test.go new file mode 100644 index 0000000000..27315f8b5c --- /dev/null +++ b/lib/netmap/storage_test.go @@ -0,0 +1,23 @@ +package netmap + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/stretchr/testify/require" +) + +func TestGetResult(t *testing.T) { + s := GetResult{} + + nm := NewNetmap() + require.NoError(t, + nm.AddNode(&bootstrap.NodeInfo{ + Address: "address", + PubKey: []byte{1, 2, 3}, + }), + ) + s.SetNetMap(nm) + + require.Equal(t, nm, s.NetMap()) +} diff --git a/lib/objio/range.go b/lib/objio/range.go new file mode 100644 index 0000000000..183fb73983 --- /dev/null +++ b/lib/objio/range.go @@ -0,0 +1,459 @@ +package objio + +import ( + "context" + "io" + "sync" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/pkg/errors" +) + +type ( + // Address is a type alias of + // Address from refs package of neofs-api-go. + Address = refs.Address + + // ChopperTable is an interface of RangeChopper storage. + ChopperTable interface { + PutChopper(addr Address, chopper RangeChopper) error + GetChopper(addr Address, rc RCType) (RangeChopper, error) + } + + // RangeChopper is an interface of the chooper of object payload range. + RangeChopper interface { + GetType() RCType + GetAddress() Address + Closed() bool + Chop(ctx context.Context, length, offset int64, fromStart bool) ([]RangeDescriptor, error) + } + + // RelativeReceiver is an interface of object relations controller. + RelativeReceiver interface { + Base(ctx context.Context, addr Address) (RangeDescriptor, error) + Neighbor(ctx context.Context, addr Address, left bool) (RangeDescriptor, error) + } + + // ChildLister is an interface of object children info storage. + ChildLister interface { + List(ctx context.Context, parent Address) ([]RangeDescriptor, error) + } + + // RangeDescriptor groups the information about object payload range. + RangeDescriptor struct { + Size int64 + Offset int64 + Addr Address + + LeftBound bool + RightBound bool + } + + chopCache struct { + rangeList []RangeDescriptor + } + + chopper struct { + *sync.RWMutex + ct RCType + addr Address + nr RelativeReceiver + cacheOffset int64 + cache *chopCache + } + + // ChopperParams groups the parameters of Scylla chopper. + ChopperParams struct { + RelativeReceiver RelativeReceiver + Addr Address + } + + charybdis struct { + skr *chopper + cl ChildLister + } + + // CharybdisParams groups the parameters of Charybdis chopper. + CharybdisParams struct { + Addr Address + ChildLister ChildLister + + ReadySelection []RangeDescriptor + } + + // RCType is an enumeration of object payload range chopper types. + RCType int + + chopperTable struct { + *sync.RWMutex + items map[RCType]map[string]RangeChopper + } +) + +const ( + // RCScylla is an RCType of payload range post-pouncing chopper. + RCScylla RCType = iota + + // RCCharybdis is an RCType of payload range pre-pouncing chopper. + RCCharybdis +) + +var errNilRelativeReceiver = errors.New("relative receiver is nil") + +var errEmptyObjectID = errors.New("object ID is empty") + +var errNilChildLister = errors.New("child lister is nil") + +var errNotFound = errors.New("object range chopper not found") + +var errInvalidBound = errors.New("invalid payload bounds") + +// NewChopperTable is a RangeChopper storage constructor. +func NewChopperTable() ChopperTable { + return &chopperTable{ + new(sync.RWMutex), + make(map[RCType]map[string]RangeChopper), + } +} + +// NewScylla constructs object payload range chopper that collects parts of a range on the go. +func NewScylla(p *ChopperParams) (RangeChopper, error) { + if p.RelativeReceiver == nil { + return nil, errNilRelativeReceiver + } + + if p.Addr.ObjectID.Empty() { + return nil, errEmptyObjectID + } + + return &chopper{ + RWMutex: new(sync.RWMutex), + ct: RCScylla, + nr: p.RelativeReceiver, + addr: p.Addr, + cache: &chopCache{ + rangeList: make([]RangeDescriptor, 0), + }, + }, nil +} + +// NewCharybdis constructs object payload range that pre-collects all parts of the range. +func NewCharybdis(p *CharybdisParams) (RangeChopper, error) { + if p.ChildLister == nil && len(p.ReadySelection) == 0 { + return nil, errNilChildLister + } + + if p.Addr.ObjectID.Empty() { + return nil, errEmptyObjectID + } + + cache := new(chopCache) + + if len(p.ReadySelection) > 0 { + cache.rangeList = p.ReadySelection + } + + return &charybdis{ + skr: &chopper{ + RWMutex: new(sync.RWMutex), + ct: RCCharybdis, + addr: p.Addr, + cache: cache, + }, + cl: p.ChildLister, + }, nil +} + +func (ct *chopperTable) PutChopper(addr Address, chopper RangeChopper) error { + ct.Lock() + defer ct.Unlock() + + sAddr := addr.String() + chopperType := chopper.GetType() + + m, ok := ct.items[chopperType] + if !ok { + m = make(map[string]RangeChopper) + } + + if _, ok := m[sAddr]; !ok { + m[sAddr] = chopper + } + + ct.items[chopperType] = m + + return nil +} + +func (ct *chopperTable) GetChopper(addr Address, rc RCType) (RangeChopper, error) { + ct.Lock() + defer ct.Unlock() + + choppers, ok := ct.items[rc] + if !ok { + return nil, errNotFound + } + + chp, ok := choppers[addr.String()] + if !ok { + return nil, errNotFound + } + + return chp, nil +} + +func (c charybdis) GetAddress() Address { + return c.skr.addr +} + +func (c charybdis) GetType() RCType { + return c.skr.ct +} + +func (c charybdis) Closed() bool { + return len(c.skr.cache.rangeList) > 0 +} + +func (c *charybdis) devour(ctx context.Context) error { + if len(c.skr.cache.rangeList) == 0 { + rngs, err := c.cl.List(ctx, c.skr.addr) + if err != nil { + return errors.Wrap(err, "charybdis.pounce faild on children list") + } + + if ln := len(rngs); ln > 0 { + rngs[0].LeftBound = true + rngs[ln-1].RightBound = true + } + + c.skr.cache.rangeList = rngs + } + + return nil +} + +func (c *charybdis) Chop(ctx context.Context, length, offset int64, fromStart bool) ([]RangeDescriptor, error) { + if err := c.devour(ctx); err != nil { + return nil, errors.Wrap(err, "charybdis.Chop failed on devour") + } + + return c.skr.Chop(ctx, length, offset, fromStart) +} + +func (sc *chopCache) Size() (res int64) { + for i := range sc.rangeList { + res += sc.rangeList[i].Size + } + + return +} + +func (sc *chopCache) touchStart() bool { + return len(sc.rangeList) > 0 && sc.rangeList[0].LeftBound +} + +func (sc *chopCache) touchEnd() bool { + ln := len(sc.rangeList) + + return ln > 0 && sc.rangeList[ln-1].RightBound +} + +func min(a, b int64) int64 { + if a < b { + return a + } + + return b +} + +func (sc *chopCache) Chop(offset, size int64) ([]RangeDescriptor, error) { + if offset*size < 0 { + return nil, errInvalidBound + } + + if offset+size > sc.Size() { + return nil, localstore.ErrOutOfRange + } + + var ( + off int64 + res = make([]RangeDescriptor, 0) + ind int + firstOffset int64 + ) + + for i := range sc.rangeList { + diff := offset - off + if diff > sc.rangeList[i].Size { + off += sc.rangeList[i].Size + continue + } else if diff < sc.rangeList[i].Size { + ind = i + firstOffset = diff + break + } + + ind = i + 1 + + break + } + + var ( + r RangeDescriptor + num int64 + ) + + for i := ind; num < size; i++ { + cut := min(size-num, sc.rangeList[i].Size-firstOffset) + r = RangeDescriptor{ + Size: cut, + Addr: sc.rangeList[i].Addr, + + LeftBound: sc.rangeList[i].LeftBound, + RightBound: sc.rangeList[i].RightBound, + } + + if i == ind { + r.Offset = firstOffset + firstOffset = 0 + } + + if cut == size-num { + r.Size = cut + } + + res = append(res, r) + + num += cut + } + + return res, nil +} + +func (c *chopper) GetAddress() Address { + return c.addr +} + +func (c *chopper) GetType() RCType { + return c.ct +} + +func (c *chopper) Closed() bool { + return c.cache.touchStart() && c.cache.touchEnd() +} + +func (c *chopper) pounce(ctx context.Context, off int64, set bool) error { + if len(c.cache.rangeList) == 0 { + child, err := c.nr.Base(ctx, c.addr) + if err != nil { + return errors.Wrap(err, "chopper.pounce failed on cache init") + } + + c.cache.rangeList = []RangeDescriptor{child} + } + + oldOff := c.cacheOffset + + defer func() { + if !set { + c.cacheOffset = oldOff + } + }() + + var ( + cacheSize = c.cache.Size() + v = c.cacheOffset + off + ) + + switch { + case v >= 0 && v <= cacheSize: + c.cacheOffset = v + return nil + case v < 0 && c.cache.touchStart(): + c.cacheOffset = 0 + return io.EOF + case v > cacheSize && c.cache.touchEnd(): + c.cacheOffset = cacheSize + return io.EOF + } + + var ( + alloc, written int64 + toLeft = v < 0 + procAddr Address + fPush = func(r RangeDescriptor) { + if toLeft { + c.cache.rangeList = append([]RangeDescriptor{r}, c.cache.rangeList...) + return + } + c.cache.rangeList = append(c.cache.rangeList, r) + } + ) + + if toLeft { + alloc = -v + procAddr = c.cache.rangeList[0].Addr + c.cacheOffset -= cacheSize + } else { + alloc = v - cacheSize + procAddr = c.cache.rangeList[len(c.cache.rangeList)-1].Addr + c.cacheOffset += cacheSize + } + + for written < alloc { + rng, err := c.nr.Neighbor(ctx, procAddr, toLeft) + if err != nil { + return errors.Wrap(err, "chopper.pounce failed on get neighbor") + } + + if diff := alloc - written; diff < rng.Size { + if toLeft { + rng.Offset = rng.Size - diff + } + + c.cacheOffset += diff + + fPush(rng) + + break + } + + c.cacheOffset += rng.Size + fPush(rng) + + written += rng.Size + + if written < alloc && + (rng.LeftBound && toLeft || rng.RightBound && !toLeft) { + return localstore.ErrOutOfRange + } + + procAddr = rng.Addr + } + + return nil +} + +func (c *chopper) Chop(ctx context.Context, length, offset int64, fromStart bool) ([]RangeDescriptor, error) { + c.Lock() + defer c.Unlock() + + if fromStart { + if err := c.pounce(ctx, -(1 << 63), true); err != nil && err != io.EOF { + return nil, errors.Wrap(err, "chopper.Chop failed on chopper.pounce to start") + } + } + + if err := c.pounce(ctx, offset, true); err != nil && err != io.EOF { + return nil, errors.Wrap(err, "chopper.Chop failed on chopper.pounce with set") + } + + if c.cache.Size()-c.cacheOffset < length { + if err := c.pounce(ctx, length, false); err != nil && err != io.EOF { + return nil, errors.Wrap(err, "chopper.Chop failed on chopper.pounce") + } + } + + return c.cache.Chop(c.cacheOffset, length) +} diff --git a/lib/objio/range_test.go b/lib/objio/range_test.go new file mode 100644 index 0000000000..6d7290d94e --- /dev/null +++ b/lib/objio/range_test.go @@ -0,0 +1,386 @@ +package objio + +import ( + "context" + "crypto/rand" + "io" + "sync" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type ( + addressSet struct { + *sync.RWMutex + items []RangeDescriptor + data [][]byte + } + + testReader struct { + pr object.PositionReader + ct ChopperTable + } +) + +func (r testReader) Read(ctx context.Context, rd RangeDescriptor, rc RCType) ([]byte, error) { + chopper, err := r.ct.GetChopper(rd.Addr, rc) + if err != nil { + return nil, errors.Wrap(err, "testReader.Read failed on get range chopper") + } + + rngs, err := chopper.Chop(ctx, rd.Size, rd.Offset, true) + if err != nil { + return nil, errors.Wrap(err, "testReader.Read failed on chopper.Chop") + } + + var sz int64 + for i := range rngs { + sz += rngs[i].Size + } + + res := make([]byte, 0, sz) + + for i := range rngs { + data, err := r.pr.PRead(ctx, rngs[i].Addr, object.Range{ + Offset: uint64(rngs[i].Offset), + Length: uint64(rngs[i].Size), + }) + if err != nil { + return nil, errors.Wrapf(err, "testReader.Read failed on PRead of range #%d", i) + } + + res = append(res, data...) + } + + return res, nil +} + +func (as addressSet) PRead(ctx context.Context, addr refs.Address, rng object.Range) ([]byte, error) { + as.RLock() + defer as.RUnlock() + + for i := range as.items { + if as.items[i].Addr.CID.Equal(addr.CID) && as.items[i].Addr.ObjectID.Equal(addr.ObjectID) { + return as.data[i][rng.Offset : rng.Offset+rng.Length], nil + } + } + + return nil, errors.New("pread failed") +} + +func (as addressSet) List(ctx context.Context, parent Address) ([]RangeDescriptor, error) { + return as.items, nil +} + +func (as addressSet) Base(ctx context.Context, addr Address) (RangeDescriptor, error) { + return as.items[0], nil +} + +func (as addressSet) Neighbor(ctx context.Context, addr Address, left bool) (RangeDescriptor, error) { + as.Lock() + defer as.Unlock() + + ind := -1 + for i := range as.items { + if as.items[i].Addr.CID.Equal(addr.CID) && as.items[i].Addr.ObjectID.Equal(addr.ObjectID) { + ind = i + break + } + } + + if ind == -1 { + return RangeDescriptor{}, errors.New("range not found") + } + + if left { + if ind > 0 { + ind-- + } else { + return RangeDescriptor{}, io.EOF + } + } else { + if ind < len(as.items)-1 { + ind++ + } else { + return RangeDescriptor{}, io.EOF + } + } + + return as.items[ind], nil +} + +func newTestNeighbor(rngs []RangeDescriptor, data [][]byte) *addressSet { + return &addressSet{ + RWMutex: new(sync.RWMutex), + items: rngs, + data: data, + } +} + +func rangeSize(rngs []RangeDescriptor) (res int64) { + for i := range rngs { + res += rngs[i].Size + } + return +} + +func TestScylla(t *testing.T) { + var ( + cid = [refs.CIDSize]byte{1} + rngs = make([]RangeDescriptor, 0, 10) + pieceSize int64 = 100 + pieceCount int64 = 99 + fullSize = pieceCount * pieceSize + ) + + for i := int64(0); i < pieceCount; i++ { + oid, err := refs.NewObjectID() + require.NoError(t, err) + + rngs = append(rngs, RangeDescriptor{ + Size: pieceSize, + Offset: 0, + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + LeftBound: i == 0, + RightBound: i == pieceCount-1, + }) + } + + oid, err := refs.NewObjectID() + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + t.Run("Zero values in scylla notch/chop", func(t *testing.T) { + scylla, err := NewScylla(&ChopperParams{ + RelativeReceiver: newTestNeighbor(rngs, nil), + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + require.NoError(t, err) + + res, err := scylla.Chop(ctx, 0, 0, true) + require.NoError(t, err) + require.Len(t, res, 0) + }) + + t.Run("Common scylla operations in both directions", func(t *testing.T) { + var ( + off = fullSize / 2 + length = fullSize / 4 + ) + + scylla, err := NewScylla(&ChopperParams{ + RelativeReceiver: newTestNeighbor(rngs, nil), + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + require.NoError(t, err) + + choppedCount := int((length-1)/pieceSize + 1) + + if pieceCount > 1 && off%pieceSize > 0 { + choppedCount++ + } + + res, err := scylla.Chop(ctx, fullSize, 0, true) + require.NoError(t, err) + require.Len(t, res, int(pieceCount)) + require.Equal(t, rangeSize(res), fullSize) + require.Equal(t, res, rngs) + + res, err = scylla.Chop(ctx, length, off, true) + require.NoError(t, err) + require.Len(t, res, choppedCount) + + for i := int64(0); i < int64(choppedCount); i++ { + require.Equal(t, res[i].Addr.ObjectID, rngs[pieceCount/2+i].Addr.ObjectID) + } + + require.Equal(t, rangeSize(res), length) + + res, err = scylla.Chop(ctx, length, -length, false) + require.NoError(t, err) + require.Len(t, res, choppedCount) + + for i := int64(0); i < int64(choppedCount); i++ { + require.Equal(t, res[i].Addr.ObjectID, rngs[pieceCount/4+i].Addr.ObjectID) + } + + require.Equal(t, rangeSize(res), length) + }) + + t.Run("Border scylla Chop", func(t *testing.T) { + var ( + err error + res []RangeDescriptor + ) + + scylla, err := NewScylla(&ChopperParams{ + RelativeReceiver: newTestNeighbor(rngs, nil), + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + require.NoError(t, err) + + res, err = scylla.Chop(ctx, fullSize, 0, false) + require.NoError(t, err) + require.Equal(t, res, rngs) + + res, err = scylla.Chop(ctx, fullSize, -100, false) + require.NoError(t, err) + require.Equal(t, res, rngs) + + res, err = scylla.Chop(ctx, fullSize, 1, false) + require.Error(t, err) + + res, err = scylla.Chop(ctx, fullSize, -fullSize, false) + require.NoError(t, err) + require.Equal(t, rangeSize(res), fullSize) + }) +} + +func TestCharybdis(t *testing.T) { + var ( + cid = [refs.CIDSize]byte{1} + rngs = make([]RangeDescriptor, 0, 10) + pieceSize int64 = 100 + pieceCount int64 = 99 + fullSize = pieceCount * pieceSize + data = make([]byte, fullSize) + dataChunks = make([][]byte, 0, pieceCount) + ) + + _, err := rand.Read(data) + require.NoError(t, err) + + for i := int64(0); i < pieceCount; i++ { + oid, err := refs.NewObjectID() + require.NoError(t, err) + + dataChunks = append(dataChunks, data[i*pieceSize:(i+1)*pieceSize]) + + rngs = append(rngs, RangeDescriptor{ + Size: pieceSize, + Offset: 0, + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + } + + oid, err := refs.NewObjectID() + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + t.Run("Zero values in scylla notch/chop", func(t *testing.T) { + charybdis, err := NewCharybdis(&CharybdisParams{ + ChildLister: newTestNeighbor(rngs, nil), + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + require.NoError(t, err) + + res, err := charybdis.Chop(ctx, 0, 0, false) + require.NoError(t, err) + require.Len(t, res, 0) + }) + + t.Run("Common charybdis operations in both directions", func(t *testing.T) { + var ( + off = fullSize / 2 + length = fullSize / 4 + ) + + charybdis, err := NewCharybdis(&CharybdisParams{ + ChildLister: newTestNeighbor(rngs, nil), + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + require.NoError(t, err) + + choppedCount := int((length-1)/pieceSize + 1) + + if pieceCount > 1 && off%pieceSize > 0 { + choppedCount++ + } + + res, err := charybdis.Chop(ctx, fullSize, 0, false) + require.NoError(t, err) + require.Len(t, res, int(pieceCount)) + require.Equal(t, rangeSize(res), fullSize) + require.Equal(t, res, rngs) + + res, err = charybdis.Chop(ctx, length, off, false) + require.NoError(t, err) + require.Len(t, res, choppedCount) + + for i := int64(0); i < int64(choppedCount); i++ { + require.Equal(t, res[i].Addr.ObjectID, rngs[pieceCount/2+i].Addr.ObjectID) + } + + require.Equal(t, rangeSize(res), length) + + res, err = charybdis.Chop(ctx, length, -length, false) + require.NoError(t, err) + require.Len(t, res, choppedCount) + + for i := int64(0); i < int64(choppedCount); i++ { + require.Equal(t, res[i].Addr.ObjectID, rngs[pieceCount/4+i].Addr.ObjectID) + } + + require.Equal(t, rangeSize(res), length) + }) + + t.Run("Border charybdis Chop", func(t *testing.T) { + var ( + err error + res []RangeDescriptor + ) + + charybdis, err := NewCharybdis(&CharybdisParams{ + ChildLister: newTestNeighbor(rngs, nil), + Addr: Address{ + ObjectID: oid, + CID: cid, + }, + }) + require.NoError(t, err) + + res, err = charybdis.Chop(ctx, fullSize, 0, false) + require.NoError(t, err) + require.Equal(t, res, rngs) + + res, err = charybdis.Chop(ctx, fullSize, -100, false) + require.NoError(t, err) + require.Equal(t, res, rngs) + + res, err = charybdis.Chop(ctx, fullSize, 1, false) + require.Error(t, err) + + res, err = charybdis.Chop(ctx, fullSize, -fullSize, false) + require.NoError(t, err) + require.Equal(t, rangeSize(res), fullSize) + }) +} diff --git a/lib/objutil/verifier.go b/lib/objutil/verifier.go new file mode 100644 index 0000000000..a31dfff433 --- /dev/null +++ b/lib/objutil/verifier.go @@ -0,0 +1,35 @@ +package objutil + +import ( + "bytes" + "context" + + "github.com/nspcc-dev/neofs-api-go/object" +) + +// Verifier is an interface for checking whether an object conforms to a certain criterion. +// Nil error is equivalent to matching the criterion. +type Verifier interface { + Verify(context.Context, *object.Object) error +} + +// MarshalHeaders marshals all object headers which are "higher" than to-th extended header. +func MarshalHeaders(obj *object.Object, to int) ([]byte, error) { + buf := new(bytes.Buffer) + + if sysHdr, err := obj.SystemHeader.Marshal(); err != nil { + return nil, err + } else if _, err := buf.Write(sysHdr); err != nil { + return nil, err + } + + for i := range obj.Headers[:to] { + if header, err := obj.Headers[i].Marshal(); err != nil { + return nil, err + } else if _, err := buf.Write(header); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} diff --git a/lib/peers/metrics.go b/lib/peers/metrics.go new file mode 100644 index 0000000000..9391f7f188 --- /dev/null +++ b/lib/peers/metrics.go @@ -0,0 +1,45 @@ +package peers + +import ( + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc/connectivity" +) + +const stateLabel = "state" + +var grpcConnections = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Help: "gRPC connections", + Name: "grpc_connections", + Namespace: "neofs", + }, + []string{stateLabel}, +) + +var conStates = []connectivity.State{ + connectivity.Idle, + connectivity.Connecting, + connectivity.Ready, + connectivity.TransientFailure, + connectivity.Shutdown, +} + +func updateMetrics(items map[connectivity.State]float64) { + for _, state := range conStates { + grpcConnections.With(prometheus.Labels{ + stateLabel: state.String(), + }).Set(items[state]) + } +} + +func init() { + prometheus.MustRegister( + grpcConnections, + ) + + for _, state := range conStates { + grpcConnections.With(prometheus.Labels{ + stateLabel: state.String(), + }).Set(0) + } +} diff --git a/lib/peers/peers.go b/lib/peers/peers.go new file mode 100644 index 0000000000..406e495a8e --- /dev/null +++ b/lib/peers/peers.go @@ -0,0 +1,455 @@ +package peers + +import ( + "context" + "net" + "sync" + "time" + + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/keepalive" + "google.golang.org/grpc/status" +) + +type ( + // Interface is an interface of network connections controller. + Interface interface { + Shutdown() error + Job(context.Context) + Address() multiaddr.Multiaddr + RemoveConnection(maddr multiaddr.Multiaddr) error + Listen(maddr multiaddr.Multiaddr) (manet.Listener, error) + Connect(ctx context.Context, maddr multiaddr.Multiaddr) (manet.Conn, error) + GRPCConnector + } + + // GRPCConnector is an interface of gRPC virtual connector. + GRPCConnector interface { + GRPCConnection(ctx context.Context, maddr multiaddr.Multiaddr, reset bool) (*grpc.ClientConn, error) + } + + // Params groups the parameters of Interface. + Params struct { + Address multiaddr.Multiaddr + Transport transport.Transport + Logger *zap.Logger + Attempts int64 + AttemptsTTL time.Duration + ConnectionTTL time.Duration + ConnectionIDLE time.Duration + MetricsTimeout time.Duration + KeepAliveTTL time.Duration + KeepAlivePingTTL time.Duration + } + + connItem struct { + sync.RWMutex + conn *grpc.ClientConn + used time.Time + } + + iface struct { + log *zap.Logger + addr multiaddr.Multiaddr // self address + tr transport.Transport + tick time.Duration + idle time.Duration + + keepAlive time.Duration + pingTTL time.Duration + + metricsTimeout time.Duration + + grpc struct { + // globalMutex used by garbage collector and other high + globalMutex *sync.RWMutex + // bookMutex resolves concurrent access to the new connection + bookMutex *sync.RWMutex + // connBook contains connection info + // it's mutex resolves concurrent access to existed connection + connBook map[string]*connItem + } + + cons struct { + *sync.RWMutex + items map[string]transport.Connection + } + + lis struct { + *sync.RWMutex + items map[string]manet.Listener + } + } +) + +const ( + defaultAttemptsCount = 5 + defaultAttemptsTTL = 30 * time.Second + defaultCloseTimer = 30 * time.Second + defaultConIdleTTL = 30 * time.Second + defaultKeepAliveTTL = 5 * time.Second + defaultMetricsTimeout = 5 * time.Second + defaultKeepAlivePingTTL = 50 * time.Millisecond +) + +var ( + // ErrDialToSelf is returned if we attempt to dial our own peer + ErrDialToSelf = errors.New("dial to self attempted") + // ErrEmptyAddress returns when you try to create Interface with empty address + ErrEmptyAddress = errors.New("self address could not be empty") + // ErrEmptyTransport returns when you try to create Interface with empty transport + ErrEmptyTransport = errors.New("transport could not be empty") +) + +var errNilMultiaddr = errors.New("empty multi-address") + +func (s *iface) Shutdown() error { + s.lis.Lock() + s.cons.Lock() + s.grpc.globalMutex.Lock() + + defer func() { + s.lis.Unlock() + s.cons.Unlock() + s.grpc.globalMutex.Unlock() + }() + + for addr := range s.cons.items { + if err := s.removeNetConnection(addr); err != nil { + return errors.Wrapf(err, "could not remove net connection `%s`", addr) + } + } + + for addr := range s.grpc.connBook { + if err := s.removeGRPCConnection(addr); err != nil { + return errors.Wrapf(err, "could not remove net connection `%s`", addr) + } + } + + for addr := range s.lis.items { + if err := s.removeListener(addr); err != nil { + return errors.Wrapf(err, "could not remove listener `%s`", addr) + } + } + + return nil +} + +// RemoveConnection from Interface. +// Used only in tests, consider removing. +func (s *iface) RemoveConnection(maddr multiaddr.Multiaddr) error { + addr, err := convertAddress(maddr) + if err != nil { + return err + } + + s.cons.Lock() + s.grpc.globalMutex.Lock() + + defer func() { + s.cons.Unlock() + s.grpc.globalMutex.Unlock() + }() + + // Try to remove connection + if err := s.removeNetConnection(maddr.String()); err != nil { + return errors.Wrapf(err, "could not remove net connection `%s`", maddr.String()) + } + + // Try to remove gRPC connection + if err := s.removeGRPCConnection(addr); err != nil { + return errors.Wrapf(err, "could not remove gRPC connection `%s`", addr) + } + + // TODO remove another connections + + return nil +} + +func (s *iface) removeListener(addr string) error { + if lis, ok := s.lis.items[addr]; ok { + if err := lis.Close(); err != nil { + return err + } + + delete(s.lis.items, addr) + } + + return nil +} + +func (s *iface) removeNetConnection(addr string) error { + // Try to remove simple connection + if con, ok := s.cons.items[addr]; ok { + if err := con.Close(); err != nil { + return err + } + + delete(s.cons.items, addr) + } + + return nil +} + +func (s *iface) removeGRPCConnection(addr string) error { + if gCon, ok := s.grpc.connBook[addr]; ok && gCon.conn != nil { + if err := gCon.conn.Close(); err != nil { + state, ok := status.FromError(err) + if !ok { + return err + } + + s.log.Debug("error state", + zap.String("address", addr), + zap.Any("code", state.Code()), + zap.String("state", state.Message()), + zap.Any("details", state.Details())) + } + } + + delete(s.grpc.connBook, addr) + + return nil +} + +// Connect to address +// Used only in tests, consider removing. +func (s *iface) Connect(ctx context.Context, maddr multiaddr.Multiaddr) (manet.Conn, error) { + var ( + err error + con transport.Connection + ) + + if maddr.Equal(s.addr) { + return nil, ErrDialToSelf + } + + s.cons.RLock() + con, ok := s.cons.items[maddr.String()] + s.cons.RUnlock() + + if ok && !con.Closed() { + return con, nil + } + + if con, err = s.newConnection(ctx, maddr, false); err != nil { + return nil, err + } + + s.cons.Lock() + s.cons.items[maddr.String()] = con + s.cons.Unlock() + + return con, nil +} + +// Listen try to find listener or creates new. +func (s *iface) Listen(maddr multiaddr.Multiaddr) (manet.Listener, error) { + // fixme: concurrency issue there, same as 5260f04d + // but it's not so bad, because `Listen()` used + // once during startup routine. + s.lis.RLock() + lis, ok := s.lis.items[maddr.String()] + s.lis.RUnlock() + + if ok { + return lis, nil + } + + lis, err := s.tr.Listen(maddr) + if err != nil { + return nil, err + } + + s.lis.Lock() + s.lis.items[maddr.String()] = lis + s.lis.Unlock() + + return lis, nil +} + +// Address of current Interface instance. +func (s *iface) Address() multiaddr.Multiaddr { + return s.addr +} + +func isGRPCClosed(con *grpc.ClientConn) bool { + switch con.GetState() { + case connectivity.Idle, connectivity.Connecting, connectivity.Ready: + return false + default: + // connectivity.TransientFailure, connectivity.Shutdown + return true + } +} + +func (s *iface) newConnection(ctx context.Context, addr multiaddr.Multiaddr, reset bool) (transport.Connection, error) { + return s.tr.Dial(ctx, addr, reset) +} + +func gRPCKeepAlive(ping, ttl time.Duration) grpc.DialOption { + return grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: ping, + Timeout: ttl, + PermitWithoutStream: true, + }) +} + +func convertAddress(maddr multiaddr.Multiaddr) (string, error) { + if maddr == nil { + return "", errNilMultiaddr + } + + addr, err := manet.ToNetAddr(maddr) + if err != nil { + return "", errors.Wrapf(err, "could not convert address `%s`", maddr) + } + + return addr.String(), nil +} + +// GRPCConnection creates gRPC connection over peers connection. +func (s *iface) GRPCConnection(ctx context.Context, maddr multiaddr.Multiaddr, reset bool) (*grpc.ClientConn, error) { + addr, err := convertAddress(maddr) + if err != nil { + return nil, errors.Wrapf(err, "could not convert `%v`", maddr) + } + + // Get global mutex on read. + // All high level function e.g. peers garbage collector + // or shutdown must use globalMutex.Lock instead + s.grpc.globalMutex.RLock() + + // Get connection item from connection book or create a new one. + // Concurrent map access resolved by bookMutex. + s.grpc.bookMutex.Lock() + + item, ok := s.grpc.connBook[addr] + if !ok { + item = new(connItem) + s.grpc.connBook[addr] = item + } + + s.grpc.bookMutex.Unlock() + + // Now lock connection item. + // This denies concurrent access to the same address, + // but allows concurrent access to a different addresses. + item.Lock() + + if item.conn != nil && !isGRPCClosed(item.conn) { + item.used = time.Now() + + item.Unlock() + s.grpc.globalMutex.RUnlock() + + return item.conn, nil + } + + // Если вышеописанные строки переместить внутрь WithDialer, + // мы получим сломанный коннекшн, но ошибка не будет возвращена, + // поэтому мы сначала проверяем коннекшн и лишь потом возвращаем + // *gRPC.ClientConn + // + // Это будет работать с `grpc.WithBlock()`, см. ниже + conn, err := grpc.DialContext(ctx, maddr.String(), + gRPCKeepAlive(s.pingTTL, s.keepAlive), + // TODO: we must provide grpc.WithInsecure() or set credentials + grpc.WithInsecure(), + grpc.WithBlock(), + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { + return s.newConnection(ctx, maddr, reset) + }), + ) + if err == nil { + item.conn = conn + item.used = time.Now() + } + + item.Unlock() + s.grpc.globalMutex.RUnlock() + + return conn, err +} + +// New create iface instance and check arguments. +func New(p Params) (Interface, error) { + if p.Address == nil { + return nil, ErrEmptyAddress + } + + if p.Transport == nil { + return nil, ErrEmptyTransport + } + + if p.Attempts <= 0 { + p.Attempts = defaultAttemptsCount + } + + if p.AttemptsTTL <= 0 { + p.AttemptsTTL = defaultAttemptsTTL + } + + if p.ConnectionTTL <= 0 { + p.ConnectionTTL = defaultCloseTimer + } + + if p.ConnectionIDLE <= 0 { + p.ConnectionIDLE = defaultConIdleTTL + } + + if p.KeepAliveTTL <= 0 { + p.KeepAliveTTL = defaultKeepAliveTTL + } + + if p.KeepAlivePingTTL <= 0 { + p.KeepAlivePingTTL = defaultKeepAlivePingTTL + } + + if p.MetricsTimeout <= 0 { + p.MetricsTimeout = defaultMetricsTimeout + } + + return &iface{ + tick: p.ConnectionTTL, + idle: p.ConnectionIDLE, + + keepAlive: p.KeepAliveTTL, + pingTTL: p.KeepAlivePingTTL, + + metricsTimeout: p.MetricsTimeout, + + log: p.Logger, + addr: p.Address, + tr: p.Transport, + grpc: struct { + globalMutex *sync.RWMutex + bookMutex *sync.RWMutex + connBook map[string]*connItem + }{ + globalMutex: new(sync.RWMutex), + bookMutex: new(sync.RWMutex), + connBook: make(map[string]*connItem), + }, + cons: struct { + *sync.RWMutex + items map[string]transport.Connection + }{ + RWMutex: new(sync.RWMutex), + items: make(map[string]transport.Connection), + }, + lis: struct { + *sync.RWMutex + items map[string]manet.Listener + }{ + RWMutex: new(sync.RWMutex), + items: make(map[string]manet.Listener), + }, + }, nil +} diff --git a/lib/peers/peers_test.go b/lib/peers/peers_test.go new file mode 100644 index 0000000000..d71ef7b52a --- /dev/null +++ b/lib/peers/peers_test.go @@ -0,0 +1,484 @@ +package peers + +import ( + "context" + "encoding" + "encoding/json" + "net" + "strings" + "sync" + "testing" + "time" + + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type ( + fakeAddress struct { + json.Marshaler + json.Unmarshaler + encoding.TextMarshaler + encoding.TextUnmarshaler + encoding.BinaryMarshaler + encoding.BinaryUnmarshaler + } + + // service is used to implement GreaterServer. + service struct{} +) + +// Hello is simple handler +func (*service) Hello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) { + return &HelloResponse{ + Message: "Hello " + req.Name, + }, nil +} + +var _ multiaddr.Multiaddr = (*fakeAddress)(nil) + +func (fakeAddress) Equal(multiaddr.Multiaddr) bool { + return false +} + +func (fakeAddress) Bytes() []byte { + return nil +} + +func (fakeAddress) String() string { + return "fake" +} + +func (fakeAddress) Protocols() []multiaddr.Protocol { + return []multiaddr.Protocol{{Name: "fake"}} +} + +func (fakeAddress) Encapsulate(multiaddr.Multiaddr) multiaddr.Multiaddr { + panic("implement me") +} + +func (fakeAddress) Decapsulate(multiaddr.Multiaddr) multiaddr.Multiaddr { + panic("implement me") +} + +func (fakeAddress) ValueForProtocol(code int) (string, error) { + return "", nil +} + +const testCount = 10 + +func newTestAddress(t *testing.T) multiaddr.Multiaddr { + lis, err := net.Listen("tcp", "0.0.0.0:0") // nolint:gosec + require.NoError(t, err) + require.NoError(t, lis.Close()) + + l, ok := lis.(*net.TCPListener) + require.True(t, ok) + + _, port, err := net.SplitHostPort(l.Addr().String()) + require.NoError(t, err) + + items := []string{ + "ip4", + "127.0.0.1", + "tcp", + port, + } + + maddr, err := multiaddr.NewMultiaddr("/" + strings.Join(items, "/")) + require.NoError(t, err) + + return maddr +} + +func createTestInterface(t *testing.T) Interface { + s, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + return s +} + +func createTestInterfaces(t *testing.T) []Interface { + var ifaces = make([]Interface, 0, testCount) + for i := 0; i < testCount; i++ { + ifaces = append(ifaces, createTestInterface(t)) + } + return ifaces +} + +func connectEachOther(t *testing.T, ifaces []Interface) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for _, s := range ifaces { + _, err := s.Listen(s.Address()) + require.NoError(t, err) + + for _, n := range ifaces { + if s.Address().Equal(n.Address()) { + continue // do not connect itself + } + + _, err = n.Connect(ctx, s.Address()) + require.NoError(t, err) + } + } +} + +func TestInterface(t *testing.T) { + t.Run("should fail on empty address", func(t *testing.T) { + _, err := New(Params{}) + require.EqualError(t, err, ErrEmptyAddress.Error()) + }) + + t.Run("should fail on empty transport", func(t *testing.T) { + _, err := New(Params{Address: newTestAddress(t)}) + require.EqualError(t, err, ErrEmptyTransport.Error()) + }) + + t.Run("try to create multiple Interface and connect each other", func(t *testing.T) { + ifaces := createTestInterfaces(t) + connectEachOther(t, ifaces) + }) + + t.Run("should fail on itself connection", func(t *testing.T) { + s := createTestInterface(t) + _, err := s.Connect(context.Background(), s.Address()) + require.EqualError(t, err, ErrDialToSelf.Error()) + }) + + t.Run("should fail when you try to remove closed connection", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + s2, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + _, err = s1.Listen(s1.Address()) + require.NoError(t, err) + + _, err = s2.Listen(s2.Address()) + require.NoError(t, err) + + con, err := s1.Connect(context.Background(), s2.Address()) + require.NoError(t, err) + require.NoError(t, con.Close()) + + err = s1.RemoveConnection(s2.Address()) + require.NoError(t, err) + }) + + t.Run("should not create connection / listener twice", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + s2, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + l1, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + l2, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + require.Equal(t, l1, l2) + + _, err = s2.Listen(s2.Address()) + require.NoError(t, err) + + c1, err := s1.Connect(context.Background(), s2.Address()) + require.NoError(t, err) + + c2, err := s1.Connect(context.Background(), s2.Address()) + require.NoError(t, err) + + require.Equal(t, c1, c2) + require.NoError(t, c1.Close()) + + err = s1.RemoveConnection(s2.Address()) + require.NoError(t, err) + }) + + t.Run("should not try to close unknown connection", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + s2, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + l1, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + l2, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + require.Equal(t, l1, l2) + + _, err = s2.Listen(s2.Address()) + require.NoError(t, err) + + _, err = s1.Connect(context.Background(), s2.Address()) + require.NoError(t, err) + + err = s1.RemoveConnection(s2.Address()) + require.NoError(t, err) + + err = s1.RemoveConnection(s2.Address()) + require.NoError(t, err) + }) + + t.Run("should shutdown without errors", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + s2, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + l1, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + l2, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + require.Equal(t, l1, l2) + + _, err = s2.Listen(s2.Address()) + require.NoError(t, err) + + _, err = s1.Connect(context.Background(), s2.Address()) + require.NoError(t, err) + + err = s1.Shutdown() + require.NoError(t, err) + + err = s2.Shutdown() + require.NoError(t, err) + }) + + t.Run("should fail, when shutdown with closed connections or listeners", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + s2, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + l1, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + l2, err := s1.Listen(s1.Address()) + require.NoError(t, err) + + require.Equal(t, l1, l2) + + lis, err := s2.Listen(s2.Address()) + require.NoError(t, err) + + con, err := s1.Connect(context.Background(), s2.Address()) + require.NoError(t, err) + + require.NoError(t, con.Close()) + + err = s1.Shutdown() + require.NoError(t, err) + + require.NoError(t, lis.Close()) + + err = s2.Shutdown() + require.Error(t, err) + }) + + t.Run("circuit breaker should start fail connection after N-fails", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + addr := newTestAddress(t) + for i := 0; i < defaultAttemptsCount*2; i++ { + _, err = s1.Connect(context.Background(), addr) + require.Error(t, err) + + if i+1 == defaultAttemptsCount { + _, err = s1.Listen(addr) + require.NoError(t, err) + } + } + }) + + t.Run("should return error on bad multi-address", func(t *testing.T) { + s1, err := New(Params{ + Address: newTestAddress(t), + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + _, err = s1.Listen(&fakeAddress{}) + require.Error(t, err) + }) + + t.Run("gRPC connection test", func(t *testing.T) { + var ( + err error + s1, s2 Interface + h = &service{} + g = grpc.NewServer() + a1, a2 = newTestAddress(t), newTestAddress(t) + tr = transport.New(5, time.Second) + _ = h + done = make(chan struct{}) + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + s1, err = New(Params{ + Address: a1, + Transport: tr, + }) + require.NoError(t, err) + + s2, err = New(Params{ + Address: a2, + Transport: tr, + }) + require.NoError(t, err) + + RegisterGreeterServer(g, h) // register service + + l, err := s1.Listen(a1) + require.NoError(t, err) + + defer l.Close() // nolint:golint + + wg := new(sync.WaitGroup) + wg.Add(1) + + go func() { + close(done) + + _ = g.Serve(manet.NetListener(l)) + + wg.Done() + }() + + <-done // wait for server is start listening connections: + + // Fail connection + con, err := s2.GRPCConnection(ctx, &fakeAddress{}, false) + require.Nil(t, con) + require.Error(t, err) + + con, err = s2.GRPCConnection(ctx, a1, false) + require.NoError(t, err) + + cli := NewGreeterClient(con) + resp, err := cli.Hello(ctx, &HelloRequest{ + Name: "Interface test", + }) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "Hello Interface test", resp.Message) + + g.GracefulStop() + + wg.Wait() + }) + + t.Run("test grpc connections", func(t *testing.T) { + var ( + ifaces = make([]Interface, 0, testCount) + addresses = make([]multiaddr.Multiaddr, 0, testCount) + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + for i := 0; i < testCount; i++ { + addresses = append(addresses, newTestAddress(t)) + + s, err := New(Params{ + Address: addresses[i], + Transport: transport.New(5, time.Second), + }) + require.NoError(t, err) + + lis, err := s.Listen(addresses[i]) + require.NoError(t, err) + + svc := &service{} + srv := grpc.NewServer() + + RegisterGreeterServer(srv, svc) + + ifaces = append(ifaces, s) + + go func() { + l := manet.NetListener(lis) + require.NoError(t, srv.Serve(l)) + }() + } + + const reqName = "test" + wg := new(sync.WaitGroup) + + for i := 0; i < testCount; i++ { + for j := 0; j < testCount; j++ { + wg.Add(1) + go func(i, j int) { + defer wg.Done() + + con, err := ifaces[i].GRPCConnection(ctx, addresses[j], false) + require.NoError(t, err) + + cli := NewGreeterClient(con) + + resp, err := cli.Hello(ctx, &HelloRequest{Name: reqName}) + require.NoError(t, err) + + require.Equal(t, "Hello "+reqName, resp.Message) + + require.NoError(t, con.Close()) + }(i, j) + + } + } + + wg.Wait() + }) +} diff --git a/lib/peers/peers_test.pb.go b/lib/peers/peers_test.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..aa6fe950de37e1ac55f2a9349099ed0ad610e111 GIT binary patch literal 15514 zcmeHOd2icB7XMrN6th}1pb}Zu;rLLK0NGqM&?Z6R6a@mWOOZpF8;Vp(%12#)_x-*1 zW++mkWV@RdO*cVHP&4!HW9Ge~x3|@^#Mf$~W1YIW_SN`O&C(=Kye*7vO_E8{RnOn3 z*Kdy0i|2<&-RAbT%945N>5+=U@%BvXG?V8^uCu($(9Pz|^**^1t$4oK45zas&6U$^ zcuAb=i(IwLORL!kruc~mUKv`oCSiU$A9uZEx;;rEH=bKtLng^xA5+ zo1~+6I?$<(J*`CaOy!Bn=CfIJOrqw*tez$q% zrs_mJ7K?O$aMSG6jfzodtowox1;r4t(LIa1L~sa9^5=`;_M zm^jjLHV5(i)Xnk0;)N#!fFMaQNbqhRjw7wkVHsjtcv9BPVNUDDI*cclny=i68^&-6 z!t&G%0}Uv%{L0dd!dSzZKI!OEO|=_m>JlCXfZ3%=SVe?ET*DHKwf0%3V<>Cplkaw$ zaH%XWY3wa|@>}u5!|X?RDu@piA&az2YMxC}7yiE@kg?$}bGS5k$};k0ec?_4*vv0y zT79RZD0!!4MV6;?FIQL1#%mWUUjBz^QjAhRTQ?rH#+L|}PQR1P@=gq($Mop%{II3| zlO^$}#Y4$7%=L7ZU$%a3Hh%i)r<2#ohxn7e)c*IFNZm*Y^{Z?3vw&^cmGd~&UNVWp zUlG~#@8dC%B}SH3W?1669_(uVg>gO{(0gg+b+Z}FV^29#b-yfr8{VGj+-X~2b*1i4 z;kV^QVDEYz_HgIyuLr!+xPZ@eLd{=huT0~^{$(i&Wd)(XdmLK zgs(F;R%n|jSdf}_ORL(O@tI^$VCyJf!yBey9APpbJvR+(V zoET?E5=l(lRCFhxTWq31t&%?}fQy)FH!?aPJFgAvS30MB299iCVk->Pv}`0}jcJ!wk|nHtS}o$LoGY^#^0PMSii9*g z6lsHB45`h@(s=1ozO69s2xG87Ks@kM!5tv-KFwXDDnikhB(xzRyNQR#yZ}#g# z*OK~diL;FG&&-@c*CxF$CUsWSb@$w9rM@9~mZ{t|dzN9Bvf-xmX*gSgxG{eU*j*B+ z(GdkU9CB*nOub*zI+CgafJLRL?gKHpX^GwYUK5mUr?N zcH2$xhWA0Exui4GKGdK_e z_6NXrm=~An?=sSU38^3&;KT~K`MJ-``8&)6Lmlul;HTST0y6C}Y6t~j84>aOJTee6 z@?_KpEZLY5c}&3P_lNx66GiPZ0V5Ox^jJ9_qZqIRLw-JF3+*u2!H}2`oAvkT-DTyt zLXuT&U;`%LvY11jcqkg=_kHGF5Dl5|kkO5K&6w8+)Q~0X^BN)3V-`J;U?>2LPV@UN zzwa?R;{wcM*#$%zaR*W3P%!XQFhDotMViHzMe_7TU91XGj?0AindC0(TFiSO8fR%l zQKB5N3fzRREp zdaToc$#^By`}{8cHsIMYuhjf54*F0C`z(RDnmDBxR+jEEfX~393UOc2b-?2SE8~KC z%ur$jz>9g*{DF^Hi z5+#8ETQK-O0V7c&^L@sSlWEO$pGmLIw3n!})_oR*I?gc>{|P-N?(x}<&*m1>qV>aU zzDK7G4aeo)&ki&FD%Dggizw8$qV|u+f~lu(4^@z)%B)f7m7Rs2rXCh4`6Z?@J#}N$ z*yyaK$vpOzmtRoBPPz0RB^i1>+dB5C<%42IyC}*h>fPICS;zLBQtA#;|Lru-XW3{Q zT~0soq<>l4(fsE^@7>q>97lhk?MijEBo5Vgfs4i?Hbma`9pZ1vq#YEh)h13yp%ay< zZnrCTcBAOcjGA@mBG{9K?EYMfCn=u+xV)QG^a+toFU<4Q{bi@I=LQH-;oGTM+_qLQ zmV!X0+PqgGdF6RjMVRgq6D=9IEp4bjN(-dsWS&!ZBi84RIcS+PRI_0kA70n<(3m|- zK51eE=Q8P;ca0LZ84jnS5X};x6KlX2IIFETMvN8~eUU}g&9KxHk)&YIzlRsY;f$-3 zPvh5GS7)OxLDqPcBnO)$I#qYt=rstW4>9`Uk)9%%XunF!b7ORpm7b2Z@8j%7Vd9=? z3d?h}Nw|@0y3qv=ZYyUdAU5DGoU?G3`|DS4^qf@xa!ZNHL&_GDTLsg)clas-jlV(^ zjSZzW)%j`Si|6vyKxw*k`nJdIiUOU?gq>KOnpbBwl4OWWh^#0dXb?Hhxp!jloREiL zgig#1zBum}OW0vgzC+H7(7_`TXnjfMxw`IfPF%@3zV;LqGEQl!X@ipI1KiWpkX4*D zKE!T%DGYPdO6K5LC1{(ANXN)%)ga{F{33ub2?hY&{&u<3ST+wLmdh`MU74lFi@u~; z4Vk-6Me}W`vn0YnNARG-MvNh6VgIZM6(nH0AtH{X`IVmGFwl58kI);ZV@A zrbsQu2caeNi<*p3i4?U)_bq75_#qX-BkWgIt4JkF?MBjQHbTM36y!HKIhld}H%kl^ zlap!O;AbNhOjkmH%yx|j{V&@y2w6%pz?5hngk4-&xsYU2iM-g;R<0Cm6AQ-lpXN_Z z2`qhfI52i+iiiw7Y4VMt4c*|y7;v!<*((oP1q0sLr6<^rBU*jD2&z)MzAlK&n1I@L zeUW?DiFK5er4olNNBXK6qDtQs}ZaY&PiL zHlmo5kx07cU211muH;I}3)PljnyxGZUR-C-E}6*3aD+>b+C?}7AZHOb(u{AbC#pwo z&?q4%*RDlqCqjsTduv>u|LEe@GJi`K{71M3W%3B*j+$e5cgHDFY7@$EYYR`-b$ARc zD9kxY8QTgB>d9l(fAnaN za{RhNtj})-}+ip!%A`mGghbL-x*v3CgY4zik#YY#Fy`GOz z6f)7$CLg}`UtG`_+?3)=em{cS(O!!Wybfbu^8%isXu^5mJj8ca!Z1b%E;|ha?i$KOR&<0F3S*&yE{yv_yFQXnjXga83h)0k-67-lR7!T!uMuX+}O%exGJf_U?vP}kHMtOCh#_AF0 z62tKr?W#|f0wS;xggWwH&!y(t9!4$3 z-xgoRIW4~B8dW|+8Zjt$IMAX)rIE~M-0+JNeC|lU=z5G52r>nmk6>+)<+fzPDAE%* zQn@=(-}p*IV7lM(l@;#=m=*$OJ>@+bUu0sxsRtfr+h}nhL-r8;J{nmQl2FDfhwXSQS*Z8i1tn=Qo z`{I*NE7oQwCLWf&xKX&V5Ff<MqUP@GVH3u~1h$J{@ zFn36Rp9jqy^}c*Ofel<-c*j0!z~wo7v_};@Gd_}xF3cZGpV?XmyN`@RS#WXG%cXcU zZeg-uZ7c%DOiPWod>XU|q_6w#|Hr;7_Wt8HUQ6do*m=!~{_Eyje}eqF`Bt~2+B|D^ zGW0yn-zR+Ce`$NSo6pUss=96b@4Elej$S=AnO>8v*_ZDQHdVboZs|_xuv=RJ|1s4r zzIoYDH^@L+mZ z*W<(SZJP#^Jy*L1HZC?R(v(raAP3x1@1TTWjV>)P9pa^1K&&iIom>6mYG0tPqAHiW zTn*qnx^ERRU`23ekV_>hf+*;}6xGx@TBSUVL@d;%)WgDG#?*S+t@lU14?;<~TAo-p z*jRUp55NGzmbJo=t-x7;&CDj-kphnXyoj;`8Z%3El~61-onrq@@QPY4MPedTawszn zzE5J@4+Xb%haIddSB<9J0D+noh&>J#QT2_#bnGIEg~Jzh+yd#Qoy5Q{%WUf-;@JF+ z5($R?7HI2&2*nmTH~vm`Em8Z|GPv?D{s6RSb;am8 s.idle { + if err := s.removeGRPCConnection(addr); err != nil { + s.log.Error("could not close connection", + zap.String("address", addr), + zap.String("target", item.conn.Target()), + zap.Stringer("idle", time.Since(item.used)), + zap.Error(err)) + continue + } + + count++ + } else { + s.log.Debug("ignore connection", + zap.String("address", addr), + zap.Stringer("idle", time.Since(item.used))) + } + } + s.grpc.globalMutex.Unlock() + + s.log.Debug("cleanup connections done", + zap.Int("closed", count)) + + tick.Reset(s.tick) + } + } + + tick.Stop() +} diff --git a/lib/placement/graph.go b/lib/placement/graph.go new file mode 100644 index 0000000000..02efc3de8c --- /dev/null +++ b/lib/placement/graph.go @@ -0,0 +1,178 @@ +package placement + +import ( + "github.com/gogo/protobuf/proto" + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/pkg/errors" +) + +// method returns copy of current Graph. +func (g *graph) copy() *graph { + var ( + place *netmap.PlacementRule + roots = make([]*netmap.Bucket, 0, len(g.roots)) + items = make([]bootstrap.NodeInfo, len(g.items)) + ) + + copy(items, g.items) + + for _, root := range g.roots { + var r *netmap.Bucket + + if root != nil { + tmp := root.Copy() + r = &tmp + } + + roots = append(roots, r) + } + + place = proto.Clone(g.place).(*netmap.PlacementRule) + + return &graph{ + roots: roots, + items: items, + place: place, + } +} + +func (g *graph) Exclude(list []multiaddr.Multiaddr) Graph { + if len(list) == 0 { + return g + } + + var ( + sub = g.copy() + ignore = make([]uint32, 0, len(list)) + ) + + for i := range list { + for j := range sub.items { + if list[i].String() == sub.items[j].Address { + ignore = append(ignore, uint32(j)) + } + } + } + + return sub.Filter(func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { + group.Exclude = ignore + return bucket.GetMaxSelection(group) + }) +} + +// Filter container by rules. +func (g *graph) Filter(rule FilterRule) Graph { + if rule == nil { + return g + } + + var ( + sub = g.copy() + roots = make([]*netmap.Bucket, len(g.roots)) + items = make([]bootstrap.NodeInfo, len(g.items)) + ) + + for i := range g.place.SFGroups { + if g.roots[i] == nil { + continue + } + + root := g.roots[i].Copy() + roots[i] = rule(g.place.SFGroups[i], &root) + } + + copy(items, g.items) + + return &graph{ + roots: roots, + items: items, + place: sub.place, + } +} + +// NodeList returns slice of MultiAddresses for current graph. +func (g *graph) NodeList() ([]multiaddr.Multiaddr, error) { + var ( + ln = uint32(len(g.items)) + result = make([]multiaddr.Multiaddr, 0, ln) + items = make([]bootstrap.NodeInfo, len(g.items)) + ) + + if ln == 0 { + return nil, ErrEmptyNodes + } + + copy(items, g.items) + + for _, root := range g.roots { + if root == nil { + continue + } + + list := root.Nodelist() + if len(list) == 0 { + continue + } + + for _, idx := range list { + if ln <= idx.N { + return nil, errors.Errorf("could not find index(%d) in list(size: %d)", ln, idx) + } + + addr, err := multiaddr.NewMultiaddr(items[idx.N].Address) + if err != nil { + return nil, errors.Wrapf(err, "could not convert multi address(%s)", g.items[idx.N].Address) + } + + result = append(result, addr) + } + } + + if len(result) == 0 { + return nil, ErrEmptyNodes + } + + return result, nil +} + +// NodeInfo returns slice of NodeInfo for current graph. +func (g *graph) NodeInfo() ([]bootstrap.NodeInfo, error) { + var ( + ln = uint32(len(g.items)) + result = make([]bootstrap.NodeInfo, 0, ln) + items = make([]bootstrap.NodeInfo, len(g.items)) + ) + + if ln == 0 { + return nil, ErrEmptyNodes + } + + copy(items, g.items) + + for _, root := range g.roots { + if root == nil { + continue + } + + list := root.Nodelist() + if len(list) == 0 { + continue + } + + for _, idx := range list { + if ln <= idx.N { + return nil, errors.Errorf("could not find index(%d) in list(size: %d)", ln, idx) + } + + result = append(result, items[idx.N]) + } + } + + if len(result) == 0 { + return nil, ErrEmptyNodes + } + + return result, nil +} diff --git a/lib/placement/interface.go b/lib/placement/interface.go new file mode 100644 index 0000000000..5c428f17cb --- /dev/null +++ b/lib/placement/interface.go @@ -0,0 +1,113 @@ +package placement + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-node/lib/container" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "go.uber.org/atomic" + "go.uber.org/zap" +) + +type ( + // Component is interface of placement service + Component interface { + // TODO leave for feature request + + NetworkState() *bootstrap.SpreadMap + Neighbours(seed, epoch uint64, full bool) []peers.ID + Update(epoch uint64, nm *netmap.NetMap) error + Query(ctx context.Context, opts ...QueryOption) (Graph, error) + } + + // QueryOptions for query request + QueryOptions struct { + CID refs.CID + Previous int + Excludes []multiaddr.Multiaddr + } + + // QueryOption settings closure + QueryOption func(*QueryOptions) + + // FilterRule bucket callback handler + FilterRule func(netmap.SFGroup, *netmap.Bucket) *netmap.Bucket + + // Graph is result of request to Placement-component + Graph interface { + Filter(rule FilterRule) Graph + Exclude(list []multiaddr.Multiaddr) Graph + NodeList() ([]multiaddr.Multiaddr, error) + NodeInfo() ([]bootstrap.NodeInfo, error) + } + + // Key to fetch node-list + Key []byte + + // Params to create Placement component + Params struct { + Log *zap.Logger + Netmap *netmap.NetMap + Peerstore peers.Store + Fetcher container.Storage + ChronologyDuration uint64 // storing number of past epochs states + } + + networkState struct { + nm *netmap.NetMap + epoch uint64 + } + + // placement is implementation of placement.Component + placement struct { + log *zap.Logger + cnr container.Storage + + chronologyDur uint64 + nmStore *netMapStore + + ps peers.Store + + healthy *atomic.Bool + } + + // graph is implementation of placement.Graph + graph struct { + roots []*netmap.Bucket + items []bootstrap.NodeInfo + place *netmap.PlacementRule + } +) + +// Copy network state. +func (ns networkState) Copy() *networkState { + return &networkState{ + nm: ns.nm.Copy(), + epoch: ns.epoch, + } +} + +// ExcludeNodes to ignore some nodes. +func ExcludeNodes(list []multiaddr.Multiaddr) QueryOption { + return func(opt *QueryOptions) { + opt.Excludes = list + } +} + +// ContainerID set by Key. +func ContainerID(cid refs.CID) QueryOption { + return func(opt *QueryOptions) { + opt.CID = cid + } +} + +// UsePreviousNetmap for query. +func UsePreviousNetmap(diff int) QueryOption { + return func(opt *QueryOptions) { + opt.Previous = diff + } +} diff --git a/lib/placement/neighbours.go b/lib/placement/neighbours.go new file mode 100644 index 0000000000..65154ee101 --- /dev/null +++ b/lib/placement/neighbours.go @@ -0,0 +1,69 @@ +package placement + +import ( + "math" + + "github.com/nspcc-dev/hrw" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "go.uber.org/zap" +) + +func calculateCount(n int) int { + if n < 30 { + return n + } + + return int(1.4*math.Log(float64(n))+9) + 1 +} + +// Neighbours peers that which are distributed by hrw(seed) +// If full flag is set, all set of peers returns. +// Otherwise, result size depends on calculateCount function. +func (p *placement) Neighbours(seed, epoch uint64, full bool) []peers.ID { + nm := p.nmStore.get(epoch) + if nm == nil { + p.log.Error("could not receive network state", + zap.Uint64("epoch", epoch), + ) + + return nil + } + + rPeers := p.listPeers(nm.ItemsCopy(), !full) + + hrw.SortSliceByValue(rPeers, seed) + + if full { + return rPeers + } + + var ( + ln = len(rPeers) + cut = calculateCount(ln) + ) + + if cut > ln { + cut = ln + } + + return rPeers[:cut] +} + +func (p *placement) listPeers(nodes netmap.Nodes, exclSelf bool) []peers.ID { + var ( + id = p.ps.SelfID() + result = make([]peers.ID, 0, len(nodes)) + ) + + for i := range nodes { + key := peers.IDFromBinary(nodes[i].PubKey) + if exclSelf && id.Equal(key) { + continue + } + + result = append(result, key) + } + + return result +} diff --git a/lib/placement/neighbours_test.go b/lib/placement/neighbours_test.go new file mode 100644 index 0000000000..8f9e43ac96 --- /dev/null +++ b/lib/placement/neighbours_test.go @@ -0,0 +1,177 @@ +package placement + +import ( + "crypto/ecdsa" + "strconv" + "testing" + + "bou.ke/monkey" + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/stretchr/testify/require" +) + +func testAddress(t *testing.T) multiaddr.Multiaddr { + addr, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/0") + require.NoError(t, err) + return addr +} + +// -- -- // + +func testPeerstore(t *testing.T) peers.Store { + p, err := peers.NewStore(peers.StoreParams{ + Key: test.DecodeKey(-1), + Logger: test.NewTestLogger(false), + Addr: testAddress(t), + }) + require.NoError(t, err) + + return p +} + +const address = "/ip4/0.0.0.0/tcp/0/p2p/" + +func TestPlacement_Neighbours(t *testing.T) { + t.Run("Placement component NPE fix test", func(t *testing.T) { + nodes := []bootstrap.NodeInfo{ + {Address: address + idFromString(t, "USA1"), Options: []string{"/Location:Europe/Country:USA/City:NewYork"}}, + {Address: address + idFromString(t, "ITL1"), Options: []string{"/Location:Europe/Country:Italy/City:Rome"}}, + {Address: address + idFromString(t, "RUS1"), Options: []string{"/Location:Europe/Country:Russia/City:SPB"}}, + } + + ps := testPeerstore(t) + nm := testNetmap(t, nodes) + + p := New(Params{ + Log: test.NewTestLogger(false), + Peerstore: ps, + }) + + require.NotPanics(t, func() { + require.NoError(t, p.Update(1, nm)) + }) + }) + + t.Run("Placement Neighbours TestSuite", func(t *testing.T) { + keys := []*ecdsa.PrivateKey{ + test.DecodeKey(0), + test.DecodeKey(1), + test.DecodeKey(2), + } + nodes := []bootstrap.NodeInfo{ + { + Address: address + idFromString(t, "USA1"), + PubKey: crypto.MarshalPublicKey(&keys[0].PublicKey), + Options: []string{"/Location:Europe/Country:USA/City:NewYork"}, + }, + { + Address: address + idFromString(t, "ITL1"), + PubKey: crypto.MarshalPublicKey(&keys[1].PublicKey), + Options: []string{"/Location:Europe/Country:Italy/City:Rome"}, + }, + { + Address: address + idFromString(t, "RUS1"), + PubKey: crypto.MarshalPublicKey(&keys[2].PublicKey), + Options: []string{"/Location:Europe/Country:Russia/City:SPB"}, + }, + } + + ps := testPeerstore(t) + nm := testNetmap(t, nodes) + + p := New(Params{ + Log: test.NewTestLogger(false), + Netmap: nm, + Peerstore: ps, + }) + + t.Run("check, that items have expected length (< 30)", func(t *testing.T) { + items := p.Neighbours(1, 0, false) + require.Len(t, items, len(nm.ItemsCopy())) + }) + + t.Run("check, that items have expected length ( > 30)", func(t *testing.T) { + opts := []string{"/Location:Europe/Country:Russia/City:SPB"} + + key, err := ps.GetPublicKey(ps.SelfID()) + require.NoError(t, err) + + keyBytes := crypto.MarshalPublicKey(key) + + addr := address + idFromString(t, "NewRUS") + err = nm.Add(addr, keyBytes, 0, opts...) + require.NoError(t, err) + + for i := 0; i < 30; i++ { + addr := address + idFromString(t, "RUS"+strconv.Itoa(i+2)) + key := test.DecodeKey(i + len(nodes)) + pub := crypto.MarshalPublicKey(&key.PublicKey) + err := nm.Add(addr, pub, 0, opts...) + require.NoError(t, err) + } + + ln := calculateCount(len(nm.ItemsCopy())) + items := p.Neighbours(1, 0, false) + require.Len(t, items, ln) + }) + + t.Run("check, that items is shuffled", func(t *testing.T) { + var cur, pre []peers.ID + for i := uint64(0); i < 10; i++ { + cur = p.Neighbours(i, 0, false) + require.NotEqual(t, pre, cur) + + pre = cur + } + }) + + t.Run("check, that we can request more items that we have", func(t *testing.T) { + require.NotPanics(t, func() { + monkey.Patch(calculateCount, func(i int) int { return i + 1 }) + defer monkey.Unpatch(calculateCount) + + p.Neighbours(1, 0, false) + }) + }) + }) + + t.Run("unknown epoch", func(t *testing.T) { + s := &placement{ + log: test.NewTestLogger(false), + nmStore: newNetMapStore(), + ps: testPeerstore(t), + } + + require.Empty(t, s.Neighbours(1, 1, false)) + }) + + t.Run("neighbors w/ set full flag", func(t *testing.T) { + var ( + n = 3 + e uint64 = 5 + nm = netmap.NewNetmap() + nms = newNetMapStore() + ) + + for i := 0; i < n; i++ { + require.NoError(t, nm.Add("node"+strconv.Itoa(i), []byte{1}, 1)) + } + + nms.put(e, nm) + + s := &placement{ + log: test.NewTestLogger(false), + nmStore: nms, + ps: testPeerstore(t), + } + + neighbors := s.Neighbours(1, e, true) + + require.Len(t, neighbors, n) + }) +} diff --git a/lib/placement/placement.go b/lib/placement/placement.go new file mode 100644 index 0000000000..dd7e8dad99 --- /dev/null +++ b/lib/placement/placement.go @@ -0,0 +1,257 @@ +package placement + +import ( + "bytes" + "context" + "strings" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/refs" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/container" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/pkg/errors" + "go.uber.org/atomic" + "go.uber.org/zap" +) + +const defaultChronologyDuration = 1 + +var ( + // ErrEmptyNodes when container doesn't contains any nodes + ErrEmptyNodes = internal.Error("container doesn't contains nodes") + + // ErrNodesBucketOmitted when in PlacementRule, Selector has not NodesBucket + ErrNodesBucketOmitted = internal.Error("nodes-bucket is omitted") + + // ErrEmptyContainer when GetMaxSelection or GetSelection returns empty result + ErrEmptyContainer = internal.Error("could not get container, it's empty") +) + +var errNilNetMap = errors.New("network map is nil") + +// New is a placement component constructor. +func New(p Params) Component { + if p.Netmap == nil { + p.Netmap = netmap.NewNetmap() + } + + if p.ChronologyDuration <= 0 { + p.ChronologyDuration = defaultChronologyDuration + } + + pl := &placement{ + log: p.Log, + cnr: p.Fetcher, + + chronologyDur: p.ChronologyDuration, + nmStore: newNetMapStore(), + + ps: p.Peerstore, + + healthy: atomic.NewBool(false), + } + + pl.nmStore.put(0, p.Netmap) + + return pl +} + +func (p *placement) Name() string { return "PresentInNetwork" } +func (p *placement) Healthy() bool { return p.healthy.Load() } + +type strNodes []bootstrap.NodeInfo + +func (n strNodes) String() string { + list := make([]string, 0, len(n)) + for i := range n { + list = append(list, n[i].Address) + } + + return `[` + strings.Join(list, ",") + `]` +} + +func (p *placement) Update(epoch uint64, nm *netmap.NetMap) error { + cnm := p.nmStore.get(p.nmStore.epoch()) + if cnm == nil { + return errNilNetMap + } + + cp := cnm.Copy() + cp.Update(nm) + + items := nm.ItemsCopy() + + p.log.Debug("update to new netmap", + zap.Stringer("nodes", strNodes(items))) + + p.log.Debug("update peerstore") + + if err := p.ps.Update(cp); err != nil { + return err + } + + var ( + pubkeyBinary []byte + healthy bool + ) + + // storage nodes must be presented in network map to be healthy + pubkey, err := p.ps.GetPublicKey(p.ps.SelfID()) + if err != nil { + p.log.Error("can't get my own public key") + } + + pubkeyBinary = crypto.MarshalPublicKey(pubkey) + + for i := range items { + if bytes.Equal(pubkeyBinary, items[i].GetPubKey()) { + healthy = true + } + + p.log.Debug("new peer for dht", + zap.Stringer("peer", peers.IDFromBinary(items[i].GetPubKey())), + zap.String("addr", items[i].GetAddress())) + } + + // make copy to previous + p.log.Debug("update previous netmap") + + if epoch > p.chronologyDur { + p.nmStore.trim(epoch - p.chronologyDur) + } + + p.log.Debug("update current netmap") + p.nmStore.put(epoch, cp) + + p.log.Debug("update current epoch") + + p.healthy.Store(healthy) + + return nil +} + +// NetworkState returns copy of current NetworkMap. +func (p *placement) NetworkState() *bootstrap.SpreadMap { + ns := p.networkState(p.nmStore.epoch()) + if ns == nil { + ns = &networkState{nm: netmap.NewNetmap()} + } + + return &bootstrap.SpreadMap{ + Epoch: ns.epoch, + NetMap: ns.nm.Items(), + } +} + +func (p *placement) networkState(epoch uint64) *networkState { + nm := p.nmStore.get(epoch) + if nm == nil { + return nil + } + + return &networkState{ + nm: nm.Copy(), + epoch: epoch, + } +} + +// Query returns graph based on container. +func (p *placement) Query(ctx context.Context, opts ...QueryOption) (Graph, error) { + var ( + items []bootstrap.NodeInfo + query QueryOptions + ignore []uint32 + ) + + for _, opt := range opts { + opt(&query) + } + + epoch := p.nmStore.epoch() + if query.Previous > 0 { + epoch -= uint64(query.Previous) + } + + state := p.networkState(epoch) + if state == nil { + return nil, errors.Errorf("could not get network state for epoch #%d", epoch) + } + + items = state.nm.Items() + + gp := container.GetParams{} + gp.SetContext(ctx) + gp.SetCID(query.CID) + + getRes, err := p.cnr.GetContainer(gp) + if err != nil { + return nil, errors.Wrap(err, "could not fetch container") + } + + for i := range query.Excludes { + for j := range items { + if query.Excludes[i].String() == items[j].Address { + ignore = append(ignore, uint32(j)) + } + } + } + + rule := getRes.Container().GetRules() + + return ContainerGraph(state.nm, &rule, ignore, query.CID) +} + +// ContainerGraph applies the placement rules to network map and returns container graph. +func ContainerGraph(nm *netmap.NetMap, rule *netmap.PlacementRule, ignore []uint32, cid refs.CID) (Graph, error) { + root := nm.Root() + roots := make([]*netmap.Bucket, 0, len(rule.SFGroups)) + + for i := range rule.SFGroups { + rule.SFGroups[i].Exclude = ignore + if ln := len(rule.SFGroups[i].Selectors); ln <= 0 || + rule.SFGroups[i].Selectors[ln-1].Key != netmap.NodesBucket { + return nil, errors.Wrapf(ErrNodesBucketOmitted, "container (%s)", cid) + } + + bigSelectors := make([]netmap.Select, len(rule.SFGroups[i].Selectors)) + for j := range rule.SFGroups[i].Selectors { + bigSelectors[j] = netmap.Select{ + Key: rule.SFGroups[i].Selectors[j].Key, + Count: rule.SFGroups[i].Selectors[j].Count, + } + + if rule.ReplFactor > 1 && rule.SFGroups[i].Selectors[j].Key == netmap.NodesBucket { + bigSelectors[j].Count *= rule.ReplFactor + } + } + + sf := netmap.SFGroup{ + Selectors: bigSelectors, + Filters: rule.SFGroups[i].Filters, + Exclude: ignore, + } + + if tree := root.Copy().GetMaxSelection(sf); tree != nil { + // fetch graph for replication factor seeded by ContainerID + if tree = tree.GetSelection(bigSelectors, cid[:]); tree == nil { + return nil, errors.Wrapf(ErrEmptyContainer, "for container(%s) with repl-factor(%d)", + cid, rule.ReplFactor) + } + + roots = append(roots, tree) + + continue + } + + return nil, errors.Wrap(ErrEmptyContainer, "empty for bigSelector") + } + + return &graph{ + roots: roots, + items: nm.ItemsCopy(), + place: rule, + }, nil +} diff --git a/lib/placement/placement_test.go b/lib/placement/placement_test.go new file mode 100644 index 0000000000..53ac8127ab --- /dev/null +++ b/lib/placement/placement_test.go @@ -0,0 +1,407 @@ +package placement + +import ( + "context" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/mr-tron/base58" + "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-multihash" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/refs" + libcnr "github.com/nspcc-dev/neofs-node/lib/container" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type ( + fakeDHT struct { + } + + fakeContainerStorage struct { + libcnr.Storage + *sync.RWMutex + items map[refs.CID]*container.Container + } +) + +var ( + testDHTCapacity = 100 +) + +// -- -- // + +func testContainerStorage() *fakeContainerStorage { + return &fakeContainerStorage{ + RWMutex: new(sync.RWMutex), + items: make(map[refs.CID]*container.Container, testDHTCapacity), + } +} + +func (f *fakeContainerStorage) GetContainer(p libcnr.GetParams) (*libcnr.GetResult, error) { + f.RLock() + val, ok := f.items[p.CID()] + f.RUnlock() + + if !ok { + return nil, errors.New("value for requested key not found in DHT") + } + + res := new(libcnr.GetResult) + res.SetContainer(val) + + return res, nil +} + +func (f *fakeContainerStorage) Put(c *container.Container) error { + id, err := c.ID() + if err != nil { + return err + } + f.Lock() + f.items[id] = c + f.Unlock() + + return nil +} + +func (f *fakeDHT) UpdatePeers([]peers.ID) { + // do nothing +} + +func (f *fakeDHT) GetValue(ctx context.Context, key string) ([]byte, error) { + panic("implement me") +} + +func (f *fakeDHT) PutValue(ctx context.Context, key string, val []byte) error { + panic("implement me") +} + +func (f *fakeDHT) Get(ctx context.Context, key string) ([]byte, error) { + panic("implement me") +} + +func (f *fakeDHT) Put(ctx context.Context, key string, val []byte) error { + panic("implement me") +} + +// -- -- // + +func testNetmap(t *testing.T, nodes []bootstrap.NodeInfo) *netmap.NetMap { + nm := netmap.NewNetmap() + + for i := range nodes { + err := nm.Add(nodes[i].Address, nil, 0, nodes[i].Options...) + require.NoError(t, err) + } + + return nm +} + +// -- -- // + +func idFromString(t *testing.T, id string) string { + buf, err := multihash.Encode([]byte(id), multihash.ID) + require.NoError(t, err) + + return (multihash.Multihash(buf)).B58String() +} + +func idFromAddress(t *testing.T, addr multiaddr.Multiaddr) string { + id, err := addr.ValueForProtocol(multiaddr.P_P2P) + require.NoError(t, err) + + buf, err := base58.Decode(id) + require.NoError(t, err) + + hs, err := multihash.Decode(buf) + require.NoError(t, err) + + return string(hs.Digest) +} + +// -- -- // + +func TestPlacement(t *testing.T) { + multiaddr.SwapToP2pMultiaddrs() + testAddress := "/ip4/0.0.0.0/tcp/0/p2p/" + key := test.DecodeKey(-1) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + ids := map[string]struct{}{ + "GRM1": {}, "GRM2": {}, "GRM3": {}, "GRM4": {}, + "SPN1": {}, "SPN2": {}, "SPN3": {}, "SPN4": {}, + } + + nodes := []bootstrap.NodeInfo{ + {Address: testAddress + idFromString(t, "USA1"), Options: []string{"/Location:Europe/Country:USA/City:NewYork"}}, + {Address: testAddress + idFromString(t, "ITL1"), Options: []string{"/Location:Europe/Country:Italy/City:Rome"}}, + {Address: testAddress + idFromString(t, "RUS1"), Options: []string{"/Location:Europe/Country:Russia/City:SPB"}}, + } + + for id := range ids { + var opts []string + switch { + case strings.Contains(id, "GRM"): + opts = append(opts, "/Location:Europe/Country:Germany/City:"+id) + case strings.Contains(id, "SPN"): + opts = append(opts, "/Location:Europe/Country:Spain/City:"+id) + } + + for i := 0; i < 4; i++ { + id := id + strconv.Itoa(i) + + nodes = append(nodes, bootstrap.NodeInfo{ + Address: testAddress + idFromString(t, id), + Options: opts, + }) + } + } + + sort.Slice(nodes, func(i, j int) bool { + return strings.Compare(nodes[i].Address, nodes[j].Address) == -1 + }) + + nm := testNetmap(t, nodes) + + cnrStorage := testContainerStorage() + + p := New(Params{ + Log: test.NewTestLogger(false), + Netmap: netmap.NewNetmap(), + Peerstore: testPeerstore(t), + Fetcher: cnrStorage, + }) + + require.NoError(t, p.Update(1, nm)) + + oid, err := refs.NewObjectID() + require.NoError(t, err) + + // filter over oid + filter := func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { + return bucket.GetSelection(group.Selectors, oid[:]) + } + + owner, err := refs.NewOwnerID(&key.PublicKey) + require.NoError(t, err) + res1, err := container.New(100, owner, 0, netmap.PlacementRule{ + ReplFactor: 2, + SFGroups: []netmap.SFGroup{ + { + Selectors: []netmap.Select{ + {Key: "Country", Count: 1}, + {Key: "City", Count: 2}, + {Key: netmap.NodesBucket, Count: 1}, + }, + Filters: []netmap.Filter{ + {Key: "Country", F: netmap.FilterIn("Germany", "Spain")}, + }, + }, + }, + }) + require.NoError(t, err) + + err = cnrStorage.Put(res1) + require.NoError(t, err) + + res2, err := container.New(100, owner, 0, netmap.PlacementRule{ + ReplFactor: 2, + SFGroups: []netmap.SFGroup{ + { + Selectors: []netmap.Select{ + {Key: "Country", Count: 1}, + {Key: netmap.NodesBucket, Count: 10}, + }, + Filters: []netmap.Filter{ + {Key: "Country", F: netmap.FilterIn("Germany", "Spain")}, + }, + }, + }, + }) + require.NoError(t, err) + + err = cnrStorage.Put(res2) + require.NoError(t, err) + + res3, err := container.New(100, owner, 0, netmap.PlacementRule{ + ReplFactor: 2, + SFGroups: []netmap.SFGroup{ + { + Selectors: []netmap.Select{ + {Key: "Country", Count: 1}, + }, + Filters: []netmap.Filter{ + {Key: "Country", F: netmap.FilterIn("Germany", "Spain")}, + }, + }, + }, + }) + require.NoError(t, err) + + err = cnrStorage.Put(res3) + require.NoError(t, err) + + t.Run("Should fail on empty container", func(t *testing.T) { + id, err := res2.ID() + require.NoError(t, err) + _, err = p.Query(ctx, ContainerID(id)) + require.EqualError(t, errors.Cause(err), ErrEmptyContainer.Error()) + }) + + t.Run("Should fail on Nodes Bucket is omitted in container", func(t *testing.T) { + id, err := res3.ID() + require.NoError(t, err) + _, err = p.Query(ctx, ContainerID(id)) + require.EqualError(t, errors.Cause(err), ErrNodesBucketOmitted.Error()) + }) + + t.Run("Should fail on unknown container (dht error)", func(t *testing.T) { + _, err = p.Query(ctx, ContainerID(refs.CID{5})) + require.Error(t, err) + }) + + id1, err := res1.ID() + require.NoError(t, err) + + g, err := p.Query(ctx, ContainerID(id1)) + require.NoError(t, err) + + t.Run("Should return error on empty items", func(t *testing.T) { + _, err = g.Filter(func(netmap.SFGroup, *netmap.Bucket) *netmap.Bucket { + return &netmap.Bucket{} + }).NodeList() + require.EqualError(t, err, ErrEmptyNodes.Error()) + }) + + t.Run("Should ignore some nodes", func(t *testing.T) { + g1, err := p.Query(ctx, ContainerID(id1)) + require.NoError(t, err) + + expect, err := g1. + Filter(filter). + NodeList() + require.NoError(t, err) + + g2, err := p.Query(ctx, ContainerID(id1)) + require.NoError(t, err) + + actual, err := g2. + Filter(filter). + NodeList() + require.NoError(t, err) + + require.Equal(t, expect, actual) + + g3, err := p.Query(ctx, ContainerID(id1)) + require.NoError(t, err) + + actual, err = g3. + Exclude(expect). + Filter(filter). + NodeList() + require.NoError(t, err) + + for _, item := range expect { + require.NotContains(t, actual, item) + } + + g4, err := p.Query(ctx, + ContainerID(id1), + ExcludeNodes(expect)) + require.NoError(t, err) + + actual, err = g4. + Filter(filter). + NodeList() + require.NoError(t, err) + + for _, item := range expect { + require.NotContains(t, actual, item) + } + }) + + t.Run("Should return error on nil Buckets", func(t *testing.T) { + _, err = g.Filter(func(netmap.SFGroup, *netmap.Bucket) *netmap.Bucket { + return nil + }).NodeList() + require.EqualError(t, err, ErrEmptyNodes.Error()) + }) + + t.Run("Should return error on empty NodeInfo's", func(t *testing.T) { + cp := g.Filter(func(netmap.SFGroup, *netmap.Bucket) *netmap.Bucket { + return nil + }) + + cp.(*graph).items = nil + + _, err := cp.NodeList() + require.EqualError(t, err, ErrEmptyNodes.Error()) + }) + + t.Run("Should return error on unknown items", func(t *testing.T) { + cp := g.Filter(func(_ netmap.SFGroup, b *netmap.Bucket) *netmap.Bucket { + return b + }) + + cp.(*graph).items = cp.(*graph).items[:5] + + _, err := cp.NodeList() + require.Error(t, err) + }) + + t.Run("Should return error on bad items", func(t *testing.T) { + cp := g.Filter(func(_ netmap.SFGroup, b *netmap.Bucket) *netmap.Bucket { + return b + }) + + for i := range cp.(*graph).items { + cp.(*graph).items[i].Address = "BadAddress" + } + + _, err := cp.NodeList() + require.EqualError(t, errors.Cause(err), "failed to parse multiaddr \"BadAddress\": must begin with /") + }) + + list, err := g. + Filter(filter). + // must return same graph on empty filter + Filter(nil). + NodeList() + require.NoError(t, err) + + // 1 Country, 2 Cities, 1 Node = 2 Nodes + require.Len(t, list, 2) + for _, item := range list { + id := idFromAddress(t, item) + require.Contains(t, ids, id[:4]) // exclude our postfix (0-4) + } +} + +func TestContainerGraph(t *testing.T) { + t.Run("selectors index out-of-range", func(t *testing.T) { + rule := new(netmap.PlacementRule) + + rule.SFGroups = append(rule.SFGroups, netmap.SFGroup{}) + + require.NotPanics(t, func() { + _, _ = ContainerGraph( + netmap.NewNetmap(), + rule, + nil, + refs.CID{}, + ) + }) + }) +} diff --git a/lib/placement/store.go b/lib/placement/store.go new file mode 100644 index 0000000000..7d27bdf0a5 --- /dev/null +++ b/lib/placement/store.go @@ -0,0 +1,66 @@ +package placement + +import ( + "sync" + + "github.com/nspcc-dev/neofs-node/lib/netmap" +) + +type ( + // NetMap is a type alias of + // NetMap from netmap package. + NetMap = netmap.NetMap + + netMapStore struct { + *sync.RWMutex + items map[uint64]*NetMap + + curEpoch uint64 + } +) + +func newNetMapStore() *netMapStore { + return &netMapStore{ + RWMutex: new(sync.RWMutex), + items: make(map[uint64]*NetMap), + } +} + +func (s *netMapStore) put(epoch uint64, nm *NetMap) { + s.Lock() + s.items[epoch] = nm + s.curEpoch = epoch + s.Unlock() +} + +func (s *netMapStore) get(epoch uint64) *NetMap { + s.RLock() + nm := s.items[epoch] + s.RUnlock() + + return nm +} + +// trim cleans all network states elder than epoch. +func (s *netMapStore) trim(epoch uint64) { + s.Lock() + m := make(map[uint64]struct{}, len(s.items)) + + for e := range s.items { + if e < epoch { + m[e] = struct{}{} + } + } + + for e := range m { + delete(s.items, e) + } + s.Unlock() +} + +func (s *netMapStore) epoch() uint64 { + s.RLock() + defer s.RUnlock() + + return s.curEpoch +} diff --git a/lib/rand/rand.go b/lib/rand/rand.go new file mode 100644 index 0000000000..b42b58e42e --- /dev/null +++ b/lib/rand/rand.go @@ -0,0 +1,46 @@ +package rand + +import ( + crand "crypto/rand" + "encoding/binary" + mrand "math/rand" +) + +type cryptoSource struct{} + +// Read is alias for crypto/rand.Read. +var Read = crand.Read + +// New constructs the source of random numbers. +func New() *mrand.Rand { + return mrand.New(&cryptoSource{}) +} + +func (s *cryptoSource) Seed(int64) {} + +func (s *cryptoSource) Int63() int64 { + return int64(s.Uint63()) +} + +func (s *cryptoSource) Uint63() uint64 { + buf := make([]byte, 8) + if _, err := crand.Read(buf); err != nil { + return 0 + } + + return binary.BigEndian.Uint64(buf) +} + +// Uint64 returns a random uint64 value. +func Uint64(r *mrand.Rand, max int64) uint64 { + if max <= 0 { + return 0 + } + + var i int64 = -1 + for i < 0 { + i = r.Int63n(max) + } + + return uint64(i) +} diff --git a/lib/replication/common.go b/lib/replication/common.go new file mode 100644 index 0000000000..7ca8c0a7a8 --- /dev/null +++ b/lib/replication/common.go @@ -0,0 +1,197 @@ +package replication + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // CID is a type alias of + // CID from refs package of neofs-api-go. + CID = refs.CID + + // Object is a type alias of + // Object from object package of neofs-api-go. + Object = object.Object + + // OwnerID is a type alias of + // OwnerID from object package of neofs-api-go. + OwnerID = object.OwnerID + + // Address is a type alias of + // Address from refs package of neofs-api-go. + Address = refs.Address + + // ObjectVerificationParams groups the parameters of stored object verification. + ObjectVerificationParams struct { + Address + Node multiaddr.Multiaddr + Handler func(valid bool, obj *Object) + LocalInvalid bool + } + + // ObjectVerifier is an interface of stored object verifier. + ObjectVerifier interface { + Verify(ctx context.Context, params *ObjectVerificationParams) bool + } + + // ObjectSource is an interface of the object storage with read access. + ObjectSource interface { + Get(ctx context.Context, addr Address) (*Object, error) + } + + // ObjectStoreParams groups the parameters for object storing. + ObjectStoreParams struct { + *Object + Nodes []ObjectLocation + Handler func(ObjectLocation, bool) + } + + // ObjectReceptacle is an interface of object storage with write access. + ObjectReceptacle interface { + Put(ctx context.Context, params ObjectStoreParams) error + } + + // ObjectCleaner Entity for removing object by address from somewhere + ObjectCleaner interface { + Del(Address) error + } + + // ContainerActualityChecker is an interface of entity + // for checking local node presence in container + // Return true if no errors && local node is in container + ContainerActualityChecker interface { + Actual(ctx context.Context, cid CID) bool + } + + // ObjectPool is a queue of objects selected for data audit. + // It is updated once in epoch. + ObjectPool interface { + Update([]Address) + Pop() (Address, error) + Undone() int + } + + // Scheduler returns slice of addresses for data audit. + // These addresses put into ObjectPool. + Scheduler interface { + SelectForReplication(limit int) ([]Address, error) + } + + // ReservationRatioReceiver is an interface of entity + // for getting reservation ratio value of object by address. + ReservationRatioReceiver interface { + ReservationRatio(ctx context.Context, objAddr Address) (int, error) + } + + // RemoteStorageSelector is an interface of entity + // for getting remote nodes from placement for object by address + // Result doesn't contain nodes from exclude list + RemoteStorageSelector interface { + SelectRemoteStorages(ctx context.Context, addr Address, excl ...multiaddr.Multiaddr) ([]ObjectLocation, error) + } + + // MultiSolver is an interface that encapsulates other different utilities. + MultiSolver interface { + AddressStore + RemoteStorageSelector + ReservationRatioReceiver + ContainerActualityChecker + EpochReceiver + WeightComparator + } + + // ObjectLocator is an itnerface of entity + // for building list current object remote nodes by address + ObjectLocator interface { + LocateObject(ctx context.Context, objAddr Address) ([]multiaddr.Multiaddr, error) + } + + // WeightComparator is an itnerface of entity + // for comparing weight by address of local node with passed node + // returns -1 if local node is weightier or on error + // returns 0 if weights are equal + // returns 1 if passed node is weightier + WeightComparator interface { + CompareWeight(ctx context.Context, addr Address, node multiaddr.Multiaddr) int + } + + // EpochReceiver is an interface of entity for getting current epoch number. + EpochReceiver interface { + Epoch() uint64 + } + + // ObjectLocation groups the information about object current remote location. + ObjectLocation struct { + Node multiaddr.Multiaddr + WeightGreater bool // true if Node field value has less index in placement vector than localhost + } + + // ObjectLocationRecord groups the information about all current locations. + ObjectLocationRecord struct { + Address + ReservationRatio int + Locations []ObjectLocation + } + + // ReplicateTask groups the information about object replication task. + // Task solver should not process nodes from exclude list, + // Task solver should perform up to Shortage replications. + ReplicateTask struct { + Address + Shortage int + ExcludeNodes []multiaddr.Multiaddr + } + + // ReplicateResult groups the information about object replication task result. + ReplicateResult struct { + *ReplicateTask + NewStorages []multiaddr.Multiaddr + } + + // PresenceChecker is an interface of object storage with presence check access. + PresenceChecker interface { + Has(address Address) (bool, error) + } + + // AddressStore is an interface of local peer's network address storage. + AddressStore interface { + SelfAddr() (multiaddr.Multiaddr, error) + } +) + +const ( + writeResultTimeout = "write result timeout" + + taskChanClosed = " process finish finish: task channel closed" + ctxDoneMsg = " process finish: context done" + + objectPoolPart = "object pool" + loggerPart = "logger" + objectVerifierPart = "object verifier" + objectReceptaclePart = "object receptacle" + remoteStorageSelectorPart = "remote storage elector" + objectSourcePart = "object source" + reservationRatioReceiverPart = "reservation ratio receiver" + objectLocatorPart = "object locator" + epochReceiverPart = "epoch receiver" + presenceCheckerPart = "object presence checker" + weightComparatorPart = "weight comparator" + addrStorePart = "address store" +) + +func instanceError(entity, part string) error { + return errors.Errorf("could not instantiate %s: empty %s", entity, part) +} + +func addressFields(addr Address) []zap.Field { + return []zap.Field{ + zap.Stringer("oid", addr.ObjectID), + zap.Stringer("cid", addr.CID), + } +} diff --git a/lib/replication/garbage.go b/lib/replication/garbage.go new file mode 100644 index 0000000000..e2f7d44b4f --- /dev/null +++ b/lib/replication/garbage.go @@ -0,0 +1,27 @@ +package replication + +import ( + "sync" +) + +type ( + garbageStore struct { + *sync.RWMutex + items []Address + } +) + +func (s *garbageStore) put(addr Address) { + s.Lock() + defer s.Unlock() + + for i := range s.items { + if s.items[i].Equal(&addr) { + return + } + } + + s.items = append(s.items, addr) +} + +func newGarbageStore() *garbageStore { return &garbageStore{RWMutex: new(sync.RWMutex)} } diff --git a/lib/replication/implementations.go b/lib/replication/implementations.go new file mode 100644 index 0000000000..708a8226c7 --- /dev/null +++ b/lib/replication/implementations.go @@ -0,0 +1,292 @@ +package replication + +import ( + "context" + "sync" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/nspcc-dev/neofs-node/lib/rand" + "github.com/pkg/errors" +) + +type ( + replicationScheduler struct { + cac ContainerActualityChecker + ls localstore.Iterator + } + + // SchedulerParams groups the parameters of scheduler constructor. + SchedulerParams struct { + ContainerActualityChecker + localstore.Iterator + } + + objectPool struct { + mu *sync.Mutex + tasks []Address + } + + multiSolver struct { + as AddressStore + pl placement.Component + } + + // MultiSolverParams groups the parameters of multi solver constructor. + MultiSolverParams struct { + AddressStore + Placement placement.Component + } +) + +const ( + errPoolExhausted = internal.Error("object pool is exhausted") + + objectPoolInstanceFailMsg = "could not create object pool" + errEmptyLister = internal.Error("empty local objects lister") + errEmptyContainerActual = internal.Error("empty container actuality checker") + + multiSolverInstanceFailMsg = "could not create multi solver" + errEmptyAddressStore = internal.Error("empty address store") + errEmptyPlacement = internal.Error("empty placement") + replicationSchedulerEntity = "replication scheduler" +) + +// NewObjectPool is an object pool constructor. +func NewObjectPool() ObjectPool { + return &objectPool{mu: new(sync.Mutex)} +} + +// NewReplicationScheduler is a replication scheduler constructor. +func NewReplicationScheduler(p SchedulerParams) (Scheduler, error) { + switch { + case p.ContainerActualityChecker == nil: + return nil, errors.Wrap(errEmptyContainerActual, objectPoolInstanceFailMsg) + case p.Iterator == nil: + return nil, errors.Wrap(errEmptyLister, objectPoolInstanceFailMsg) + } + + return &replicationScheduler{ + cac: p.ContainerActualityChecker, + ls: p.Iterator, + }, nil +} + +// NewMultiSolver is a multi solver constructor. +func NewMultiSolver(p MultiSolverParams) (MultiSolver, error) { + switch { + case p.Placement == nil: + return nil, errors.Wrap(errEmptyPlacement, multiSolverInstanceFailMsg) + case p.AddressStore == nil: + return nil, errors.Wrap(errEmptyAddressStore, multiSolverInstanceFailMsg) + } + + return &multiSolver{ + as: p.AddressStore, + pl: p.Placement, + }, nil +} + +func (s *objectPool) Update(pool []Address) { + s.mu.Lock() + defer s.mu.Unlock() + + s.tasks = pool +} + +func (s *objectPool) Undone() int { + s.mu.Lock() + defer s.mu.Unlock() + + return len(s.tasks) +} + +func (s *objectPool) Pop() (Address, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.tasks) == 0 { + return Address{}, errPoolExhausted + } + + head := s.tasks[0] + s.tasks = s.tasks[1:] + + return head, nil +} + +func (s *replicationScheduler) SelectForReplication(limit int) ([]Address, error) { + // Attention! This routine might be inefficient with big number of objects + // and containers. Consider using fast traversal and filtering algorithms + // with sieve of bloom filters. + migration := make([]Address, 0, limit) + replication := make([]Address, 0) + ctx := context.Background() + + if err := s.ls.Iterate(nil, func(meta *localstore.ObjectMeta) bool { + if s.cac.Actual(ctx, meta.Object.SystemHeader.CID) { + replication = append(replication, *meta.Object.Address()) + } else { + migration = append(migration, *meta.Object.Address()) + } + return len(migration) >= limit + }); err != nil { + return nil, err + } + + lnM := len(migration) + lnR := len(replication) + edge := 0 + + // I considered using rand.Perm() and appending elements in `for` cycle. + // But it seems, that shuffling is efficient even when `limit-lnM` + // is 1000 times smaller than `lnR`. But it can be discussed and changed + // later anyway. + if lnM < limit { + r := rand.New() + r.Shuffle(lnR, func(i, j int) { + replication[i], replication[j] = replication[j], replication[i] + }) + + edge = min(limit-lnM, lnR) + } + + return append(migration, replication[:edge]...), nil +} + +func (s *multiSolver) Epoch() uint64 { return s.pl.NetworkState().Epoch } + +func (s *multiSolver) SelfAddr() (multiaddr.Multiaddr, error) { return s.as.SelfAddr() } +func (s *multiSolver) ReservationRatio(ctx context.Context, addr Address) (int, error) { + graph, err := s.pl.Query(ctx, placement.ContainerID(addr.CID)) + if err != nil { + return 0, errors.Wrap(err, "reservation ratio computation failed on placement query") + } + + nodes, err := graph.Filter(func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { + return bucket.GetSelection(group.Selectors, addr.ObjectID.Bytes()) + }).NodeList() + if err != nil { + return 0, errors.Wrap(err, "reservation ratio computation failed on graph node list") + } + + return len(nodes), nil +} + +func (s *multiSolver) SelectRemoteStorages(ctx context.Context, addr Address, excl ...multiaddr.Multiaddr) ([]ObjectLocation, error) { + selfAddr, err := s.as.SelfAddr() + if err != nil { + return nil, errors.Wrap(err, "select remote storage nodes failed on get self address") + } + + nodes, err := s.selectNodes(ctx, addr, excl...) + if err != nil { + return nil, errors.Wrap(err, "select remote storage nodes failed on get node list") + } + + var ( + metSelf bool + selfIndex = -1 + res = make([]ObjectLocation, 0, len(nodes)) + ) + + for i := range nodes { + if nodes[i].Equal(selfAddr) { + metSelf = true + selfIndex = i + } + + res = append(res, ObjectLocation{ + Node: nodes[i], + WeightGreater: !metSelf, + }) + } + + if selfIndex != -1 { + res = append(res[:selfIndex], res[selfIndex+1:]...) + } + + return res, nil +} + +func (s *multiSolver) selectNodes(ctx context.Context, addr Address, excl ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + graph, err := s.pl.Query(ctx, placement.ContainerID(addr.CID)) + if err != nil { + return nil, errors.Wrap(err, "select remote storage nodes failed on placement query") + } + + filter := func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { return bucket } + if !addr.ObjectID.Empty() { + filter = func(group netmap.SFGroup, bucket *netmap.Bucket) *netmap.Bucket { + return bucket.GetSelection(group.Selectors, addr.ObjectID.Bytes()) + } + } + + return graph.Exclude(excl).Filter(filter).NodeList() +} + +func (s *multiSolver) Actual(ctx context.Context, cid CID) bool { + graph, err := s.pl.Query(ctx, placement.ContainerID(cid)) + if err != nil { + return false + } + + nodes, err := graph.NodeList() + if err != nil { + return false + } + + selfAddr, err := s.as.SelfAddr() + if err != nil { + return false + } + + for i := range nodes { + if nodes[i].Equal(selfAddr) { + return true + } + } + + return false +} + +func (s *multiSolver) CompareWeight(ctx context.Context, addr Address, node multiaddr.Multiaddr) int { + selfAddr, err := s.as.SelfAddr() + if err != nil { + return -1 + } + + if selfAddr.Equal(node) { + return 0 + } + + excl := make([]multiaddr.Multiaddr, 0) + + for { + nodes, err := s.selectNodes(ctx, addr, excl...) + if err != nil { + return -1 + } + + for j := range nodes { + if nodes[j].Equal(selfAddr) { + return -1 + } else if nodes[j].Equal(node) { + return 1 + } + } + + excl = append(excl, nodes[0]) // TODO: when it will become relevant to append full nodes slice + } +} + +func min(a, b int) int { + if a < b { + return a + } + + return b +} diff --git a/lib/replication/location_detector.go b/lib/replication/location_detector.go new file mode 100644 index 0000000000..d010e48f5a --- /dev/null +++ b/lib/replication/location_detector.go @@ -0,0 +1,154 @@ +package replication + +import ( + "context" + "time" + + "go.uber.org/zap" +) + +type ( + // ObjectLocationDetector is an interface of entity + // that listens tasks to detect object current locations in network. + ObjectLocationDetector interface { + Process(ctx context.Context) chan<- Address + Subscribe(ch chan<- *ObjectLocationRecord) + } + + objectLocationDetector struct { + weightComparator WeightComparator + objectLocator ObjectLocator + reservationRatioReceiver ReservationRatioReceiver + presenceChecker PresenceChecker + log *zap.Logger + + taskChanCap int + resultTimeout time.Duration + resultChan chan<- *ObjectLocationRecord + } + + // LocationDetectorParams groups the parameters of location detector's constructor. + LocationDetectorParams struct { + WeightComparator + ObjectLocator + ReservationRatioReceiver + PresenceChecker + *zap.Logger + + TaskChanCap int + ResultTimeout time.Duration + } +) + +const ( + defaultLocationDetectorChanCap = 10 + defaultLocationDetectorResultTimeout = time.Second + locationDetectorEntity = "object location detector" +) + +func (s *objectLocationDetector) Subscribe(ch chan<- *ObjectLocationRecord) { s.resultChan = ch } + +func (s *objectLocationDetector) Process(ctx context.Context) chan<- Address { + ch := make(chan Address, s.taskChanCap) + go s.processRoutine(ctx, ch) + + return ch +} + +func (s *objectLocationDetector) writeResult(locationRecord *ObjectLocationRecord) { + if s.resultChan == nil { + return + } + select { + case s.resultChan <- locationRecord: + case <-time.After(s.resultTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *objectLocationDetector) processRoutine(ctx context.Context, taskChan <-chan Address) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn(locationDetectorEntity+ctxDoneMsg, zap.Error(ctx.Err())) + break loop + case addr, ok := <-taskChan: + if !ok { + s.log.Warn(locationDetectorEntity + taskChanClosed) + break loop + } else if has, err := s.presenceChecker.Has(addr); err != nil || !has { + continue loop + } + s.handleTask(ctx, addr) + } + } + close(s.resultChan) +} + +func (s *objectLocationDetector) handleTask(ctx context.Context, addr Address) { + var ( + err error + log = s.log.With(addressFields(addr)...) + locationRecord = &ObjectLocationRecord{addr, 0, nil} + ) + + if locationRecord.ReservationRatio, err = s.reservationRatioReceiver.ReservationRatio(ctx, addr); err != nil { + log.Error("reservation ratio computation failure", zap.Error(err)) + return + } + + nodes, err := s.objectLocator.LocateObject(ctx, addr) + if err != nil { + log.Error("locate object failure", zap.Error(err)) + return + } + + for i := range nodes { + locationRecord.Locations = append(locationRecord.Locations, ObjectLocation{ + Node: nodes[i], + WeightGreater: s.weightComparator.CompareWeight(ctx, addr, nodes[i]) == 1, + }) + } + + log.Debug("current location record created", + zap.Int("reservation ratio", locationRecord.ReservationRatio), + zap.Any("storage nodes exclude self", locationRecord.Locations)) + + s.writeResult(locationRecord) +} + +// NewLocationDetector is an object location detector's constructor. +func NewLocationDetector(p *LocationDetectorParams) (ObjectLocationDetector, error) { + switch { + case p.PresenceChecker == nil: + return nil, instanceError(locationDetectorEntity, presenceCheckerPart) + case p.ObjectLocator == nil: + return nil, instanceError(locationDetectorEntity, objectLocatorPart) + case p.ReservationRatioReceiver == nil: + return nil, instanceError(locationDetectorEntity, reservationRatioReceiverPart) + case p.Logger == nil: + return nil, instanceError(locationDetectorEntity, loggerPart) + case p.WeightComparator == nil: + return nil, instanceError(locationDetectorEntity, weightComparatorPart) + } + + if p.TaskChanCap <= 0 { + p.TaskChanCap = defaultLocationDetectorChanCap + } + + if p.ResultTimeout <= 0 { + p.ResultTimeout = defaultLocationDetectorResultTimeout + } + + return &objectLocationDetector{ + weightComparator: p.WeightComparator, + objectLocator: p.ObjectLocator, + reservationRatioReceiver: p.ReservationRatioReceiver, + presenceChecker: p.PresenceChecker, + log: p.Logger, + taskChanCap: p.TaskChanCap, + resultTimeout: p.ResultTimeout, + resultChan: nil, + }, nil +} diff --git a/lib/replication/manager.go b/lib/replication/manager.go new file mode 100644 index 0000000000..57d7d17aee --- /dev/null +++ b/lib/replication/manager.go @@ -0,0 +1,347 @@ +package replication + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" +) + +type ( + // Manager is an interface of object manager, + Manager interface { + Process(ctx context.Context) + HandleEpoch(ctx context.Context, epoch uint64) + } + + manager struct { + objectPool ObjectPool + managerTimeout time.Duration + objectVerifier ObjectVerifier + log *zap.Logger + + locationDetector ObjectLocationDetector + storageValidator StorageValidator + replicator ObjectReplicator + restorer ObjectRestorer + placementHonorer PlacementHonorer + + // internal task channels + detectLocationTaskChan chan<- Address + restoreTaskChan chan<- Address + + pushTaskTimeout time.Duration + + // internal result channels + replicationResultChan <-chan *ReplicateResult + restoreResultChan <-chan Address + + garbageChanCap int + replicateResultChanCap int + restoreResultChanCap int + + garbageChan <-chan Address + garbageStore *garbageStore + + epochCh chan uint64 + scheduler Scheduler + + poolSize int + poolExpansionRate float64 + } + + // ManagerParams groups the parameters of object manager's constructor. + ManagerParams struct { + Interval time.Duration + PushTaskTimeout time.Duration + PlacementHonorerEnabled bool + ReplicateTaskChanCap int + RestoreTaskChanCap int + GarbageChanCap int + InitPoolSize int + ExpansionRate float64 + + ObjectPool + ObjectVerifier + + PlacementHonorer + ObjectLocationDetector + StorageValidator + ObjectReplicator + ObjectRestorer + + *zap.Logger + + Scheduler + } +) + +const ( + managerEntity = "replication manager" + + redundantCopiesBeagleName = "BEAGLE_REDUNDANT_COPIES" + + defaultInterval = 3 * time.Second + defaultPushTaskTimeout = time.Second + + defaultGarbageChanCap = 10 + defaultReplicateResultChanCap = 10 + defaultRestoreResultChanCap = 10 +) + +func (s *manager) Name() string { return redundantCopiesBeagleName } + +func (s *manager) HandleEpoch(ctx context.Context, epoch uint64) { + select { + case s.epochCh <- epoch: + case <-ctx.Done(): + return + case <-time.After(s.managerTimeout): + // this timeout must never happen + // if timeout happens in runtime, then something is definitely wrong! + s.log.Warn("replication scheduler is busy") + } +} + +func (s *manager) Process(ctx context.Context) { + // starting object restorer + // bind manager to push restore tasks to restorer + s.restoreTaskChan = s.restorer.Process(ctx) + + // bind manager to listen object restorer results + restoreResultChan := make(chan Address, s.restoreResultChanCap) + s.restoreResultChan = restoreResultChan + s.restorer.Subscribe(restoreResultChan) + + // starting location detector + // bind manager to push locate tasks to location detector + s.detectLocationTaskChan = s.locationDetector.Process(ctx) + + locationsHandlerStartFn := s.storageValidator.Process + if s.placementHonorer != nil { + locationsHandlerStartFn = s.placementHonorer.Process + + // starting storage validator + // bind placement honorer to push validate tasks to storage validator + s.placementHonorer.Subscribe(s.storageValidator.Process(ctx)) + } + + // starting location handler component + // bind location detector to push tasks to location handler component + s.locationDetector.Subscribe(locationsHandlerStartFn(ctx)) + + // bind manager to listen object replicator results + replicateResultChan := make(chan *ReplicateResult, s.replicateResultChanCap) + s.replicationResultChan = replicateResultChan + s.replicator.Subscribe(replicateResultChan) + + // starting replicator + // bind storage validator to push replicate tasks to replicator + s.storageValidator.SubscribeReplication(s.replicator.Process(ctx)) + garbageChan := make(chan Address, s.garbageChanCap) + s.garbageChan = garbageChan + s.storageValidator.SubscribeGarbage(garbageChan) + + go s.taskRoutine(ctx) + go s.resultRoutine(ctx) + s.processRoutine(ctx) +} + +func resultLog(s1, s2 string) string { + return fmt.Sprintf(managerEntity+" %s process finish: %s", s1, s2) +} + +func (s *manager) writeDetectLocationTask(addr Address) { + if s.detectLocationTaskChan == nil { + return + } + select { + case s.detectLocationTaskChan <- addr: + case <-time.After(s.pushTaskTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *manager) writeRestoreTask(addr Address) { + if s.restoreTaskChan == nil { + return + } + select { + case s.restoreTaskChan <- addr: + case <-time.After(s.pushTaskTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *manager) resultRoutine(ctx context.Context) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn(resultLog("result", ctxDoneMsg), zap.Error(ctx.Err())) + break loop + case addr, ok := <-s.restoreResultChan: + if !ok { + s.log.Warn(resultLog("result", "restorer result channel closed")) + break loop + } + s.log.Info("object successfully restored", addressFields(addr)...) + case res, ok := <-s.replicationResultChan: + if !ok { + s.log.Warn(resultLog("result", "replicator result channel closed")) + break loop + } else if len(res.NewStorages) > 0 { + s.log.Info("object successfully replicated", + append(addressFields(res.Address), zap.Any("new storages", res.NewStorages))...) + } + case addr, ok := <-s.garbageChan: + if !ok { + s.log.Warn(resultLog("result", "garbage channel closed")) + break loop + } + s.garbageStore.put(addr) + } + } +} + +func (s *manager) taskRoutine(ctx context.Context) { +loop: + for { + if task, err := s.objectPool.Pop(); err == nil { + select { + case <-ctx.Done(): + s.log.Warn(resultLog("task", ctxDoneMsg), zap.Error(ctx.Err())) + break loop + default: + s.distributeTask(ctx, task) + } + } else { + // if object pool is empty, check it again after a while + time.Sleep(s.managerTimeout) + } + } + close(s.restoreTaskChan) + close(s.detectLocationTaskChan) +} + +func (s *manager) processRoutine(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case epoch := <-s.epochCh: + var delta int + + // undone - amount of objects we couldn't process in last epoch + undone := s.objectPool.Undone() + if undone > 0 { + // if there are unprocessed objects, then lower your estimation + delta = -undone + } else { + // otherwise try to expand + delta = int(float64(s.poolSize) * s.poolExpansionRate) + } + + tasks, err := s.scheduler.SelectForReplication(s.poolSize + delta) + if err != nil { + s.log.Warn("can't select objects for replication", zap.Error(err)) + } + + // if there are NOT enough objects to fill the pool, do not change it + // otherwise expand or shrink it with the delta value + if len(tasks) >= s.poolSize+delta { + s.poolSize += delta + } + + s.objectPool.Update(tasks) + + s.log.Info("replication schedule updated", + zap.Int("unprocessed_tasks", undone), + zap.Int("next_tasks", len(tasks)), + zap.Int("pool_size", s.poolSize), + zap.Uint64("new_epoch", epoch)) + } + } +} + +// Function takes object from storage by address (if verify +// If verify flag is set object stored incorrectly (Verify returned error) - restore task is planned +// otherwise validate task is planned. +func (s *manager) distributeTask(ctx context.Context, addr Address) { + if !s.objectVerifier.Verify(ctx, &ObjectVerificationParams{Address: addr}) { + s.writeRestoreTask(addr) + return + } + + s.writeDetectLocationTask(addr) +} + +// NewManager is an object manager's constructor. +func NewManager(p ManagerParams) (Manager, error) { + switch { + case p.ObjectPool == nil: + return nil, instanceError(managerEntity, objectPoolPart) + case p.ObjectVerifier == nil: + return nil, instanceError(managerEntity, objectVerifierPart) + case p.Logger == nil: + return nil, instanceError(managerEntity, loggerPart) + case p.ObjectLocationDetector == nil: + return nil, instanceError(managerEntity, locationDetectorEntity) + case p.StorageValidator == nil: + return nil, instanceError(managerEntity, storageValidatorEntity) + case p.ObjectReplicator == nil: + return nil, instanceError(managerEntity, objectReplicatorEntity) + case p.ObjectRestorer == nil: + return nil, instanceError(managerEntity, objectRestorerEntity) + case p.PlacementHonorer == nil && p.PlacementHonorerEnabled: + return nil, instanceError(managerEntity, placementHonorerEntity) + case p.Scheduler == nil: + return nil, instanceError(managerEntity, replicationSchedulerEntity) + } + + if p.Interval <= 0 { + p.Interval = defaultInterval + } + + if p.PushTaskTimeout <= 0 { + p.PushTaskTimeout = defaultPushTaskTimeout + } + + if p.GarbageChanCap <= 0 { + p.GarbageChanCap = defaultGarbageChanCap + } + + if p.ReplicateTaskChanCap <= 0 { + p.ReplicateTaskChanCap = defaultReplicateResultChanCap + } + + if p.RestoreTaskChanCap <= 0 { + p.RestoreTaskChanCap = defaultRestoreResultChanCap + } + + if !p.PlacementHonorerEnabled { + p.PlacementHonorer = nil + } + + return &manager{ + objectPool: p.ObjectPool, + managerTimeout: p.Interval, + objectVerifier: p.ObjectVerifier, + log: p.Logger, + locationDetector: p.ObjectLocationDetector, + storageValidator: p.StorageValidator, + replicator: p.ObjectReplicator, + restorer: p.ObjectRestorer, + placementHonorer: p.PlacementHonorer, + pushTaskTimeout: p.PushTaskTimeout, + garbageChanCap: p.GarbageChanCap, + replicateResultChanCap: p.ReplicateTaskChanCap, + restoreResultChanCap: p.RestoreTaskChanCap, + garbageStore: newGarbageStore(), + epochCh: make(chan uint64), + scheduler: p.Scheduler, + poolSize: p.InitPoolSize, + poolExpansionRate: p.ExpansionRate, + }, nil +} diff --git a/lib/replication/object_replicator.go b/lib/replication/object_replicator.go new file mode 100644 index 0000000000..37167286aa --- /dev/null +++ b/lib/replication/object_replicator.go @@ -0,0 +1,188 @@ +package replication + +import ( + "context" + "time" + + "github.com/multiformats/go-multiaddr" + "go.uber.org/zap" +) + +type ( + // ObjectReplicator is an interface of entity + // that listens object replication tasks. + // Result includes new object storage list. + ObjectReplicator interface { + Process(ctx context.Context) chan<- *ReplicateTask + Subscribe(ch chan<- *ReplicateResult) + } + + objectReplicator struct { + objectReceptacle ObjectReceptacle + remoteStorageSelector RemoteStorageSelector + objectSource ObjectSource + presenceChecker PresenceChecker + log *zap.Logger + + taskChanCap int + resultTimeout time.Duration + resultChan chan<- *ReplicateResult + } + + // ObjectReplicatorParams groups the parameters of replicator's constructor. + ObjectReplicatorParams struct { + RemoteStorageSelector + ObjectSource + ObjectReceptacle + PresenceChecker + *zap.Logger + + TaskChanCap int + ResultTimeout time.Duration + } +) + +const ( + defaultReplicatorChanCap = 10 + defaultReplicatorResultTimeout = time.Second + objectReplicatorEntity = "object replicator" +) + +func (s *objectReplicator) Subscribe(ch chan<- *ReplicateResult) { s.resultChan = ch } + +func (s *objectReplicator) Process(ctx context.Context) chan<- *ReplicateTask { + ch := make(chan *ReplicateTask, s.taskChanCap) + go s.processRoutine(ctx, ch) + + return ch +} + +func (s *objectReplicator) writeResult(replicateResult *ReplicateResult) { + if s.resultChan == nil { + return + } + select { + case s.resultChan <- replicateResult: + case <-time.After(s.resultTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *objectReplicator) processRoutine(ctx context.Context, taskChan <-chan *ReplicateTask) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn(objectReplicatorEntity+" process finish: context completed", + zap.Error(ctx.Err())) + break loop + case replicateTask, ok := <-taskChan: + if !ok { + s.log.Warn(objectReplicatorEntity + " process finish: task channel closed") + break loop + } else if has, err := s.presenceChecker.Has(replicateTask.Address); err != nil || !has { + continue loop + } + s.handleTask(ctx, replicateTask) + } + } + close(s.resultChan) +} + +func (s *objectReplicator) handleTask(ctx context.Context, task *ReplicateTask) { + obj, err := s.objectSource.Get(ctx, task.Address) + if err != nil { + s.log.Warn("get object from storage failure", zap.Error(err)) + return + } + + res := &ReplicateResult{ + ReplicateTask: task, + NewStorages: make([]multiaddr.Multiaddr, 0, task.Shortage), + } + + for len(res.NewStorages) < task.Shortage { + nodesInfo, err := s.remoteStorageSelector.SelectRemoteStorages(ctx, task.Address, task.ExcludeNodes...) + if err != nil { + break + } + + for i := 0; i < len(nodesInfo); i++ { + if contains(res.NewStorages, nodesInfo[i].Node) { + nodesInfo = append(nodesInfo[:i], nodesInfo[i+1:]...) + i-- + + continue + } + } + + if len(nodesInfo) > task.Shortage { + nodesInfo = nodesInfo[:task.Shortage] + } + + if len(nodesInfo) == 0 { + break + } + + if err := s.objectReceptacle.Put(ctx, ObjectStoreParams{ + Object: obj, + Nodes: nodesInfo, + Handler: func(location ObjectLocation, success bool) { + if success { + res.NewStorages = append(res.NewStorages, location.Node) + } else { + task.ExcludeNodes = append(task.ExcludeNodes, location.Node) + } + }, + }); err != nil { + s.log.Warn("replicate object failure", zap.Error(err)) + break + } + } + + s.writeResult(res) +} + +func contains(list []multiaddr.Multiaddr, item multiaddr.Multiaddr) bool { + for i := range list { + if list[i].Equal(item) { + return true + } + } + + return false +} + +// NewReplicator is an object replicator's constructor. +func NewReplicator(p ObjectReplicatorParams) (ObjectReplicator, error) { + switch { + case p.ObjectReceptacle == nil: + return nil, instanceError(objectReplicatorEntity, objectReceptaclePart) + case p.ObjectSource == nil: + return nil, instanceError(objectReplicatorEntity, objectSourcePart) + case p.RemoteStorageSelector == nil: + return nil, instanceError(objectReplicatorEntity, remoteStorageSelectorPart) + case p.PresenceChecker == nil: + return nil, instanceError(objectReplicatorEntity, presenceCheckerPart) + case p.Logger == nil: + return nil, instanceError(objectReplicatorEntity, loggerPart) + } + + if p.TaskChanCap <= 0 { + p.TaskChanCap = defaultReplicatorChanCap + } + + if p.ResultTimeout <= 0 { + p.ResultTimeout = defaultReplicatorResultTimeout + } + + return &objectReplicator{ + objectReceptacle: p.ObjectReceptacle, + remoteStorageSelector: p.RemoteStorageSelector, + objectSource: p.ObjectSource, + presenceChecker: p.PresenceChecker, + log: p.Logger, + taskChanCap: p.TaskChanCap, + resultTimeout: p.ResultTimeout, + }, nil +} diff --git a/lib/replication/object_restorer.go b/lib/replication/object_restorer.go new file mode 100644 index 0000000000..00e70d87b6 --- /dev/null +++ b/lib/replication/object_restorer.go @@ -0,0 +1,173 @@ +package replication + +import ( + "context" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "go.uber.org/zap" +) + +type ( + // ObjectRestorer is an interface of entity + // that listen tasks to restore object by address. + // Restorer doesn't recheck if object is actually corrupted. + // Restorer writes result to subscriber only if restoration was successful. + ObjectRestorer interface { + Process(ctx context.Context) chan<- Address + Subscribe(ch chan<- Address) + } + + objectRestorer struct { + objectVerifier ObjectVerifier + remoteStorageSelector RemoteStorageSelector + objectReceptacle ObjectReceptacle + epochReceiver EpochReceiver + presenceChecker PresenceChecker + log *zap.Logger + + taskChanCap int + resultTimeout time.Duration + resultChan chan<- Address + } + + // ObjectRestorerParams groups the parameters of object restorer's constructor. + ObjectRestorerParams struct { + ObjectVerifier + ObjectReceptacle + EpochReceiver + RemoteStorageSelector + PresenceChecker + *zap.Logger + + TaskChanCap int + ResultTimeout time.Duration + } +) + +const ( + defaultRestorerChanCap = 10 + defaultRestorerResultTimeout = time.Second + objectRestorerEntity = "object restorer" +) + +func (s *objectRestorer) Subscribe(ch chan<- Address) { s.resultChan = ch } + +func (s *objectRestorer) Process(ctx context.Context) chan<- Address { + ch := make(chan Address, s.taskChanCap) + go s.processRoutine(ctx, ch) + + return ch +} + +func (s *objectRestorer) writeResult(refInfo Address) { + if s.resultChan == nil { + return + } + select { + case s.resultChan <- refInfo: + case <-time.After(s.resultTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *objectRestorer) processRoutine(ctx context.Context, taskChan <-chan Address) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn(objectRestorerEntity+ctxDoneMsg, zap.Error(ctx.Err())) + break loop + case addr, ok := <-taskChan: + if !ok { + s.log.Warn(objectRestorerEntity + taskChanClosed) + break loop + } else if has, err := s.presenceChecker.Has(addr); err != nil || !has { + continue loop + } + s.handleTask(ctx, addr) + } + } + close(s.resultChan) +} + +func (s *objectRestorer) handleTask(ctx context.Context, addr Address) { + var ( + receivedObj *Object + exclNodes = make([]multiaddr.Multiaddr, 0) + ) + +loop: + for { + nodesInfo, err := s.remoteStorageSelector.SelectRemoteStorages(ctx, addr, exclNodes...) + if err != nil { + break + } + + for i := range nodesInfo { + info := nodesInfo[i] + if s.objectVerifier.Verify(ctx, &ObjectVerificationParams{ + Address: addr, + Node: nodesInfo[i].Node, + Handler: func(valid bool, obj *Object) { + if valid { + receivedObj = obj + } else { + exclNodes = append(exclNodes, info.Node) + } + }, + LocalInvalid: true, + }) { + break loop + } + } + } + + if err := s.objectReceptacle.Put( + context.WithValue(ctx, localstore.StoreEpochValue, s.epochReceiver.Epoch()), + ObjectStoreParams{Object: receivedObj}, + ); err != nil { + s.log.Warn("put object to local storage failure", append(addressFields(addr), zap.Error(err))...) + return + } + + s.writeResult(addr) +} + +// NewObjectRestorer is an object restorer's constructor. +func NewObjectRestorer(p *ObjectRestorerParams) (ObjectRestorer, error) { + switch { + case p.Logger == nil: + return nil, instanceError(objectRestorerEntity, loggerPart) + case p.ObjectVerifier == nil: + return nil, instanceError(objectRestorerEntity, objectVerifierPart) + case p.ObjectReceptacle == nil: + return nil, instanceError(objectRestorerEntity, objectReceptaclePart) + case p.RemoteStorageSelector == nil: + return nil, instanceError(objectRestorerEntity, remoteStorageSelectorPart) + case p.EpochReceiver == nil: + return nil, instanceError(objectRestorerEntity, epochReceiverPart) + case p.PresenceChecker == nil: + return nil, instanceError(objectRestorerEntity, presenceCheckerPart) + } + + if p.TaskChanCap <= 0 { + p.TaskChanCap = defaultRestorerChanCap + } + + if p.ResultTimeout <= 0 { + p.ResultTimeout = defaultRestorerResultTimeout + } + + return &objectRestorer{ + objectVerifier: p.ObjectVerifier, + remoteStorageSelector: p.RemoteStorageSelector, + objectReceptacle: p.ObjectReceptacle, + epochReceiver: p.EpochReceiver, + presenceChecker: p.PresenceChecker, + log: p.Logger, + taskChanCap: p.TaskChanCap, + resultTimeout: p.ResultTimeout, + }, nil +} diff --git a/lib/replication/placement_honorer.go b/lib/replication/placement_honorer.go new file mode 100644 index 0000000000..9a5ac3ccd3 --- /dev/null +++ b/lib/replication/placement_honorer.go @@ -0,0 +1,198 @@ +package replication + +import ( + "context" + "time" + + "github.com/multiformats/go-multiaddr" + "go.uber.org/zap" +) + +type ( + // PlacementHonorer is an interface of entity + // that listens tasks to piece out placement rule of container for particular object. + PlacementHonorer interface { + Process(ctx context.Context) chan<- *ObjectLocationRecord + Subscribe(ch chan<- *ObjectLocationRecord) + } + + placementHonorer struct { + objectSource ObjectSource + objectReceptacle ObjectReceptacle + remoteStorageSelector RemoteStorageSelector + presenceChecker PresenceChecker + log *zap.Logger + + taskChanCap int + resultTimeout time.Duration + resultChan chan<- *ObjectLocationRecord + } + + // PlacementHonorerParams groups the parameters of placement honorer's constructor. + PlacementHonorerParams struct { + ObjectSource + ObjectReceptacle + RemoteStorageSelector + PresenceChecker + *zap.Logger + + TaskChanCap int + ResultTimeout time.Duration + } +) + +const ( + defaultPlacementHonorerChanCap = 10 + defaultPlacementHonorerResultTimeout = time.Second + placementHonorerEntity = "placement honorer" +) + +func (s *placementHonorer) Subscribe(ch chan<- *ObjectLocationRecord) { s.resultChan = ch } + +func (s *placementHonorer) Process(ctx context.Context) chan<- *ObjectLocationRecord { + ch := make(chan *ObjectLocationRecord, s.taskChanCap) + go s.processRoutine(ctx, ch) + + return ch +} + +func (s *placementHonorer) writeResult(locationRecord *ObjectLocationRecord) { + if s.resultChan == nil { + return + } + select { + case s.resultChan <- locationRecord: + case <-time.After(s.resultTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *placementHonorer) processRoutine(ctx context.Context, taskChan <-chan *ObjectLocationRecord) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn(placementHonorerEntity+ctxDoneMsg, zap.Error(ctx.Err())) + break loop + case locationRecord, ok := <-taskChan: + if !ok { + s.log.Warn(placementHonorerEntity + taskChanClosed) + break loop + } else if has, err := s.presenceChecker.Has(locationRecord.Address); err != nil || !has { + continue loop + } + s.handleTask(ctx, locationRecord) + } + } + close(s.resultChan) +} + +func (s *placementHonorer) handleTask(ctx context.Context, locationRecord *ObjectLocationRecord) { + defer s.writeResult(locationRecord) + + var ( + err error + log = s.log.With(addressFields(locationRecord.Address)...) + copiesShortage = locationRecord.ReservationRatio - 1 + exclNodes = make([]multiaddr.Multiaddr, 0) + procLocations []ObjectLocation + ) + + obj, err := s.objectSource.Get(ctx, locationRecord.Address) + if err != nil { + log.Warn("get object failure", zap.Error(err)) + return + } + + tombstone := obj.IsTombstone() + + for copiesShortage > 0 { + nodesInfo, err := s.remoteStorageSelector.SelectRemoteStorages(ctx, locationRecord.Address, exclNodes...) + if err != nil { + log.Warn("select remote storage nodes failure", + zap.Stringer("object", locationRecord.Address), + zap.Any("exclude nodes", exclNodes), + zap.String("error", err.Error()), + ) + + return + } + + if !tombstone { + procLocations = make([]ObjectLocation, 0, len(nodesInfo)) + loop: + for i := range nodesInfo { + for j := range locationRecord.Locations { + if locationRecord.Locations[j].Node.Equal(nodesInfo[i].Node) { + copiesShortage-- + continue loop + } + } + procLocations = append(procLocations, nodesInfo[i]) + } + + if len(procLocations) == 0 { + return + } + } else { + procLocations = nodesInfo + } + + if err := s.objectReceptacle.Put(ctx, ObjectStoreParams{ + Object: obj, + Nodes: procLocations, + Handler: func(loc ObjectLocation, success bool) { + if success { + copiesShortage-- + if tombstone { + for i := range locationRecord.Locations { + if locationRecord.Locations[i].Node.Equal(loc.Node) { + return + } + } + } + locationRecord.Locations = append(locationRecord.Locations, loc) + } else { + exclNodes = append(exclNodes, loc.Node) + } + }, + }); err != nil { + s.log.Warn("put object to new nodes failure", zap.Error(err)) + return + } + } +} + +// NewPlacementHonorer is a placement honorer's constructor. +func NewPlacementHonorer(p PlacementHonorerParams) (PlacementHonorer, error) { + switch { + case p.RemoteStorageSelector == nil: + return nil, instanceError(placementHonorerEntity, remoteStorageSelectorPart) + case p.ObjectSource == nil: + return nil, instanceError(placementHonorerEntity, objectSourcePart) + case p.ObjectReceptacle == nil: + return nil, instanceError(placementHonorerEntity, objectReceptaclePart) + case p.Logger == nil: + return nil, instanceError(placementHonorerEntity, loggerPart) + case p.PresenceChecker == nil: + return nil, instanceError(placementHonorerEntity, presenceCheckerPart) + } + + if p.TaskChanCap <= 0 { + p.TaskChanCap = defaultPlacementHonorerChanCap + } + + if p.ResultTimeout <= 0 { + p.ResultTimeout = defaultPlacementHonorerResultTimeout + } + + return &placementHonorer{ + objectSource: p.ObjectSource, + objectReceptacle: p.ObjectReceptacle, + remoteStorageSelector: p.RemoteStorageSelector, + presenceChecker: p.PresenceChecker, + log: p.Logger, + taskChanCap: p.TaskChanCap, + resultTimeout: p.ResultTimeout, + }, nil +} diff --git a/lib/replication/storage_validator.go b/lib/replication/storage_validator.go new file mode 100644 index 0000000000..4dd058c888 --- /dev/null +++ b/lib/replication/storage_validator.go @@ -0,0 +1,194 @@ +package replication + +import ( + "context" + "time" + + "github.com/multiformats/go-multiaddr" + "go.uber.org/zap" +) + +type ( + // StorageValidator is an interface of entity + // that listens and performs task of storage validation on remote nodes. + // Validation can result to the need to replicate or clean object. + StorageValidator interface { + Process(ctx context.Context) chan<- *ObjectLocationRecord + SubscribeReplication(ch chan<- *ReplicateTask) + SubscribeGarbage(ch chan<- Address) + } + + storageValidator struct { + objectVerifier ObjectVerifier + log *zap.Logger + presenceChecker PresenceChecker + addrstore AddressStore + + taskChanCap int + resultTimeout time.Duration + replicateResultChan chan<- *ReplicateTask + garbageChan chan<- Address + } + + // StorageValidatorParams groups the parameters of storage validator's constructor. + StorageValidatorParams struct { + ObjectVerifier + PresenceChecker + *zap.Logger + + TaskChanCap int + ResultTimeout time.Duration + AddrStore AddressStore + } +) + +const ( + defaultStorageValidatorChanCap = 10 + defaultStorageValidatorResultTimeout = time.Second + + storageValidatorEntity = "storage validator" +) + +func (s *storageValidator) SubscribeReplication(ch chan<- *ReplicateTask) { + s.replicateResultChan = ch +} + +func (s *storageValidator) SubscribeGarbage(ch chan<- Address) { s.garbageChan = ch } + +func (s *storageValidator) Process(ctx context.Context) chan<- *ObjectLocationRecord { + ch := make(chan *ObjectLocationRecord, s.taskChanCap) + go s.processRoutine(ctx, ch) + + return ch +} + +func (s *storageValidator) writeReplicateResult(replicateTask *ReplicateTask) { + if s.replicateResultChan == nil { + return + } + select { + case s.replicateResultChan <- replicateTask: + case <-time.After(s.resultTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *storageValidator) writeGarbage(addr Address) { + if s.garbageChan == nil { + return + } + select { + case s.garbageChan <- addr: + case <-time.After(s.resultTimeout): + s.log.Warn(writeResultTimeout) + } +} + +func (s *storageValidator) processRoutine(ctx context.Context, taskChan <-chan *ObjectLocationRecord) { +loop: + for { + select { + case <-ctx.Done(): + s.log.Warn(storageValidatorEntity+ctxDoneMsg, zap.Error(ctx.Err())) + break loop + case locationRecord, ok := <-taskChan: + if !ok { + s.log.Warn(storageValidatorEntity + taskChanClosed) + break loop + } else if has, err := s.presenceChecker.Has(locationRecord.Address); err != nil || !has { + continue loop + } + s.handleTask(ctx, locationRecord) + } + } + close(s.replicateResultChan) + close(s.garbageChan) +} + +func (s *storageValidator) handleTask(ctx context.Context, locationRecord *ObjectLocationRecord) { + selfAddr, err := s.addrstore.SelfAddr() + if err != nil { + s.log.Error("storage validator can't obtain self address") + return + } + + var ( + weightierCounter int + replicateTask = &ReplicateTask{ + Address: locationRecord.Address, + Shortage: locationRecord.ReservationRatio - 1, // taking account of object correctly stored in local store + ExcludeNodes: nodesFromLocations(locationRecord.Locations, selfAddr), + } + ) + + for i := range locationRecord.Locations { + loc := locationRecord.Locations[i] + + if s.objectVerifier.Verify(ctx, &ObjectVerificationParams{ + Address: locationRecord.Address, + Node: locationRecord.Locations[i].Node, + Handler: func(valid bool, _ *Object) { + if valid { + replicateTask.Shortage-- + if loc.WeightGreater { + weightierCounter++ + } + } + }, + }); weightierCounter >= locationRecord.ReservationRatio { + s.writeGarbage(locationRecord.Address) + return + } + } + + if replicateTask.Shortage > 0 { + s.writeReplicateResult(replicateTask) + } +} + +// nodesFromLocations must ignore self address, because it is used in +// storage validator during replication. We must ignore our own stored +// objects during replication and work with remote hosts and check their +// verification info. +func nodesFromLocations(locations []ObjectLocation, selfaddr multiaddr.Multiaddr) []multiaddr.Multiaddr { + res := make([]multiaddr.Multiaddr, 0, len(locations)) + + for i := range locations { + if !locations[i].Node.Equal(selfaddr) { + res = append(res, locations[i].Node) + } + } + + return res +} + +// NewStorageValidator is a storage validator's constructor. +func NewStorageValidator(p StorageValidatorParams) (StorageValidator, error) { + switch { + case p.Logger == nil: + return nil, instanceError(storageValidatorEntity, loggerPart) + case p.ObjectVerifier == nil: + return nil, instanceError(storageValidatorEntity, objectVerifierPart) + case p.PresenceChecker == nil: + return nil, instanceError(storageValidatorEntity, presenceCheckerPart) + case p.AddrStore == nil: + return nil, instanceError(storageValidatorEntity, addrStorePart) + } + + if p.TaskChanCap <= 0 { + p.TaskChanCap = defaultStorageValidatorChanCap + } + + if p.ResultTimeout <= 0 { + p.ResultTimeout = defaultStorageValidatorResultTimeout + } + + return &storageValidator{ + objectVerifier: p.ObjectVerifier, + log: p.Logger, + presenceChecker: p.PresenceChecker, + taskChanCap: p.TaskChanCap, + resultTimeout: p.ResultTimeout, + addrstore: p.AddrStore, + }, nil +} diff --git a/lib/storage/storage.go b/lib/storage/storage.go new file mode 100644 index 0000000000..11775c3d2f --- /dev/null +++ b/lib/storage/storage.go @@ -0,0 +1,122 @@ +package storage + +import ( + "io" + + "github.com/nspcc-dev/neofs-node/lib/buckets" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +type ( + store struct { + blob core.Bucket + + meta core.Bucket + + spaceMetrics core.Bucket + } + + sizer interface { + Size() int64 + } + + // Params for create Core.Storage component + Params struct { + Buckets []core.BucketType + Viper *viper.Viper + Logger *zap.Logger + } +) + +// New creates Core.Storage component. +func New(p Params) (core.Storage, error) { + var ( + err error + bs = make(map[core.BucketType]core.Bucket) + ) + + for _, name := range p.Buckets { + if bs[name], err = buckets.NewBucket(name, p.Logger, p.Viper); err != nil { + return nil, err + } + } + + return &store{ + blob: bs[core.BlobStore], + + meta: bs[core.MetaStore], + + spaceMetrics: bs[core.SpaceMetricsStore], + }, nil +} + +// GetBucket returns available bucket by type or an error. +func (s *store) GetBucket(name core.BucketType) (core.Bucket, error) { + switch name { + case core.BlobStore: + if s.blob == nil { + return nil, errors.Errorf("bucket(`%s`) not initialized", core.BlobStore) + } + + return s.blob, nil + case core.MetaStore: + if s.meta == nil { + return nil, errors.Errorf("bucket(`%s`) not initialized", core.MetaStore) + } + + return s.meta, nil + case core.SpaceMetricsStore: + if s.spaceMetrics == nil { + return nil, errors.Errorf("bucket(`%s`) not initialized", core.SpaceMetricsStore) + } + + return s.spaceMetrics, nil + default: + return nil, errors.Errorf("bucket for type `%s` not implemented", name) + } +} + +// Size of all buckets. +func (s *store) Size() int64 { + var ( + all int64 + sizers = []sizer{ + s.blob, + s.meta, + s.spaceMetrics, + } + ) + + for _, item := range sizers { + if item == nil { + continue + } + + all += item.Size() + } + + return all +} + +// Close all buckets. +func (s *store) Close() error { + var closers = []io.Closer{ + s.blob, + s.meta, + } + + for _, item := range closers { + if item == nil { + continue + } + + if err := item.Close(); err != nil { + return err + } + } + + return nil +} diff --git a/lib/test/bucket.go b/lib/test/bucket.go new file mode 100644 index 0000000000..024a2ab46a --- /dev/null +++ b/lib/test/bucket.go @@ -0,0 +1,144 @@ +package test + +import ( + "sync" + + "github.com/mr-tron/base58" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" +) + +type ( + testBucket struct { + sync.RWMutex + items map[string][]byte + } +) + +const ( + errOverflow = internal.Error("overflow") + errNotFound = internal.Error("not found") +) + +// Bucket constructs test core.Bucket implementation. +func Bucket() core.Bucket { + return &testBucket{ + items: make(map[string][]byte), + } +} + +func (t *testBucket) Get(key []byte) ([]byte, error) { + t.Lock() + defer t.Unlock() + + val, ok := t.items[base58.Encode(key)] + if !ok { + return nil, core.ErrNotFound + } + + return val, nil +} + +func (t *testBucket) Set(key, value []byte) error { + t.Lock() + defer t.Unlock() + + t.items[base58.Encode(key)] = value + + return nil +} + +func (t *testBucket) Del(key []byte) error { + t.RLock() + defer t.RUnlock() + + delete(t.items, base58.Encode(key)) + + return nil +} + +func (t *testBucket) Has(key []byte) bool { + t.RLock() + defer t.RUnlock() + + _, ok := t.items[base58.Encode(key)] + + return ok +} + +func (t *testBucket) Size() (res int64) { + t.RLock() + defer t.RUnlock() + + for _, v := range t.items { + res += int64(len(v)) + } + + return +} + +func (t *testBucket) List() ([][]byte, error) { + t.Lock() + defer t.Unlock() + + res := make([][]byte, 0) + + for k := range t.items { + sk, err := base58.Decode(k) + if err != nil { + return nil, err + } + + res = append(res, sk) + } + + return res, nil +} + +func (t *testBucket) Iterate(f core.FilterHandler) error { + t.RLock() + defer t.RUnlock() + + for k, v := range t.items { + key, err := base58.Decode(k) + if err != nil { + continue + } + + if !f(key, v) { + return core.ErrIteratingAborted + } + } + + return nil +} + +func (t *testBucket) Close() error { + t.Lock() + defer t.Unlock() + + for k := range t.items { + delete(t.items, k) + } + + return nil +} + +func (t *testBucket) PRead(key []byte, rng object.Range) ([]byte, error) { + t.RLock() + defer t.RUnlock() + + k := base58.Encode(key) + + v, ok := t.items[k] + if !ok { + return nil, errNotFound + } + + if rng.Offset+rng.Length > uint64(len(v)) { + return nil, errOverflow + } + + return v[rng.Offset : rng.Offset+rng.Length], nil +} diff --git a/lib/test/keys.go b/lib/test/keys.go new file mode 100644 index 0000000000..3b87bfb3f0 --- /dev/null +++ b/lib/test/keys.go @@ -0,0 +1,142 @@ +package test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "strconv" +) + +// Keys is a list of test private keys in hex format. +var Keys = []string{ + "307702010104203ee1fd84dd7199925f8d32f897aaa7f2d6484aa3738e5e0abd03f8240d7c6d8ca00a06082a8648ce3d030107a1440342000475099c302b77664a2508bec1cae47903857b762c62713f190e8d99912ef76737f36191e4c0ea50e47b0e0edbae24fd6529df84f9bd63f87219df3a086efe9195", + "3077020101042035f2b425109b17b1d8f3b5c50daea1091e27d2452bce1126080bd4b98de9bb67a00a06082a8648ce3d030107a144034200045188d33a3113ac77fea0c17137e434d704283c234400b9b70bcdf4829094374abb5818767e460a94f36046ffcef44576fa59ef0e5f31fb86351c06c3d84e156c", + "30770201010420f20cd67ed4ea58307945f5e89a5e016b463fbcad610ee9a7b5e0094a780c63afa00a06082a8648ce3d030107a14403420004c4c574d1bbe7efb2feaeed99e6c03924d6d3c9ad76530437d75c07bff3ddcc0f3f7ef209b4c5156b7395dfa4479dd6aca00d8b0419c2d0ff34de73fad4515694", + "30770201010420335cd4300acc9594cc9a0b8c5b3b3148b29061d019daac1b97d0fbc884f0281ea00a06082a8648ce3d030107a14403420004563eece0b9035e679d28e2d548072773c43ce44a53cb7f30d3597052210dbb70674d8eefac71ca17b3dc6499c9167e833b2c079b2abfe87a5564c2014c6132ca", + "30770201010420063a502c7127688e152ce705f626ca75bf0b62c5106018460f1b2a0d86567546a00a06082a8648ce3d030107a14403420004f8152966ad33b3c2622bdd032f5989fbd63a9a3af34e12eefee912c37defc8801ef16cc2c16120b3359b7426a7609af8f4185a05dcd42e115ae0df0758bc4b4c", + "30770201010420714c3ae55534a1d065ea1213f40a3b276ec50c75eb37ee5934780e1a48027fa2a00a06082a8648ce3d030107a1440342000452d9fd2376f6b3bcb4706cad54ec031d95a1a70414129286c247cd2bc521f73fa8874a6a6466b9d111631645d891e3692688d19c052c244e592a742173ea8984", + "30770201010420324b97d5f2c68e402b6723c600c3a7350559cc90018f9bfce0deed3d57890916a00a06082a8648ce3d030107a1440342000451ec65b2496b1d8ece3efe68a8b57ce7bc75b4171f07fa5b26c63a27fb4f92169c1b15150a8bace13f322b554127eca12155130c0b729872935fd714df05df5e", + "3077020101042086ebcc716545e69a52a7f9a41404583e17984a20d96fafe9a98de0ac420a2f88a00a06082a8648ce3d030107a144034200045f7d63e18e6b896730f45989b7a8d00c0b86c75c2b834d903bc681833592bdcc25cf189e6ddef7b22217fd442b9825f17a985e7e2020b20188486dd53be9073e", + "3077020101042021a5b7932133e23d4ebb7a39713defd99fc94edfc909cf24722754c9077f0d61a00a06082a8648ce3d030107a14403420004d351a4c87ec3b33e62610cb3fd197962c0081bbe1b1b888bc41844f4c6df9cd3fd4637a6f35aa3d4531fecc156b1707504f37f9ef154beebc622afc29ab3f896", + "3077020101042081ef410f78e459fa110908048fc8923fe1e84d7ce75f78f32b8c114c572bfb87a00a06082a8648ce3d030107a144034200046e3859e6ab43c0f45b7891761f0da86a7b62f931f3d963efd3103924920a73b32ce5bc8f14d8fb31e63ccd336b0016eeb951323c915339ca6c4c1ebc01bbeb2b", + "307702010104209dd827fa67faf3912e981b8dbccafb6ded908957ba67cf4c5f37c07d33abb6c5a00a06082a8648ce3d030107a14403420004e5cb5ae6a1bd3861a6b233c9e13fa0183319f601d0f4e99b27461e28f473e822de395d15c1e14d29a6bd4b597547e8c5d09a7dd3a722a739bb76936c1ad43c0e", + "3077020101042005a03e332e1aff5273c52c38ec6c5a1593170ddf8d13989a8a160d894566fc6ba00a06082a8648ce3d030107a144034200045a11611542f07f2d5666de502994ef61f069674513811df42290254c26f71134100fed43ea8ecd9833be9abb42d95be8661f790c15b41ca20db5b4df4f664fb4", + "307702010104206e833f66daf44696cafc63297ff88e16ba13feefa5b6ab3b92a771ff593e96d0a00a06082a8648ce3d030107a14403420004434e0e3ec85c1edaf614f91b7e3203ab4d8e7e1c8a2042223f882fc04da7b1f77f8f2ee3b290ecfa6470a1c416a22b368d05578beb25ec31bcf60aff2e3ffcd4", + "30770201010420937c4796b9fc62fde4521c18289f0e610cf9b5ebf976be8d292bc8306cee2011a00a06082a8648ce3d030107a14403420004ba5951adddf8eb9bc5dac2c03a33584d321f902353c0aadccd3158256b294f5aa9cd5215201d74de2906630d8cefb4f298ff89caa29b5c90f9d15294f8d785bc", + "307702010104204b002204533f9b2fb035087df7f4288e496fc84e09299765de7a6cd61e6a32bca00a06082a8648ce3d030107a1440342000441abcf37a4d0962156c549de8497120b87e5e370a967188ab1d2d7abce53711dfd692a37f30018e2d14030185b16a8e0b9ca61dca82bfe6d8fc55c836355b770", + "3077020101042093ffa35f1977b170a0343986537de367f59ea5a8bd4a8fdd01c5d9700a7282dba00a06082a8648ce3d030107a144034200040e01090b297cf536740b5c0abb15afba03139b0d4b647fdd0c01d457936499c19283cf7b1aee2899923e879c97ddeffe4a1fa2bffc59d331b55982972524b45b", + "307702010104201c1a2209a2b6f445fb63b9c6469d3edc01c99bab10957f0cbe5fad2b1c548975a00a06082a8648ce3d030107a144034200040c8fd2da7bad95b6b3782c0a742476ffcb35e5bc539ea19bbccb5ed05265da3ab51ec39afd01fbee800e05ec0eb94b68854cd9c3de6ab028d011c53085ffc1b3", + "30770201010420b524d8cba99619f1f9559e2fe38b2c6d84a484d38574a92e56977f79eac8b537a00a06082a8648ce3d030107a14403420004a6d7d0db0cc0a46860fb912a7ace42c801d8d693e2678f07c3f5b9ea3cb0311169cbd96b0b9fc78f81e73d2d432b2c224d8d84380125ecc126481ee322335740", + "307702010104207681725fec424a0c75985acfb7be7baed18b43ec7a18c0b47aa757849444557ca00a06082a8648ce3d030107a14403420004bd4453efc74d7dedf442b6fc249848c461a0c636bb6a85c86a194add1f8a5fac9bf0c04ece3f233c5aba2dee0d8a2a11b6a297edae60c0bc0536454ce0b5f9dd", + "30770201010420ae43929b14666baa934684c20a03358cda860b89208824fac56b48f80920edc4a00a06082a8648ce3d030107a14403420004d706b0d86743d6052375aa5aa1a3613c87dccfe704dc85b4ed4f49a84a248a94582202927ec0c082234919f3ce6617152ba0d02497b81c61284261ce86cef905", + "3077020101042089d600f43c47ab98e00225e9b2d4a6c7ab771490f856d4679d9e1e0cca3009d0a00a06082a8648ce3d030107a144034200048515055045543e429173fc8f9f56a070bd4314b2b3005437d8504e6b6885f85101409b933e27c0de11415aee516d0d1b474088a437ece496ceb4f1c131e9ea40", + "3077020101042015518dcf888c7b241dac1c8bfa19d99f7fdba7ba37ed57d69bbbd95bb376ea4ca00a06082a8648ce3d030107a1440342000459e88d92efaa5277d60948feaa0bcd14388da00e35f9bae8282985441788f8beb2b84b71b1ae8aa24d64bb83759b80e3f05c07a791ffe10079c0e1694d74618c", + "307702010104203e840868a96e59ca10f048202cce02e51655a932ff0ac98a7b5589a8df17f580a00a06082a8648ce3d030107a14403420004f296414e914dcefd29bc8a493f8aedc683e5514a8ec5160637bee40ebaa85a421a363c8f7ce3ed113e97d2c4b6d9cd31d21698a54fce8d8e280a6be9ee4fbca9", + "30770201010420aa746067891cf005286d56d53092f77961f828bf5bf11aade18c8a458090d39aa00a06082a8648ce3d030107a144034200044af5ad2dacbb32ab795ab734d26bae6c098bd2ba9ca607542174d61b49ca3c07786aeb0c96908793a63d4f20cd370a77b7ec65e6b285c6337764e7ae3cd5fa1c", + "307702010104207135cbd831d52e778622c21ed035df9e3c6e4128de38fbf4d165a0583b5b4a29a00a06082a8648ce3d030107a1440342000412e2b9e11f288d8db60fbb00456f5969e2816a214a295d8e4d38fbacab6b0a7e0cdb8557e53d408244083f192d8a604d5b764ab44b467e34664ca82e012b60ab", + "3077020101042064b839ca26c42e2e97e94da5589db2de18597a12d6167fdfe0d20e932de747a2a00a06082a8648ce3d030107a1440342000481e90c2173b720447ae28361149598a7245ed51c3881a89353da25b8e574b8c9b2d80b2563efe5d9a0184b57af2431116c8a4ad8071ef2764ca3d3744c638401", + "30770201010420a56df8e6349520d27c36eb1e9675720c702d562842c859cd54b3d866f2cada30a00a06082a8648ce3d030107a14403420004dc08beb5b857f6da13ae1116e40a6e4e4b5aaebc8040eae0b3037c243b1c24def39de670380472df7aa98cb9e0f1132bc4afc0629d80a24c54b8ad600cb24cd4", + "30770201010420bd2dd18485a9667673b2c38c2ad51cc756a199d18fe1100acf29b647a549171ea00a06082a8648ce3d030107a1440342000422825ffe8b3416b6755a7076a7dc6f746ff29ee0a4455dceb0f3262127d51c9bb53f2c204636da8d7a09961274d7c7ba2ef3c771e83fb996ffe3f9882c530ffd", + "307702010104203058a0c8de5c6d4a5c7f64883e7d3c9f5097c8bc073cc482421e903b37123c06a00a06082a8648ce3d030107a14403420004f959705673c2f4112673e43d1d876ca71c64153abb6c9f58d1c3b3c1f8c213ee346833fb695eb533664d596a68e42150a21b405e3a08ed70af5f568275a7a79f", + "307702010104202bd9035bf38e7c4580abc377a6e9c31aa9bdaff90af2ce688eda9a532c83875ea00a06082a8648ce3d030107a14403420004918010ea3387786c6a257996ec74d7ee4e1703b3b811118f4e89fabfef7c694495191848a0d590313a0be9784644ef98e0f0f7e50fed5bee3fa48d66edbcd2b5", + "30770201010420aa055d6cbe96e1cfbe39530bc4b7a976baff53ce399956f0d8241750d3379990a00a06082a8648ce3d030107a1440342000444e8b6deda76c12320a8c5b7a48141ebf5dc9288df79a0f418ab92d82061d10118b8bce9fb200e5009a19fb0e19036762b3ef85440405f43225d6ee3350bf96c", + "30770201010420b8712525a79c7bd3df2a9dbabde1a111078a7ef30687a2efe0f0c4b4a23f2aa0a00a06082a8648ce3d030107a144034200049dc9e3d836a834f6d14ae99dfc70ad9b65c84f351c8dbc4f9b1b61c238051fb1db23e43d4b6e17803e21ebc44fe2f66742e306daa8c4ca7d79c6dd01fc1a4e4e", + "3077020101042086c18b56c4a2264b37c18a7937f026ab07ca6076eeea1ab90376492efb7875d9a00a06082a8648ce3d030107a144034200042f169311f2fae406de3c4a64fec94a22c35972281922a69e7657185997ae59fb3f69ac94295e58681cfbd263f8e6fbce144cc7925b71d90f57de3f3e10588321", + "30770201010420f58221355e1b2da73d66de482ec1edcb8597f3967d00d1356f4678fea6ad67e6a00a06082a8648ce3d030107a14403420004238cc44f02fa566e249a9697a078b9d38eba06012d54a29a430843a18df7a0a4207d704a360399db95eca591f2f81b6c50390467f293a1623b4757bdb4138101", + "30770201010420b10888a0157d524667fd575683bdcded4628a65149fde59b7340781b0cf2e36ea00a06082a8648ce3d030107a14403420004222ba11430b8719929c726aec74e8e70893e2960bc2bbee70fbaa6d88fa2a346adf0c450ea9823f0ba77d334fcd476ea036a62199338d7aa32e56c708d7a8caa", + "30770201010420edf001bd24c92e4f65789aae228223e77df71ce9bbfd7ce4d236ea3648e1f7fea00a06082a8648ce3d030107a1440342000472693c95786ab9f4e7c923338ce98bd068e28b71f84b77e7adb378c2ce2d8f1a2e13833df1afe4569367d7a4eee3abf50124299a28045a0073ea324f5ddb45ea", + "30770201010420e2649e591fc9072dd55573e41fc4ebfdf1db118951e4b7b2a98027ac9a4f7702a00a06082a8648ce3d030107a144034200046e34c9dea1836671f1ef259d7c3ee678c2f92d092af2518413fe9ba153a07ca8e9938784876e90cfa2989a00a83b1ac599c87a8d15be8001e46dfbfe018156a2", + "3077020101042069cd9b710f25613794751aed951004c888d4611aefa45abc23abff218e608290a00a06082a8648ce3d030107a14403420004dcf8ff34ab841720ff8dc08b60a14f41689e65f979a1af69b5e106f4262a2cb0947c9619e980caf20b3e7c8f15e60fc31c5b611c8a58370ba8201c9b6b932bd4", + "307702010104202898cef1944aaf90fddf433390323a02a79938568cf99f6c25bc9aa9e5cddb0aa00a06082a8648ce3d030107a1440342000491a1c20420f5005f5761419e4dcd0d9da0cf2ea4733f6d98a3d0c124f284cabdc65eafd9d2cad9b1122fca791c8b37997feed130c5725ea797cf07c61fb82734", + "30770201010420e568bd3ffa639aa418e7d5bc9e83f3f56690ebf645015ff7f0e216d76045efd5a00a06082a8648ce3d030107a144034200042424b498297124037db950bf2a1e652ba7f977363f4f69d7308531d27bf392219d93cb78f4379b7ffb16f3e7be311e208af2409bd33000fd25a8707ac6bec76b", + "307702010104205163d5d5eea4db97fccc692871f257842fdaca0eca967d29924242f7a2c56ad7a00a06082a8648ce3d030107a144034200044e2ca8312122039c3374db08851710d3b9a2efcbd8f5df004ec7b60a348aee32466f799b5957d39845f451071bb1f3bb99f25bf43196e7c772f7b84f39221b3a", + "30770201010420301eb936d2737886ab2fbf670952f9ba0d324827b81801810bfd60c89e8ca862a00a06082a8648ce3d030107a14403420004455454b1f3828a2328a8925c4c98bd6e37dece276efb3299d8b7d78c9d7e6f978b14d021c07bae0c18a623fc52ab2fec1523a89b2fd0cda373e9c9442a3545f2", + "3077020101042032c12a9bca8070c131b0a46944c17adf35eb44079f3c887fc3b93740bb9c03fca00a06082a8648ce3d030107a14403420004e61da413c4d5dbc6c004089d96a3cb55f4b20b70c544f3823a7a6322c53e134fcb8a885729ef284d68d23e0a58009d48b369f9c4f5a665a8880a48606491dd8a", + "30770201010420aa2b40742722b81c6ffd5c47b94b8be747da259e172a82d27ebc525c8f46d17aa00a06082a8648ce3d030107a14403420004f87a863ed11592cf4f96e837038b105d155f5e09a31386ab4604234e8a975d49a9612b4597b7fb206087b70a26bce4aca31edb253530e6da83ce16beefa99f60", + "307702010104202a70a0c827b4ce8d433e800ab0818b1401b220fadea75feff655251ee4317556a00a06082a8648ce3d030107a14403420004a5c9209fd53dc1ce2c873782ec507db5e0f9cc78292a84ecafc5bab16c2e4d786a882ad77ad999f3d6ba676ad80354ad376dabc4fa03a6c15ead3aa16f213bc5", + "307702010104202787d04901f48c81774171ef2e2a4d440b81f7fa1f12ab93d8e79ffab3416a1ca00a06082a8648ce3d030107a14403420004010d32df4d50343609932a923f11422e3bea5fa1319fb8ce0cc800f66aa38b3f7fda1bc17c824278734baa3d9b7f52262eeacbca21304b74ba4795b5055b1e9f", + "3077020101042032423728a897144d4fb95090ca0ac67a23eb22e2f7f925cbddaf542eeaec8faaa00a06082a8648ce3d030107a14403420004c37f9fec5b1be5b0286300ace6a5d25df8189d29604145a77b6578a4e3956ed3d9af48f8ee1e39868bba9e359e5444984f0428755e29d2012f235c9a56749148", + "30770201010420d5bd2a3867937e0b903d19113e859ca9f6497f4af082894a6911cef3a3a12d35a00a06082a8648ce3d030107a14403420004435b2e891c46023f422119f18a04c75b9322ea4aaddd10a0568438310896388bf7037e98bd5979a6f0839acb07dead1f2f973640dcc11dcee1de8a07c0b3dd80", + "30770201010420590edcf1f2b6ee6c1b836ace33b934597883a00ce84fe812a4b3e22432846972a00a06082a8648ce3d030107a14403420004183d7cad633cb0f4ab774f4dc19b9db87e7ef97b0f4d43ac395d2409dabbe5339dbad661c7c2fd05606e2edb08f8ace660f73bf5232011262d563603f61d2353", + "30770201010420a0ea4e16cf8c7c641d70aea82192fb9303aab6e7b5cd72586ba287d50f4612d6a00a06082a8648ce3d030107a1440342000482a72d31e71f0aea778cb42b324abf853cb4e4e8d4b2ae0e5130480073e911f183134c047a7e1cd41a845a38057ea51a1527923518cbf47c3e195a9f44e1d242", + "307702010104209e04b00c8d0f96ddb2fbb48cfc199905bfbfcc894acb77b56bf16a945a7c7d08a00a06082a8648ce3d030107a1440342000405efd203dcddfb66d514be0de2b35050b83e3738096cd35398165bfdbe34d34c0d96a4e6df503903c75c2c06b66b02b15cd7bf74c147d7a9f0a5e53b83c5762d", + "30770201010420aa69f1cc2cb3482a12af4b1614d6dde01216f1cad1c9f03c681daa8648b75b37a00a06082a8648ce3d030107a1440342000474ffec1297420d0cf730b42942058699d803ab618e1e40ccf9cc17f71f62b3123d863fbf8fae37b6c958892af6151159f74e2a568917bfc2f4e00c55c32b52e7", + "3077020101042090a04300e8d6ed9f44422a2cf93817604bf1f6233c4333ba0db20ab726852fa4a00a06082a8648ce3d030107a144034200049e6f2001baf2b6fb25e3273907ed7320f494de6b5882c4c4b9bcee7ddc60274e064cc68c64325c001f07a505722062d1ca9774a2cc1e0cd28fe5f807865bfcc1", + "3077020101042088945c19c6ce3e63f8d8a421616391d83bec79a0c590f1607b247ffa0c677dd3a00a06082a8648ce3d030107a1440342000492d17d410f9eabf7ae4509a92494e9fe94a72947f24e60c5bb6e12b2cde3c1bfe5305a0d759138069d44268f174136971ecb752df602c282e48d40f43a8734e3", + "3077020101042079d14eacdc4f21dc5284bd8487dcb2c22e9e53e71909474f922bf695f49cf23ea00a06082a8648ce3d030107a1440342000428039292c5bcf3593639bf5835ec9411ffd3ac236c0186697623930b5ca63f32ff41df5217e7def770d9a0de87f61526497bd9aaa95d924e0a17d85958e7c095", + "30770201010420a6ac867ff8d00aaad23198415868a64e59217b4d22474752a146fcb52204dfa5a00a06082a8648ce3d030107a14403420004a5f37a779265c55cd4f5a7f3bffc4679395898046eb9d67d8670be39001de5a7bc010b0d218561626272989c5952e8e0d95d2590f78eec44dc62a46184956301", + "30770201010420df446014577f6081113cd7d33c6ba91b9ac3d083e76f8873358f83129e2d0111a00a06082a8648ce3d030107a14403420004da0c932759f50ad705507f876138c2c6e012764abc8764a6dd609e6ad06099952b120be71690bc091591f1aa8d7d6e9365deddbc958bc87ff150358ad33f7537", + "30770201010420b3351033eaaee3a9ea27cd7dc54aa2c8d787b14b7d428165f1a04a59c6d5b0f2a00a06082a8648ce3d030107a14403420004da3984fb8152403a9fb9068b16f9afb5c900f24230e205567b4405ee3cad2db3ff46968489d494b38d0c85fcc4aecccb61fc00dca54c8fd99ee5bf5e2616f1b7", + "30770201010420deedbcef7f6821f6aab2b15ce198f5eb2064f6eb461a6b7776b4da35c81b1506a00a06082a8648ce3d030107a1440342000405422b86ce66b18e68f0fb14f28e4ed9b1f7ee84f57957f4e4b4c6b0c392e6357e4698fb707f590be1b915622ec8da476071a56919211f6e5e888284d4e33f06", + "3077020101042078c3db0d3b1114cb99f1d0bea0d3aec9067b26964e2b85fe9df4789b24cb3da5a00a06082a8648ce3d030107a144034200046874e52d7d58b6697b407b0c0eea3cfeb528e34fca1589c5031e11aae1ad1f9280e7a4c37ddf28479cd07b4246ce9398e0e24f99946f87e08532fa26b8fb8016", + "30770201010420f0ba42553b146cf088d3a5a3645782fe675d23561897ced7f1270a8d05cfdaaaa00a06082a8648ce3d030107a14403420004c250e12f3aa1fb6261c57cdb091cd90d82917e103711425888477b9da4359d2803aaf0015638294c7c0baa4ec77ba8fceff5ee7f15ea087a4174f58d518006dd", + "307702010104207f2c0fc4b0e418b2d4c72a63fdc27f158f6ad44c26d161f489714525b6a13db1a00a06082a8648ce3d030107a144034200041d83885672021e783d8bd995d187f407bbda2c6bed5e8fabc7c6c5cb304a85eaffa12dad7ba874ac45f4258fffe07534843ff7fe76075470f2c77104d781688f", + "30770201010420d3de828ac9742704d4e6981ce1fc8c473e508eda3a121cda420dacbdf39d48e9a00a06082a8648ce3d030107a14403420004c78abfc4a5c0eb3ee0c9817d1790b7ca9fd528d0bc727f9daf63f4212097538b6888b9de2ae4dff29895500be456fe0ccbee340aecb546d1558b08c3718aaa4a", + "30770201010420d9c4e477b56f2ff0b211acd82b450336276534b350747315152a4923e6e65294a00a06082a8648ce3d030107a14403420004fbd540966b03fe2c2314f20248d345e3e9b92d6a7cfea22d1b5367f01b32d616f317e00cea1f659437b4302610abba8abb0f2bfce0a91b952e9565159c1e464e", + "30770201010420fb84f4a426fa12920c2cf7c2d821280530c0fa93960ded8c20120511dc1d5069a00a06082a8648ce3d030107a14403420004c0177f13c6e00bb9029df089006a332192bdf12a782c60a8d00d110c53db67c344584f22677695a7f1629db1600b0559ced49ac931b08cc6a58e5ea436bde2f8", + "30770201010420653ce060214028f7aa584910f0925d702bde18d52d8e530f07dd5004076eb614a00a06082a8648ce3d030107a1440342000433668d0c9085feae4b285fe260a316e24f24c0bb8e442583e23284bf5a962cd0357cd63ac4d1cdda58afb201bceee911ebe7cf134652dc4390f4e328f6cb5d65", + "307702010104206123b7d5b8c53b2a2a95dd2e42fe550617b7520fe9bd94a99045addb828ad847a00a06082a8648ce3d030107a1440342000487c10fdeaabf8072dcea0dc5b18be4d72f2b8298bc891ea0a11d202438b7598ac588f16a9cd697f8220434d4e15ff4c82daaae63955525633335843069434aea", + "3077020101042000b793c9b8553ee7bec21cd966f5aaff59a07d1fa3fa86e0164bcd2f7f4dd586a00a06082a8648ce3d030107a1440342000419d4179dbeae7fa87e356f0406c327239d34e540cd7db5174a81bd6197738bc72e46fe4bd1512dc4b35950b2c1e78e6f8f54980193be78d45e4d97a837455777", + "307702010104200fb1a771004f6be6300eccd603b9c9e269fbdd69e5eb183d7acad51b0b205b88a00a06082a8648ce3d030107a14403420004d3b7fa62bacff49714ef28a955cdc30f4aef323293ac3aebab824892dfa3306f2ec319f5bca1771b956b4a9b1c2f565dc08b29c07ec84623932a5d6fb59be6c7", + "30770201010420fe6907b91407619fdc95153cd59df061e88095678801008d3901f29c7c434243a00a06082a8648ce3d030107a14403420004796fcea7889128f8060b04e9000381fd3d80fe68f000063b182fe9d8984e740c387c4ed4c6729e8c715c576fe355a9b7dda6890c55b15ae6013fd51e8858b2f2", + "30770201010420111eaff6db3b279d014b45b3da091909f054f37c350c237fe9d51b4342811299a00a06082a8648ce3d030107a144034200047d51f9178725c4134579ac6d0cb84745e0d2068ccf72d30c02dd431547f868d1cb93b5774c7e1eb9582e2151521ff16cdf80b3ba4646d64f7982066f9eb679f0", + "30770201010420631d01e6aaa68e6c36e3425b984df02bc5b54e81951479f7cea8fd1b804bab57a00a06082a8648ce3d030107a14403420004fa1b1ed9ff904f1f050577e05b5175e897d462598fdd323c8ef25f6072dfa43034baa0119e64092fb44f7a04d59d16ba8645f52cfb7775a6536c00f7fc2ee2f1", + "307702010104201ec553d14d45acdf147dba5fcbc3a42a1f763411d5c206d03600ed810b0cf106a00a06082a8648ce3d030107a14403420004e9a309a24d1061204087de10e5bc64b6d45369399a5a402d630ca2d04b34ae9d27d491e5fadd5d082e14454e6b2a572a24904ba2a8dc7430b20d361134188589", + "307702010104206d31e401bb20968106a058f8df70cd5fb8e9aaca0b01a176649712aa594ff600a00a06082a8648ce3d030107a144034200048555a2f9e7256c57b406c729d2d8da12c009f219e81cecb522cb3c494dcc1c76ac6d2f641dafe816065482fb88916e1a719672c82406556e16c32cf90752a92f", + "307702010104208ada3d6ea6000cecbfcc3eafc5d1b0674fabece2b4ed8e9192200021b8861da0a00a06082a8648ce3d030107a14403420004a99e7ed75a2e28e30d8bad1a779f2a48bded02db32b22715c804d8eeadfbf453d063f099874cb170a10d613f6b6b3be0dbdb44c79fc34f81f68aeff570193e78", + "30770201010420d066dfb8f6ba957e19656d5b2362df0fb27075836ec7141ce344f76aa364c3cea00a06082a8648ce3d030107a14403420004597fd2183c21f6d04fa686e813cf7f838594e2e9c95b86ce34b8871674d78cc685b0918fd623e3019d8c7b67104395b1f94fc3338d0772e306572236bab59c39", + "307702010104202c291b04d43060f4c2fd896b7a9b6b4f847fb590f6774b78a0dff2513b32f55ca00a06082a8648ce3d030107a14403420004e80bd7e6445ee6947616e235f59bbecbaa0a49737be3b969363ee8d3cfccbbc42a0a1282de0f27c135c34afad7e5c563c674e3d18f8abcad4a73c8c79dad3efa", + "3077020101042029af306b5c8e677768355076ba86113411023024189e687d8b9c4dee12f156fda00a06082a8648ce3d030107a144034200049d7d21e6e1e586b5868853a3751618de597241215fb2328331d2f273299a11295fe6ccd5d990bf33cf0cdcda9944bf34094d5ffa4e5512ee4a55c9f5a8c25294", + "3077020101042022e65c9fc484173b9c931261d54d2cf34b70deccb19ce0a84ce3b08bc2e0648ba00a06082a8648ce3d030107a14403420004ea9ee4ab7475ebaff6ea2a290fc77aafa4b893447d1a033f40400b4d62ee923a31d06fe5f28dbc2ebec467ebd2e002a9ea72057f0b0c60fe564584a6539376ad", + "307702010104205000583dc21cb6fd26df1c7d6e4efb9b47ceff73c0d94ed453bae0c13a9e5795a00a06082a8648ce3d030107a144034200045a6a5b5886b01f54dfa0788f15d3542aec160843a57e723008d1b984dd572ecb8935662daaba53d756d45442efbae067f52b0b151899a645afb663205babddd3", + "30770201010420997431e73eae00f476bb1a221b4cc9dfd18d787be207b7069141627f61ba752da00a06082a8648ce3d030107a144034200047c89dc8c46a27e20c37b0ecf1150e8b92c2dd4dc534a25545f87a5f0c44fdbf4dee2af5bcdc4012f0acee168aeb55bb4d24738fac105fc056928ff5870491047", + "307702010104207dc10db95a597a80e916d7f8e4e419b609d767538fe9732bcc5f9d783c605a2ba00a06082a8648ce3d030107a144034200042e2ae4fae087a11fcdf9565670164c229337ed87b5056687c6bceeb84108db9a88b9e5d96a0cf121255ceefce0bb5239608768bb841e6687dbd9626222eb5187", + "307702010104209056e22b347f5f1839f1a53f1250d098616ff04db0b49b1fddb18b987930cec7a00a06082a8648ce3d030107a1440342000427cc4c7fb5d7ac047161aee78e812ad264ba25dd878684637308674ea693817b20a5e3672de6a92dfbf82f641268052fa742e6f35ff91c617334f09f89bd1218", + "30770201010420554ea6cfeb2cc4f1e29c08e65317d72731ee03940af9ff6a141b761d5d054db6a00a06082a8648ce3d030107a14403420004a6121746c0553ede0944da8a7f304831fcefb51b40acf78016d41cc45cc5f7e9a1b22bbea028daab5cb4c39cadf84da442749cbfc04536d6f85c3254ec7a0805", + "30770201010420f53ff1c7db3c4e7c734bf7396a1a5364ac2dfe4b794b118aada6bab72cde8969a00a06082a8648ce3d030107a1440342000414b11ec158e3f9d558bd1da1ed0e38c92b1ad55834f3ce08e456747279dd9ed1143cff4f5e8d70189f4b114e3cd609105d6eb8f431f392487e4c9e16a152dba1", + "30770201010420b3f394090547f5dcb2e77cef65e03a3b7d1c953cd0e069553da2795ab0adc950a00a06082a8648ce3d030107a14403420004a1a9dbe5d6dfa2dfb039aebabe96b12faf97c994e1430323d074ecbd90ef075e0fe9dc7d5eef2483d485ffb0b4a01b01e131754fb38059a1365d342d5175397a", + "30770201010420bf13c42fa84c409161f9d73ce20fd85b20c5381914aa2a2375452b34cd352022a00a06082a8648ce3d030107a14403420004e0134214a5349a235cee406ad942ca105ef871a7e4c922ef4769466d8495c78b82f6c49270c8cd913e0cf407cdab679dd9914090ea91122ca9fb654ebcfce57d", + "30770201010420440d975b65bf585d0813137fe041461de59221856eaf255479b5e69721cfb30da00a06082a8648ce3d030107a14403420004935a9626ddb7bd6fbcd2ad9d9333851bbc64b9997cb8e43b1a17f8e9968ed6b0e5d2edf105fbabc9bd745fa2120ac527bbfefb6e8ed96844f80b8e27b6d9a549", + "307702010104209ea2dc59260408165d6c42205aa52e275f81c39d9bf5b1b9c8187ade875e8068a00a06082a8648ce3d030107a14403420004bc570aa24df0306cb761ee9fb22e61f59ae4f11e8804491d8651084f191c800d1e6b16e4bc3693b88f9bef82849f3cd6914a15cae60322c1f4822a2bdf426782", + "30770201010420505b596fb71a2e36c0ba07da03442a721f3f1832dcac19631d6c11b36ab81986a00a06082a8648ce3d030107a1440342000472cfb26cf07faa4e6e9d328214677b5eb51cd2e35717ac661d732115e592a07482bf966a31792cc993bdf816a732069ed423871b53fb3c7eabab2f4d3d272013", + "3077020101042089a9d5b397c521db4bb4a5f3e8f2043e43bb5617a2070e7bfa30dd2dbf1815a1a00a06082a8648ce3d030107a1440342000468d2aeaf641b839095644cfd4b72ab97d0bf3fae1ed36e9f81d9aff333b0123f7b846f6ca61dbbd4e10988e740463addef793994a1498987883ecf237f18bc40", + "307702010104200919a89aedb4e20cfcd2cb568c8de18b1b60b5da17aaea3be9804eb5bc3280f5a00a06082a8648ce3d030107a14403420004139812ec6bd62fd3ce71040d87cc07671948ff82300fae5f3af80dcd4e22c870c0102c4add460b2cbbeeb298f58037fc645da20aa8f5531a5ff56d3e5b2d1944", + "30770201010420b145fc69cfabff378f390f0a99fb98ddc8ba9228cb1adf9c7099c6393a24567aa00a06082a8648ce3d030107a14403420004b660084cb05e005fb163011663fee6946f354714565069968f16e89e9a7aac45610f05502ff9d9e3cd0fdc88083bd8840a518b71135e59a0f0f235636d5eb7c4", + "3077020101042082d39168f289e784ace49bfdd523297b524c494f83fe7d04dd2f055b48d636b9a00a06082a8648ce3d030107a14403420004ea4021da5eec4e7f333059625ecbad3969676cf625cbf0da316f55f50ccd40e6174fdb7023c07abdb3ca91203acbcb5e78e1601f1a9aa616c5019ac5b2222ff4", + "3077020101042066a1ebc23e993674bfdc3b9721c280b7f3c1599903063ea7899b848b942a6169a00a06082a8648ce3d030107a144034200046bdb182c6c0c1f9ea898c3847bc4b46014cb8da6a02d75b7bed3c4a9a4e9c8836d4ce22fe68b68ae56a91fb435c7ea8f05bca8e8fcb1d6b77770d419f99e51da", + "30770201010420fa2cda21b761c46fcc5b54d47b045e24affdb95425e859bb367a07950119ab6ba00a06082a8648ce3d030107a144034200044b9e4cee102ad23fea3357f8f5f95ab9d60d34086ba4b39d5f37cbc61998ac9658ec56033ad72977d41e449d449f5aac2bc653ea8038fc04a011ff02ec49e088", + "3077020101042028acfb3c41b7be1d9d0506ac3702c363ffd767dd738dc8ab581ad7add2ec8872a00a06082a8648ce3d030107a144034200047467dedfb8c9a7d9496d4898d6ace0fba063545ab0d345d8b63b90871927ed269645a745a7335ca511d86a366f24e7832477842b4041a9ab564c5fbce49e4df8", + "307702010104202e57b8b867bd95a8dfcdd2cb8f82ea41bff21610019afd6e2367e755dec5b944a00a06082a8648ce3d030107a144034200048f97eb2d6ee2d3da8746d8d4f84469ea765fb0d1412b167b6d8a916b5f968b4d64ede5ea6d6e08ec0de192262fcb3ebed49e9d17858261affed84827b38c6cc9", + "3077020101042021a904281e4c31386ce34a5b52af3a068caa65819fbcf0ca76ab6041ecdaf454a00a06082a8648ce3d030107a1440342000405f9b7894a97fcddfc3285b8e974718606616fe07c70b7ab2bfb28a85fb3014c2610ab9e8e6da8ae3da032837d3a14b1e791d2633bdd8551b4817a080b9aa697", + "3077020101042089c2c73d08bd03da4c3111aa0b78bb1edc5243d8e119513035d3741e851dec1ca00a06082a8648ce3d030107a14403420004ec9ebc34f45150334fd1d8c92274fe43c5b3b059f15cb1963f6cf7d54bc6b1b0b4ef1c5d56d2d06ab54ce2e7606e0fa5d2f188a2d593b22d9cf6a0098aa00cb6", +} + +// DecodeKey creates a test private key. +func DecodeKey(i int) *ecdsa.PrivateKey { + if i < 0 { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic("could not generate uniq key") + } + + return key + } + + if current, size := i, len(Keys); current >= size { + panic("add more test keys, used " + strconv.Itoa(current) + " from " + strconv.Itoa(size)) + } + + buf, err := hex.DecodeString(Keys[i]) + if err != nil { + panic("could not hex.Decode: " + err.Error()) + } + + key, err := x509.ParseECPrivateKey(buf) + if err != nil { + panic("could x509.ParseECPrivateKey: " + err.Error()) + } + + return key +} diff --git a/lib/test/logger.go b/lib/test/logger.go new file mode 100644 index 0000000000..1ba431371d --- /dev/null +++ b/lib/test/logger.go @@ -0,0 +1,30 @@ +package test + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +const sampling = 1000 + +// NewTestLogger creates test logger. +func NewTestLogger(debug bool) *zap.Logger { + if debug { + cfg := zap.NewDevelopmentConfig() + cfg.Sampling = &zap.SamplingConfig{ + Initial: sampling, + Thereafter: sampling, + } + + cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + + log, err := cfg.Build() + if err != nil { + panic("could not prepare logger: " + err.Error()) + } + + return log + } + + return zap.L() +} diff --git a/lib/transformer/alias.go b/lib/transformer/alias.go new file mode 100644 index 0000000000..a18098bf52 --- /dev/null +++ b/lib/transformer/alias.go @@ -0,0 +1,25 @@ +package transformer + +import ( + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/storagegroup" +) + +type ( + // Object is a type alias of + // Object from object package of neofs-api-go. + Object = object.Object + + // ObjectID is a type alias of + // ObjectID from refs package of neofs-api-go. + ObjectID = refs.ObjectID + + // CID is a type alias of + // CID from refs package of neofs-api-go. + CID = refs.CID + + // StorageGroup is a type alias of + // StorageGroup from storagegroup package of neofs-api-go. + StorageGroup = storagegroup.StorageGroup +) diff --git a/lib/transformer/put_test.go b/lib/transformer/put_test.go new file mode 100644 index 0000000000..ddd7affd36 --- /dev/null +++ b/lib/transformer/put_test.go @@ -0,0 +1,764 @@ +package transformer + +import ( + "bytes" + "context" + "crypto/sha256" + "io" + "math/rand" + "sort" + "testing" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testPutEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ io.Writer = (*testPutEntity)(nil) + _ EpochReceiver = (*testPutEntity)(nil) + _ Transformer = (*testPutEntity)(nil) + _ storagegroup.InfoReceiver = (*testPutEntity)(nil) + _ objutil.Verifier = (*testPutEntity)(nil) +) + +func (s *testPutEntity) Verify(_ context.Context, obj *Object) error { + if s.f != nil { + s.f(obj) + } + return s.err +} + +func (s *testPutEntity) Write(p []byte) (int, error) { + if s.f != nil { + s.f(p) + } + return 0, s.err +} + +func (s *testPutEntity) Transform(_ context.Context, u ProcUnit, h ...ProcUnitHandler) error { + if s.f != nil { + s.f(u, h) + } + return s.err +} + +func (s *testPutEntity) GetSGInfo(_ context.Context, cid CID, group []ObjectID) (*StorageGroup, error) { + if s.f != nil { + s.f(cid, group) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*StorageGroup), nil +} + +func (s *testPutEntity) Epoch() uint64 { return s.res.(uint64) } + +func TestNewTransformer(t *testing.T) { + validParams := Params{ + SGInfoReceiver: new(testPutEntity), + EpochReceiver: new(testPutEntity), + SizeLimit: 1, + Verifier: new(testPutEntity), + } + + t.Run("valid params", func(t *testing.T) { + res, err := NewTransformer(validParams) + require.NoError(t, err) + require.NotNil(t, res) + }) + t.Run("non-positive size", func(t *testing.T) { + p := validParams + p.SizeLimit = 0 + _, err := NewTransformer(p) + require.EqualError(t, err, errors.Wrap(errInvalidSizeLimit, transformerInstanceFailMsg).Error()) + }) + t.Run("empty SG info receiver", func(t *testing.T) { + p := validParams + p.SGInfoReceiver = nil + _, err := NewTransformer(p) + require.EqualError(t, err, errors.Wrap(errEmptySGInfoRecv, transformerInstanceFailMsg).Error()) + }) + t.Run("empty epoch receiver", func(t *testing.T) { + p := validParams + p.EpochReceiver = nil + _, err := NewTransformer(p) + require.EqualError(t, err, errors.Wrap(errEmptyEpochReceiver, transformerInstanceFailMsg).Error()) + }) + t.Run("empty object verifier", func(t *testing.T) { + p := validParams + p.Verifier = nil + _, err := NewTransformer(p) + require.EqualError(t, err, errors.Wrap(errEmptyVerifier, transformerInstanceFailMsg).Error()) + }) +} + +func Test_transformer(t *testing.T) { + ctx := context.TODO() + + u := ProcUnit{ + Head: &Object{ + Payload: testData(t, 10), + }, + Payload: new(emptyReader), + } + + handlers := []ProcUnitHandler{func(context.Context, ProcUnit) error { return nil }} + + t.Run("preliminary transformation failure", func(t *testing.T) { + // create custom error for test + pErr := internal.Error("test error for prelim transformer") + + s := &transformer{ + tPrelim: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct prelim transformer params", func(t *testing.T) { + require.Equal(t, u, items[0]) + require.Empty(t, items[1]) + }) + }, + err: pErr, // force Transformer to return pErr + }, + } + + // ascertain that error returns as expected + require.EqualError(t, s.Transform(ctx, u, handlers...), pErr.Error()) + }) + + t.Run("size limiter error/correct sign processing", func(t *testing.T) { + // create custom error for test + sErr := internal.Error("test error for signer") + lErr := internal.Error("test error for size limiter") + + s := &transformer{ + tPrelim: new(testPutEntity), + tSizeLim: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct size limiter params", func(t *testing.T) { + require.Equal(t, u, items[0]) + hs := items[1].([]ProcUnitHandler) + require.Len(t, hs, 1) + require.EqualError(t, hs[0](ctx, u), sErr.Error()) + }) + }, + err: lErr, // force Transformer to return lErr + }, + tSign: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct signer params", func(t *testing.T) { + require.Equal(t, u, items[0]) + require.Equal(t, handlers, items[1]) + }) + }, + err: sErr, // force Transformer to return sErr + }, + } + + // ascertain that error returns as expected + require.EqualError(t, s.Transform(ctx, u, handlers...), lErr.Error()) + }) +} + +func Test_preliminaryTransformer(t *testing.T) { + ctx := context.TODO() + + u := ProcUnit{ + Head: &Object{ + Payload: testData(t, 10), + }, + Payload: new(emptyReader), + } + + t.Run("field moulder failure", func(t *testing.T) { + // create custom error for test + mErr := internal.Error("test error for field moulder") + + s := &preliminaryTransformer{ + fMoulder: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct field moulder params", func(t *testing.T) { + require.Equal(t, u, items[0]) + require.Empty(t, items[1]) + }) + }, + err: mErr, // force Transformer to return mErr + }, + } + + // ascertain that error returns as expected + require.EqualError(t, s.Transform(ctx, u), mErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + // create custom error for test + sgErr := internal.Error("test error for SG moulder") + + s := &preliminaryTransformer{ + fMoulder: new(testPutEntity), + sgMoulder: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct field moulder params", func(t *testing.T) { + require.Equal(t, u, items[0]) + require.Empty(t, items[1]) + }) + }, + err: sgErr, // force Transformer to return sgErr + }, + } + + // ascertain that error returns as expected + require.EqualError(t, s.Transform(ctx, u), sgErr.Error()) + }) +} + +func Test_readChunk(t *testing.T) { + t.Run("empty slice", func(t *testing.T) { + t.Run("missing checksum header", func(t *testing.T) { + obj := new(Object) + + _, h := obj.LastHeader(object.HeaderType(object.PayloadChecksumHdr)) + require.Nil(t, h) + + require.NoError(t, readChunk(ProcUnit{ + Head: obj, + Payload: bytes.NewBuffer(testData(t, 10)), + }, nil, nil, nil)) + + _, h = obj.LastHeader(object.HeaderType(object.PayloadChecksumHdr)) + + require.NotNil(t, h) + require.Equal(t, sha256.New().Sum(nil), h.Value.(*object.Header_PayloadChecksum).PayloadChecksum) + }) + + t.Run("existing checksum header", func(t *testing.T) { + h := &object.Header_PayloadChecksum{PayloadChecksum: testData(t, 10)} + + obj := &Object{Headers: []object.Header{{Value: h}}} + + require.NoError(t, readChunk(ProcUnit{ + Head: obj, + Payload: bytes.NewBuffer(testData(t, 10)), + }, nil, nil, nil)) + + require.NotNil(t, h) + require.Equal(t, sha256.New().Sum(nil), h.PayloadChecksum) + }) + }) + + t.Run("non-empty slice", func(t *testing.T) { + t.Run("non-full data", func(t *testing.T) { + var ( + size = 10 + buf = testData(t, size) + r = bytes.NewBuffer(buf[:size-1]) + ) + + require.EqualError(t, + readChunk(ProcUnit{Head: new(Object), Payload: r}, buf, nil, nil), + ErrPayloadEOF.Error(), + ) + }) + + t.Run("hash accumulator write", func(t *testing.T) { + var ( + d = testData(t, 10) + srcHash = sha256.Sum256(d) + hAcc = sha256.New() + buf = bytes.NewBuffer(d) + b = make([]byte, len(d)) + obj = new(Object) + + srcHomoHash = hash.Sum(d) + homoHashHdr = &object.Header_HomoHash{HomoHash: hash.Sum(make([]byte, 0))} + ) + + t.Run("failure", func(t *testing.T) { + hErr := internal.Error("test error for hash writer") + b := testData(t, len(d)) + + require.EqualError(t, readChunk(EmptyPayloadUnit(new(Object)), b, &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct accumulator params", func(t *testing.T) { + require.Equal(t, b, items[0]) + }) + }, + err: hErr, + }, nil), hErr.Error()) + }) + + require.NoError(t, readChunk(ProcUnit{Head: obj, Payload: buf}, b, hAcc, homoHashHdr)) + + _, h := obj.LastHeader(object.HeaderType(object.PayloadChecksumHdr)) + require.NotNil(t, h) + require.Equal(t, srcHash[:], h.Value.(*object.Header_PayloadChecksum).PayloadChecksum) + + require.Equal(t, srcHash[:], hAcc.Sum(nil)) + require.Equal(t, srcHomoHash, homoHashHdr.HomoHash) + }) + }) +} + +func Test_headSigner(t *testing.T) { + ctx := context.TODO() + + t.Run("invalid input", func(t *testing.T) { + t.Run("missing token", func(t *testing.T) { + u := ProcUnit{Head: new(Object)} + require.Error(t, u.Head.Verify()) + s := &headSigner{verifier: &testPutEntity{err: internal.Error("")}} + require.EqualError(t, s.Transform(ctx, u), errNoToken.Error()) + }) + + t.Run("with token", func(t *testing.T) { + u := ProcUnit{Head: new(Object)} + + verifier, err := implementations.NewLocalHeadIntegrityVerifier(core.NewNeoKeyVerifier()) + require.NoError(t, err) + + require.Error(t, u.Head.Verify()) + + privateToken, err := session.NewPrivateToken(0) + require.NoError(t, err) + ctx := context.WithValue(ctx, PrivateSessionToken, privateToken) + + s := &headSigner{ + verifier: &testPutEntity{ + err: internal.Error(""), + }, + } + + key := &privateToken.PrivateKey().PublicKey + + u.Head.SystemHeader.OwnerID, err = refs.NewOwnerID(key) + require.NoError(t, err) + u.Head.AddHeader(&object.Header{ + Value: &object.Header_PublicKey{ + PublicKey: &object.PublicKey{ + Value: crypto.MarshalPublicKey(key), + }, + }, + }) + + require.NoError(t, s.Transform(ctx, u, func(_ context.Context, unit ProcUnit) error { + require.NoError(t, verifier.Verify(ctx, unit.Head)) + _, h := unit.Head.LastHeader(object.HeaderType(object.IntegrityHdr)) + require.NotNil(t, h) + d, err := objutil.MarshalHeaders(unit.Head, len(unit.Head.Headers)-1) + require.NoError(t, err) + cs := sha256.Sum256(d) + require.Equal(t, cs[:], h.Value.(*object.Header_Integrity).Integrity.GetHeadersChecksum()) + return nil + })) + + t.Run("valid input", func(t *testing.T) { + s := &headSigner{verifier: new(testPutEntity)} + require.NoError(t, s.Transform(ctx, u, func(_ context.Context, unit ProcUnit) error { + require.Equal(t, u, unit) + return nil + })) + }) + }) + }) +} + +func Test_fieldMoulder(t *testing.T) { + ctx := context.TODO() + epoch := uint64(100) + + fMoulder := &fieldMoulder{epochRecv: &testPutEntity{res: epoch}} + + t.Run("no token", func(t *testing.T) { + require.EqualError(t, new(fieldMoulder).Transform(ctx, ProcUnit{}), errNoToken.Error()) + }) + + t.Run("with token", func(t *testing.T) { + token := new(service.Token) + token.SetID(service.TokenID{1, 2, 3}) + + ctx := context.WithValue(ctx, PublicSessionToken, token) + + u := ProcUnit{Head: new(Object)} + + _, h := u.Head.LastHeader(object.HeaderType(object.TokenHdr)) + require.Nil(t, h) + + require.NoError(t, fMoulder.Transform(ctx, u)) + + _, h = u.Head.LastHeader(object.HeaderType(object.TokenHdr)) + require.Equal(t, token, h.Value.(*object.Header_Token).Token) + + require.False(t, u.Head.SystemHeader.ID.Empty()) + require.NotZero(t, u.Head.SystemHeader.CreatedAt.UnixTime) + require.Equal(t, epoch, u.Head.SystemHeader.CreatedAt.Epoch) + require.Equal(t, uint64(1), u.Head.SystemHeader.Version) + }) +} + +func Test_sgMoulder(t *testing.T) { + ctx := context.TODO() + + t.Run("invalid SG linking", func(t *testing.T) { + t.Run("w/ header and w/o links", func(t *testing.T) { + obj := new(Object) + obj.SetStorageGroup(new(storagegroup.StorageGroup)) + require.EqualError(t, new(sgMoulder).Transform(ctx, ProcUnit{Head: obj}), ErrInvalidSGLinking.Error()) + }) + + t.Run("w/o header and w/ links", func(t *testing.T) { + obj := new(Object) + addLink(obj, object.Link_StorageGroup, ObjectID{}) + require.EqualError(t, new(sgMoulder).Transform(ctx, ProcUnit{Head: obj}), ErrInvalidSGLinking.Error()) + }) + }) + + t.Run("non-SG", func(t *testing.T) { + obj := new(Object) + require.NoError(t, new(sgMoulder).Transform(ctx, ProcUnit{Head: obj})) + }) + + t.Run("receive SG info", func(t *testing.T) { + cid := testObjectAddress(t).CID + group := make([]ObjectID, 5) + for i := range group { + group[i] = testObjectAddress(t).ObjectID + } + + t.Run("failure", func(t *testing.T) { + obj := &Object{SystemHeader: object.SystemHeader{CID: cid}} + + obj.SetStorageGroup(new(storagegroup.StorageGroup)) + for i := range group { + addLink(obj, object.Link_StorageGroup, group[i]) + } + + sgErr := internal.Error("test error for SG info receiver") + + mSG := &sgMoulder{ + sgInfoRecv: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct SG info receiver params", func(t *testing.T) { + cp := make([]ObjectID, len(group)) + copy(cp, group) + sort.Sort(storagegroup.IDList(cp)) + require.Equal(t, cid, items[0]) + require.Equal(t, cp, items[1]) + }) + }, + err: sgErr, + }, + } + + require.EqualError(t, mSG.Transform(ctx, ProcUnit{Head: obj}), sgErr.Error()) + }) + }) + + t.Run("correct result", func(t *testing.T) { + obj := new(Object) + obj.SetStorageGroup(new(storagegroup.StorageGroup)) + addLink(obj, object.Link_StorageGroup, ObjectID{}) + + sgInfo := &storagegroup.StorageGroup{ + ValidationDataSize: 19, + ValidationHash: hash.Sum(testData(t, 10)), + } + + mSG := &sgMoulder{ + sgInfoRecv: &testPutEntity{ + res: sgInfo, + }, + } + + require.NoError(t, mSG.Transform(ctx, ProcUnit{Head: obj})) + + _, h := obj.LastHeader(object.HeaderType(object.StorageGroupHdr)) + require.NotNil(t, h) + require.Equal(t, sgInfo, h.Value.(*object.Header_StorageGroup).StorageGroup) + }) +} + +func Test_sizeLimiter(t *testing.T) { + ctx := context.TODO() + + t.Run("limit entry", func(t *testing.T) { + payload := testData(t, 10) + payloadSize := uint64(len(payload) - 1) + + u := ProcUnit{ + Head: &Object{SystemHeader: object.SystemHeader{ + PayloadLength: payloadSize, + }}, + Payload: bytes.NewBuffer(payload[:payloadSize]), + } + + sl := &sizeLimiter{limit: payloadSize} + + t.Run("cut payload", func(t *testing.T) { + require.Error(t, sl.Transform(ctx, ProcUnit{ + Head: &Object{SystemHeader: object.SystemHeader{PayloadLength: payloadSize}}, + Payload: bytes.NewBuffer(payload[:payloadSize-1]), + })) + }) + + require.NoError(t, sl.Transform(ctx, u, func(_ context.Context, unit ProcUnit) error { + _, err := unit.Payload.Read(make([]byte, 1)) + require.EqualError(t, err, io.EOF.Error()) + require.Equal(t, payload[:payloadSize], unit.Head.Payload) + _, h := unit.Head.LastHeader(object.HeaderType(object.HomoHashHdr)) + require.NotNil(t, h) + require.Equal(t, hash.Sum(payload[:payloadSize]), h.Value.(*object.Header_HomoHash).HomoHash) + return nil + })) + }) + + t.Run("limit exceed", func(t *testing.T) { + payload := testData(t, 100) + sizeLimit := uint64(len(payload)) / 13 + + pToken, err := session.NewPrivateToken(0) + require.NoError(t, err) + + srcObj := &object.Object{ + SystemHeader: object.SystemHeader{ + Version: 12, + PayloadLength: uint64(len(payload)), + ID: testObjectAddress(t).ObjectID, + OwnerID: object.OwnerID{1, 2, 3}, + CID: testObjectAddress(t).CID, + }, + Headers: []object.Header{ + {Value: &object.Header_UserHeader{UserHeader: &object.UserHeader{Key: "key", Value: "value"}}}, + }, + } + + u := ProcUnit{ + Head: srcObj, + Payload: bytes.NewBuffer(payload), + } + + epoch := uint64(77) + + sl := &sizeLimiter{ + limit: sizeLimit, + epochRecv: &testPutEntity{res: epoch}, + } + + t.Run("no token", func(t *testing.T) { + require.EqualError(t, sl.Transform(ctx, ProcUnit{ + Head: &Object{ + SystemHeader: object.SystemHeader{ + PayloadLength: uint64(len(payload)), + }, + }, + Payload: bytes.NewBuffer(payload), + }), errNoToken.Error()) + }) + + ctx := context.WithValue(ctx, PrivateSessionToken, pToken) + + t.Run("cut payload", func(t *testing.T) { + require.Error(t, sl.Transform(ctx, ProcUnit{ + Head: &Object{ + SystemHeader: object.SystemHeader{ + PayloadLength: uint64(len(payload)) + 1, + }, + }, + Payload: bytes.NewBuffer(payload), + })) + }) + + objs := make([]Object, 0) + + t.Run("handler error", func(t *testing.T) { + hErr := internal.Error("test error for handler") + + require.EqualError(t, sl.Transform(ctx, ProcUnit{ + Head: &Object{ + SystemHeader: object.SystemHeader{PayloadLength: uint64(len(payload))}, + Headers: make([]object.Header, 0), + }, + Payload: bytes.NewBuffer(payload), + }, func(context.Context, ProcUnit) error { return hErr }), hErr.Error()) + }) + + require.NoError(t, sl.Transform(ctx, u, func(_ context.Context, unit ProcUnit) error { + _, err := unit.Payload.Read(make([]byte, 1)) + require.EqualError(t, err, io.EOF.Error()) + objs = append(objs, *unit.Head.Copy()) + return nil + })) + + ln := len(objs) + + res := make([]byte, 0, len(payload)) + + zObj := objs[ln-1] + require.Zero(t, zObj.SystemHeader.PayloadLength) + require.Empty(t, zObj.Payload) + require.Empty(t, zObj.Links(object.Link_Next)) + require.Empty(t, zObj.Links(object.Link_Previous)) + require.Empty(t, zObj.Links(object.Link_Parent)) + children := zObj.Links(object.Link_Child) + require.Len(t, children, ln-1) + for i := range objs[:ln-1] { + require.Equal(t, objs[i].SystemHeader.ID, children[i]) + } + + for i := range objs[:ln-1] { + res = append(res, objs[i].Payload...) + if i == 0 { + require.Equal(t, objs[i].Links(object.Link_Next)[0], objs[i+1].SystemHeader.ID) + require.True(t, objs[i].Links(object.Link_Previous)[0].Empty()) + } else if i < ln-2 { + require.Equal(t, objs[i].Links(object.Link_Previous)[0], objs[i-1].SystemHeader.ID) + require.Equal(t, objs[i].Links(object.Link_Next)[0], objs[i+1].SystemHeader.ID) + } else { + _, h := objs[i].LastHeader(object.HeaderType(object.HomoHashHdr)) + require.NotNil(t, h) + require.Equal(t, hash.Sum(payload), h.Value.(*object.Header_HomoHash).HomoHash) + require.Equal(t, objs[i].Links(object.Link_Previous)[0], objs[i-1].SystemHeader.ID) + require.True(t, objs[i].Links(object.Link_Next)[0].Empty()) + } + } + + require.Equal(t, payload, res) + }) +} + +// testData returns size bytes of random data. +func testData(t *testing.T, size int) []byte { + res := make([]byte, size) + _, err := rand.Read(res) + require.NoError(t, err) + return res +} + +// testObjectAddress returns new random object address. +func testObjectAddress(t *testing.T) refs.Address { + oid, err := refs.NewObjectID() + require.NoError(t, err) + return refs.Address{CID: refs.CIDForBytes(testData(t, refs.CIDSize)), ObjectID: oid} +} + +func TestIntegration(t *testing.T) { + ownerKey := test.DecodeKey(1) + + ownerID, err := refs.NewOwnerID(&ownerKey.PublicKey) + require.NoError(t, err) + + privToken, err := session.NewPrivateToken(0) + require.NoError(t, err) + + pkBytes, err := session.PublicSessionToken(privToken) + require.NoError(t, err) + + ctx := context.WithValue(context.TODO(), PrivateSessionToken, privToken) + + pubToken := new(service.Token) + pubToken.SetID(service.TokenID{1, 2, 3}) + pubToken.SetSessionKey(pkBytes) + pubToken.SetOwnerID(ownerID) + pubToken.SetOwnerKey(crypto.MarshalPublicKey(&ownerKey.PublicKey)) + require.NoError(t, service.AddSignatureWithKey(ownerKey, service.NewSignedSessionToken(pubToken))) + + ctx = context.WithValue(ctx, PublicSessionToken, pubToken) + + t.Run("non-SG object", func(t *testing.T) { + t.Run("with split", func(t *testing.T) { + tr, err := NewTransformer(Params{ + SGInfoReceiver: new(testPutEntity), + EpochReceiver: &testPutEntity{res: uint64(1)}, + SizeLimit: 13, + Verifier: &testPutEntity{ + err: internal.Error(""), // force verifier to return non-nil error + }, + }) + require.NoError(t, err) + + payload := make([]byte, 20) + _, err = rand.Read(payload) + require.NoError(t, err) + + obj := &Object{ + SystemHeader: object.SystemHeader{ + PayloadLength: uint64(len(payload)), + CID: CID{3}, + }, + Headers: []object.Header{ + {Value: &object.Header_UserHeader{UserHeader: &object.UserHeader{Key: "key", Value: "value"}}}, + }, + } + + obj.SystemHeader.OwnerID = ownerID + + obj.SetHeader(&object.Header{ + Value: &object.Header_Token{ + Token: pubToken, + }, + }) + + testTransformer(t, ctx, ProcUnit{ + Head: obj, + Payload: bytes.NewBuffer(payload), + }, tr, payload) + }) + }) +} + +func testTransformer(t *testing.T, ctx context.Context, u ProcUnit, tr Transformer, src []byte) { + objList := make([]Object, 0) + verifier, err := implementations.NewLocalHeadIntegrityVerifier(core.NewNeoKeyVerifier()) + require.NoError(t, err) + + require.NoError(t, tr.Transform(ctx, u, func(_ context.Context, unit ProcUnit) error { + require.NoError(t, verifier.Verify(ctx, unit.Head)) + objList = append(objList, *unit.Head.Copy()) + return nil + })) + + reverse := NewRestorePipeline(SplitRestorer()) + + res, err := reverse.Restore(ctx, objList...) + require.NoError(t, err) + + integrityVerifier, err := implementations.NewLocalIntegrityVerifier(core.NewNeoKeyVerifier()) + require.NoError(t, err) + require.NoError(t, integrityVerifier.Verify(ctx, &res[0])) + + require.Equal(t, src, res[0].Payload) + _, h := res[0].LastHeader(object.HeaderType(object.HomoHashHdr)) + require.True(t, hash.Sum(src).Equal(h.Value.(*object.Header_HomoHash).HomoHash)) +} + +func addLink(o *Object, t object.Link_Type, id ObjectID) { + o.AddHeader(&object.Header{Value: &object.Header_Link{ + Link: &object.Link{Type: t, ID: id}, + }}) +} diff --git a/lib/transformer/restore.go b/lib/transformer/restore.go new file mode 100644 index 0000000000..6242bb7611 --- /dev/null +++ b/lib/transformer/restore.go @@ -0,0 +1,126 @@ +package transformer + +import ( + "context" + "sync" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/pkg/errors" +) + +type ( + // ObjectRestorer is an interface of object restorer. + ObjectRestorer interface { + Type() object.Transform_Type + Restore(context.Context, ...Object) ([]Object, error) + } + + restorePipeline struct { + ObjectRestorer + *sync.RWMutex + items map[object.Transform_Type]ObjectRestorer + } + + splitRestorer struct{} +) + +var errEmptyObjList = errors.New("object list is empty") + +var errMissingParentLink = errors.New("missing parent link") + +func (s *restorePipeline) Restore(ctx context.Context, srcObjs ...Object) ([]Object, error) { + if len(srcObjs) == 0 { + return nil, errEmptyInput + } + + s.RLock() + defer s.RUnlock() + + var ( + objs = srcObjs + err error + ) + + for { + _, th := objs[0].LastHeader(object.HeaderType(object.TransformHdr)) + if th == nil { + break + } + + transform := th.Value.(*object.Header_Transform).Transform + + tr, ok := s.items[transform.Type] + if !ok { + return nil, errors.Errorf("missing restorer (%s)", transform.Type) + } + + if objs, err = tr.Restore(ctx, objs...); err != nil { + return nil, errors.Wrapf(err, "restoration failed (%s)", transform.Type) + } + } + + return objs, nil +} + +// NewRestorePipeline is a constructor of the pipeline of object restorers. +func NewRestorePipeline(t ...ObjectRestorer) ObjectRestorer { + m := make(map[object.Transform_Type]ObjectRestorer, len(t)) + + for i := range t { + m[t[i].Type()] = t[i] + } + + return &restorePipeline{ + RWMutex: new(sync.RWMutex), + items: m, + } +} + +func (*splitRestorer) Type() object.Transform_Type { + return object.Transform_Split +} + +func (*splitRestorer) Restore(ctx context.Context, objs ...Object) ([]Object, error) { + if len(objs) == 0 { + return nil, errEmptyObjList + } + + chain, err := GetChain(objs...) + if err != nil { + return nil, errors.Wrap(err, "could not get chain of objects") + } + + obj := chain[len(chain)-1] + + var ( + size uint64 + p = make([]byte, 0, len(chain[0].Payload)*len(chain)) + ) + + for j := 0; j < len(chain); j++ { + p = append(p, chain[j].Payload...) + size += chain[j].SystemHeader.PayloadLength + } + + obj.SystemHeader.PayloadLength = size + obj.Payload = p + + parent, err := lastLink(&obj, object.Link_Parent) + if err != nil { + return nil, errMissingParentLink + } + + obj.SystemHeader.ID = parent + + err = deleteTransformer(&obj, object.Transform_Split) + if err != nil { + return nil, err + } + + return []Object{obj}, nil +} + +// SplitRestorer is a splitted object restorer's constructor. +func SplitRestorer() ObjectRestorer { + return new(splitRestorer) +} diff --git a/lib/transformer/transformer.go b/lib/transformer/transformer.go new file mode 100644 index 0000000000..0016035b31 --- /dev/null +++ b/lib/transformer/transformer.go @@ -0,0 +1,528 @@ +package transformer + +import ( + "context" + "crypto/sha256" + "io" + "sort" + "time" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/pkg/errors" +) + +type ( + // Type is a type alias of + // Type from object package of neofs-api-go. + Type = object.Transform_Type + + // ProcUnit groups the information about transforming unit. + ProcUnit struct { + Head *Object + Payload io.Reader + } + + // ProcUnitHandler is a handling ProcUnit function. + ProcUnitHandler func(context.Context, ProcUnit) error + + // Transformer is an interface of object transformer. + Transformer interface { + Transform(context.Context, ProcUnit, ...ProcUnitHandler) error + } + + // EpochReceiver is an interface of epoch number container with read access. + EpochReceiver interface { + Epoch() uint64 + } + + transformer struct { + tPrelim Transformer + tSizeLim Transformer + tSign Transformer + } + + preliminaryTransformer struct { + fMoulder Transformer + sgMoulder Transformer + } + + fieldMoulder struct { + epochRecv EpochReceiver + } + + sgMoulder struct { + sgInfoRecv storagegroup.InfoReceiver + } + + sizeLimiter struct { + limit uint64 + epochRecv EpochReceiver + } + + headSigner struct { + verifier objutil.Verifier + } + + emptyReader struct{} + + // Params groups the parameters of object transformer's constructor. + Params struct { + SGInfoReceiver storagegroup.InfoReceiver + EpochReceiver EpochReceiver + SizeLimit uint64 + Verifier objutil.Verifier + } +) + +// ErrPayloadEOF is returned by Transformer that +// received unexpected end of object payload. +const ErrPayloadEOF = internal.Error("payload EOF") + +const ( + verifyHeadersCount = 2 // payload checksum, integrity + splitHeadersCount = 4 // flag, parent, left, right + + errEmptyInput = internal.Error("empty input") + + transformerInstanceFailMsg = "could not create transformer instance" + errEmptySGInfoRecv = internal.Error("empty storage group info receivers") + errInvalidSizeLimit = internal.Error("non-positive object size limit") + errEmptyEpochReceiver = internal.Error("empty epoch receiver") + errEmptyVerifier = internal.Error("empty object verifier") + + // ErrInvalidSGLinking is returned by Transformer that received + // an object with broken storage group links. + ErrInvalidSGLinking = internal.Error("invalid storage group linking") + + // PrivateSessionToken is a context key for session.PrivateToken. + PrivateSessionToken = "private token" + + // PublicSessionToken is a context key for service.SessionToken. + PublicSessionToken = "public token" + + errNoToken = internal.Error("no token provided") +) + +var errChainNotFound = errors.New("chain not found") + +var errCutChain = errors.New("GetChain failed: chain is not full") + +var errMissingTransformHdr = errors.New("cannot find transformer header") + +// NewTransformer is an object transformer's constructor. +func NewTransformer(p Params) (Transformer, error) { + switch { + case p.SizeLimit <= 0: + return nil, errors.Wrap(errInvalidSizeLimit, transformerInstanceFailMsg) + case p.EpochReceiver == nil: + return nil, errors.Wrap(errEmptyEpochReceiver, transformerInstanceFailMsg) + case p.SGInfoReceiver == nil: + return nil, errors.Wrap(errEmptySGInfoRecv, transformerInstanceFailMsg) + case p.Verifier == nil: + return nil, errors.Wrap(errEmptyVerifier, transformerInstanceFailMsg) + } + + return &transformer{ + tPrelim: &preliminaryTransformer{ + fMoulder: &fieldMoulder{ + epochRecv: p.EpochReceiver, + }, + sgMoulder: &sgMoulder{ + sgInfoRecv: p.SGInfoReceiver, + }, + }, + tSizeLim: &sizeLimiter{ + limit: p.SizeLimit, + epochRecv: p.EpochReceiver, + }, + tSign: &headSigner{ + verifier: p.Verifier, + }, + }, nil +} + +func (s *transformer) Transform(ctx context.Context, unit ProcUnit, handlers ...ProcUnitHandler) error { + if err := s.tPrelim.Transform(ctx, unit); err != nil { + return err + } + + return s.tSizeLim.Transform(ctx, unit, func(ctx context.Context, unit ProcUnit) error { + return s.tSign.Transform(ctx, unit, handlers...) + }) +} + +func (s *preliminaryTransformer) Transform(ctx context.Context, unit ProcUnit, _ ...ProcUnitHandler) error { + if err := s.fMoulder.Transform(ctx, unit); err != nil { + return err + } + + return s.sgMoulder.Transform(ctx, unit) +} + +// TODO: simplify huge function. +func (s *sizeLimiter) Transform(ctx context.Context, unit ProcUnit, handlers ...ProcUnitHandler) error { + if unit.Head.SystemHeader.PayloadLength <= s.limit { + homoHashHdr := &object.Header_HomoHash{HomoHash: hash.Sum(make([]byte, 0))} + + unit.Head.AddHeader(&object.Header{Value: homoHashHdr}) + + buf := make([]byte, unit.Head.SystemHeader.PayloadLength) + + if err := readChunk(unit, buf, nil, homoHashHdr); err != nil { + return err + } + + unit.Head.Payload = buf + + return procHandlers(ctx, EmptyPayloadUnit(unit.Head), handlers...) + } + + var ( + err error + buf = make([]byte, s.limit) + hAcc = sha256.New() + srcHdrLen = len(unit.Head.Headers) + pObj = unit.Head + resObj = ProcUnit{ + Head: &Object{ + SystemHeader: object.SystemHeader{ + Version: pObj.SystemHeader.Version, + OwnerID: pObj.SystemHeader.OwnerID, + CID: pObj.SystemHeader.CID, + CreatedAt: object.CreationPoint{ + UnixTime: time.Now().Unix(), + Epoch: s.epochRecv.Epoch(), + }, + }, + }, + Payload: unit.Payload, + } + left, right = &object.Link{Type: object.Link_Previous}, &object.Link{Type: object.Link_Next} + hashAccHdr, hashHdr = new(object.Header_PayloadChecksum), new(object.Header_PayloadChecksum) + homoHashAccHdr = &object.Header_HomoHash{HomoHash: hash.Sum(make([]byte, 0))} + childCount = pObj.SystemHeader.PayloadLength/s.limit + 1 + ) + + if right.ID, err = refs.NewObjectID(); err != nil { + return err + } + + splitHeaders := make([]object.Header, 0, 3*verifyHeadersCount+splitHeadersCount+childCount) + + splitHeaders = append(splitHeaders, pObj.Headers...) + splitHeaders = append(splitHeaders, []object.Header{ + {Value: &object.Header_Transform{Transform: &object.Transform{Type: object.Transform_Split}}}, + {Value: &object.Header_Link{Link: &object.Link{ + Type: object.Link_Parent, + ID: unit.Head.SystemHeader.ID, + }}}, + {Value: &object.Header_Link{Link: left}}, + {Value: &object.Header_Link{Link: right}}, + {Value: hashHdr}, + {Value: &object.Header_Integrity{Integrity: new(object.IntegrityHeader)}}, + {Value: homoHashAccHdr}, + {Value: hashAccHdr}, + {Value: &object.Header_Integrity{Integrity: new(object.IntegrityHeader)}}, + }...) + + children := splitHeaders[srcHdrLen+2*verifyHeadersCount+splitHeadersCount+1:] + pObj.Headers = splitHeaders[:srcHdrLen+2*verifyHeadersCount+splitHeadersCount] + + for tail := pObj.SystemHeader.PayloadLength; tail > 0; tail -= min(tail, s.limit) { + size := min(tail, s.limit) + + resObj.Head.Headers = pObj.Headers[:len(pObj.Headers)-verifyHeadersCount-1] + if err = readChunk(resObj, buf[:size], hAcc, homoHashAccHdr); err != nil { + return err + } + + resObj.Head.SystemHeader.PayloadLength = size + resObj.Head.Payload = buf[:size] + left.ID, resObj.Head.SystemHeader.ID = resObj.Head.SystemHeader.ID, right.ID + + if tail <= s.limit { + right.ID = ObjectID{} + + temp := make([]object.Header, verifyHeadersCount+1) // +1 for homomorphic hash + + copy(temp, pObj.Headers[srcHdrLen:]) + + hashAccHdr.PayloadChecksum = hAcc.Sum(nil) + + copy(pObj.Headers[srcHdrLen:srcHdrLen+verifyHeadersCount+1], + pObj.Headers[len(pObj.Headers)-verifyHeadersCount:]) + + resObj.Head.Headers = pObj.Headers[:srcHdrLen+verifyHeadersCount] + + if err = signWithToken(ctx, &Object{ + SystemHeader: pObj.SystemHeader, + Headers: resObj.Head.Headers, + }); err != nil { + return err + } + + copy(pObj.Headers[srcHdrLen+2*(verifyHeadersCount+1):], + pObj.Headers[srcHdrLen+verifyHeadersCount+1:srcHdrLen+verifyHeadersCount+splitHeadersCount]) + + copy(pObj.Headers[srcHdrLen+verifyHeadersCount+1:], temp) + + resObj.Head.Headers = pObj.Headers[:len(pObj.Headers)] + } else if right.ID, err = refs.NewObjectID(); err != nil { + return err + } + + if err := procHandlers(ctx, EmptyPayloadUnit(resObj.Head), handlers...); err != nil { + return err + } + + children = append(children, object.Header{Value: &object.Header_Link{Link: &object.Link{ + Type: object.Link_Child, + ID: resObj.Head.SystemHeader.ID, + }}}) + } + + pObj.SystemHeader.PayloadLength = 0 + pObj.Headers = append(pObj.Headers[:srcHdrLen], children...) + + if err := readChunk(unit, nil, nil, nil); err != nil { + return err + } + + return procHandlers(ctx, EmptyPayloadUnit(pObj), handlers...) +} + +func readChunk(unit ProcUnit, buf []byte, hAcc io.Writer, homoHashAcc *object.Header_HomoHash) (err error) { + var csHdr *object.Header_PayloadChecksum + + if _, v := unit.Head.LastHeader(object.HeaderType(object.PayloadChecksumHdr)); v == nil { + csHdr = new(object.Header_PayloadChecksum) + + unit.Head.Headers = append(unit.Head.Headers, object.Header{Value: csHdr}) + } else { + csHdr = v.Value.(*object.Header_PayloadChecksum) + } + + if _, err = io.ReadFull(unit.Payload, buf); err != nil && err != io.EOF { + if errors.Is(err, io.ErrUnexpectedEOF) { + err = ErrPayloadEOF + } + + return + } else if hAcc != nil { + if _, err = hAcc.Write(buf); err != nil { + return + } + } + + if homoHashAcc != nil { + if homoHashAcc.HomoHash, err = hash.Concat([]hash.Hash{homoHashAcc.HomoHash, hash.Sum(buf)}); err != nil { + return + } + } + + h := sha256.Sum256(buf) + csHdr.PayloadChecksum = h[:] + + return nil +} + +func (s *headSigner) Transform(ctx context.Context, unit ProcUnit, handlers ...ProcUnitHandler) error { + if s.verifier.Verify(ctx, unit.Head) != nil { + if err := signWithToken(ctx, unit.Head); err != nil { + return err + } + } + + return procHandlers(ctx, unit, handlers...) +} + +func signWithToken(ctx context.Context, obj *Object) error { + integrityHdr := new(object.IntegrityHeader) + + if pToken, ok := ctx.Value(PrivateSessionToken).(session.PrivateToken); !ok { + return errNoToken + } else if hdrData, err := objutil.MarshalHeaders(obj, len(obj.Headers)); err != nil { + return err + } else { + cs := sha256.Sum256(hdrData) + integrityHdr.SetHeadersChecksum(cs[:]) + if err = service.AddSignatureWithKey(pToken.PrivateKey(), integrityHdr); err != nil { + return err + } + } + + obj.AddHeader(&object.Header{Value: &object.Header_Integrity{Integrity: integrityHdr}}) + + return nil +} + +func (s *fieldMoulder) Transform(ctx context.Context, unit ProcUnit, _ ...ProcUnitHandler) (err error) { + token, ok := ctx.Value(PublicSessionToken).(*service.Token) + if !ok { + return errNoToken + } + + unit.Head.AddHeader(&object.Header{ + Value: &object.Header_Token{ + Token: token, + }, + }) + + if unit.Head.SystemHeader.ID.Empty() { + if unit.Head.SystemHeader.ID, err = refs.NewObjectID(); err != nil { + return + } + } + + if unit.Head.SystemHeader.CreatedAt.UnixTime == 0 { + unit.Head.SystemHeader.CreatedAt.UnixTime = time.Now().Unix() + } + + if unit.Head.SystemHeader.CreatedAt.Epoch == 0 { + unit.Head.SystemHeader.CreatedAt.Epoch = s.epochRecv.Epoch() + } + + if unit.Head.SystemHeader.Version == 0 { + unit.Head.SystemHeader.Version = 1 + } + + return nil +} + +func (s *sgMoulder) Transform(ctx context.Context, unit ProcUnit, _ ...ProcUnitHandler) error { + sgLinks := unit.Head.Links(object.Link_StorageGroup) + + group, err := unit.Head.StorageGroup() + + if nonEmptyList := len(sgLinks) > 0; (err == nil) != nonEmptyList { + return ErrInvalidSGLinking + } else if err != nil || !group.Empty() { + return nil + } + + sort.Sort(storagegroup.IDList(sgLinks)) + + sgInfo, err := s.sgInfoRecv.GetSGInfo(ctx, unit.Head.SystemHeader.CID, sgLinks) + if err != nil { + return err + } + + unit.Head.SetStorageGroup(sgInfo) + + return nil +} + +func procHandlers(ctx context.Context, unit ProcUnit, handlers ...ProcUnitHandler) error { + for i := range handlers { + if err := handlers[i](ctx, unit); err != nil { + return err + } + } + + return nil +} + +func (*emptyReader) Read([]byte) (n int, err error) { return 0, io.EOF } + +// EmptyPayloadUnit returns ProcUnit with Object from argument and empty payload reader +// that always returns (0, io.EOF). +func EmptyPayloadUnit(head *Object) ProcUnit { return ProcUnit{Head: head, Payload: new(emptyReader)} } + +func min(a, b uint64) uint64 { + if a < b { + return a + } + + return b +} + +// GetChain builds a list of objects in the hereditary chain. +// In case of impossibility to do this, an error is returned. +func GetChain(srcObjs ...Object) ([]Object, error) { + var ( + err error + first, id ObjectID + res = make([]Object, 0, len(srcObjs)) + m = make(map[ObjectID]*Object, len(srcObjs)) + ) + + // Fill map with all objects + for i := range srcObjs { + m[srcObjs[i].SystemHeader.ID] = &srcObjs[i] + + prev, err := lastLink(&srcObjs[i], object.Link_Previous) + if err == nil && prev.Empty() { // then it is first + id, err = lastLink(&srcObjs[i], object.Link_Next) + if err != nil { + return nil, errors.Wrap(err, "GetChain failed: missing first object next links") + } + + first = srcObjs[i].SystemHeader.ID + } + } + + // Check first presence + if first.Empty() { + return nil, errChainNotFound + } + + res = append(res, *m[first]) + + // Iterate chain + for count := 0; !id.Empty() && count < len(srcObjs); count++ { + nextObj, ok := m[id] + if !ok { + return nil, errors.Errorf("GetChain failed: missing next object %s", id) + } + + id, err = lastLink(nextObj, object.Link_Next) + if err != nil { + return nil, errors.Wrap(err, "GetChain failed: missing object next links") + } + + res = append(res, *nextObj) + } + + // Check last chain element has empty next (prevent cut chain) + id, err = lastLink(&res[len(res)-1], object.Link_Next) + if err != nil { + return nil, errors.Wrap(err, "GetChain failed: missing object next links") + } else if !id.Empty() { + return nil, errCutChain + } + + return res, nil +} + +func deleteTransformer(o *Object, t object.Transform_Type) error { + n, th := o.LastHeader(object.HeaderType(object.TransformHdr)) + if th == nil || th.Value.(*object.Header_Transform).Transform.Type != t { + return errMissingTransformHdr + } + + o.Headers = o.Headers[:n] + + return nil +} + +func lastLink(o *Object, t object.Link_Type) (res ObjectID, err error) { + for i := len(o.Headers) - 1; i >= 0; i-- { + if v, ok := o.Headers[i].Value.(*object.Header_Link); ok { + if v.Link.GetType() == t { + res = v.Link.ID + return + } + } + } + + err = errors.Errorf("object.lastLink: links of type %s not found", t) + + return +} diff --git a/lib/transport/connection.go b/lib/transport/connection.go new file mode 100644 index 0000000000..bb051b4a94 --- /dev/null +++ b/lib/transport/connection.go @@ -0,0 +1,39 @@ +package transport + +import ( + "sync/atomic" + + manet "github.com/multiformats/go-multiaddr-net" +) + +type ( + // Connection is an interface of network connection. + Connection interface { + manet.Conn + Closed() bool + } + + conn struct { + manet.Conn + closed *int32 + } +) + +func newConnection(con manet.Conn) Connection { + return &conn{ + Conn: con, + closed: new(int32), + } +} + +// Closed checks that connection closed. +func (c *conn) Closed() bool { return atomic.LoadInt32(c.closed) == 1 } + +// Close connection and write state. +func (c *conn) Close() error { + if atomic.CompareAndSwapInt32(c.closed, 0, 1) { + return c.Conn.Close() + } + + return nil +} diff --git a/lib/transport/object.go b/lib/transport/object.go new file mode 100644 index 0000000000..0965265e15 --- /dev/null +++ b/lib/transport/object.go @@ -0,0 +1,107 @@ +package transport + +import ( + "context" + "io" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" +) + +type ( + // ObjectTransport is an interface of the executor of object remote operations. + ObjectTransport interface { + Transport(context.Context, ObjectTransportParams) + } + + // ObjectTransportParams groups the parameters of remote object operation. + ObjectTransportParams struct { + TransportInfo MetaInfo + TargetNode multiaddr.Multiaddr + ResultHandler ResultHandler + } + + // ResultHandler is an interface of remote object operation's result handler. + ResultHandler interface { + HandleResult(context.Context, multiaddr.Multiaddr, interface{}, error) + } + + // MetaInfo is an interface of the container of cross-operation values. + MetaInfo interface { + GetTTL() uint32 + GetTimeout() time.Duration + service.SessionTokenSource + GetRaw() bool + Type() object.RequestType + service.BearerTokenSource + service.ExtendedHeadersSource + } + + // SearchInfo is an interface of the container of object Search operation parameters. + SearchInfo interface { + MetaInfo + GetCID() refs.CID + GetQuery() []byte + } + + // PutInfo is an interface of the container of object Put operation parameters. + PutInfo interface { + MetaInfo + GetHead() *object.Object + Payload() io.Reader + CopiesNumber() uint32 + } + + // AddressInfo is an interface of the container of object request by Address. + AddressInfo interface { + MetaInfo + GetAddress() refs.Address + } + + // GetInfo is an interface of the container of object Get operation parameters. + GetInfo interface { + AddressInfo + } + + // HeadInfo is an interface of the container of object Head operation parameters. + HeadInfo interface { + GetInfo + GetFullHeaders() bool + } + + // RangeInfo is an interface of the container of object GetRange operation parameters. + RangeInfo interface { + AddressInfo + GetRange() object.Range + } + + // RangeHashInfo is an interface of the container of object GetRangeHash operation parameters. + RangeHashInfo interface { + AddressInfo + GetRanges() []object.Range + GetSalt() []byte + } +) + +const ( + // KeyID is a filter key to object ID field. + KeyID = "ID" + + // KeyTombstone is a filter key to tombstone header. + KeyTombstone = "TOMBSTONE" + + // KeyStorageGroup is a filter key to storage group link. + KeyStorageGroup = "STORAGE_GROUP" + + // KeyNoChildren is a filter key to objects w/o child links. + KeyNoChildren = "LEAF" + + // KeyParent is a filter key to parent link. + KeyParent = "PARENT" + + // KeyHasParent is a filter key to objects with parent link. + KeyHasParent = "HAS_PAR" +) diff --git a/lib/transport/transport.go b/lib/transport/transport.go new file mode 100644 index 0000000000..4e06fedd39 --- /dev/null +++ b/lib/transport/transport.go @@ -0,0 +1,76 @@ +package transport + +import ( + "context" + "fmt" + "time" + + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr-net" + circuit "github.com/rubyist/circuitbreaker" +) + +type ( + // Transport is an interface of network connection listener. + Transport interface { + Dial(context.Context, multiaddr.Multiaddr, bool) (Connection, error) + Listen(multiaddr.Multiaddr) (manet.Listener, error) + } + + transport struct { + threshold int64 + timeout time.Duration + panel *circuit.Panel + } +) + +const defaultBreakerName = "_NeoFS" + +func (t *transport) Dial(ctx context.Context, addr multiaddr.Multiaddr, reset bool) (Connection, error) { + var ( + con manet.Conn + breaker = t.breakerLookup(addr) + ) + + if reset { + breaker.Reset() + } + + err := breaker.CallContext(ctx, func() (errCall error) { + var d manet.Dialer + con, errCall = d.DialContext(ctx, addr) + return errCall + }, t.timeout) + + if err != nil { + return nil, err + } + + return newConnection(con), nil +} + +func (t *transport) Listen(addr multiaddr.Multiaddr) (manet.Listener, error) { + return manet.Listen(addr) +} + +func (t *transport) breakerLookup(addr fmt.Stringer) *circuit.Breaker { + panel := defaultBreakerName + addr.String() + + cb, ok := t.panel.Get(panel) + if !ok { + cb = circuit.NewConsecutiveBreaker(t.threshold) + t.panel.Add(panel, cb) + } + + return cb +} + +// New is a transport component's constructor. +func New(threshold int64, timeout time.Duration) Transport { + breaker := circuit.NewConsecutiveBreaker(threshold) + + panel := circuit.NewPanel() + panel.Add(defaultBreakerName, breaker) + + return &transport{panel: panel, threshold: threshold, timeout: timeout} +} diff --git a/lib/transport/transport_test.go b/lib/transport/transport_test.go new file mode 100644 index 0000000000..bd3bd2838b --- /dev/null +++ b/lib/transport/transport_test.go @@ -0,0 +1,61 @@ +package transport + +import ( + "context" + "net" + "testing" + "time" + + manet "github.com/multiformats/go-multiaddr-net" + circuit "github.com/rubyist/circuitbreaker" + "github.com/stretchr/testify/require" +) + +func TestTransport(t *testing.T) { + var ( + attempts = int64(5) + lc net.ListenConfig + tr = New(attempts, time.Second) + ctx, cancel = context.WithCancel(context.TODO()) + ) + + defer cancel() + + lis1, err := lc.Listen(ctx, "tcp", ":0") + require.NoError(t, err) + + addr1, err := manet.FromNetAddr(lis1.Addr()) + require.NoError(t, err) + + _, err = tr.Dial(ctx, addr1, false) + require.NoError(t, err) + + lis2, err := lc.Listen(ctx, "tcp", ":0") + require.NoError(t, err) + + addr2, err := manet.FromNetAddr(lis2.Addr()) + require.NoError(t, err) + + _, err = tr.Dial(ctx, addr1, false) + require.NoError(t, err) + + require.NoError(t, lis1.Close()) + + for i := int64(0); i < 10; i++ { + _, err = tr.Dial(ctx, addr1, false) + require.Error(t, err) + + if i >= attempts { + require.EqualError(t, err, circuit.ErrBreakerOpen.Error()) + } + + _, err = tr.Dial(ctx, addr2, false) + require.NoError(t, err) + } + + time.Sleep(time.Second) + + _, err = tr.Dial(ctx, addr1, false) + require.Error(t, err) + require.NotContains(t, err.Error(), circuit.ErrBreakerOpen.Error()) +} diff --git a/misc/build.go b/misc/build.go new file mode 100644 index 0000000000..f4dab30637 --- /dev/null +++ b/misc/build.go @@ -0,0 +1,18 @@ +package misc + +const ( + // NodeName is an application name. + NodeName = "neofs-node" + + // Prefix is an application prefix. + Prefix = "neofs" + + // Build is an application build time. + Build = "now" + + // Version is an application version. + Version = "dev" + + // Debug is an application debug mode flag. + Debug = "true" +) diff --git a/modules/bootstrap/healthy.go b/modules/bootstrap/healthy.go new file mode 100644 index 0000000000..bd93fd0fd9 --- /dev/null +++ b/modules/bootstrap/healthy.go @@ -0,0 +1,95 @@ +package bootstrap + +import ( + "crypto/ecdsa" + "sync" + + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/nspcc-dev/neofs-node/services/public/state" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + healthyParams struct { + dig.In + + Logger *zap.Logger + Viper *viper.Viper + Place placement.Component + Checkers []state.HealthChecker `group:"healthy"` + + // for ChangeState + PrivateKey *ecdsa.PrivateKey + + MorphNetmapContract *implementations.MorphNetmapContract + } + + healthyResult struct { + dig.Out + + HealthyClient HealthyClient + + StateService state.Service + } + + // HealthyClient is an interface of healthiness checking tool. + HealthyClient interface { + Healthy() error + } + + healthyClient struct { + *sync.RWMutex + healthy func() error + } +) + +const ( + errUnhealthy = internal.Error("unhealthy") +) + +func (h *healthyClient) setHandler(handler func() error) { + if handler == nil { + return + } + + h.Lock() + h.healthy = handler + h.Unlock() +} + +func (h *healthyClient) Healthy() error { + if h.healthy == nil { + return errUnhealthy + } + + return h.healthy() +} + +func newHealthy(p healthyParams) (res healthyResult, err error) { + sp := state.Params{ + Stater: p.Place, + Logger: p.Logger, + Viper: p.Viper, + Checkers: p.Checkers, + PrivateKey: p.PrivateKey, + MorphNetmapContract: p.MorphNetmapContract, + } + + if res.StateService, err = state.New(sp); err != nil { + return + } + + healthyClient := &healthyClient{ + RWMutex: new(sync.RWMutex), + } + + healthyClient.setHandler(res.StateService.Healthy) + + res.HealthyClient = healthyClient + + return +} diff --git a/modules/bootstrap/module.go b/modules/bootstrap/module.go new file mode 100644 index 0000000000..8b31ed2e81 --- /dev/null +++ b/modules/bootstrap/module.go @@ -0,0 +1,10 @@ +package bootstrap + +import ( + "github.com/nspcc-dev/neofs-node/lib/fix/module" +) + +// Module is a module of bootstrap component. +var Module = module.Module{ + {Constructor: newHealthy}, +} diff --git a/modules/grpc/billing.go b/modules/grpc/billing.go new file mode 100644 index 0000000000..d8500c2656 --- /dev/null +++ b/modules/grpc/billing.go @@ -0,0 +1,141 @@ +package grpc + +import ( + "context" + + "github.com/gogo/protobuf/proto" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" +) + +type ( + billingStream struct { + grpc.ServerStream + *grpc.StreamServerInfo + + input int + output int + cid string + } + + cider interface { + CID() refs.CID + } +) + +const ( + typeInput = "input" + typeOutput = "output" + + labelType = "type" + labelMethod = "method" + labelContainer = "container" +) + +var ( + serviceBillingBytes = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "neofs", + Name: "billing_bytes", + Help: "Count of bytes received / sent for method and container", + }, []string{labelType, labelMethod, labelContainer}) + + serviceBillingCalls = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "neofs", + Name: "billing_calls", + Help: "Count of calls for api methods", + }, []string{labelMethod, labelContainer}) +) + +func init() { + // Register billing metrics + prometheus.MustRegister(serviceBillingBytes) + prometheus.MustRegister(serviceBillingCalls) +} + +func getProtoSize(val interface{}) int { + if msg, ok := val.(proto.Message); ok && msg != nil { + return proto.Size(msg) + } + + return 0 +} + +func getProtoContainer(val interface{}) string { + if t, ok := val.(cider); ok && t != nil { + return t.CID().String() + } + + return "" +} + +func (b *billingStream) RecvMsg(msg interface{}) error { + err := b.ServerStream.RecvMsg(msg) + b.input += getProtoSize(msg) + + if cid := getProtoContainer(msg); cid != "" { + b.cid = cid + } + + return err +} + +func (b *billingStream) SendMsg(msg interface{}) error { + b.output += getProtoSize(msg) + + return b.ServerStream.SendMsg(msg) +} + +func (b *billingStream) report() { + labels := prometheus.Labels{ + labelMethod: b.FullMethod, + labelContainer: b.cid, + } + + serviceBillingCalls.With(labels).Inc() + + labels[labelType] = typeInput + serviceBillingBytes.With(labels).Add(float64(b.input)) + + labels[labelType] = typeOutput + serviceBillingBytes.With(labels).Add(float64(b.output)) +} + +func streamBilling(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + stream := &billingStream{ + ServerStream: ss, + StreamServerInfo: info, + } + + err := handler(srv, stream) + + stream.report() + + return err +} + +func unaryBilling(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (res interface{}, err error) { + input := getProtoSize(req) + cid := getProtoContainer(req) + + labels := prometheus.Labels{ + labelMethod: info.FullMethod, + labelContainer: cid, + } + + serviceBillingCalls.With(labels).Inc() + + if res, err = handler(ctx, req); err != nil { + return + } + + output := getProtoSize(res) + + labels[labelType] = typeInput + serviceBillingBytes.With(labels).Add(float64(input)) + + labels[labelType] = typeOutput + serviceBillingBytes.With(labels).Add(float64(output)) + + return +} diff --git a/modules/grpc/module.go b/modules/grpc/module.go new file mode 100644 index 0000000000..7e26603919 --- /dev/null +++ b/modules/grpc/module.go @@ -0,0 +1,10 @@ +package grpc + +import ( + "github.com/nspcc-dev/neofs-node/lib/fix/module" +) + +// Module is a gRPC layer module. +var Module = module.Module{ + {Constructor: routing}, +} diff --git a/modules/grpc/routing.go b/modules/grpc/routing.go new file mode 100644 index 0000000000..d0fc6fca65 --- /dev/null +++ b/modules/grpc/routing.go @@ -0,0 +1,118 @@ +// About "github.com/nspcc-dev/neofs-node/lib/grpc" +// there's just alias for "google.golang.org/grpc" +// with Service-interface + +package grpc + +import ( + middleware "github.com/grpc-ecosystem/go-grpc-middleware" + gZap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" + prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +type ( + // Service interface + Service interface { + Name() string + Register(*grpc.Server) + } + + // ServerParams to create gRPC-server + // and provide service-handlers + ServerParams struct { + dig.In + + Services []Service + Logger *zap.Logger + Viper *viper.Viper + } + + // ServicesResult ... + ServicesResult struct { + dig.Out + + Services []Service + } + + // Server type-alias + Server = grpc.Server + + // CallOption type-alias + CallOption = grpc.CallOption + + // ClientConn type-alias + ClientConn = grpc.ClientConn + + // ServerOption type-alias + ServerOption = grpc.ServerOption +) + +var ( + // DialContext func-alias + DialContext = grpc.DialContext + + // WithBlock func-alias + WithBlock = grpc.WithBlock + + // WithInsecure func-alias + WithInsecure = grpc.WithInsecure +) + +// NewServer creates a gRPC server which has no service registered and has not +// started to accept requests yet. +func NewServer(opts ...ServerOption) *Server { + return grpc.NewServer(opts...) +} + +// creates new gRPC server and attach handlers. +func routing(p ServerParams) *grpc.Server { + var ( + options []ServerOption + stream []grpc.StreamServerInterceptor + unary []grpc.UnaryServerInterceptor + ) + + if p.Viper.GetBool("node.grpc.billing") { + unary = append(unary, unaryBilling) + stream = append(stream, streamBilling) + } + + if p.Viper.GetBool("node.grpc.logging") { + stream = append(stream, gZap.StreamServerInterceptor(p.Logger)) + unary = append(unary, gZap.UnaryServerInterceptor(p.Logger)) + } + + if p.Viper.GetBool("node.grpc.metrics") { + stream = append(stream, prometheus.StreamServerInterceptor) + unary = append(unary, prometheus.UnaryServerInterceptor) + } + + // Add stream options: + if len(stream) > 0 { + options = append(options, + grpc.StreamInterceptor(middleware.ChainStreamServer(stream...)), + ) + } + + // Add unary options: + if len(unary) > 0 { + options = append(options, + grpc.UnaryInterceptor(middleware.ChainUnaryServer(unary...)), + ) + } + + g := grpc.NewServer(options...) + + // Service services here: + for _, service := range p.Services { + p.Logger.Info("register gRPC service", + zap.String("service", service.Name())) + service.Register(g) + } + + return g +} diff --git a/modules/morph/balance.go b/modules/morph/balance.go new file mode 100644 index 0000000000..df39644214 --- /dev/null +++ b/modules/morph/balance.go @@ -0,0 +1,67 @@ +package morph + +import ( + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/services/public/accounting" + "github.com/pkg/errors" + "go.uber.org/dig" +) + +type balanceContractResult struct { + dig.Out + + BalanceContract implementations.MorphBalanceContract + + AccountingService accounting.Service +} + +// BalanceContractName is a name of Balance contract config sub-section. +const BalanceContractName = "balance" + +const ( + balanceContractBalanceOfOpt = "balance_of_method" + + balanceContractDecimalsOfOpt = "decimals_method" +) + +// BalanceContractBalanceOfOptPath is a path to balanceOf method name option. +func BalanceContractBalanceOfOptPath() string { + return optPath(prefix, BalanceContractName, balanceContractBalanceOfOpt) +} + +// BalanceContractDecimalsOfOptPath is a path to decimals method name option. +func BalanceContractDecimalsOfOptPath() string { + return optPath(prefix, BalanceContractName, balanceContractDecimalsOfOpt) +} + +func newBalanceContract(p contractParams) (res balanceContractResult, err error) { + client, ok := p.MorphContracts[BalanceContractName] + if !ok { + err = errors.Errorf("missing %s contract client", BalanceContractName) + return + } + + morphClient := implementations.MorphBalanceContract{} + morphClient.SetBalanceContractClient(client) + + morphClient.SetBalanceOfMethodName( + p.Viper.GetString( + BalanceContractBalanceOfOptPath(), + ), + ) + morphClient.SetDecimalsMethodName( + p.Viper.GetString( + BalanceContractDecimalsOfOptPath(), + ), + ) + + if res.AccountingService, err = accounting.New(accounting.Params{ + MorphBalanceContract: morphClient, + }); err != nil { + return + } + + res.BalanceContract = morphClient + + return +} diff --git a/modules/morph/common.go b/modules/morph/common.go new file mode 100644 index 0000000000..6584f7ae68 --- /dev/null +++ b/modules/morph/common.go @@ -0,0 +1,140 @@ +package morph + +import ( + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +// SmartContracts maps smart contract name to contract client. +type SmartContracts map[string]implementations.StaticContractClient + +// EventHandlers maps notification event name to handler information. +type EventHandlers map[string]event.HandlerInfo + +type morphContractsParams struct { + dig.In + + Viper *viper.Viper + + GoClient *goclient.Client + + Listener event.Listener +} + +type contractParams struct { + dig.In + + Viper *viper.Viper + + Logger *zap.Logger + + MorphContracts SmartContracts + + NodeInfo bootstrap.NodeInfo +} + +func newMorphContracts(p morphContractsParams) (SmartContracts, EventHandlers, error) { + mContracts := make(map[string]implementations.StaticContractClient, len(ContractNames)) + mHandlers := make(map[string]event.HandlerInfo) + + for _, contractName := range ContractNames { + scHash, err := util.Uint160DecodeStringLE( + p.Viper.GetString( + ScriptHashOptPath(contractName), + ), + ) + if err != nil { + return nil, nil, err + } + + fee := util.Fixed8FromInt64( + p.Viper.GetInt64( + InvocationFeeOptPath(contractName), + ), + ) + + mContracts[contractName], err = implementations.NewStaticContractClient(p.GoClient, scHash, fee) + if err != nil { + return nil, nil, err + } + + // set event parsers + parserInfo := event.ParserInfo{} + parserInfo.SetScriptHash(scHash) + + handlerInfo := event.HandlerInfo{} + handlerInfo.SetScriptHash(scHash) + + for _, item := range mParsers[contractName] { + parserInfo.SetParser(item.parser) + + optPath := ContractEventOptPath(contractName, item.typ) + + typEvent := event.TypeFromString( + p.Viper.GetString(optPath), + ) + + parserInfo.SetType(typEvent) + handlerInfo.SetType(typEvent) + + p.Listener.SetParser(parserInfo) + + mHandlers[optPath] = handlerInfo + } + } + + return mContracts, mHandlers, nil +} + +const prefix = "morph" + +const ( + endpointOpt = "endpoint" + + dialTimeoutOpt = "dial_timeout" + + magicNumberOpt = "magic_number" + + scriptHashOpt = "script_hash" + + invocationFeeOpt = "invocation_fee" +) + +// ContractNames is a list of smart contract names. +var ContractNames = []string{ + containerContractName, + reputationContractName, + NetmapContractName, + BalanceContractName, +} + +// EndpointOptPath returns the config path to goclient endpoint. +func EndpointOptPath() string { + return optPath(prefix, endpointOpt) +} + +// MagicNumberOptPath returns the config path to goclient magic number. +func MagicNumberOptPath() string { + return optPath(prefix, magicNumberOpt) +} + +// DialTimeoutOptPath returns the config path to goclient dial timeout. +func DialTimeoutOptPath() string { + return optPath(prefix, dialTimeoutOpt) +} + +// ScriptHashOptPath calculates the config path to script hash config of particular contract. +func ScriptHashOptPath(name string) string { + return optPath(prefix, name, scriptHashOpt) +} + +// InvocationFeeOptPath calculates the config path to invocation fee config of particular contract. +func InvocationFeeOptPath(name string) string { + return optPath(prefix, name, invocationFeeOpt) +} diff --git a/modules/morph/container.go b/modules/morph/container.go new file mode 100644 index 0000000000..770bf4b748 --- /dev/null +++ b/modules/morph/container.go @@ -0,0 +1,122 @@ +package morph + +import ( + "github.com/nspcc-dev/neofs-node/lib/acl" + "github.com/nspcc-dev/neofs-node/lib/container" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/pkg/errors" + "go.uber.org/dig" +) + +type containerContractResult struct { + dig.Out + + ContainerContract *implementations.MorphContainerContract + + BinaryExtendedACLStore acl.BinaryExtendedACLStore + + ExtendedACLSource acl.ExtendedACLSource + + ContainerStorage container.Storage +} + +const ( + containerContractName = "container" + + containerContractSetEACLOpt = "set_eacl_method" + + containerContractEACLOpt = "get_eacl_method" + + containerContractPutOpt = "put_method" + + containerContractGetOpt = "get_method" + + containerContractDelOpt = "delete_method" + + containerContractListOpt = "list_method" +) + +// ContainerContractSetEACLOptPath returns the config path to set eACL method name of Container contract. +func ContainerContractSetEACLOptPath() string { + return optPath(prefix, containerContractName, containerContractSetEACLOpt) +} + +// ContainerContractEACLOptPath returns the config path to get eACL method name of Container contract. +func ContainerContractEACLOptPath() string { + return optPath(prefix, containerContractName, containerContractEACLOpt) +} + +// ContainerContractPutOptPath returns the config path to put container method name of Container contract. +func ContainerContractPutOptPath() string { + return optPath(prefix, containerContractName, containerContractPutOpt) +} + +// ContainerContractGetOptPath returns the config path to get container method name of Container contract. +func ContainerContractGetOptPath() string { + return optPath(prefix, containerContractName, containerContractGetOpt) +} + +// ContainerContractDelOptPath returns the config path to delete container method name of Container contract. +func ContainerContractDelOptPath() string { + return optPath(prefix, containerContractName, containerContractDelOpt) +} + +// ContainerContractListOptPath returns the config path to list containers method name of Container contract. +func ContainerContractListOptPath() string { + return optPath(prefix, containerContractName, containerContractListOpt) +} + +func newContainerContract(p contractParams) (res containerContractResult, err error) { + client, ok := p.MorphContracts[containerContractName] + if !ok { + err = errors.Errorf("missing %s contract client", containerContractName) + return + } + + morphClient := new(implementations.MorphContainerContract) + morphClient.SetContainerContractClient(client) + + morphClient.SetEACLSetMethodName( + p.Viper.GetString( + ContainerContractSetEACLOptPath(), + ), + ) + morphClient.SetEACLGetMethodName( + p.Viper.GetString( + ContainerContractEACLOptPath(), + ), + ) + morphClient.SetContainerGetMethodName( + p.Viper.GetString( + ContainerContractGetOptPath(), + ), + ) + morphClient.SetContainerPutMethodName( + p.Viper.GetString( + ContainerContractPutOptPath(), + ), + ) + morphClient.SetContainerDeleteMethodName( + p.Viper.GetString( + ContainerContractDelOptPath(), + ), + ) + morphClient.SetContainerListMethodName( + p.Viper.GetString( + ContainerContractListOptPath(), + ), + ) + + res.ContainerContract = morphClient + + res.BinaryExtendedACLStore = morphClient + + res.ExtendedACLSource, err = implementations.ExtendedACLSourceFromBinary(res.BinaryExtendedACLStore) + if err != nil { + return + } + + res.ContainerStorage = morphClient + + return res, nil +} diff --git a/modules/morph/event.go b/modules/morph/event.go new file mode 100644 index 0000000000..4df3f486cd --- /dev/null +++ b/modules/morph/event.go @@ -0,0 +1,28 @@ +package morph + +import ( + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event/netmap" +) + +const eventOpt = "event" + +// NewEpochEventType is a config section of new epoch notification event. +const NewEpochEventType = "new_epoch" + +// ContractEventOptPath returns the config path to notification event name of particular contract. +func ContractEventOptPath(contract, event string) string { + return optPath(prefix, contract, eventOpt, event) +} + +var mParsers = map[string][]struct { + typ string + parser event.Parser +}{ + NetmapContractName: { + { + typ: NewEpochEventType, + parser: netmap.ParseNewEpoch, + }, + }, +} diff --git a/modules/morph/goclient.go b/modules/morph/goclient.go new file mode 100644 index 0000000000..dd0359f2c1 --- /dev/null +++ b/modules/morph/goclient.go @@ -0,0 +1,32 @@ +package morph + +import ( + "context" + "crypto/ecdsa" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neofs-node/lib/blockchain/goclient" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type morphClientParams struct { + dig.In + + Viper *viper.Viper + + Logger *zap.Logger + + Key *ecdsa.PrivateKey +} + +func newMorphClient(p morphClientParams) (*goclient.Client, error) { + return goclient.New(context.Background(), &goclient.Params{ + Log: p.Logger, + Key: p.Key, + Endpoint: p.Viper.GetString(optPath(prefix, endpointOpt)), + DialTimeout: p.Viper.GetDuration(optPath(prefix, dialTimeoutOpt)), + Magic: netmode.Magic(p.Viper.GetUint32(optPath(prefix, magicNumberOpt))), + }) +} diff --git a/modules/morph/listener.go b/modules/morph/listener.go new file mode 100644 index 0000000000..4c334ced9e --- /dev/null +++ b/modules/morph/listener.go @@ -0,0 +1,53 @@ +package morph + +import ( + "context" + + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/nspcc-dev/neofs-node/lib/blockchain/subscriber" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type eventListenerParams struct { + dig.In + + Viper *viper.Viper + + Logger *zap.Logger +} + +var listenerPrefix = optPath(prefix, "listener") + +const ( + listenerEndpointOpt = "endpoint" + + listenerDialTimeoutOpt = "dial_timeout" +) + +// ListenerEndpointOptPath returns the config path to event listener's endpoint. +func ListenerEndpointOptPath() string { + return optPath(listenerPrefix, listenerEndpointOpt) +} + +// ListenerDialTimeoutOptPath returns the config path to event listener's dial timeout. +func ListenerDialTimeoutOptPath() string { + return optPath(listenerPrefix, listenerDialTimeoutOpt) +} + +func newEventListener(p eventListenerParams) (event.Listener, error) { + sub, err := subscriber.New(context.Background(), &subscriber.Params{ + Log: p.Logger, + Endpoint: p.Viper.GetString(ListenerEndpointOptPath()), + DialTimeout: p.Viper.GetDuration(ListenerDialTimeoutOptPath()), + }) + if err != nil { + return nil, err + } + + return event.NewListener(event.ListenerParams{ + Logger: p.Logger, + Subscriber: sub, + }) +} diff --git a/modules/morph/module.go b/modules/morph/module.go new file mode 100644 index 0000000000..c2ae26378e --- /dev/null +++ b/modules/morph/module.go @@ -0,0 +1,22 @@ +package morph + +import ( + "strings" + + "github.com/nspcc-dev/neofs-node/lib/fix/module" +) + +// Module is a Neo:Morph module. +var Module = module.Module{ + {Constructor: newMorphClient}, + {Constructor: newMorphContracts}, + {Constructor: newContainerContract}, + {Constructor: newReputationContract}, + {Constructor: newNetmapContract}, + {Constructor: newEventListener}, + {Constructor: newBalanceContract}, +} + +func optPath(sections ...string) string { + return strings.Join(sections, ".") +} diff --git a/modules/morph/netmap.go b/modules/morph/netmap.go new file mode 100644 index 0000000000..3c5e4f66ae --- /dev/null +++ b/modules/morph/netmap.go @@ -0,0 +1,115 @@ +package morph + +import ( + "github.com/nspcc-dev/neofs-node/lib/boot" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/ir" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/pkg/errors" + "go.uber.org/dig" +) + +type netmapContractResult struct { + dig.Out + + NetmapContract *implementations.MorphNetmapContract + + NetMapStorage netmap.Storage + + IRStorage ir.Storage + + StorageBootController boot.StorageBootController +} + +const ( + // NetmapContractName is a Netmap contract's config section name. + NetmapContractName = "netmap" + + netmapContractAddPeerOpt = "add_peer_method" + + netmapContractNewEpochOpt = "new_epoch_method" + + netmapContractNetmapOpt = "netmap_method" + + netmapContractUpdStateOpt = "update_state_method" + + netmapContractIRListOpt = "ir_list_method" +) + +// NetmapContractAddPeerOptPath returns the config path to add peer method of Netmap contract. +func NetmapContractAddPeerOptPath() string { + return optPath(prefix, NetmapContractName, netmapContractAddPeerOpt) +} + +// NetmapContractNewEpochOptPath returns the config path to new epoch method of Netmap contract. +func NetmapContractNewEpochOptPath() string { + return optPath(prefix, NetmapContractName, netmapContractNewEpochOpt) +} + +// NetmapContractNetmapOptPath returns the config path to get netmap method of Netmap contract. +func NetmapContractNetmapOptPath() string { + return optPath(prefix, NetmapContractName, netmapContractNetmapOpt) +} + +// NetmapContractUpdateStateOptPath returns the config path to update state method of Netmap contract. +func NetmapContractUpdateStateOptPath() string { + return optPath(prefix, NetmapContractName, netmapContractUpdStateOpt) +} + +// NetmapContractIRListOptPath returns the config path to inner ring list method of Netmap contract. +func NetmapContractIRListOptPath() string { + return optPath(prefix, NetmapContractName, netmapContractIRListOpt) +} + +func newNetmapContract(p contractParams) (res netmapContractResult, err error) { + client, ok := p.MorphContracts[NetmapContractName] + if !ok { + err = errors.Errorf("missing %s contract client", NetmapContractName) + return + } + + morphClient := new(implementations.MorphNetmapContract) + morphClient.SetNetmapContractClient(client) + + morphClient.SetAddPeerMethodName( + p.Viper.GetString( + NetmapContractAddPeerOptPath(), + ), + ) + morphClient.SetNewEpochMethodName( + p.Viper.GetString( + NetmapContractNewEpochOptPath(), + ), + ) + morphClient.SetNetMapMethodName( + p.Viper.GetString( + NetmapContractNetmapOptPath(), + ), + ) + morphClient.SetUpdateStateMethodName( + p.Viper.GetString( + NetmapContractUpdateStateOptPath(), + ), + ) + morphClient.SetIRListMethodName( + p.Viper.GetString( + NetmapContractIRListOptPath(), + ), + ) + + bootCtrl := boot.StorageBootController{} + bootCtrl.SetPeerBootstrapper(morphClient) + bootCtrl.SetLogger(p.Logger) + + bootPrm := boot.StorageBootParams{} + bootPrm.SetNodeInfo(&p.NodeInfo) + + bootCtrl.SetBootParams(bootPrm) + + res.StorageBootController = bootCtrl + res.NetmapContract = morphClient + res.NetMapStorage = morphClient + res.IRStorage = morphClient + + return res, nil +} diff --git a/modules/morph/reputation.go b/modules/morph/reputation.go new file mode 100644 index 0000000000..e8c12434c8 --- /dev/null +++ b/modules/morph/reputation.go @@ -0,0 +1,59 @@ +package morph + +import ( + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/pkg/errors" + "go.uber.org/dig" +) + +type reputationContractResult struct { + dig.Out + + ReputationContract implementations.MorphReputationContract +} + +const ( + reputationContractName = "reputation" + + reputationContractPutOpt = "put_method" + + reputationContractListOpt = "list_method" +) + +// ReputationContractPutOptPath returns the config path to put method of Reputation contract. +func ReputationContractPutOptPath() string { + return optPath(prefix, reputationContractName, reputationContractPutOpt) +} + +// ReputationContractListOptPath returns the config path to list method of Reputation contract. +func ReputationContractListOptPath() string { + return optPath(prefix, reputationContractName, reputationContractListOpt) +} + +func newReputationContract(p contractParams, ps peers.Store) (res reputationContractResult, err error) { + cli, ok := p.MorphContracts[reputationContractName] + if !ok { + err = errors.Errorf("missing %s contract client", reputationContractName) + return + } + + morphClient := implementations.MorphReputationContract{} + morphClient.SetReputationContractClient(cli) + morphClient.SetPublicKeyStore(ps) + + morphClient.SetPutMethodName( + p.Viper.GetString( + ReputationContractPutOptPath(), + ), + ) + morphClient.SetListMethodName( + p.Viper.GetString( + ReputationContractListOptPath(), + ), + ) + + res.ReputationContract = morphClient + + return +} diff --git a/modules/network/http.go b/modules/network/http.go new file mode 100644 index 0000000000..21fbd7226d --- /dev/null +++ b/modules/network/http.go @@ -0,0 +1,49 @@ +package network + +import ( + "github.com/fasthttp/router" + svc "github.com/nspcc-dev/neofs-node/modules/bootstrap" + "github.com/valyala/fasthttp" + "go.uber.org/dig" +) + +type ( + handlerParams struct { + dig.In + + Healthy svc.HealthyClient + } +) + +const ( + healthyState = "NeoFS node is " + defaultContentType = "text/plain; charset=utf-8" +) + +func newHTTPHandler(p handlerParams) (fasthttp.RequestHandler, error) { + r := router.New() + r.RedirectTrailingSlash = true + + r.GET("/-/ready/", func(c *fasthttp.RequestCtx) { + c.SetStatusCode(fasthttp.StatusOK) + c.SetBodyString(healthyState + "ready") + }) + + r.GET("/-/healthy/", func(c *fasthttp.RequestCtx) { + code := fasthttp.StatusOK + msg := "healthy" + + err := p.Healthy.Healthy() + if err != nil { + code = fasthttp.StatusBadRequest + msg = "unhealthy: " + err.Error() + } + + c.Response.Reset() + c.SetStatusCode(code) + c.SetContentType(defaultContentType) + c.SetBodyString(healthyState + msg) + }) + + return r.Handler, nil +} diff --git a/modules/network/module.go b/modules/network/module.go new file mode 100644 index 0000000000..95c6041f33 --- /dev/null +++ b/modules/network/module.go @@ -0,0 +1,20 @@ +package network + +import ( + "github.com/nspcc-dev/neofs-node/lib/fix/module" + "github.com/nspcc-dev/neofs-node/lib/fix/web" +) + +// Module is a network layer module. +var Module = module.Module{ + {Constructor: newMuxer}, + {Constructor: newPeers}, + {Constructor: newPlacement}, + {Constructor: newTransport}, + + // Metrics is prometheus handler + {Constructor: web.NewMetrics}, + // Profiler is pprof handler + {Constructor: web.NewProfiler}, + {Constructor: newHTTPHandler}, +} diff --git a/modules/network/muxer.go b/modules/network/muxer.go new file mode 100644 index 0000000000..63ad8fc5b3 --- /dev/null +++ b/modules/network/muxer.go @@ -0,0 +1,57 @@ +package network + +import ( + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/lib/muxer" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/spf13/viper" + "github.com/valyala/fasthttp" + "go.uber.org/dig" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +type muxerParams struct { + dig.In + + Logger *zap.Logger + P2P *grpc.Server + + Peers peers.Interface + + Address multiaddr.Multiaddr + ShutdownTTL time.Duration `name:"shutdown_ttl"` + API fasthttp.RequestHandler + Viper *viper.Viper +} + +const appName = "neofs-node" + +func newFastHTTPServer(p muxerParams) *fasthttp.Server { + srv := new(fasthttp.Server) + srv.Name = appName + srv.ReadBufferSize = p.Viper.GetInt("muxer.http.read_buffer_size") + srv.WriteBufferSize = p.Viper.GetInt("muxer.http.write_buffer_size") + srv.ReadTimeout = p.Viper.GetDuration("muxer.http.read_timeout") + srv.WriteTimeout = p.Viper.GetDuration("muxer.http.write_timeout") + srv.GetOnly = true + srv.DisableHeaderNamesNormalizing = true + srv.NoDefaultServerHeader = true + srv.NoDefaultContentType = true + srv.Handler = p.API + + return srv +} + +func newMuxer(p muxerParams) muxer.Mux { + return muxer.New(muxer.Params{ + P2P: p.P2P, + Peers: p.Peers, + Logger: p.Logger, + Address: p.Address, + ShutdownTTL: p.ShutdownTTL, + API: newFastHTTPServer(p), + }) +} diff --git a/modules/network/peers.go b/modules/network/peers.go new file mode 100644 index 0000000000..f9af19c0a7 --- /dev/null +++ b/modules/network/peers.go @@ -0,0 +1,41 @@ +package network + +import ( + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type peersParams struct { + dig.In + + Viper *viper.Viper + Logger *zap.Logger + Address multiaddr.Multiaddr + Transport transport.Transport +} + +func newTransport(v *viper.Viper) transport.Transport { + return transport.New( + v.GetInt64("transport.attempts_count"), + v.GetDuration("transport.attempts_ttl"), + ) +} + +func newPeers(p peersParams) (peers.Interface, error) { + return peers.New(peers.Params{ + Logger: p.Logger, + Address: p.Address, + Transport: p.Transport, + Attempts: p.Viper.GetInt64("peers.attempts_count"), + AttemptsTTL: p.Viper.GetDuration("peers.attempts_ttl"), + ConnectionTTL: p.Viper.GetDuration("peers.connections_ttl"), + ConnectionIDLE: p.Viper.GetDuration("peers.connections_idle"), + MetricsTimeout: p.Viper.GetDuration("peers.metrics_timeout"), + KeepAliveTTL: p.Viper.GetDuration("peers.keep_alive.ttl"), + KeepAlivePingTTL: p.Viper.GetDuration("peers.keep_alive.ping"), + }) +} diff --git a/modules/network/placement.go b/modules/network/placement.go new file mode 100644 index 0000000000..36959efdf8 --- /dev/null +++ b/modules/network/placement.go @@ -0,0 +1,79 @@ +package network + +import ( + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + netmapevent "github.com/nspcc-dev/neofs-node/lib/blockchain/event/netmap" + libcnr "github.com/nspcc-dev/neofs-node/lib/container" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/nspcc-dev/neofs-node/modules/morph" + "github.com/nspcc-dev/neofs-node/services/public/state" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + placementParams struct { + dig.In + + Log *zap.Logger + Peers peers.Store + Fetcher libcnr.Storage + + MorphEventListener event.Listener + + NetMapStorage netmap.Storage + + MorphEventHandlers morph.EventHandlers + } + + placementOutput struct { + dig.Out + + Placement placement.Component + Healthy state.HealthChecker `group:"healthy"` + } +) + +const defaultChronologyDuraion = 2 + +func newPlacement(p placementParams) placementOutput { + place := placement.New(placement.Params{ + Log: p.Log, + Peerstore: p.Peers, + Fetcher: p.Fetcher, + ChronologyDuration: defaultChronologyDuraion, + }) + + if handlerInfo, ok := p.MorphEventHandlers[morph.ContractEventOptPath( + morph.NetmapContractName, + morph.NewEpochEventType, + )]; ok { + handlerInfo.SetHandler(func(ev event.Event) { + nmRes, err := p.NetMapStorage.GetNetMap(netmap.GetParams{}) + if err != nil { + p.Log.Error("could not get network map", + zap.String("error", err.Error()), + ) + return + } + + if err := place.Update( + ev.(netmapevent.NewEpoch).EpochNumber(), + nmRes.NetMap(), + ); err != nil { + p.Log.Error("could not update network map in placement component", + zap.String("error", err.Error()), + ) + } + }) + + p.MorphEventListener.RegisterHandler(handlerInfo) + } + + return placementOutput{ + Placement: place, + Healthy: place.(state.HealthChecker), + } +} diff --git a/modules/node/audit.go b/modules/node/audit.go new file mode 100644 index 0000000000..a2c02b2880 --- /dev/null +++ b/modules/node/audit.go @@ -0,0 +1,63 @@ +package node + +import ( + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/services/public/object" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +type ( + cnrHandlerParams struct { + *viper.Viper + *zap.Logger + Placer implementations.ObjectPlacer + PeerStore peers.Store + Peers peers.Interface + TimeoutsPrefix string + Key *ecdsa.PrivateKey + + TokenStore session.PrivateTokenStore + } +) + +func newObjectsContainerHandler(p cnrHandlerParams) (implementations.SelectiveContainerExecutor, error) { + as, err := implementations.NewAddressStore(p.PeerStore, p.Logger) + if err != nil { + return nil, err + } + + multiTransport, err := object.NewMultiTransport(object.MultiTransportParams{ + AddressStore: as, + EpochReceiver: p.Placer, + RemoteService: object.NewRemoteService(p.Peers), + Logger: p.Logger, + Key: p.Key, + PutTimeout: p.Viper.GetDuration(p.TimeoutsPrefix + ".timeouts.put"), + GetTimeout: p.Viper.GetDuration(p.TimeoutsPrefix + ".timeouts.get"), + HeadTimeout: p.Viper.GetDuration(p.TimeoutsPrefix + ".timeouts.head"), + SearchTimeout: p.Viper.GetDuration(p.TimeoutsPrefix + ".timeouts.search"), + RangeHashTimeout: p.Viper.GetDuration(p.TimeoutsPrefix + ".timeouts.range_hash"), + DialTimeout: p.Viper.GetDuration("object.dial_timeout"), + + PrivateTokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + exec, err := implementations.NewContainerTraverseExecutor(multiTransport) + if err != nil { + return nil, err + } + + return implementations.NewObjectContainerHandler(implementations.ObjectContainerHandlerParams{ + NodeLister: p.Placer, + Executor: exec, + Logger: p.Logger, + }) +} diff --git a/modules/node/container.go b/modules/node/container.go new file mode 100644 index 0000000000..af081cb4c4 --- /dev/null +++ b/modules/node/container.go @@ -0,0 +1,31 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/lib/acl" + libcnr "github.com/nspcc-dev/neofs-node/lib/container" + svc "github.com/nspcc-dev/neofs-node/modules/bootstrap" + "github.com/nspcc-dev/neofs-node/services/public/container" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type cnrParams struct { + dig.In + + Logger *zap.Logger + + Healthy svc.HealthyClient + + ExtendedACLStore acl.BinaryExtendedACLStore + + ContainerStorage libcnr.Storage +} + +func newContainerService(p cnrParams) (container.Service, error) { + return container.New(container.Params{ + Logger: p.Logger, + Healthy: p.Healthy, + Store: p.ContainerStorage, + ExtendedACLStore: p.ExtendedACLStore, + }) +} diff --git a/modules/node/core.go b/modules/node/core.go new file mode 100644 index 0000000000..665836eeec --- /dev/null +++ b/modules/node/core.go @@ -0,0 +1,29 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/storage" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +func listBuckets(v *viper.Viper) []core.BucketType { + var ( + items = v.GetStringMap("storage") + result = make([]core.BucketType, 0, len(items)) + ) + + for name := range items { + result = append(result, core.BucketType(name)) + } + + return result +} + +func newStorage(l *zap.Logger, v *viper.Viper) (core.Storage, error) { + return storage.New(storage.Params{ + Viper: v, + Logger: l, + Buckets: listBuckets(v), + }) +} diff --git a/modules/node/localstore.go b/modules/node/localstore.go new file mode 100644 index 0000000000..7be10bed0f --- /dev/null +++ b/modules/node/localstore.go @@ -0,0 +1,64 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/meta" + "github.com/nspcc-dev/neofs-node/lib/metrics" + "go.uber.org/atomic" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + localstoreParams struct { + dig.In + + Logger *zap.Logger + Storage core.Storage + Counter *atomic.Float64 + Collector metrics.Collector + } + + metaIterator struct { + iter localstore.Iterator + } +) + +func newMetaIterator(iter localstore.Iterator) meta.Iterator { + return &metaIterator{iter: iter} +} + +func (m *metaIterator) Iterate(handler meta.IterateFunc) error { + return m.iter.Iterate(nil, func(objMeta *localstore.ObjectMeta) bool { + return handler == nil || handler(objMeta.Object) != nil + }) +} + +func newLocalstore(p localstoreParams) (localstore.Localstore, error) { + metaBucket, err := p.Storage.GetBucket(core.MetaStore) + if err != nil { + return nil, err + } + + blobBucket, err := p.Storage.GetBucket(core.BlobStore) + if err != nil { + return nil, err + } + + local, err := localstore.New(localstore.Params{ + BlobBucket: blobBucket, + MetaBucket: metaBucket, + Logger: p.Logger, + Collector: p.Collector, + }) + if err != nil { + return nil, err + } + + iter := newMetaIterator(local) + p.Collector.SetCounter(local) + p.Collector.SetIterator(iter) + + return local, nil +} diff --git a/modules/node/metrics.go b/modules/node/metrics.go new file mode 100644 index 0000000000..0faad5d1cb --- /dev/null +++ b/modules/node/metrics.go @@ -0,0 +1,52 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/metrics" + mService "github.com/nspcc-dev/neofs-node/services/metrics" + "github.com/spf13/viper" + "go.uber.org/atomic" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + metricsParams struct { + dig.In + + Logger *zap.Logger + Options []string `name:"node_options"` + Viper *viper.Viper + Store core.Storage + } + + metricsServiceParams struct { + dig.In + + Logger *zap.Logger + Collector metrics.Collector + } +) + +func newObjectCounter() *atomic.Float64 { return atomic.NewFloat64(0) } + +func newMetricsService(p metricsServiceParams) (mService.Service, error) { + return mService.New(mService.Params{ + Logger: p.Logger, + Collector: p.Collector, + }) +} + +func newMetricsCollector(p metricsParams) (metrics.Collector, error) { + store, err := p.Store.GetBucket(core.SpaceMetricsStore) + if err != nil { + return nil, err + } + + return metrics.New(metrics.Params{ + Options: p.Options, + Logger: p.Logger, + Interval: p.Viper.GetDuration("metrics_collector.interval"), + MetricsStore: store, + }) +} diff --git a/modules/node/module.go b/modules/node/module.go new file mode 100644 index 0000000000..83a81b484f --- /dev/null +++ b/modules/node/module.go @@ -0,0 +1,91 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/nspcc-dev/neofs-node/lib/boot" + "github.com/nspcc-dev/neofs-node/lib/fix/module" + "github.com/nspcc-dev/neofs-node/lib/fix/worker" + "github.com/nspcc-dev/neofs-node/lib/metrics" + "github.com/nspcc-dev/neofs-node/lib/netmap" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/replication" + "github.com/nspcc-dev/neofs-node/modules/bootstrap" + "github.com/nspcc-dev/neofs-node/modules/grpc" + "github.com/nspcc-dev/neofs-node/modules/morph" + "github.com/nspcc-dev/neofs-node/modules/network" + "github.com/nspcc-dev/neofs-node/modules/settings" + "github.com/nspcc-dev/neofs-node/modules/workers" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type jobParams struct { + dig.In + + Logger *zap.Logger + Viper *viper.Viper + Peers peers.Store + + Replicator replication.Manager + PeersInterface peers.Interface + Metrics metrics.Collector + + MorphEventListener event.Listener + + StorageBootController boot.StorageBootController +} + +// Module is a NeoFS node module. +var Module = module.Module{ + {Constructor: attachJobs}, + {Constructor: newPeerstore}, + {Constructor: attachServices}, + {Constructor: netmap.NewNetmap}, + {Constructor: newStorage}, + {Constructor: newMetricsCollector}, + {Constructor: newObjectCounter}, + + // -- Container gRPC handlers -- // + {Constructor: newContainerService}, + + // -- gRPC Services -- // + + // -- Local store -- // + {Constructor: newLocalstore}, + + // -- Object manager -- // + {Constructor: newObjectManager}, + + // -- Replication manager -- // + {Constructor: newReplicationManager}, + + // -- Session service -- // + {Constructor: session.NewMapTokenStore}, + {Constructor: newSessionService}, + + // -- Placement tool -- // + {Constructor: newPlacementTool}, + + // metrics service -- // + {Constructor: newMetricsService}, +}.Append( + // app specific modules: + grpc.Module, + network.Module, + workers.Module, + settings.Module, + bootstrap.Module, + morph.Module, +) + +func attachJobs(p jobParams) worker.Jobs { + return worker.Jobs{ + "peers": p.PeersInterface.Job, + "metrics": p.Metrics.Start, + "event_listener": p.MorphEventListener.Listen, + "replicator": p.Replicator.Process, + "boot": p.StorageBootController.Bootstrap, + } +} diff --git a/modules/node/objectmanager.go b/modules/node/objectmanager.go new file mode 100644 index 0000000000..6d96f5c718 --- /dev/null +++ b/modules/node/objectmanager.go @@ -0,0 +1,219 @@ +package node + +import ( + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/hash" + apiobj "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/session" + libacl "github.com/nspcc-dev/neofs-node/lib/acl" + "github.com/nspcc-dev/neofs-node/lib/container" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/ir" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/services/public/object" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + objectManagerParams struct { + dig.In + + Logger *zap.Logger + Viper *viper.Viper + LocalStore localstore.Localstore + + PeersInterface peers.Interface + + Peers peers.Store + Placement placement.Component + TokenStore session.PrivateTokenStore + Options []string `name:"node_options"` + Key *ecdsa.PrivateKey + + IRStorage ir.Storage + + EpochReceiver implementations.EpochReceiver + + Placer implementations.ObjectPlacer + + ExtendedACLStore libacl.ExtendedACLSource + + ContainerStorage container.Storage + } +) + +const ( + transformersSectionPath = "object.transformers." + + aclMandatorySetBits = 0x04040444 +) + +const xorSalitor = "xor" + +func newObjectManager(p objectManagerParams) (object.Service, error) { + var sltr object.Salitor + + if p.Viper.GetString("object.salitor") == xorSalitor { + sltr = hash.SaltXOR + } + + as, err := implementations.NewAddressStore(p.Peers, p.Logger) + if err != nil { + return nil, err + } + + rs := object.NewRemoteService(p.PeersInterface) + + pto := p.Viper.GetDuration("object.put.timeout") + gto := p.Viper.GetDuration("object.get.timeout") + hto := p.Viper.GetDuration("object.head.timeout") + sto := p.Viper.GetDuration("object.search.timeout") + rhto := p.Viper.GetDuration("object.range_hash.timeout") + dto := p.Viper.GetDuration("object.dial_timeout") + + tr, err := object.NewMultiTransport(object.MultiTransportParams{ + AddressStore: as, + EpochReceiver: p.EpochReceiver, + RemoteService: rs, + Logger: p.Logger, + Key: p.Key, + PutTimeout: pto, + GetTimeout: gto, + HeadTimeout: hto, + SearchTimeout: sto, + RangeHashTimeout: rhto, + DialTimeout: dto, + + PrivateTokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + exec, err := implementations.NewContainerTraverseExecutor(tr) + if err != nil { + return nil, err + } + + selectiveExec, err := implementations.NewObjectContainerHandler(implementations.ObjectContainerHandlerParams{ + NodeLister: p.Placer, + Executor: exec, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + sgInfoRecv, err := implementations.NewStorageGroupInfoReceiver(implementations.StorageGroupInfoReceiverParams{ + SelectiveContainerExecutor: selectiveExec, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + verifier, err := implementations.NewLocalIntegrityVerifier( + core.NewNeoKeyVerifier(), + ) + if err != nil { + return nil, err + } + + trans, err := transformer.NewTransformer(transformer.Params{ + SGInfoReceiver: sgInfoRecv, + EpochReceiver: p.EpochReceiver, + SizeLimit: uint64(p.Viper.GetInt64(transformersSectionPath+"payload_limiter.max_payload_size") * apiobj.UnitsKB), + Verifier: verifier, + }) + if err != nil { + return nil, err + } + + aclChecker := libacl.NewMaskedBasicACLChecker(aclMandatorySetBits, libacl.DefaultAndFilter) + + aclHelper, err := implementations.NewACLHelper(p.ContainerStorage) + if err != nil { + return nil, err + } + + verifier, err = implementations.NewLocalHeadIntegrityVerifier( + core.NewNeoKeyVerifier(), + ) + if err != nil { + return nil, err + } + + return object.New(&object.Params{ + Verifier: verifier, + Salitor: sltr, + LocalStore: p.LocalStore, + MaxProcessingSize: p.Viper.GetUint64("object.max_processing_size") * uint64(apiobj.UnitsMB), + StorageCapacity: bootstrap.NodeInfo{Options: p.Options}.Capacity() * uint64(apiobj.UnitsGB), + PoolSize: p.Viper.GetInt("object.workers_count"), + Placer: p.Placer, + Transformer: trans, + ObjectRestorer: transformer.NewRestorePipeline( + transformer.SplitRestorer(), + ), + RemoteService: rs, + AddressStore: as, + Logger: p.Logger, + TokenStore: p.TokenStore, + EpochReceiver: p.EpochReceiver, + ContainerNodesLister: p.Placer, + Key: p.Key, + CheckACL: p.Viper.GetBool("object.check_acl"), + DialTimeout: p.Viper.GetDuration("object.dial_timeout"), + MaxPayloadSize: p.Viper.GetUint64("object.transformers.payload_limiter.max_payload_size") * uint64(apiobj.UnitsKB), + PutParams: object.OperationParams{ + Timeout: pto, + LogErrors: p.Viper.GetBool("object.put.log_errs"), + }, + GetParams: object.OperationParams{ + Timeout: gto, + LogErrors: p.Viper.GetBool("object.get.log_errs"), + }, + HeadParams: object.OperationParams{ + Timeout: hto, + LogErrors: p.Viper.GetBool("object.head.log_errs"), + }, + DeleteParams: object.OperationParams{ + Timeout: p.Viper.GetDuration("object.delete.timeout"), + LogErrors: p.Viper.GetBool("object.get.log_errs"), + }, + SearchParams: object.OperationParams{ + Timeout: sto, + LogErrors: p.Viper.GetBool("object.search.log_errs"), + }, + RangeParams: object.OperationParams{ + Timeout: p.Viper.GetDuration("object.range.timeout"), + LogErrors: p.Viper.GetBool("object.range.log_errs"), + }, + RangeHashParams: object.OperationParams{ + Timeout: rhto, + LogErrors: p.Viper.GetBool("object.range_hash.log_errs"), + }, + Assembly: p.Viper.GetBool("object.assembly"), + + WindowSize: p.Viper.GetInt("object.window_size"), + + ACLHelper: aclHelper, + BasicACLChecker: aclChecker, + IRStorage: p.IRStorage, + ContainerLister: p.Placer, + + SGInfoReceiver: sgInfoRecv, + + OwnerKeyVerifier: core.NewNeoKeyVerifier(), + + ExtendedACLSource: p.ExtendedACLStore, + }) +} diff --git a/modules/node/peerstore.go b/modules/node/peerstore.go new file mode 100644 index 0000000000..1ccd1f1d66 --- /dev/null +++ b/modules/node/peerstore.go @@ -0,0 +1,28 @@ +package node + +import ( + "crypto/ecdsa" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/lib/peers" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type peerstoreParams struct { + dig.In + + Logger *zap.Logger + PrivateKey *ecdsa.PrivateKey + Address multiaddr.Multiaddr + Store peers.Storage `optional:"true"` +} + +func newPeerstore(p peerstoreParams) (peers.Store, error) { + return peers.NewStore(peers.StoreParams{ + Storage: p.Store, + Logger: p.Logger, + Addr: p.Address, + Key: p.PrivateKey, + }) +} diff --git a/modules/node/placement.go b/modules/node/placement.go new file mode 100644 index 0000000000..9834f7b605 --- /dev/null +++ b/modules/node/placement.go @@ -0,0 +1,33 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/placement" + "go.uber.org/dig" +) + +type ( + placementToolParams struct { + dig.In + + Placement placement.Component + } + + placementToolResult struct { + dig.Out + + Placer implementations.ObjectPlacer + + Receiver implementations.EpochReceiver + } +) + +func newPlacementTool(p placementToolParams) (res placementToolResult, err error) { + if res.Placer, err = implementations.NewObjectPlacer(p.Placement); err != nil { + return + } + + res.Receiver = res.Placer + + return +} diff --git a/modules/node/replication.go b/modules/node/replication.go new file mode 100644 index 0000000000..546fdda9b4 --- /dev/null +++ b/modules/node/replication.go @@ -0,0 +1,394 @@ +package node + +import ( + "context" + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event" + "github.com/nspcc-dev/neofs-node/lib/blockchain/event/netmap" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/ir" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/nspcc-dev/neofs-node/lib/replication" + "github.com/nspcc-dev/neofs-node/modules/morph" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + replicationManagerParams struct { + dig.In + + Viper *viper.Viper + + PeersInterface peers.Interface + + LocalStore localstore.Localstore + Peers peers.Store + Placement placement.Component + Logger *zap.Logger + Lister ir.Storage + Key *ecdsa.PrivateKey + + Placer implementations.ObjectPlacer + + TokenStore session.PrivateTokenStore + + MorphEventListener event.Listener + MorphEventHandlers morph.EventHandlers + } +) + +const ( + mainReplicationPrefix = "replication" + managerPrefix = "manager" + placementHonorerPrefix = "placement_honorer" + locationDetectorPrefix = "location_detector" + storageValidatorPrefix = "storage_validator" + replicatorPrefix = "replicator" + restorerPrefix = "restorer" +) + +func newReplicationManager(p replicationManagerParams) (replication.Manager, error) { + as, err := implementations.NewAddressStore(p.Peers, p.Logger) + if err != nil { + return nil, err + } + + ms, err := replication.NewMultiSolver(replication.MultiSolverParams{ + AddressStore: as, + Placement: p.Placement, + }) + if err != nil { + return nil, err + } + + op := replication.NewObjectPool() + + schd, err := replication.NewReplicationScheduler(replication.SchedulerParams{ + ContainerActualityChecker: ms, + Iterator: p.LocalStore, + }) + if err != nil { + return nil, err + } + + integrityVerifier, err := implementations.NewLocalIntegrityVerifier( + core.NewNeoKeyVerifier(), + ) + if err != nil { + return nil, err + } + + verifier, err := implementations.NewObjectValidator(&implementations.ObjectValidatorParams{ + AddressStore: ms, + Localstore: p.LocalStore, + Logger: p.Logger, + Verifier: integrityVerifier, + }) + if err != nil { + return nil, err + } + + placementHonorer, err := newPlacementHonorer(p, ms) + if err != nil { + return nil, err + } + + locationDetector, err := newLocationDetector(p, ms) + if err != nil { + return nil, err + } + + storageValidator, err := newStorageValidator(p, ms) + if err != nil { + return nil, err + } + + replicator, err := newObjectReplicator(p, ms) + if err != nil { + return nil, err + } + + restorer, err := newRestorer(p, ms) + if err != nil { + return nil, err + } + + prefix := mainReplicationPrefix + "." + managerPrefix + "." + capPrefix := prefix + "capacities." + + mngr, err := replication.NewManager(replication.ManagerParams{ + Interval: p.Viper.GetDuration(prefix + "read_pool_interval"), + PushTaskTimeout: p.Viper.GetDuration(prefix + "push_task_timeout"), + InitPoolSize: p.Viper.GetInt(prefix + "pool_size"), + ExpansionRate: p.Viper.GetFloat64(prefix + "pool_expansion_rate"), + PlacementHonorerEnabled: p.Viper.GetBool(prefix + "placement_honorer_enabled"), + ReplicateTaskChanCap: p.Viper.GetInt(capPrefix + "replicate"), + RestoreTaskChanCap: p.Viper.GetInt(capPrefix + "restore"), + GarbageChanCap: p.Viper.GetInt(capPrefix + "garbage"), + ObjectPool: op, + ObjectVerifier: verifier, + PlacementHonorer: placementHonorer, + ObjectLocationDetector: locationDetector, + StorageValidator: storageValidator, + ObjectReplicator: replicator, + ObjectRestorer: restorer, + Scheduler: schd, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + if handlerInfo, ok := p.MorphEventHandlers[morph.ContractEventOptPath( + morph.NetmapContractName, + morph.NewEpochEventType, + )]; ok { + handlerInfo.SetHandler(func(ev event.Event) { + mngr.HandleEpoch( + context.Background(), + ev.(netmap.NewEpoch).EpochNumber(), + ) + }) + + p.MorphEventListener.RegisterHandler(handlerInfo) + } + + return mngr, nil +} + +func newPlacementHonorer(p replicationManagerParams, rss replication.RemoteStorageSelector) (replication.PlacementHonorer, error) { + prefix := mainReplicationPrefix + "." + placementHonorerPrefix + + och, err := newObjectsContainerHandler(cnrHandlerParams{ + Viper: p.Viper, + Logger: p.Logger, + Placer: p.Placer, + PeerStore: p.Peers, + Peers: p.PeersInterface, + TimeoutsPrefix: prefix, + Key: p.Key, + + TokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + storage, err := implementations.NewObjectStorage(implementations.ObjectStorageParams{ + Localstore: p.LocalStore, + SelectiveContainerExecutor: och, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + return replication.NewPlacementHonorer(replication.PlacementHonorerParams{ + ObjectSource: storage, + ObjectReceptacle: storage, + RemoteStorageSelector: rss, + PresenceChecker: p.LocalStore, + Logger: p.Logger, + TaskChanCap: p.Viper.GetInt(prefix + ".chan_capacity"), + ResultTimeout: p.Viper.GetDuration(prefix + ".result_timeout"), + }) +} + +func newLocationDetector(p replicationManagerParams, ms replication.MultiSolver) (replication.ObjectLocationDetector, error) { + prefix := mainReplicationPrefix + "." + locationDetectorPrefix + + och, err := newObjectsContainerHandler(cnrHandlerParams{ + Viper: p.Viper, + Logger: p.Logger, + Placer: p.Placer, + PeerStore: p.Peers, + Peers: p.PeersInterface, + TimeoutsPrefix: prefix, + Key: p.Key, + + TokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + locator, err := implementations.NewObjectLocator(implementations.LocatorParams{ + SelectiveContainerExecutor: och, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + return replication.NewLocationDetector(&replication.LocationDetectorParams{ + WeightComparator: ms, + ObjectLocator: locator, + ReservationRatioReceiver: ms, + PresenceChecker: p.LocalStore, + Logger: p.Logger, + TaskChanCap: p.Viper.GetInt(prefix + ".chan_capacity"), + ResultTimeout: p.Viper.GetDuration(prefix + ".result_timeout"), + }) +} + +func newStorageValidator(p replicationManagerParams, as replication.AddressStore) (replication.StorageValidator, error) { + prefix := mainReplicationPrefix + "." + storageValidatorPrefix + + var sltr implementations.Salitor + + switch v := p.Viper.GetString(prefix + ".salitor"); v { + case xorSalitor: + sltr = hash.SaltXOR + default: + return nil, errors.Errorf("unsupported salitor: %s", v) + } + + och, err := newObjectsContainerHandler(cnrHandlerParams{ + Viper: p.Viper, + Logger: p.Logger, + Placer: p.Placer, + PeerStore: p.Peers, + Peers: p.PeersInterface, + TimeoutsPrefix: prefix, + Key: p.Key, + + TokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + headVerifier, err := implementations.NewLocalHeadIntegrityVerifier( + core.NewNeoKeyVerifier(), + ) + if err != nil { + return nil, err + } + + verifier, err := implementations.NewObjectValidator(&implementations.ObjectValidatorParams{ + AddressStore: as, + Localstore: p.LocalStore, + SelectiveContainerExecutor: och, + Logger: p.Logger, + Salitor: sltr, + SaltSize: p.Viper.GetInt(prefix + ".salt_size"), + MaxPayloadRangeSize: p.Viper.GetUint64(prefix + ".max_payload_range_size"), + PayloadRangeCount: p.Viper.GetInt(prefix + ".payload_range_count"), + Verifier: headVerifier, + }) + if err != nil { + return nil, err + } + + return replication.NewStorageValidator(replication.StorageValidatorParams{ + ObjectVerifier: verifier, + PresenceChecker: p.LocalStore, + Logger: p.Logger, + TaskChanCap: p.Viper.GetInt(prefix + ".chan_capacity"), + ResultTimeout: p.Viper.GetDuration(prefix + ".result_timeout"), + AddrStore: as, + }) +} + +func newObjectReplicator(p replicationManagerParams, rss replication.RemoteStorageSelector) (replication.ObjectReplicator, error) { + prefix := mainReplicationPrefix + "." + replicatorPrefix + + och, err := newObjectsContainerHandler(cnrHandlerParams{ + Viper: p.Viper, + Logger: p.Logger, + Placer: p.Placer, + PeerStore: p.Peers, + Peers: p.PeersInterface, + TimeoutsPrefix: prefix, + Key: p.Key, + + TokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + storage, err := implementations.NewObjectStorage(implementations.ObjectStorageParams{ + Localstore: p.LocalStore, + SelectiveContainerExecutor: och, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + return replication.NewReplicator(replication.ObjectReplicatorParams{ + RemoteStorageSelector: rss, + ObjectSource: storage, + ObjectReceptacle: storage, + PresenceChecker: p.LocalStore, + Logger: p.Logger, + TaskChanCap: p.Viper.GetInt(prefix + ".chan_capacity"), + ResultTimeout: p.Viper.GetDuration(prefix + ".result_timeout"), + }) +} + +func newRestorer(p replicationManagerParams, ms replication.MultiSolver) (replication.ObjectRestorer, error) { + prefix := mainReplicationPrefix + "." + restorerPrefix + + och, err := newObjectsContainerHandler(cnrHandlerParams{ + Viper: p.Viper, + Logger: p.Logger, + Placer: p.Placer, + PeerStore: p.Peers, + Peers: p.PeersInterface, + TimeoutsPrefix: prefix, + Key: p.Key, + + TokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + integrityVerifier, err := implementations.NewLocalIntegrityVerifier( + core.NewNeoKeyVerifier(), + ) + if err != nil { + return nil, err + } + + verifier, err := implementations.NewObjectValidator(&implementations.ObjectValidatorParams{ + AddressStore: ms, + Localstore: p.LocalStore, + SelectiveContainerExecutor: och, + Logger: p.Logger, + Verifier: integrityVerifier, + }) + if err != nil { + return nil, err + } + + storage, err := implementations.NewObjectStorage(implementations.ObjectStorageParams{ + Localstore: p.LocalStore, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + return replication.NewObjectRestorer(&replication.ObjectRestorerParams{ + ObjectVerifier: verifier, + ObjectReceptacle: storage, + EpochReceiver: ms, + RemoteStorageSelector: ms, + PresenceChecker: p.LocalStore, + Logger: p.Logger, + TaskChanCap: p.Viper.GetInt(prefix + ".chan_capacity"), + ResultTimeout: p.Viper.GetDuration(prefix + ".result_timeout"), + }) +} diff --git a/modules/node/services.go b/modules/node/services.go new file mode 100644 index 0000000000..d6c3cadcac --- /dev/null +++ b/modules/node/services.go @@ -0,0 +1,36 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/modules/grpc" + "github.com/nspcc-dev/neofs-node/services/metrics" + "github.com/nspcc-dev/neofs-node/services/public/accounting" + "github.com/nspcc-dev/neofs-node/services/public/container" + "github.com/nspcc-dev/neofs-node/services/public/object" + "github.com/nspcc-dev/neofs-node/services/public/session" + "github.com/nspcc-dev/neofs-node/services/public/state" + "go.uber.org/dig" +) + +type servicesParams struct { + dig.In + + Status state.Service + Container container.Service + Object object.Service + Session session.Service + Accounting accounting.Service + Metrics metrics.Service +} + +func attachServices(p servicesParams) grpc.ServicesResult { + return grpc.ServicesResult{ + Services: []grpc.Service{ + p.Status, + p.Container, + p.Accounting, + p.Metrics, + p.Session, + p.Object, + }, + } +} diff --git a/modules/node/session.go b/modules/node/session.go new file mode 100644 index 0000000000..aaa2527796 --- /dev/null +++ b/modules/node/session.go @@ -0,0 +1,26 @@ +package node + +import ( + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/services/public/session" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type sessionParams struct { + dig.In + + Logger *zap.Logger + + TokenStore session.TokenStore + + EpochReceiver implementations.EpochReceiver +} + +func newSessionService(p sessionParams) (session.Service, error) { + return session.New(session.Params{ + TokenStore: p.TokenStore, + Logger: p.Logger, + EpochReceiver: p.EpochReceiver, + }), nil +} diff --git a/modules/settings/address.go b/modules/settings/address.go new file mode 100644 index 0000000000..c1c9722c4e --- /dev/null +++ b/modules/settings/address.go @@ -0,0 +1,109 @@ +package settings + +import ( + "net" + "strconv" + "strings" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/pkg/errors" +) + +const ( + protoTCP = "tcp" + protoUDP = "udp" + protoQUIC = "quic" +) + +const emptyAddr = "0.0.0.0" + +const ip4ColonCount = 1 + +var ( + errEmptyAddress = internal.Error("`node.address` could not be empty") + errEmptyProtocol = internal.Error("`node.protocol` could not be empty") + errUnknownProtocol = internal.Error("`node.protocol` unknown protocol") + errEmptyShutdownTTL = internal.Error("`node.shutdown_ttl` could not be empty") +) + +func ipVersion(address string) string { + if strings.Count(address, ":") > ip4ColonCount { + return "ip6" + } + + return "ip4" +} + +func prepareAddress(address string) (string, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return "", errors.Wrapf(err, "could not fetch host/port: %s", address) + } else if host == "" { + host = emptyAddr + } + + addr, err := net.ResolveIPAddr("ip", host) + if err != nil { + return "", errors.Wrapf(err, "could not resolve address: %s:%s", host, port) + } + + return net.JoinHostPort(addr.IP.String(), port), nil +} + +func resolveAddress(proto, address string) (string, string, error) { + var ( + ip net.IP + host, port string + ) + + switch proto { + case protoTCP: + addr, err := net.ResolveTCPAddr(protoTCP, address) + if err != nil { + return "", "", errors.Wrapf(err, "could not parse address: '%s'", address) + } + + ip = addr.IP + port = strconv.Itoa(addr.Port) + case protoUDP, protoQUIC: + addr, err := net.ResolveUDPAddr(protoUDP, address) + if err != nil { + return "", "", errors.Wrapf(err, "could not parse address: '%s'", address) + } + + ip = addr.IP + port = strconv.Itoa(addr.Port) + default: + return "", "", errors.Wrapf(errUnknownProtocol, "unknown protocol: '%s'", proto) + } + + if host = ip.String(); ip == nil { + host = emptyAddr + } + + return host, port, nil +} + +func multiAddressFromProtoAddress(proto, addr string) (multiaddr.Multiaddr, error) { + var ( + err error + host, port string + ipVer = ipVersion(addr) + ) + + if host, port, err = resolveAddress(proto, addr); err != nil { + return nil, errors.Wrapf(err, "could not resolve address: (%s) '%s'", proto, addr) + } + + items := []string{ + ipVer, + host, + proto, + port, + } + + addr = "/" + strings.Join(items, "/") + + return multiaddr.NewMultiaddr(addr) +} diff --git a/modules/settings/module.go b/modules/settings/module.go new file mode 100644 index 0000000000..1e075103d5 --- /dev/null +++ b/modules/settings/module.go @@ -0,0 +1,10 @@ +package settings + +import ( + "github.com/nspcc-dev/neofs-node/lib/fix/module" +) + +// Module is a node settings module. +var Module = module.Module{ + {Constructor: newNodeSettings}, +} diff --git a/modules/settings/node.go b/modules/settings/node.go new file mode 100644 index 0000000000..47b940e69d --- /dev/null +++ b/modules/settings/node.go @@ -0,0 +1,149 @@ +package settings + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "strconv" + "strings" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/pkg/errors" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + nodeSettings struct { + dig.Out + + Address multiaddr.Multiaddr + PrivateKey *ecdsa.PrivateKey + NodeOpts []string `name:"node_options"` + ShutdownTTL time.Duration `name:"shutdown_ttl"` + + NodeInfo bootstrap.NodeInfo + } +) + +const generateKey = "generated" + +var errEmptyNodeSettings = errors.New("node settings could not be empty") + +func newNodeSettings(v *viper.Viper, l *zap.Logger) (cfg nodeSettings, err error) { + // check, that we have node settings in provided config + if !v.IsSet("node") { + err = errEmptyNodeSettings + return + } + + // try to load and setup ecdsa.PrivateKey + key := v.GetString("node.private_key") + switch key { + case "": + err = crypto.ErrEmptyPrivateKey + return cfg, err + case generateKey: + if cfg.PrivateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil { + return cfg, err + } + default: + if cfg.PrivateKey, err = crypto.LoadPrivateKey(key); err != nil { + return cfg, errors.Wrap(err, "cannot unmarshal private key") + } + } + + id := peers.IDFromPublicKey(&cfg.PrivateKey.PublicKey) + pub := crypto.MarshalPublicKey(&cfg.PrivateKey.PublicKey) + l.Debug("private key loaded successful", + zap.String("file", v.GetString("node.private_key")), + zap.Binary("public", pub), + zap.Stringer("node-id", id)) + + var ( + addr string + proto string + ) + + // fetch shutdown timeout from settings + if cfg.ShutdownTTL = v.GetDuration("node.shutdown_ttl"); cfg.ShutdownTTL == 0 { + return cfg, errEmptyShutdownTTL + } + + // fetch address and protocol from settings + if addr = v.GetString("node.address"); addr == "" { + return cfg, errors.Wrapf(errEmptyAddress, "given '%s'", addr) + } else if addr, err := prepareAddress(addr); err != nil { + return cfg, err + } else if proto = v.GetString("node.proto"); proto == "" { + return cfg, errors.Wrapf(errEmptyProtocol, "given '%s'", proto) + } else if cfg.Address, err = multiAddressFromProtoAddress(proto, addr); err != nil { + return cfg, errors.Wrapf(err, "given '%s' '%s'", proto, addr) + } + + // add well-known options + items := map[string]string{ + "Capacity": "capacity", + "Price": "price", + "Location": "location", + "Country": "country", + "City": "city", + } + + // TODO: use const namings + prefix := "node." + + for opt, path := range items { + val := v.GetString(prefix + path) + if len(val) == 0 { + err = errors.Errorf("node option %s must be set explicitly", opt) + return + } + + cfg.NodeOpts = append(cfg.NodeOpts, + fmt.Sprintf("/%s:%s", + opt, + val, + ), + ) + } + + // add other options + + var ( + i int + val string + ) +loop: + for ; ; i++ { + val = v.GetString("node.options." + strconv.Itoa(i)) + if val == "" { + break + } + + for opt := range items { + if strings.Contains(val, "/"+opt) { + continue loop + } + } + + cfg.NodeOpts = append(cfg.NodeOpts, val) + } + + cfg.NodeInfo = bootstrap.NodeInfo{ + Address: cfg.Address.String(), + PubKey: crypto.MarshalPublicKey(&cfg.PrivateKey.PublicKey), + Options: cfg.NodeOpts, + } + + l.Debug("loaded node options", + zap.Strings("options", cfg.NodeOpts)) + + return cfg, err +} diff --git a/modules/workers/module.go b/modules/workers/module.go new file mode 100644 index 0000000000..275a5faf28 --- /dev/null +++ b/modules/workers/module.go @@ -0,0 +1,10 @@ +package workers + +import ( + "github.com/nspcc-dev/neofs-node/lib/fix/module" +) + +// Module is a workers module. +var Module = module.Module{ + {Constructor: prepare}, +} diff --git a/modules/workers/prepare.go b/modules/workers/prepare.go new file mode 100644 index 0000000000..ea5411fbf4 --- /dev/null +++ b/modules/workers/prepare.go @@ -0,0 +1,132 @@ +package workers + +import ( + "context" + "time" + + "github.com/nspcc-dev/neofs-node/lib/fix/worker" + "github.com/spf13/viper" + "go.uber.org/dig" + "go.uber.org/zap" +) + +type ( + // Result returns wrapped workers group for DI. + Result struct { + dig.Out + + Workers []*worker.Job + } + + // Params is dependencies for create workers slice. + Params struct { + dig.In + + Jobs worker.Jobs + Viper *viper.Viper + Logger *zap.Logger + } +) + +func prepare(p Params) worker.Workers { + w := worker.New() + + for name, handler := range p.Jobs { + if job := byConfig(name, handler, p.Logger, p.Viper); job != nil { + p.Logger.Debug("worker: add new job", + zap.String("name", name)) + + w.Add(job) + } + } + + return w +} + +func byTicker(d time.Duration, h worker.Handler) worker.Handler { + return func(ctx context.Context) { + ticker := time.NewTicker(d) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + default: + select { + case <-ctx.Done(): + return + case <-ticker.C: + h(ctx) + } + } + } + } +} + +func byTimer(d time.Duration, h worker.Handler) worker.Handler { + return func(ctx context.Context) { + timer := time.NewTimer(d) + defer timer.Stop() + + for { + select { + case <-ctx.Done(): + return + default: + select { + case <-ctx.Done(): + return + case <-timer.C: + h(ctx) + timer.Reset(d) + } + } + } + } +} + +func byConfig(name string, h worker.Handler, l *zap.Logger, v *viper.Viper) worker.Handler { + var job worker.Handler + + if !v.IsSet("workers." + name) { + l.Info("worker: has no configuration", + zap.String("worker", name)) + return nil + } + + if v.GetBool("workers." + name + ".disabled") { + l.Info("worker: disabled", + zap.String("worker", name)) + return nil + } + + if ticker := v.GetDuration("workers." + name + ".ticker"); ticker > 0 { + job = byTicker(ticker, h) + } + + if timer := v.GetDuration("workers." + name + ".timer"); timer > 0 { + job = byTimer(timer, h) + } + + if v.GetBool("workers." + name + ".immediately") { + return func(ctx context.Context) { + h(ctx) + + if job == nil { + return + } + + // check context before run immediately job again + select { + case <-ctx.Done(): + return + default: + } + + job(ctx) + } + } + + return job +} diff --git a/services/metrics/service.go b/services/metrics/service.go new file mode 100644 index 0000000000..1acd9970e1 --- /dev/null +++ b/services/metrics/service.go @@ -0,0 +1,60 @@ +package metrics + +import ( + "context" + + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/metrics" + "github.com/nspcc-dev/neofs-node/modules/grpc" + "go.uber.org/zap" +) + +type ( + // Service is an interface of the server of Metrics service. + Service interface { + MetricsServer + grpc.Service + } + + // Params groups the parameters of Metrics service server's constructor. + Params struct { + Logger *zap.Logger + Collector metrics.Collector + } + + serviceMetrics struct { + log *zap.Logger + col metrics.Collector + } +) + +const ( + errEmptyLogger = internal.Error("empty logger") + errEmptyCollector = internal.Error("empty metrics collector") +) + +// New is a Metrics service server's constructor. +func New(p Params) (Service, error) { + switch { + case p.Logger == nil: + return nil, errEmptyLogger + case p.Collector == nil: + return nil, errEmptyCollector + } + + return &serviceMetrics{ + log: p.Logger, + col: p.Collector, + }, nil +} + +func (s *serviceMetrics) ResetSpaceCounter(_ context.Context, _ *ResetSpaceRequest) (*ResetSpaceResponse, error) { + s.col.UpdateSpaceUsage() + return &ResetSpaceResponse{}, nil +} + +func (s *serviceMetrics) Name() string { return "metrics" } + +func (s *serviceMetrics) Register(srv *grpc.Server) { + RegisterMetricsServer(srv, s) +} diff --git a/services/metrics/service.pb.go b/services/metrics/service.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..14062bfba755eb87860a989942b0556f2d57adcd GIT binary patch literal 13467 zcmeHOdvDuD68~HJ6ni=}pb}XRTYjm@0l7Tx475p*I7NZL=kl?Xd7(&^r2MGs?|#3T z*`+9nl9eEr_IeE(mfD@2_sq`DGQFO9mIhjlb)qx3(1G%9)HF+r)Zf9_&Nv;XUG@C6 zdiDBLy?B0d+HLlFDo=PlWCe2 z%4s(IG%56Tp<3pp)og?l{KNyVyk51&QE@Tzx_&z8jnmjo#yy_!W}&QMX72Q>CtPe z*=~}i-s(_iI`Op<(Q{R#DxXcKna*>ivnv`W%KrTCRyMY&e9TnF9e zrJJd9^+fE_{oc*;3pXxDp|S1@LKK!m#76fd37Lh{iwGog31CMc&T;0IWuwZ(lQdSZ1A+BKwCRzuqQxD3T2IRrr zCR{2nDjIu3{`^LK@g)C2XE})PDMA)&m()BRXD*^(MWAN0xS48LG1tXA(A01B&od3= zd6CWhLftkSKmPdR`K$DO@=@RD;Jc_m7{zA#_D=ou?>tRLt)13S0y#@E?Wf};`URmz ze?FXfH-*N_GGq=A-}Rwl^CwD*yXCVihw zB+eYq*r;vXbxv>6tw6Q6Kx#s_MA=b+hQmyvB+84(S6-UN6#+RsSU_ZmMMyAVrM|wt zKA(sx5%THzigj(qaJ-J1ERk$C8+S^_hylXr!8yDSBj24mUi%CKNr!}1qbF)&v7Dyw z$a()h3z69X1}JbxP=9%%k|<_GmN)!n3;?>rUt=ebNX|M04C>pcO=obEJo9Y^6lqyA zTS(8VAvh3hV)o6!)2V%*ww_BCFE-tV~u#u#}YUO=X7BrDfaYGFiJRnQ^6q zt+J4u%7tL}tsZ0V)Y&QOG{>na9vo~l?G8$FIig^}QZ3iQ7IrT#qm@m_1O(V@QD#{+ zGFeVvqDpaEn~zSdj=HCqq56vVhkM|#qf5PnT?hs5;Aj^oyaHz;RMf5-{}N57s6>gU zd4l59)@<~z2dJ6)*GH%u`q%vF`TdxIG>`O|yfnR`z#Z*LK5ur(2x-w@q{29i1@NVP*)ZUjI0t2qGTC4W@5u36YyB? z!ppH}U$|qEf+%ESp^#zm4N_K`&$A*&z}OB&nFArf3xvQS>r}`bGMPOVU$fGT>ex2N zJnJ&210Lx!B61A$OU)&(DEKDLy7D z8n95iJhUegu*e4tDl&zPXei2LpE+dmKJzY+BI^;4>@s8+u!=(F#jl_~;`e~b?DDL~ zQi@-NJiE_NpnzX_{4OyeZX%EeB0<1FV)ua2?TJY%i)CC;d!ihMk+niVW(Cha!yHQ- z9I$dkn;wfVu6DpE`l2Y-zJ#QxbRhm#c}i@{6AlW=K$7C%m1f7QTLg&l1%r58b94xrA-P>IG*md=tu`NcK$lax#Eahl^C z-_uDz$9bIUwTn~eSiODoEbrL1NJh;oY7bo$#WWxF(1Z(8U)pQ64Y$9}_1=A@uTXOX zZC83IOXARJB}7AoPDk@C`58FHghRY8nYV)zSxu9pqtYolZ{2QJ4DZJA>nSytaE@N6 z24s8Y(`cIUCe5uKQx+I4LlLH}YBF@&_OU`yq4Mrl2Z(+cj!jLUrnS?frVddBZaNM zwDKOOlsseeO{c~i?{_*x=lcY`;8;(Pp>(jwf$t{ho+>@@bP%8}qu6sVHO2ZBdJ5b; zH67Rz2e(pl6J;B4m%d$k+r#zHTRUE+fBs}WRQf7xUFolzCbq*(d34rF0W~&3D&H^C zpwhZD-oy>_3TUPNwgvZsik;lVwRj74g)VKV$`GG&^73FogSg{x9-JFI=j2%!p?aB@ z&nx~CWX0*v|B194BeM}Xw3a0K-QIOLEw7}yK>Lc0J5B}E^dBjDu-w zLW$(Txx<70O059~EC({c9MC)%a&bN6LPn#KHMf>vq)d?Y*8{fy1qTS8 z7EjIjS~}(PmVJ&kGWk>4Ic_j823%~G_6~!fVB>rH?6mg7h@j8rAzEp;*M+1Rlcl!d zzMX8RX;n1w^lAoK70wV_b4{?o{o}nJPD~*~Bx;Bj| zVr3-eB4g5}#^}N|Tt$tM+7V2vV8Dyp%Pl zrCojr@Av4Y9DybP_*?_!$EEt)6E%4J_>js9PH-P&`V1jRh>x7h2mR~AQ2k2HwLS$A z)?m>g-URf~Bi@2JMXW5w0+U&YJTmj9>u$0sU9Kb~8kcdXN^s%5Qj7GN;%DTCL_OM& zALgN-tldMbBmN$Ftr#4w0^=4HZ5m5B^mz$?@Y4NGCkI34`qx2QJyHj4)q`D)jq>yt z>ph#0RcH!D$GKLIA1h$|m*K&oI+n<^;W_GHuZ=&J%9;-!=J)ao8wc;shaE%dKmtRC7ADn5_>7#W1){Mb# zBuMC_D`1F%gCP?rZ^mC_*-28o#=$j=(<`Gt;O5(NS}BZSrMYLf(GjI|e0rbg>#6o} ztbFnMWx32F!8p8l^%+g@8%18D!BYGx;R-E#6t6GaWCLcCp+n`V#~i_iUk{y~kCp=b zz7a+`4qnYBL<_R-?K+GX2HT^cryo>7nrWGQ0E~w;3&BbNM5|GXaku&RO-_sNX-1V( zp!ptW#OwJ;kCqn7+;leO23eBg8#ekyw>+#x&?= zCjL#(Mtj5J?=V-EaE@=L{EKpu=>|Gv3^Wt=*+avLMQR|K~E^uD&c0>Gfq+)Ygl8Zr7u;x zPub-EXL|a-Nl#+(uauuGu|E^kp9yNY7^qE9%NvU6W!M^exu>zIYzokjyjWX8{ddYG z+%SH2#e}uBiQH_HTNX`@DO$a@;%es83#YR1k-}-IGBQO&2}I?nPY1C+*9_a%oV@FE zZCWil?T)ocjZ!pJ_QHATSy{eDlXfvx*HUZ}qWOlrGdNo;D;s59DNCthVQ~kQ5(!)N zksaUVaX!g$$s+BZ?Thi>Rxx!>RxQmNXd$5vQ>f1A41}+3YrTwwSgOzK6iHwP;Y!tJ zU0_r7$7dut|AHxNfjzbWM?agG%{3Hr(O1iq6;RVJm6}4Ya$ZS-*Wo{?P+u-O8N<$w zCAYum2t$lli)d4W0e#J}!ETFXG<_4GTDQNbh$er7mZZonl{81n*``)UX63THwqzvT znJ=D+2>zQRtqUI1pXAi|hcatO+Hd-CVNw43kMew!;CNC*xHmNB4*3IsV8-9>&+M1& wxbLS^QEdjb=oM<~8Uk=eJ;v>7YEG&K&jx$e return its owner + if token := req.GetSessionToken(); token != nil { + return token.GetOwnerID(), crypto.UnmarshalPublicKey(token.GetOwnerKey()), nil + } + + signKeys := req.GetSignKeyPairs() + if len(signKeys) == 0 { + return OwnerID{}, nil, errMissingSignatures + } + + firstKey := signKeys[0].GetPublicKey() + if firstKey == nil { + return OwnerID{}, nil, crypto.ErrEmptyPublicKey + } + + owner, err := refs.NewOwnerID(firstKey) + + return owner, firstKey, err +} + +// HeadersOfType returns request or object headers. +func (s serviceRequestInfo) HeadersOfType(typ acl.HeaderType) ([]acl.Header, bool) { + switch typ { + default: + return nil, true + case acl.HdrTypeRequest: + return libacl.TypedHeaderSourceFromExtendedHeaders(s.req).HeadersOfType(typ) + case acl.HdrTypeObjSys, acl.HdrTypeObjUsr: + obj, ok := s.objHdrSrc.getHeaders() + if !ok { + return nil, false + } + + return libacl.TypedHeaderSourceFromObject(obj).HeadersOfType(typ) + } +} + +// Key returns a binary representation of sender public key. +func (s serviceRequestInfo) Key() []byte { + _, key, err := requestOwner(s.req) + if err != nil { + return nil + } + + return crypto.MarshalPublicKey(key) +} + +// TypeOf returns true of object request type corresponds to passed OperationType. +func (s serviceRequestInfo) TypeOf(opType acl.OperationType) bool { + switch s.req.Type() { + case object.RequestGet: + return opType == acl.OpTypeGet + case object.RequestPut: + return opType == acl.OpTypePut + case object.RequestHead: + return opType == acl.OpTypeHead + case object.RequestSearch: + return opType == acl.OpTypeSearch + case object.RequestDelete: + return opType == acl.OpTypeDelete + case object.RequestRange: + return opType == acl.OpTypeRange + case object.RequestRangeHash: + return opType == acl.OpTypeRangeHash + default: + return false + } +} + +// TargetOf return true if target field is equal to passed ACL target. +func (s serviceRequestInfo) TargetOf(target acl.Target) bool { + return s.target == target +} + +func (s requestObjHdrSrc) getHeaders() (*Object, bool) { + switch s.req.Type() { + case object.RequestSearch: + // object header filters is not supported in Search request now + return nil, true + case object.RequestPut: + // for Put we get object headers from request + return s.req.(transport.PutInfo).GetHead(), true + default: + tReq := &transportRequest{ + serviceRequest: s.req, + } + + // for other requests we get object headers from local storage + m, err := s.ls.Meta(tReq.GetAddress()) + if err == nil { + return m.GetObject(), true + } + + return nil, false + } +} + +type requestActionParams struct { + eaclSrc libacl.ExtendedACLSource + + request serviceRequest + + objHdrSrc objectHeadersSource + + target acl.Target +} + +func (s reqActionCalc) calculateRequestAction(ctx context.Context, p requestActionParams) acl.ExtendedACLAction { + // get EACL table + table, err := p.eaclSrc.GetExtendedACLTable(ctx, p.request.CID()) + if err != nil { + s.log.Warn("could not get extended acl of the container", + zap.Stringer("cid", p.request.CID()), + zap.String("error", err.Error()), + ) + + return acl.ActionUndefined + } + + // create RequestInfo instance + reqInfo := &serviceRequestInfo{ + target: p.target, + req: p.request, + objHdrSrc: p.objHdrSrc, + } + + // calculate ACL action + return s.extACLChecker.Action(table, reqInfo) +} + +func (s aclInfoReceiver) getACLInfo(ctx context.Context, req serviceRequest) (*aclInfo, error) { + rule, err := s.basicACLGetter.GetBasicACL(ctx, req.CID()) + if err != nil { + return nil, err + } + + isBearer, err := s.basicChecker.Bearer(rule, req.Type()) + if err != nil { + return nil, err + } + + // fetch target from the request + target := s.targetFinder.Target(ctx, req) + + return &aclInfo{ + rule: rule, + + checkExtended: target != acl.Target_System && s.basicChecker.Extended(rule), + + target: target, + + checkBearer: target != acl.Target_System && isBearer && req.GetBearerToken() != nil, + }, nil +} + +func (s eaclFromBearer) GetExtendedACLTable(ctx context.Context, cid CID) (acl.ExtendedACLTable, error) { + table := acl.WrapEACLTable(nil) + + if err := table.UnmarshalBinary(s.bearer.GetACLRules()); err != nil { + return nil, err + } + + return table, nil +} diff --git a/services/public/object/acl_test.go b/services/public/object/acl_test.go new file mode 100644 index 0000000000..0527913767 --- /dev/null +++ b/services/public/object/acl_test.go @@ -0,0 +1,512 @@ +package object + +import ( + "context" + "crypto/ecdsa" + "errors" + "testing" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + libacl "github.com/nspcc-dev/neofs-node/lib/acl" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/ir" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/stretchr/testify/require" +) + +type ( + testACLEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + RequestTargeter + implementations.ACLHelper + implementations.ContainerNodesLister + implementations.ContainerOwnerChecker + acl.ExtendedACLTable + libacl.RequestInfo + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +type testBasicChecker struct { + libacl.BasicChecker + + actionErr error + action bool + + sticky bool + + extended bool + + bearer bool +} + +func (t *testACLEntity) calculateRequestAction(context.Context, requestActionParams) acl.ExtendedACLAction { + return t.res.(acl.ExtendedACLAction) +} + +func (t *testACLEntity) buildRequestInfo(req serviceRequest, target acl.Target) (libacl.RequestInfo, error) { + if t.f != nil { + t.f(req, target) + } + + if t.err != nil { + return nil, t.err + } + + return t.res.(libacl.RequestInfo), nil +} + +func (t *testACLEntity) Action(table acl.ExtendedACLTable, req libacl.RequestInfo) acl.ExtendedACLAction { + if t.f != nil { + t.f(table, req) + } + + return t.res.(acl.ExtendedACLAction) +} + +func (t *testACLEntity) GetExtendedACLTable(_ context.Context, cid CID) (acl.ExtendedACLTable, error) { + if t.f != nil { + t.f(cid) + } + + if t.err != nil { + return nil, t.err + } + + return t.res.(acl.ExtendedACLTable), nil +} + +func (s *testBasicChecker) Extended(uint32) bool { + return s.extended +} + +func (s *testBasicChecker) Sticky(uint32) bool { + return s.sticky +} + +func (s *testBasicChecker) Bearer(uint32, object.RequestType) (bool, error) { + return s.bearer, nil +} + +func (s *testBasicChecker) Action(uint32, object.RequestType, acl.Target) (bool, error) { + return s.action, s.actionErr +} + +func (t *testACLEntity) GetBasicACL(context.Context, CID) (uint32, error) { + if t.err != nil { + return 0, t.err + } + + return t.res.(uint32), nil +} + +func (t *testACLEntity) Target(context.Context, serviceRequest) acl.Target { + return t.res.(acl.Target) +} + +func (t *testACLEntity) CID() CID { return CID{} } + +func (t *testACLEntity) Type() object.RequestType { return t.res.(object.RequestType) } + +func (t *testACLEntity) GetBearerToken() service.BearerToken { return nil } + +func (t *testACLEntity) GetOwner() (*ecdsa.PublicKey, error) { + if t.err != nil { + return nil, t.err + } + + return t.res.(*ecdsa.PublicKey), nil +} + +func (t testACLEntity) GetIRInfo(ir.GetInfoParams) (*ir.GetInfoResult, error) { + if t.err != nil { + return nil, t.err + } + + res := new(ir.GetInfoResult) + res.SetInfo(*t.res.(*ir.Info)) + + return res, nil +} + +func (t *testACLEntity) ContainerNodesInfo(ctx context.Context, cid CID, prev int) ([]bootstrap.NodeInfo, error) { + if t.err != nil { + return nil, t.err + } + + return t.res.([][]bootstrap.NodeInfo)[prev], nil +} + +func (t *testACLEntity) IsContainerOwner(_ context.Context, cid CID, owner OwnerID) (bool, error) { + if t.f != nil { + t.f(cid, owner) + } + if t.err != nil { + return false, t.err + } + + return t.res.(bool), nil +} + +func (t testACLEntity) GetSignKeyPairs() []service.SignKeyPair { + if t.res == nil { + return nil + } + return t.res.([]service.SignKeyPair) +} + +func TestPreprocessor(t *testing.T) { + ctx := context.TODO() + + t.Run("empty request", func(t *testing.T) { + require.PanicsWithValue(t, pmEmptyServiceRequest, func() { + _ = new(aclPreProcessor).preProcess(ctx, nil) + }) + }) + + t.Run("everything is okay", func(t *testing.T) { + rule := uint32(0x00000003) + // set F-bit + rule |= 1 << 28 + + checker := new(libacl.BasicACLChecker) + + preprocessor := aclPreProcessor{ + log: test.NewTestLogger(false), + aclInfoReceiver: aclInfoReceiver{ + basicACLGetter: &testACLEntity{res: rule}, + basicChecker: checker, + targetFinder: &testACLEntity{res: acl.Target_Others}, + }, + basicChecker: checker, + } + require.NoError(t, preprocessor.preProcess(ctx, &testACLEntity{res: object.RequestGet})) + + preprocessor.aclInfoReceiver.targetFinder = &testACLEntity{res: acl.Target_System} + require.Error(t, preprocessor.preProcess(ctx, &testACLEntity{res: object.RequestGet})) + preprocessor.aclInfoReceiver.targetFinder = &testACLEntity{res: acl.Target_User} + require.Error(t, preprocessor.preProcess(ctx, &testACLEntity{res: object.RequestGet})) + }) + + t.Run("can't fetch container", func(t *testing.T) { + preprocessor := aclPreProcessor{ + log: test.NewTestLogger(false), + aclInfoReceiver: aclInfoReceiver{ + basicACLGetter: &testACLEntity{err: container.ErrNotFound}, + targetFinder: &testACLEntity{res: acl.Target_Others}, + }, + } + require.Error(t, preprocessor.preProcess(ctx, &testACLEntity{res: object.RequestGet})) + + }) + + t.Run("sticky bit", func(t *testing.T) { + checker := &testBasicChecker{ + actionErr: nil, + action: true, + sticky: true, + } + + s := &aclPreProcessor{ + log: test.NewTestLogger(false), + aclInfoReceiver: aclInfoReceiver{ + basicACLGetter: &testACLEntity{ + res: uint32(0), + }, + basicChecker: checker, + targetFinder: &testACLEntity{ + res: acl.Target_User, + }, + }, + basicChecker: checker, + } + + ownerKey := &test.DecodeKey(0).PublicKey + + ownerID, err := refs.NewOwnerID(ownerKey) + require.NoError(t, err) + + okItems := []func() []serviceRequest{ + // Read requests + func() []serviceRequest { + return []serviceRequest{ + new(object.GetRequest), + new(object.HeadRequest), + new(object.SearchRequest), + new(GetRangeRequest), + new(object.GetRangeHashRequest), + } + }, + // PutRequest / DeleteRequest (w/o token) + func() []serviceRequest { + req := object.MakePutRequestHeader(&Object{ + SystemHeader: SystemHeader{ + OwnerID: ownerID, + }, + }) + req.AddSignKey(nil, ownerKey) + putReq := &putRequest{ + PutRequest: req, + } + + delReq := new(object.DeleteRequest) + delReq.OwnerID = ownerID + delReq.AddSignKey(nil, ownerKey) + + return []serviceRequest{putReq, delReq} + }, + // PutRequest / DeleteRequest (w/ token) + func() []serviceRequest { + token := new(service.Token) + token.SetOwnerID(ownerID) + token.SetOwnerKey(crypto.MarshalPublicKey(ownerKey)) + + req := object.MakePutRequestHeader(&Object{ + SystemHeader: SystemHeader{ + OwnerID: ownerID, + }, + }) + req.SetToken(token) + putReq := &putRequest{ + PutRequest: req, + } + + delReq := new(object.DeleteRequest) + delReq.OwnerID = ownerID + delReq.SetToken(token) + + return []serviceRequest{putReq, delReq} + }, + } + + failItems := []func() []serviceRequest{ + // PutRequest / DeleteRequest (w/o token and wrong owner) + func() []serviceRequest { + otherOwner := ownerID + otherOwner[0]++ + + req := object.MakePutRequestHeader(&Object{ + SystemHeader: SystemHeader{ + OwnerID: otherOwner, + }, + }) + req.AddSignKey(nil, ownerKey) + putReq := &putRequest{ + PutRequest: req, + } + + delReq := new(object.DeleteRequest) + delReq.OwnerID = otherOwner + delReq.AddSignKey(nil, ownerKey) + + return []serviceRequest{putReq, delReq} + }, + // PutRequest / DeleteRequest (w/ token w/ wrong owner) + func() []serviceRequest { + otherOwner := ownerID + otherOwner[0]++ + + token := new(service.Token) + token.SetOwnerID(ownerID) + token.SetOwnerKey(crypto.MarshalPublicKey(ownerKey)) + + req := object.MakePutRequestHeader(&Object{ + SystemHeader: SystemHeader{ + OwnerID: otherOwner, + }, + }) + req.SetToken(token) + putReq := &putRequest{ + PutRequest: req, + } + + delReq := new(object.DeleteRequest) + delReq.OwnerID = otherOwner + delReq.SetToken(token) + + return []serviceRequest{putReq, delReq} + }, + } + + for _, ok := range okItems { + for _, req := range ok() { + require.NoError(t, s.preProcess(ctx, req)) + } + } + + for _, fail := range failItems { + for _, req := range fail() { + require.Error(t, s.preProcess(ctx, req)) + } + } + }) + + t.Run("extended ACL", func(t *testing.T) { + target := acl.Target_Others + + req := &testACLEntity{ + res: object.RequestGet, + } + + actCalc := new(testACLEntity) + + checker := &testBasicChecker{ + action: true, + extended: true, + } + + s := &aclPreProcessor{ + log: test.NewTestLogger(false), + aclInfoReceiver: aclInfoReceiver{ + basicACLGetter: &testACLEntity{ + res: uint32(1), + }, + basicChecker: checker, + targetFinder: &testACLEntity{ + res: target, + }, + }, + basicChecker: checker, + + reqActionCalc: actCalc, + } + + // force to return non-ActionAllow + actCalc.res = acl.ActionAllow + 1 + require.EqualError(t, s.preProcess(ctx, req), errAccessDenied.Error()) + + // force to return ActionAllow + actCalc.res = acl.ActionAllow + require.NoError(t, s.preProcess(ctx, req)) + }) +} + +func TestTargetFinder(t *testing.T) { + ctx := context.TODO() + irKey := test.DecodeKey(2) + containerKey := test.DecodeKey(3) + prevContainerKey := test.DecodeKey(4) + + irInfo := new(ir.Info) + irNode := ir.Node{} + irNode.SetKey(crypto.MarshalPublicKey(&irKey.PublicKey)) + irInfo.SetNodes([]ir.Node{irNode}) + + finder := &targetFinder{ + log: test.NewTestLogger(false), + irStorage: &testACLEntity{ + res: irInfo, + }, + cnrLister: &testACLEntity{res: [][]bootstrap.NodeInfo{ + {{PubKey: crypto.MarshalPublicKey(&containerKey.PublicKey)}}, + {{PubKey: crypto.MarshalPublicKey(&prevContainerKey.PublicKey)}}, + }}, + } + + t.Run("trusted node", func(t *testing.T) { + + pk := &test.DecodeKey(0).PublicKey + + ownerKey := &test.DecodeKey(1).PublicKey + owner, err := refs.NewOwnerID(ownerKey) + require.NoError(t, err) + + token := new(service.Token) + token.SetSessionKey(crypto.MarshalPublicKey(pk)) + token.SetOwnerKey(crypto.MarshalPublicKey(ownerKey)) + token.SetOwnerID(owner) + + req := new(object.SearchRequest) + req.ContainerID = CID{1, 2, 3} + req.SetToken(token) + req.AddSignKey(nil, pk) + + finder.cnrOwnerChecker = &testACLEntity{ + f: func(items ...interface{}) { + require.Equal(t, req.CID(), items[0]) + require.Equal(t, owner, items[1]) + }, + res: true, + } + + require.Equal(t, acl.Target_User, finder.Target(ctx, req)) + }) + + t.Run("container owner", func(t *testing.T) { + finder.cnrOwnerChecker = &testACLEntity{res: true} + + req := new(object.SearchRequest) + req.AddSignKey(nil, &test.DecodeKey(0).PublicKey) + + require.Equal(t, acl.Target_User, finder.Target(ctx, req)) + }) + + t.Run("system owner", func(t *testing.T) { + finder.cnrOwnerChecker = &testACLEntity{res: false} + + req := new(object.SearchRequest) + req.AddSignKey(nil, &irKey.PublicKey) + require.Equal(t, acl.Target_System, finder.Target(ctx, req)) + + req = new(object.SearchRequest) + req.AddSignKey(nil, &containerKey.PublicKey) + require.Equal(t, acl.Target_System, finder.Target(ctx, req)) + + req = new(object.SearchRequest) + req.AddSignKey(nil, &prevContainerKey.PublicKey) + require.Equal(t, acl.Target_System, finder.Target(ctx, req)) + }) + + t.Run("other owner", func(t *testing.T) { + finder.cnrOwnerChecker = &testACLEntity{res: false} + + req := new(object.SearchRequest) + req.AddSignKey(nil, &test.DecodeKey(0).PublicKey) + require.Equal(t, acl.Target_Others, finder.Target(ctx, req)) + }) + + t.Run("can't fetch request owner", func(t *testing.T) { + req := new(object.SearchRequest) + + require.Equal(t, acl.Target_Unknown, finder.Target(ctx, req)) + }) + + t.Run("can't fetch container", func(t *testing.T) { + finder.cnrOwnerChecker = &testACLEntity{err: container.ErrNotFound} + + req := new(object.SearchRequest) + req.AddSignKey(nil, &test.DecodeKey(0).PublicKey) + require.Equal(t, acl.Target_Unknown, finder.Target(ctx, req)) + }) + + t.Run("can't fetch ir list", func(t *testing.T) { + finder.cnrOwnerChecker = &testACLEntity{res: false} + finder.irStorage = &testACLEntity{err: errors.New("blockchain is busy")} + + req := new(object.SearchRequest) + req.AddSignKey(nil, &test.DecodeKey(0).PublicKey) + require.Equal(t, acl.Target_Unknown, finder.Target(ctx, req)) + }) + + t.Run("can't fetch container list", func(t *testing.T) { + finder.cnrOwnerChecker = &testACLEntity{res: false} + finder.cnrLister = &testACLEntity{err: container.ErrNotFound} + + req := new(object.SearchRequest) + req.AddSignKey(nil, &test.DecodeKey(0).PublicKey) + require.Equal(t, acl.Target_Unknown, finder.Target(ctx, req)) + }) +} diff --git a/services/public/object/bearer.go b/services/public/object/bearer.go new file mode 100644 index 0000000000..ec01dc5847 --- /dev/null +++ b/services/public/object/bearer.go @@ -0,0 +1,72 @@ +package object + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/pkg/errors" +) + +type bearerTokenVerifier interface { + verifyBearerToken(context.Context, CID, service.BearerToken) error +} + +type complexBearerVerifier struct { + items []bearerTokenVerifier +} + +type bearerActualityVerifier struct { + epochRecv EpochReceiver +} + +type bearerOwnershipVerifier struct { + cnrOwnerChecker implementations.ContainerOwnerChecker +} + +type bearerSignatureVerifier struct{} + +var errWrongBearerOwner = errors.New("bearer author is not a container owner") + +func (s complexBearerVerifier) verifyBearerToken(ctx context.Context, cid CID, token service.BearerToken) error { + for i := range s.items { + if err := s.items[i].verifyBearerToken(ctx, cid, token); err != nil { + return err + } + } + + return nil +} + +func (s bearerActualityVerifier) verifyBearerToken(_ context.Context, _ CID, token service.BearerToken) error { + local := s.epochRecv.Epoch() + validUntil := token.ExpirationEpoch() + + if local > validUntil { + return errors.Errorf("bearer token is expired (local %d, valid until %d)", + local, + validUntil, + ) + } + + return nil +} + +func (s bearerOwnershipVerifier) verifyBearerToken(ctx context.Context, cid CID, token service.BearerToken) error { + isOwner, err := s.cnrOwnerChecker.IsContainerOwner(ctx, cid, token.GetOwnerID()) + if err != nil { + return err + } else if !isOwner { + return errWrongBearerOwner + } + + return nil +} + +func (s bearerSignatureVerifier) verifyBearerToken(_ context.Context, _ CID, token service.BearerToken) error { + return service.VerifySignatureWithKey( + crypto.UnmarshalPublicKey(token.GetOwnerKey()), + service.NewVerifiedBearerToken(token), + ) +} diff --git a/services/public/object/capacity.go b/services/public/object/capacity.go new file mode 100644 index 0000000000..d0cc58c82d --- /dev/null +++ b/services/public/object/capacity.go @@ -0,0 +1,19 @@ +package object + +func (s *objectService) RelativeAvailableCap() float64 { + diff := float64(s.ls.Size()) / float64(s.storageCap) + if 1-diff < 0 { + return 0 + } + + return 1 - diff +} + +func (s *objectService) AbsoluteAvailableCap() uint64 { + localSize := uint64(s.ls.Size()) + if localSize > s.storageCap { + return 0 + } + + return s.storageCap - localSize +} diff --git a/services/public/object/capacity_test.go b/services/public/object/capacity_test.go new file mode 100644 index 0000000000..deb34afb3d --- /dev/null +++ b/services/public/object/capacity_test.go @@ -0,0 +1,75 @@ +package object + +import ( + "testing" + + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testCapacityEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + localstore.Localstore + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var _ localstore.Localstore = (*testCapacityEntity)(nil) + +func (s *testCapacityEntity) Size() int64 { return s.res.(int64) } + +func TestObjectService_RelativeAvailableCap(t *testing.T) { + localStoreSize := int64(100) + + t.Run("oversize", func(t *testing.T) { + s := objectService{ + ls: &testCapacityEntity{res: localStoreSize}, + storageCap: uint64(localStoreSize - 1), + } + + require.Zero(t, s.RelativeAvailableCap()) + }) + + t.Run("correct calculation", func(t *testing.T) { + s := objectService{ + ls: &testCapacityEntity{res: localStoreSize}, + storageCap: 13 * uint64(localStoreSize), + } + + require.Equal(t, 1-float64(localStoreSize)/float64(s.storageCap), s.RelativeAvailableCap()) + }) +} + +func TestObjectService_AbsoluteAvailableCap(t *testing.T) { + localStoreSize := int64(100) + + t.Run("free space", func(t *testing.T) { + s := objectService{ + ls: &testCapacityEntity{res: localStoreSize}, + storageCap: uint64(localStoreSize), + } + + require.Zero(t, s.AbsoluteAvailableCap()) + s.storageCap-- + require.Zero(t, s.AbsoluteAvailableCap()) + }) + + t.Run("correct calculation", func(t *testing.T) { + s := objectService{ + ls: &testCapacityEntity{res: localStoreSize}, + storageCap: uint64(localStoreSize) + 12, + } + + require.Equal(t, s.storageCap-uint64(localStoreSize), s.AbsoluteAvailableCap()) + }) +} diff --git a/services/public/object/delete.go b/services/public/object/delete.go new file mode 100644 index 0000000000..8e8c5e2a52 --- /dev/null +++ b/services/public/object/delete.go @@ -0,0 +1,285 @@ +package object + +import ( + "context" + "crypto/sha256" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + objectRemover interface { + delete(context.Context, deleteInfo) error + } + + coreObjRemover struct { + delPrep deletePreparer + straightRem objectRemover + tokenStore session.PrivateTokenStore + + // Set of potential deletePreparer errors that won't be converted into errDeletePrepare + mErr map[error]struct{} + + log *zap.Logger + } + + straightObjRemover struct { + tombCreator tombstoneCreator + objStorer objectStorer + } + + tombstoneCreator interface { + createTombstone(context.Context, deleteInfo) *Object + } + + coreTombCreator struct{} + + deletePreparer interface { + prepare(context.Context, deleteInfo) ([]deleteInfo, error) + } + + coreDelPreparer struct { + timeout time.Duration + childLister objectChildrenLister + } + + deleteInfo interface { + transport.AddressInfo + GetOwnerID() OwnerID + } + + rawDeleteInfo struct { + rawAddrInfo + ownerID OwnerID + } +) + +const emRemovePart = "could not remove object part #%d of #%d" + +var ( + _ tombstoneCreator = (*coreTombCreator)(nil) + _ deleteInfo = (*rawDeleteInfo)(nil) + _ deletePreparer = (*coreDelPreparer)(nil) + _ objectRemover = (*straightObjRemover)(nil) + _ objectRemover = (*coreObjRemover)(nil) + _ deleteInfo = (*transportRequest)(nil) + + checksumOfEmptyPayload = sha256.Sum256([]byte{}) +) + +func (s *objectService) Delete(ctx context.Context, req *object.DeleteRequest) (res *object.DeleteResponse, err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestDelete), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestDelete, + e: err, + }) + }() + + if _, err = s.requestHandler.handleRequest(ctx, handleRequestParams{ + request: req, + executor: s, + }); err != nil { + return + } + + res = makeDeleteResponse() + err = s.respPreparer.prepareResponse(ctx, req, res) + + return +} + +func (s *coreObjRemover) delete(ctx context.Context, dInfo deleteInfo) error { + token := dInfo.GetSessionToken() + if token == nil { + return errNilToken + } + + key := session.PrivateTokenKey{} + key.SetOwnerID(dInfo.GetOwnerID()) + key.SetTokenID(token.GetID()) + + pToken, err := s.tokenStore.Fetch(key) + if err != nil { + return &detailedError{ + error: errTokenRetrieval, + d: privateTokenRecvDetails(token.GetID(), token.GetOwnerID()), + } + } + + deleteList, err := s.delPrep.prepare(ctx, dInfo) + if err != nil { + if _, ok := s.mErr[errors.Cause(err)]; !ok { + s.log.Error("delete info preparation failure", + zap.String("error", err.Error()), + ) + + err = errDeletePrepare + } + + return err + } + + ctx = contextWithValues(ctx, + transformer.PrivateSessionToken, pToken, + transformer.PublicSessionToken, token, + implementations.BearerToken, dInfo.GetBearerToken(), + implementations.ExtendedHeaders, dInfo.ExtendedHeaders(), + ) + + for i := range deleteList { + if err := s.straightRem.delete(ctx, deleteList[i]); err != nil { + return errors.Wrapf(err, emRemovePart, i+1, len(deleteList)) + } + } + + return nil +} + +func (s *coreDelPreparer) prepare(ctx context.Context, src deleteInfo) ([]deleteInfo, error) { + var ( + ownerID = src.GetOwnerID() + token = src.GetSessionToken() + addr = src.GetAddress() + bearer = src.GetBearerToken() + extHdrs = src.ExtendedHeaders() + ) + + dInfo := newRawDeleteInfo() + dInfo.setOwnerID(ownerID) + dInfo.setAddress(addr) + dInfo.setTTL(service.NonForwardingTTL) + dInfo.setSessionToken(token) + dInfo.setBearerToken(bearer) + dInfo.setExtendedHeaders(extHdrs) + dInfo.setTimeout(s.timeout) + + ctx = contextWithValues(ctx, + transformer.PublicSessionToken, src.GetSessionToken(), + implementations.BearerToken, bearer, + implementations.ExtendedHeaders, extHdrs, + ) + + children := s.childLister.children(ctx, addr) + + res := make([]deleteInfo, 0, len(children)+1) + + res = append(res, dInfo) + + for i := range children { + dInfo = newRawDeleteInfo() + dInfo.setOwnerID(ownerID) + dInfo.setAddress(Address{ + ObjectID: children[i], + CID: addr.CID, + }) + dInfo.setTTL(service.NonForwardingTTL) + dInfo.setSessionToken(token) + dInfo.setBearerToken(bearer) + dInfo.setExtendedHeaders(extHdrs) + dInfo.setTimeout(s.timeout) + + res = append(res, dInfo) + } + + return res, nil +} + +func (s *straightObjRemover) delete(ctx context.Context, dInfo deleteInfo) error { + putInfo := newRawPutInfo() + putInfo.setHead( + s.tombCreator.createTombstone(ctx, dInfo), + ) + putInfo.setSessionToken(dInfo.GetSessionToken()) + putInfo.setBearerToken(dInfo.GetBearerToken()) + putInfo.setExtendedHeaders(dInfo.ExtendedHeaders()) + putInfo.setTTL(dInfo.GetTTL()) + putInfo.setTimeout(dInfo.GetTimeout()) + + _, err := s.objStorer.putObject(ctx, putInfo) + + return err +} + +func (s *coreTombCreator) createTombstone(ctx context.Context, dInfo deleteInfo) *Object { + addr := dInfo.GetAddress() + obj := &Object{ + SystemHeader: SystemHeader{ + ID: addr.ObjectID, + CID: addr.CID, + OwnerID: dInfo.GetOwnerID(), + }, + Headers: []Header{ + { + Value: &object.Header_Tombstone{ + Tombstone: new(object.Tombstone), + }, + }, + { + Value: &object.Header_PayloadChecksum{ + PayloadChecksum: checksumOfEmptyPayload[:], + }, + }, + }, + } + + return obj +} + +func (s *rawDeleteInfo) GetAddress() Address { + return s.addr +} + +func (s *rawDeleteInfo) setAddress(addr Address) { + s.addr = addr +} + +func (s *rawDeleteInfo) GetOwnerID() OwnerID { + return s.ownerID +} + +func (s *rawDeleteInfo) setOwnerID(id OwnerID) { + s.ownerID = id +} + +func (s *rawDeleteInfo) setAddrInfo(v *rawAddrInfo) { + s.rawAddrInfo = *v + s.setType(object.RequestDelete) +} + +func newRawDeleteInfo() *rawDeleteInfo { + res := new(rawDeleteInfo) + + res.setAddrInfo(newRawAddressInfo()) + + return res +} + +func (s *transportRequest) GetToken() *session.Token { + return s.serviceRequest.(*object.DeleteRequest).GetToken() +} +func (s *transportRequest) GetHead() *Object { + return &Object{SystemHeader: SystemHeader{ + ID: s.serviceRequest.(*object.DeleteRequest).Address.ObjectID, + }} +} + +func (s *transportRequest) GetOwnerID() OwnerID { + return s.serviceRequest.(*object.DeleteRequest).OwnerID +} diff --git a/services/public/object/delete_test.go b/services/public/object/delete_test.go new file mode 100644 index 0000000000..c954a7c35c --- /dev/null +++ b/services/public/object/delete_test.go @@ -0,0 +1,449 @@ +package object + +import ( + "context" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/rand" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testDeleteEntity struct { + // Set of interfaces which testDeleteEntity must implement, but some methods from those does not call. + session.PrivateTokenStore + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ EpochReceiver = (*testDeleteEntity)(nil) + _ objectStorer = (*testDeleteEntity)(nil) + _ tombstoneCreator = (*testDeleteEntity)(nil) + _ objectChildrenLister = (*testDeleteEntity)(nil) + _ objectRemover = (*testDeleteEntity)(nil) + _ requestHandler = (*testDeleteEntity)(nil) + _ deletePreparer = (*testDeleteEntity)(nil) + _ responsePreparer = (*testDeleteEntity)(nil) +) + +func (s *testDeleteEntity) verify(context.Context, *session.Token, *Object) error { + return nil +} + +func (s *testDeleteEntity) Fetch(id session.PrivateTokenKey) (session.PrivateToken, error) { + if s.f != nil { + s.f(id) + } + if s.err != nil { + return nil, s.err + } + return s.res.(session.PrivateToken), nil +} + +func (s *testDeleteEntity) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + if s.f != nil { + s.f(req, resp) + } + return s.err +} + +func (s *testDeleteEntity) Epoch() uint64 { return s.res.(uint64) } + +func (s *testDeleteEntity) putObject(_ context.Context, p transport.PutInfo) (*Address, error) { + if s.f != nil { + s.f(p) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Address), nil +} + +func (s *testDeleteEntity) createTombstone(_ context.Context, p deleteInfo) *Object { + if s.f != nil { + s.f(p) + } + return s.res.(*Object) +} + +func (s *testDeleteEntity) children(ctx context.Context, addr Address) []ID { + if s.f != nil { + s.f(addr, ctx) + } + return s.res.([]ID) +} + +func (s *testDeleteEntity) delete(ctx context.Context, p deleteInfo) error { + if s.f != nil { + s.f(p, ctx) + } + return s.err +} + +func (s *testDeleteEntity) prepare(_ context.Context, p deleteInfo) ([]deleteInfo, error) { + if s.f != nil { + s.f(p) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]deleteInfo), nil +} + +func (s *testDeleteEntity) handleRequest(_ context.Context, p handleRequestParams) (interface{}, error) { + if s.f != nil { + s.f(p) + } + return s.res, s.err +} + +func Test_objectService_Delete(t *testing.T) { + ctx := context.TODO() + req := &object.DeleteRequest{Address: testObjectAddress(t)} + + t.Run("handler error", func(t *testing.T) { + rhErr := internal.Error("test error for request handler") + + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testDeleteEntity{ + f: func(items ...interface{}) { + t.Run("correct request handler params", func(t *testing.T) { + p := items[0].(handleRequestParams) + require.Equal(t, req, p.request) + require.Equal(t, s, p.executor) + }) + }, + err: rhErr, // force requestHandler to return rhErr + } + + res, err := s.Delete(ctx, req) + // ascertain that error returns as expected + require.EqualError(t, err, rhErr.Error()) + require.Nil(t, res) + }) + + t.Run("correct result", func(t *testing.T) { + s := objectService{ + requestHandler: new(testDeleteEntity), + respPreparer: &testDeleteEntity{res: new(object.DeleteResponse)}, + + statusCalculator: newStatusCalculator(), + } + + res, err := s.Delete(ctx, req) + require.NoError(t, err) + require.Equal(t, new(object.DeleteResponse), res) + }) +} + +func Test_coreObjRemover_delete(t *testing.T) { + ctx := context.TODO() + pToken, err := session.NewPrivateToken(0) + require.NoError(t, err) + + addr := testObjectAddress(t) + + token := new(service.Token) + token.SetAddress(addr) + + req := newRawDeleteInfo() + req.setAddress(addr) + req.setSessionToken(token) + + t.Run("nil token", func(t *testing.T) { + s := new(coreObjRemover) + + req := newRawDeleteInfo() + require.Nil(t, req.GetSessionToken()) + + require.EqualError(t, s.delete(ctx, req), errNilToken.Error()) + }) + + t.Run("prepare error", func(t *testing.T) { + dpErr := internal.Error("test error for delete preparer") + + dp := &testDeleteEntity{ + f: func(items ...interface{}) { + t.Run("correct delete preparer params", func(t *testing.T) { + require.Equal(t, req, items[0]) + }) + }, + err: dpErr, // force deletePreparer to return dpErr + } + + s := &coreObjRemover{ + delPrep: dp, + tokenStore: &testDeleteEntity{res: pToken}, + mErr: map[error]struct{}{ + dpErr: {}, + }, + log: zap.L(), + } + + // ascertain that error returns as expected + require.EqualError(t, s.delete(ctx, req), dpErr.Error()) + + dp.err = internal.Error("some other error") + + // ascertain that error returns as expected + require.EqualError(t, s.delete(ctx, req), errDeletePrepare.Error()) + }) + + t.Run("straight remover error", func(t *testing.T) { + dInfo := newRawDeleteInfo() + dInfo.setAddress(addr) + dInfo.setSessionToken(token) + + list := []deleteInfo{ + dInfo, + } + + srErr := internal.Error("test error for straight remover") + + s := &coreObjRemover{ + delPrep: &testDeleteEntity{ + res: list, // force deletePreparer to return list + }, + straightRem: &testDeleteEntity{ + f: func(items ...interface{}) { + t.Run("correct straight remover params", func(t *testing.T) { + require.Equal(t, list[0], items[0]) + + ctx := items[1].(context.Context) + + require.Equal(t, + dInfo.GetSessionToken(), + ctx.Value(transformer.PublicSessionToken), + ) + + require.Equal(t, + pToken, + ctx.Value(transformer.PrivateSessionToken), + ) + }) + }, + err: srErr, // force objectRemover to return srErr + }, + tokenStore: &testDeleteEntity{res: pToken}, + } + + // ascertain that error returns as expected + require.EqualError(t, s.delete(ctx, req), errors.Wrapf(srErr, emRemovePart, 1, 1).Error()) + }) + + t.Run("success", func(t *testing.T) { + dInfo := newRawDeleteInfo() + dInfo.setAddress(addr) + dInfo.setSessionToken(token) + + list := []deleteInfo{ + dInfo, + } + + s := &coreObjRemover{ + delPrep: &testDeleteEntity{ + res: list, // force deletePreparer to return list + }, + straightRem: &testDeleteEntity{ + err: nil, // force objectRemover to return empty error + }, + tokenStore: &testDeleteEntity{res: pToken}, + } + + // ascertain that nil error returns + require.NoError(t, s.delete(ctx, req)) + }) +} + +func Test_coreDelPreparer_prepare(t *testing.T) { + var ( + ctx = context.TODO() + ownerID = OwnerID{1, 2, 3} + addr = testObjectAddress(t) + timeout = 5 * time.Second + token = new(service.Token) + childCount = 10 + children = make([]ID, 0, childCount) + ) + + req := newRawDeleteInfo() + req.setAddress(addr) + req.setSessionToken(token) + req.setOwnerID(ownerID) + + token.SetID(session.TokenID{1, 2, 3}) + + for i := 0; i < childCount; i++ { + children = append(children, testObjectAddress(t).ObjectID) + } + + s := &coreDelPreparer{ + timeout: timeout, + childLister: &testDeleteEntity{ + f: func(items ...interface{}) { + t.Run("correct children lister params", func(t *testing.T) { + require.Equal(t, addr, items[0]) + require.Equal(t, + token, + items[1].(context.Context).Value(transformer.PublicSessionToken), + ) + }) + }, + res: children, + }, + } + + res, err := s.prepare(ctx, req) + require.NoError(t, err) + + require.Len(t, res, childCount+1) + + for i := range res { + require.Equal(t, timeout, res[i].GetTimeout()) + require.Equal(t, token, res[i].GetSessionToken()) + require.Equal(t, uint32(service.NonForwardingTTL), res[i].GetTTL()) + + a := res[i].GetAddress() + require.Equal(t, addr.CID, a.CID) + if i > 0 { + require.Equal(t, children[i-1], a.ObjectID) + } else { + require.Equal(t, addr.ObjectID, a.ObjectID) + } + } +} + +func Test_straightObjRemover_delete(t *testing.T) { + var ( + ctx = context.TODO() + addr = testObjectAddress(t) + ttl = uint32(10) + timeout = 5 * time.Second + token = new(service.Token) + obj = &Object{SystemHeader: SystemHeader{ID: addr.ObjectID, CID: addr.CID}} + ) + + token.SetID(session.TokenID{1, 2, 3}) + + req := newRawDeleteInfo() + req.setTTL(ttl) + req.setTimeout(timeout) + req.setAddress(testObjectAddress(t)) + req.setSessionToken(token) + + t.Run("correct result", func(t *testing.T) { + osErr := internal.Error("test error for object storer") + + s := &straightObjRemover{ + tombCreator: &testDeleteEntity{ + f: func(items ...interface{}) { + t.Run("correct tombstone creator params", func(t *testing.T) { + require.Equal(t, req, items[0]) + }) + }, + res: obj, + }, + objStorer: &testDeleteEntity{ + f: func(items ...interface{}) { + t.Run("correct object storer params", func(t *testing.T) { + p := items[0].(transport.PutInfo) + require.Equal(t, timeout, p.GetTimeout()) + require.Equal(t, ttl, p.GetTTL()) + require.Equal(t, obj, p.GetHead()) + require.Equal(t, token, p.GetSessionToken()) + }) + }, + err: osErr, // force objectStorer to return osErr + }, + } + + // ascertain that error returns as expected + require.EqualError(t, s.delete(ctx, req), osErr.Error()) + }) +} + +func Test_coreTombCreator_createTombstone(t *testing.T) { + var ( + ctx = context.TODO() + addr = testObjectAddress(t) + ownerID = OwnerID{1, 2, 3} + ) + + req := newRawDeleteInfo() + req.setAddress(addr) + req.setOwnerID(ownerID) + + t.Run("correct result", func(t *testing.T) { + s := new(coreTombCreator) + + res := s.createTombstone(ctx, req) + require.Equal(t, addr.CID, res.SystemHeader.CID) + require.Equal(t, addr.ObjectID, res.SystemHeader.ID) + require.Equal(t, ownerID, res.SystemHeader.OwnerID) + + _, tsHdr := res.LastHeader(object.HeaderType(object.TombstoneHdr)) + require.NotNil(t, tsHdr) + require.Equal(t, new(object.Tombstone), tsHdr.Value.(*object.Header_Tombstone).Tombstone) + }) +} + +func Test_deleteInfo(t *testing.T) { + t.Run("address", func(t *testing.T) { + addr := testObjectAddress(t) + + req := newRawDeleteInfo() + req.setAddress(addr) + + require.Equal(t, addr, req.GetAddress()) + }) + + t.Run("owner ID", func(t *testing.T) { + ownerID := OwnerID{} + _, err := rand.Read(ownerID[:]) + require.NoError(t, err) + + req := newRawDeleteInfo() + req.setOwnerID(ownerID) + require.Equal(t, ownerID, req.GetOwnerID()) + + tReq := &transportRequest{serviceRequest: &object.DeleteRequest{OwnerID: ownerID}} + require.Equal(t, ownerID, tReq.GetOwnerID()) + }) + + t.Run("token", func(t *testing.T) { + token := new(session.Token) + _, err := rand.Read(token.ID[:]) + require.NoError(t, err) + + req := newRawDeleteInfo() + req.setSessionToken(token) + require.Equal(t, token, req.GetSessionToken()) + + dReq := new(object.DeleteRequest) + dReq.SetToken(token) + tReq := &transportRequest{serviceRequest: dReq} + require.Equal(t, token, tReq.GetSessionToken()) + }) +} diff --git a/services/public/object/execution.go b/services/public/object/execution.go new file mode 100644 index 0000000000..a8880930aa --- /dev/null +++ b/services/public/object/execution.go @@ -0,0 +1,471 @@ +package object + +import ( + "bytes" + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/placement" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + operationExecutor interface { + executeOperation(context.Context, transport.MetaInfo, responseItemHandler) error + } + + coreOperationExecutor struct { + pre executionParamsComputer + fin operationFinalizer + loc operationExecutor + } + + operationFinalizer interface { + completeExecution(context.Context, operationParams) error + } + + computableParams struct { + addr Address + stopCount int + allowPartialResult bool + tryPreviousNetMap bool + selfForward bool + maxRecycleCount int + reqType object.RequestType + } + + responseItemHandler interface { + handleItem(interface{}) + } + + operationParams struct { + computableParams + metaInfo transport.MetaInfo + itemHandler responseItemHandler + } + + coreOperationFinalizer struct { + curPlacementBuilder placementBuilder + prevPlacementBuilder placementBuilder + interceptorPreparer interceptorPreparer + workerPool WorkerPool + traverseExec implementations.ContainerTraverseExecutor + resLogger resultLogger + log *zap.Logger + } + + localFullObjectReceiver interface { + getObject(context.Context, Address) (*Object, error) + } + + localHeadReceiver interface { + headObject(context.Context, Address) (*Object, error) + } + + localObjectStorer interface { + putObject(context.Context, *Object) error + } + + localQueryImposer interface { + imposeQuery(context.Context, CID, []byte, int) ([]Address, error) + } + + localRangeReader interface { + getRange(context.Context, Address, Range) ([]byte, error) + } + + localRangeHasher interface { + getHashes(context.Context, Address, []Range, []byte) ([]Hash, error) + } + + localStoreExecutor struct { + salitor Salitor + epochRecv EpochReceiver + localStore localstore.Localstore + } + + localOperationExecutor struct { + objRecv localFullObjectReceiver + headRecv localHeadReceiver + objStore localObjectStorer + queryImp localQueryImposer + rngReader localRangeReader + rngHasher localRangeHasher + } + + coreHandler struct { + traverser containerTraverser + itemHandler responseItemHandler + resLogger resultLogger + reqType object.RequestType + } + + executionParamsComputer interface { + computeParams(*computableParams, transport.MetaInfo) + } + + coreExecParamsComp struct{} + + resultTracker interface { + trackResult(context.Context, resultItems) + } + + interceptorPreparer interface { + prepareInterceptor(interceptorItems) (func(context.Context, multiaddr.Multiaddr) bool, error) + } + + interceptorItems struct { + selfForward bool + handler transport.ResultHandler + metaInfo transport.MetaInfo + itemHandler responseItemHandler + } + + coreInterceptorPreparer struct { + localExec operationExecutor + addressStore implementations.AddressStore + } + + resultItems struct { + requestType object.RequestType + node multiaddr.Multiaddr + satisfactory bool + } + + idleResultTracker struct { + } + + resultLogger interface { + logErr(object.RequestType, multiaddr.Multiaddr, error) + } + + coreResultLogger struct { + mLog map[object.RequestType]struct{} + log *zap.Logger + } +) + +const ( + errIncompleteOperation = internal.Error("operation is not completed") + + emRangeReadFail = "could not read %d range data" +) + +var ( + _ resultTracker = (*idleResultTracker)(nil) + _ executionParamsComputer = (*coreExecParamsComp)(nil) + _ operationFinalizer = (*coreOperationFinalizer)(nil) + _ operationExecutor = (*localOperationExecutor)(nil) + _ operationExecutor = (*coreOperationExecutor)(nil) + _ transport.ResultHandler = (*coreHandler)(nil) + _ localFullObjectReceiver = (*localStoreExecutor)(nil) + _ localHeadReceiver = (*localStoreExecutor)(nil) + _ localObjectStorer = (*localStoreExecutor)(nil) + _ localRangeReader = (*localStoreExecutor)(nil) + _ localRangeHasher = (*localStoreExecutor)(nil) + _ resultLogger = (*coreResultLogger)(nil) +) + +func (s *coreExecParamsComp) computeParams(p *computableParams, req transport.MetaInfo) { + switch p.reqType = req.Type(); p.reqType { + case object.RequestPut: + if req.GetTTL() < service.NonForwardingTTL { + p.stopCount = 1 + } else { + p.stopCount = int(req.(transport.PutInfo).CopiesNumber()) + } + + p.allowPartialResult = false + p.tryPreviousNetMap = false + p.selfForward = false + p.addr = *req.(transport.PutInfo).GetHead().Address() + p.maxRecycleCount = 0 + case object.RequestGet: + p.stopCount = 1 + p.allowPartialResult = false + p.tryPreviousNetMap = true + p.selfForward = false + p.addr = req.(transport.AddressInfo).GetAddress() + p.maxRecycleCount = 0 + case object.RequestHead: + p.stopCount = 1 + p.allowPartialResult = false + p.tryPreviousNetMap = true + p.selfForward = false + p.addr = req.(transport.AddressInfo).GetAddress() + p.maxRecycleCount = 0 + case object.RequestSearch: + p.stopCount = -1 // to traverse all possible nodes in current and prev container + p.allowPartialResult = true + p.tryPreviousNetMap = true + p.selfForward = false + p.addr = Address{CID: req.(transport.SearchInfo).GetCID()} + p.maxRecycleCount = 0 + case object.RequestRange: + p.stopCount = 1 + p.allowPartialResult = false + p.tryPreviousNetMap = false + p.selfForward = false + p.addr = req.(transport.AddressInfo).GetAddress() + p.maxRecycleCount = 0 + case object.RequestRangeHash: + p.stopCount = 1 + p.allowPartialResult = false + p.tryPreviousNetMap = false + p.selfForward = false + p.addr = req.(transport.AddressInfo).GetAddress() + p.maxRecycleCount = 0 + } +} + +func (s idleResultTracker) trackResult(context.Context, resultItems) {} + +func (s *coreOperationExecutor) executeOperation(ctx context.Context, req transport.MetaInfo, h responseItemHandler) error { + // if TTL is zero then execute local operation + if req.GetTTL() < service.NonForwardingTTL { + return s.loc.executeOperation(ctx, req, h) + } + + p := new(computableParams) + s.pre.computeParams(p, req) + + return s.fin.completeExecution(ctx, operationParams{ + computableParams: *p, + metaInfo: req, + itemHandler: h, + }) +} + +func (s *coreOperationFinalizer) completeExecution(ctx context.Context, p operationParams) error { + traverser := newContainerTraverser(&traverseParams{ + tryPrevNM: p.tryPreviousNetMap, + addr: p.addr, + curPlacementBuilder: s.curPlacementBuilder, + prevPlacementBuilder: s.prevPlacementBuilder, + maxRecycleCount: p.maxRecycleCount, + stopCount: p.stopCount, + }) + + handler := &coreHandler{ + traverser: traverser, + itemHandler: p.itemHandler, + resLogger: s.resLogger, + reqType: p.reqType, + } + + interceptor, err := s.interceptorPreparer.prepareInterceptor(interceptorItems{ + selfForward: p.selfForward, + handler: handler, + metaInfo: p.metaInfo, + itemHandler: p.itemHandler, + }) + if err != nil { + return err + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + s.traverseExec.Execute(ctx, implementations.TraverseParams{ + TransportInfo: p.metaInfo, + Handler: handler, + Traverser: traverser, + WorkerPool: s.workerPool, + ExecutionInterceptor: interceptor, + }) + + switch err := errors.Cause(traverser.Err()); err { + case container.ErrNotFound: + return &detailedError{ + error: errContainerNotFound, + d: containerDetails(p.addr.CID, descContainerNotFound), + } + case placement.ErrEmptyNodes: + if !p.allowPartialResult { + return errIncompleteOperation + } + + return nil + default: + if err != nil { + s.log.Error("traverse failure", + zap.String("error", err.Error()), + ) + + err = errPlacementProblem + } else if !p.allowPartialResult && !traverser.finished() { + err = errIncompleteOperation + } + + return err + } +} + +func (s *coreInterceptorPreparer) prepareInterceptor(p interceptorItems) (func(context.Context, multiaddr.Multiaddr) bool, error) { + selfAddr, err := s.addressStore.SelfAddr() + if err != nil { + return nil, err + } + + return func(ctx context.Context, node multiaddr.Multiaddr) (res bool) { + if node.Equal(selfAddr) { + p.handler.HandleResult(ctx, selfAddr, nil, + s.localExec.executeOperation(ctx, p.metaInfo, p.itemHandler)) + return !p.selfForward + } + + return false + }, nil +} + +func (s *coreHandler) HandleResult(ctx context.Context, n multiaddr.Multiaddr, r interface{}, e error) { + ok := e == nil + + s.traverser.add(n, ok) + + if ok && r != nil { + s.itemHandler.handleItem(r) + } + + s.resLogger.logErr(s.reqType, n, e) +} + +func (s *coreResultLogger) logErr(t object.RequestType, n multiaddr.Multiaddr, e error) { + if e == nil { + return + } else if _, ok := s.mLog[t]; !ok { + return + } + + s.log.Error("object request failure", + zap.Stringer("type", t), + zap.Stringer("node", n), + zap.String("error", e.Error()), + ) +} + +func (s *localOperationExecutor) executeOperation(ctx context.Context, req transport.MetaInfo, h responseItemHandler) error { + switch req.Type() { + case object.RequestPut: + obj := req.(transport.PutInfo).GetHead() + if err := s.objStore.putObject(ctx, obj); err != nil { + return err + } + + h.handleItem(obj.Address()) + case object.RequestGet: + obj, err := s.objRecv.getObject(ctx, req.(transport.AddressInfo).GetAddress()) + if err != nil { + return err + } + + h.handleItem(obj) + case object.RequestHead: + head, err := s.headRecv.headObject(ctx, req.(transport.AddressInfo).GetAddress()) + if err != nil { + return err + } + + h.handleItem(head) + case object.RequestSearch: + r := req.(transport.SearchInfo) + + addrList, err := s.queryImp.imposeQuery(ctx, r.GetCID(), r.GetQuery(), 1) // TODO: add query version to SearchInfo + if err != nil { + return err + } + + h.handleItem(addrList) + case object.RequestRange: + r := req.(transport.RangeInfo) + + rangesData, err := s.rngReader.getRange(ctx, r.GetAddress(), r.GetRange()) + if err != nil { + return err + } + + h.handleItem(bytes.NewReader(rangesData)) + case object.RequestRangeHash: + r := req.(transport.RangeHashInfo) + + rangesHashes, err := s.rngHasher.getHashes(ctx, r.GetAddress(), r.GetRanges(), r.GetSalt()) + if err != nil { + return err + } + + h.handleItem(rangesHashes) + default: + return errors.Errorf(pmWrongRequestType, req) + } + + return nil +} + +func (s *localStoreExecutor) getHashes(ctx context.Context, addr Address, ranges []Range, salt []byte) ([]Hash, error) { + res := make([]Hash, 0, len(ranges)) + + for i := range ranges { + chunk, err := s.localStore.PRead(ctx, addr, ranges[i]) + if err != nil { + return nil, errors.Wrapf(err, emRangeReadFail, i+1) + } + + res = append(res, hash.Sum(s.salitor(chunk, salt))) + } + + return res, nil +} + +func (s *localStoreExecutor) getRange(ctx context.Context, addr Address, r Range) ([]byte, error) { + return s.localStore.PRead(ctx, addr, r) +} + +func (s *localStoreExecutor) putObject(ctx context.Context, obj *Object) error { + ctx = context.WithValue(ctx, localstore.StoreEpochValue, s.epochRecv.Epoch()) + + switch err := s.localStore.Put(ctx, obj); err { + // TODO: add all error cases + case nil: + return nil + default: + return errPutLocal + } +} + +func (s *localStoreExecutor) headObject(_ context.Context, addr Address) (*Object, error) { + m, err := s.localStore.Meta(addr) + if err != nil { + switch errors.Cause(err) { + case core.ErrNotFound: + return nil, errIncompleteOperation + default: + return nil, err + } + } + + return m.Object, nil +} + +func (s *localStoreExecutor) getObject(_ context.Context, addr Address) (*Object, error) { + obj, err := s.localStore.Get(addr) + if err != nil { + switch errors.Cause(err) { + case core.ErrNotFound: + return nil, errIncompleteOperation + default: + return nil, err + } + } + + return obj, nil +} diff --git a/services/public/object/execution_test.go b/services/public/object/execution_test.go new file mode 100644 index 0000000000..81af16a62d --- /dev/null +++ b/services/public/object/execution_test.go @@ -0,0 +1,1207 @@ +package object + +import ( + "context" + "io" + "io/ioutil" + "testing" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testExecutionEntity struct { + // Set of interfaces which testExecutionEntity must implement, but some methods from those does not call. + transport.MetaInfo + localstore.Localstore + containerTraverser + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +func (s *testExecutionEntity) HandleResult(_ context.Context, n multiaddr.Multiaddr, r interface{}, e error) { + if s.f != nil { + s.f(n, r, e) + } +} + +var ( + _ transport.ResultHandler = (*testExecutionEntity)(nil) + _ interceptorPreparer = (*testExecutionEntity)(nil) + _ implementations.ContainerTraverseExecutor = (*testExecutionEntity)(nil) + _ WorkerPool = (*testExecutionEntity)(nil) + _ operationExecutor = (*testExecutionEntity)(nil) + _ placementBuilder = (*testExecutionEntity)(nil) + _ implementations.AddressStore = (*testExecutionEntity)(nil) + _ executionParamsComputer = (*testExecutionEntity)(nil) + _ operationFinalizer = (*testExecutionEntity)(nil) + _ EpochReceiver = (*testExecutionEntity)(nil) + _ localstore.Localstore = (*testExecutionEntity)(nil) + _ containerTraverser = (*testExecutionEntity)(nil) + _ responseItemHandler = (*testExecutionEntity)(nil) + _ resultTracker = (*testExecutionEntity)(nil) + _ localObjectStorer = (*testExecutionEntity)(nil) + _ localFullObjectReceiver = (*testExecutionEntity)(nil) + _ localHeadReceiver = (*testExecutionEntity)(nil) + _ localQueryImposer = (*testExecutionEntity)(nil) + _ localRangeReader = (*testExecutionEntity)(nil) + _ localRangeHasher = (*testExecutionEntity)(nil) +) + +func (s *testExecutionEntity) prepareInterceptor(p interceptorItems) (func(context.Context, multiaddr.Multiaddr) bool, error) { + if s.f != nil { + s.f(p) + } + if s.err != nil { + return nil, s.err + } + return s.res.(func(context.Context, multiaddr.Multiaddr) bool), nil +} + +func (s *testExecutionEntity) Execute(_ context.Context, p implementations.TraverseParams) { + if s.f != nil { + s.f(p) + } +} + +func (s *testExecutionEntity) Submit(func()) error { + return s.err +} + +func (s *testExecutionEntity) executeOperation(ctx context.Context, r transport.MetaInfo, h responseItemHandler) error { + if s.f != nil { + s.f(r, h) + } + return s.err +} + +func (s *testExecutionEntity) buildPlacement(_ context.Context, a Address, n ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + if s.f != nil { + s.f(a, n) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]multiaddr.Multiaddr), nil +} + +func (s *testExecutionEntity) getHashes(_ context.Context, a Address, r []Range, sa []byte) ([]Hash, error) { + if s.f != nil { + s.f(a, r, sa) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]Hash), nil +} + +func (s *testExecutionEntity) getRange(_ context.Context, addr Address, rngs Range) ([]byte, error) { + if s.f != nil { + s.f(addr, rngs) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]byte), nil +} + +func (s *testExecutionEntity) imposeQuery(_ context.Context, c CID, d []byte, v int) ([]Address, error) { + if s.f != nil { + s.f(c, d, v) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]Address), nil +} + +func (s *testExecutionEntity) headObject(_ context.Context, addr Address) (*Object, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Object), nil +} + +func (s *testExecutionEntity) getObject(_ context.Context, addr Address) (*Object, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Object), nil +} + +func (s *testExecutionEntity) putObject(_ context.Context, obj *Object) error { + if s.f != nil { + s.f(obj) + } + return s.err +} + +func (s *testExecutionEntity) trackResult(_ context.Context, p resultItems) { + if s.f != nil { + s.f(p) + } +} + +func (s *testExecutionEntity) handleItem(v interface{}) { + if s.f != nil { + s.f(v) + } +} + +func (s *testExecutionEntity) add(n multiaddr.Multiaddr, b bool) { + if s.f != nil { + s.f(n, b) + } +} + +func (s *testExecutionEntity) done(n multiaddr.Multiaddr) bool { + if s.f != nil { + s.f(n) + } + return s.res.(bool) +} + +func (s *testExecutionEntity) close() { + if s.f != nil { + s.f() + } +} + +func (s *testExecutionEntity) PRead(ctx context.Context, addr Address, rng Range) ([]byte, error) { + if s.f != nil { + s.f(addr, rng) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]byte), nil +} + +func (s *testExecutionEntity) Put(ctx context.Context, obj *Object) error { + if s.f != nil { + s.f(ctx, obj) + } + return s.err +} + +func (s *testExecutionEntity) Get(addr Address) (*Object, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Object), nil +} + +func (s *testExecutionEntity) Meta(addr Address) (*Meta, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Meta), nil +} + +func (s *testExecutionEntity) Has(addr Address) (bool, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return false, s.err + } + return s.res.(bool), nil +} + +func (s *testExecutionEntity) Epoch() uint64 { return s.res.(uint64) } + +func (s *testExecutionEntity) completeExecution(_ context.Context, p operationParams) error { + if s.f != nil { + s.f(p) + } + return s.err +} + +func (s *testExecutionEntity) computeParams(p *computableParams, r transport.MetaInfo) { + if s.f != nil { + s.f(p, r) + } +} + +func (s *testExecutionEntity) SelfAddr() (multiaddr.Multiaddr, error) { + if s.err != nil { + return nil, s.err + } + return s.res.(multiaddr.Multiaddr), nil +} + +func (s *testExecutionEntity) Type() object.RequestType { + return s.res.(object.RequestType) +} + +func Test_typeOfRequest(t *testing.T) { + t.Run("correct mapping", func(t *testing.T) { + items := []struct { + exp object.RequestType + v transport.MetaInfo + }{ + {exp: object.RequestSearch, v: &transportRequest{serviceRequest: new(object.SearchRequest)}}, + {exp: object.RequestSearch, v: newRawSearchInfo()}, + {exp: object.RequestPut, v: new(putRequest)}, + {exp: object.RequestPut, v: &transportRequest{serviceRequest: new(object.PutRequest)}}, + {exp: object.RequestGet, v: newRawGetInfo()}, + {exp: object.RequestGet, v: &transportRequest{serviceRequest: new(object.GetRequest)}}, + {exp: object.RequestHead, v: newRawHeadInfo()}, + {exp: object.RequestHead, v: &transportRequest{serviceRequest: new(object.HeadRequest)}}, + {exp: object.RequestRange, v: newRawRangeInfo()}, + {exp: object.RequestRange, v: &transportRequest{serviceRequest: new(GetRangeRequest)}}, + {exp: object.RequestRangeHash, v: newRawRangeHashInfo()}, + {exp: object.RequestRangeHash, v: &transportRequest{serviceRequest: new(object.GetRangeHashRequest)}}, + } + + for i := range items { + require.Equal(t, items[i].exp, items[i].v.Type()) + } + }) +} + +func Test_coreExecParamsComp_computeParams(t *testing.T) { + s := new(coreExecParamsComp) + addr := testObjectAddress(t) + + t.Run("put", func(t *testing.T) { + addr := testObjectAddress(t) + + p := new(computableParams) + r := &putRequest{PutRequest: &object.PutRequest{ + R: &object.PutRequest_Header{ + Header: &object.PutRequest_PutHeader{ + Object: &Object{ + SystemHeader: SystemHeader{ + ID: addr.ObjectID, + CID: addr.CID, + }, + }, + }, + }, + }} + + s.computeParams(p, r) + + t.Run("non-forwarding behavior", func(t *testing.T) { + require.Equal(t, 1, p.stopCount) + }) + + r.SetTTL(service.NonForwardingTTL) + + s.computeParams(p, r) + + require.False(t, p.allowPartialResult) + require.False(t, p.tryPreviousNetMap) + require.False(t, p.selfForward) + require.Equal(t, addr, p.addr) + require.Equal(t, 0, p.maxRecycleCount) + require.Equal(t, 0, int(r.CopiesNumber())) + }) + + t.Run("get", func(t *testing.T) { + p := new(computableParams) + + r := newRawGetInfo() + r.setAddress(addr) + + s.computeParams(p, r) + + require.Equal(t, 1, p.stopCount) + require.False(t, p.allowPartialResult) + require.True(t, p.tryPreviousNetMap) + require.False(t, p.selfForward) + require.Equal(t, addr, p.addr) + require.Equal(t, 0, p.maxRecycleCount) + }) + + t.Run("head", func(t *testing.T) { + p := new(computableParams) + r := &transportRequest{serviceRequest: &object.HeadRequest{Address: addr}} + + s.computeParams(p, r) + + require.Equal(t, 1, p.stopCount) + require.False(t, p.allowPartialResult) + require.True(t, p.tryPreviousNetMap) + require.False(t, p.selfForward) + require.Equal(t, addr, p.addr) + require.Equal(t, 0, p.maxRecycleCount) + }) + + t.Run("search", func(t *testing.T) { + p := new(computableParams) + r := &transportRequest{serviceRequest: &object.SearchRequest{ContainerID: addr.CID}} + + s.computeParams(p, r) + + require.Equal(t, -1, p.stopCount) + require.True(t, p.allowPartialResult) + require.True(t, p.tryPreviousNetMap) + require.False(t, p.selfForward) + require.Equal(t, addr.CID, p.addr.CID) + require.True(t, p.addr.ObjectID.Empty()) + require.Equal(t, 0, p.maxRecycleCount) + }) + + t.Run("range", func(t *testing.T) { + p := new(computableParams) + + r := newRawRangeInfo() + r.setAddress(addr) + + s.computeParams(p, r) + + require.Equal(t, 1, p.stopCount) + require.False(t, p.allowPartialResult) + require.False(t, p.tryPreviousNetMap) + require.False(t, p.selfForward) + require.Equal(t, addr, p.addr) + require.Equal(t, 0, p.maxRecycleCount) + }) + + t.Run("range hash", func(t *testing.T) { + p := new(computableParams) + + r := newRawRangeHashInfo() + r.setAddress(addr) + + s.computeParams(p, r) + + require.Equal(t, 1, p.stopCount) + require.False(t, p.allowPartialResult) + require.False(t, p.tryPreviousNetMap) + require.False(t, p.selfForward) + require.Equal(t, addr, p.addr) + require.Equal(t, 0, p.maxRecycleCount) + }) +} + +func Test_coreOperationExecutor_executeOperation(t *testing.T) { + ctx := context.TODO() + + t.Run("correct result", func(t *testing.T) { + t.Run("error", func(t *testing.T) { + p := new(testExecutionEntity) + req := newRawPutInfo() + req.setTTL(1) + finErr := internal.Error("test error for operation finalizer") + + s := &coreOperationExecutor{ + pre: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct params computer arguments", func(t *testing.T) { + require.Equal(t, computableParams{}, *items[0].(*computableParams)) + require.Equal(t, req, items[1].(transport.MetaInfo)) + }) + }, + }, + fin: &testExecutionEntity{ + f: func(items ...interface{}) { + par := items[0].(operationParams) + require.Equal(t, req, par.metaInfo) + require.Equal(t, p, par.itemHandler) + }, + err: finErr, + }, + loc: new(testExecutionEntity), + } + + require.EqualError(t, + s.executeOperation(ctx, req, p), + finErr.Error(), + ) + }) + + t.Run("zero ttl", func(t *testing.T) { + p := new(testExecutionEntity) + req := newRawPutInfo() + finErr := internal.Error("test error for operation finalizer") + + s := &coreOperationExecutor{ + loc: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, p, items[1]) + }, + err: finErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, p), + finErr.Error(), + ) + }) + }) +} + +func Test_localStoreExecutor(t *testing.T) { + ctx := context.TODO() + addr := testObjectAddress(t) + + t.Run("put", func(t *testing.T) { + epoch := uint64(100) + obj := new(Object) + putErr := internal.Error("test error for put") + + ls := &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct local store put params", func(t *testing.T) { + v, ok := items[0].(context.Context).Value(localstore.StoreEpochValue).(uint64) + require.True(t, ok) + require.Equal(t, epoch, v) + + require.Equal(t, obj, items[1].(*Object)) + }) + }, + } + + s := &localStoreExecutor{ + epochRecv: &testExecutionEntity{ + res: epoch, + }, + localStore: ls, + } + + require.NoError(t, s.putObject(ctx, obj)) + + ls.err = putErr + + require.EqualError(t, + s.putObject(ctx, obj), + errPutLocal.Error(), + ) + }) + + t.Run("get", func(t *testing.T) { + t.Run("error", func(t *testing.T) { + getErr := internal.Error("test error for get") + + ls := &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct local store get params", func(t *testing.T) { + require.Equal(t, addr, items[0].(Address)) + }) + }, + err: getErr, + } + + s := &localStoreExecutor{ + localStore: ls, + } + + res, err := s.getObject(ctx, addr) + require.EqualError(t, err, getErr.Error()) + require.Nil(t, res) + + ls.err = errors.Wrap(core.ErrNotFound, "wrap message") + + res, err = s.getObject(ctx, addr) + require.EqualError(t, err, errIncompleteOperation.Error()) + require.Nil(t, res) + }) + + t.Run("success", func(t *testing.T) { + obj := new(Object) + + s := &localStoreExecutor{ + localStore: &testExecutionEntity{ + res: obj, + }, + } + + res, err := s.getObject(ctx, addr) + require.NoError(t, err) + require.Equal(t, obj, res) + }) + }) + + t.Run("head", func(t *testing.T) { + t.Run("error", func(t *testing.T) { + headErr := internal.Error("test error for head") + + ls := &testExecutionEntity{ + err: headErr, + } + + s := &localStoreExecutor{ + localStore: ls, + } + + res, err := s.headObject(ctx, addr) + require.EqualError(t, err, headErr.Error()) + require.Nil(t, res) + + ls.err = errors.Wrap(core.ErrNotFound, "wrap message") + + res, err = s.headObject(ctx, addr) + require.EqualError(t, err, errIncompleteOperation.Error()) + require.Nil(t, res) + }) + + t.Run("success", func(t *testing.T) { + obj := new(Object) + + s := &localStoreExecutor{ + localStore: &testExecutionEntity{ + res: &Meta{Object: obj}, + }, + } + + res, err := s.headObject(ctx, addr) + require.NoError(t, err) + require.Equal(t, obj, res) + }) + }) + + t.Run("get range", func(t *testing.T) { + t.Run("error", func(t *testing.T) { + rngErr := internal.Error("test error for range reader") + + s := &localStoreExecutor{ + localStore: &testExecutionEntity{ + err: rngErr, + }, + } + + res, err := s.getRange(ctx, addr, Range{}) + require.EqualError(t, err, rngErr.Error()) + require.Empty(t, res) + }) + + t.Run("success", func(t *testing.T) { + rng := Range{Offset: 1, Length: 1} + + d := testData(t, 10) + + s := &localStoreExecutor{ + localStore: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct local store pread params", func(t *testing.T) { + require.Equal(t, addr, items[0].(Address)) + require.Equal(t, rng, items[1].(Range)) + }) + }, + res: d, + }, + } + + res, err := s.getRange(ctx, addr, rng) + require.NoError(t, err) + require.Equal(t, d, res) + }) + }) + + t.Run("get range hash", func(t *testing.T) { + t.Run("empty range list", func(t *testing.T) { + s := &localStoreExecutor{ + localStore: new(testExecutionEntity), + } + + res, err := s.getHashes(ctx, addr, nil, nil) + require.NoError(t, err) + require.Empty(t, res) + }) + + t.Run("error", func(t *testing.T) { + rhErr := internal.Error("test error for range hasher") + + s := &localStoreExecutor{ + localStore: &testExecutionEntity{ + err: rhErr, + }, + } + + res, err := s.getHashes(ctx, addr, make([]Range, 1), nil) + require.EqualError(t, err, errors.Wrapf(rhErr, emRangeReadFail, 1).Error()) + require.Empty(t, res) + }) + + t.Run("success", func(t *testing.T) { + rngs := []Range{ + {Offset: 0, Length: 0}, + {Offset: 1, Length: 1}, + } + + d := testData(t, 64) + salt := testData(t, 20) + + callNum := 0 + + s := &localStoreExecutor{ + salitor: hash.SaltXOR, + localStore: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct local store pread params", func(t *testing.T) { + require.Equal(t, addr, items[0].(Address)) + require.Equal(t, rngs[callNum], items[1].(Range)) + callNum++ + }) + }, + res: d, + }, + } + + res, err := s.getHashes(ctx, addr, rngs, salt) + require.NoError(t, err) + require.Len(t, res, len(rngs)) + for i := range rngs { + require.Equal(t, hash.Sum(hash.SaltXOR(d, salt)), res[i]) + } + }) + }) +} + +func Test_coreHandler_HandleResult(t *testing.T) { + ctx := context.TODO() + node := testNode(t, 1) + + t.Run("error", func(t *testing.T) { + handled := false + err := internal.Error("") + + s := &coreHandler{ + traverser: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct traverser params", func(t *testing.T) { + require.Equal(t, node, items[0].(multiaddr.Multiaddr)) + require.False(t, items[1].(bool)) + }) + }, + }, + itemHandler: &testExecutionEntity{ + f: func(items ...interface{}) { + handled = true + }, + }, + resLogger: new(coreResultLogger), + } + + s.HandleResult(ctx, node, nil, err) + + require.False(t, handled) + }) + + t.Run("success", func(t *testing.T) { + handled := false + res := testData(t, 10) + + s := &coreHandler{ + traverser: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct traverser params", func(t *testing.T) { + require.Equal(t, node, items[0].(multiaddr.Multiaddr)) + require.True(t, items[1].(bool)) + }) + }, + }, + itemHandler: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, res, items[0]) + }, + }, + resLogger: new(coreResultLogger), + } + + s.HandleResult(ctx, node, res, nil) + + require.False(t, handled) + }) +} + +func Test_localOperationExecutor_executeOperation(t *testing.T) { + ctx := context.TODO() + + addr := testObjectAddress(t) + + obj := &Object{ + SystemHeader: SystemHeader{ + ID: addr.ObjectID, + CID: addr.CID, + }, + } + + t.Run("wrong type", func(t *testing.T) { + req := &testExecutionEntity{ + res: object.RequestType(-1), + } + + require.EqualError(t, + new(localOperationExecutor).executeOperation(ctx, req, nil), + errors.Errorf(pmWrongRequestType, req).Error(), + ) + }) + + t.Run("put", func(t *testing.T) { + req := &putRequest{PutRequest: &object.PutRequest{ + R: &object.PutRequest_Header{ + Header: &object.PutRequest_PutHeader{ + Object: obj, + }, + }, + }} + + t.Run("error", func(t *testing.T) { + putErr := internal.Error("test error for put") + + s := &localOperationExecutor{ + objStore: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, obj, items[0].(*Object)) + }, + err: putErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, nil), + putErr.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + h := &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, *items[0].(*Address)) + }, + } + + s := &localOperationExecutor{ + objStore: new(testExecutionEntity), + } + + require.NoError(t, s.executeOperation(ctx, req, h)) + }) + }) + + t.Run("get", func(t *testing.T) { + req := newRawGetInfo() + req.setAddress(addr) + + t.Run("error", func(t *testing.T) { + getErr := internal.Error("test error for get") + + s := &localOperationExecutor{ + objRecv: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0].(Address)) + }, + err: getErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, nil), + getErr.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + h := &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, obj, items[0].(*Object)) + }, + } + + s := &localOperationExecutor{ + objRecv: &testExecutionEntity{ + res: obj, + }, + } + + require.NoError(t, s.executeOperation(ctx, req, h)) + }) + }) + + t.Run("head", func(t *testing.T) { + req := &transportRequest{serviceRequest: &object.HeadRequest{ + Address: addr, + }} + + t.Run("error", func(t *testing.T) { + headErr := internal.Error("test error for head") + + s := &localOperationExecutor{ + headRecv: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0].(Address)) + }, + err: headErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, nil), + headErr.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + h := &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, obj, items[0].(*Object)) + }, + } + + s := &localOperationExecutor{ + headRecv: &testExecutionEntity{ + res: obj, + }, + } + + require.NoError(t, s.executeOperation(ctx, req, h)) + }) + }) + + t.Run("search", func(t *testing.T) { + cid := testObjectAddress(t).CID + testQuery := testData(t, 10) + + req := &transportRequest{serviceRequest: &object.SearchRequest{ + ContainerID: cid, + Query: testQuery, + }} + + t.Run("error", func(t *testing.T) { + searchErr := internal.Error("test error for search") + + s := &localOperationExecutor{ + queryImp: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, cid, items[0].(CID)) + require.Equal(t, testQuery, items[1].([]byte)) + require.Equal(t, 1, items[2].(int)) + }, + err: searchErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, nil), + searchErr.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + addrList := testAddrList(t, 5) + + h := &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, addrList, items[0].([]Address)) + }, + } + + s := &localOperationExecutor{ + queryImp: &testExecutionEntity{ + res: addrList, + }, + } + + require.NoError(t, s.executeOperation(ctx, req, h)) + }) + }) + + t.Run("get range", func(t *testing.T) { + rng := Range{Offset: 1, Length: 1} + + req := newRawRangeInfo() + req.setAddress(addr) + req.setRange(rng) + + t.Run("error", func(t *testing.T) { + rrErr := internal.Error("test error for range reader") + + s := &localOperationExecutor{ + rngReader: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0].(Address)) + require.Equal(t, rng, items[1].(Range)) + }, + err: rrErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, nil), + rrErr.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + data := testData(t, 10) + + h := &testExecutionEntity{ + f: func(items ...interface{}) { + d, err := ioutil.ReadAll(items[0].(io.Reader)) + require.NoError(t, err) + require.Equal(t, data, d) + }, + } + + s := &localOperationExecutor{ + rngReader: &testExecutionEntity{ + res: data, + }, + } + + require.NoError(t, s.executeOperation(ctx, req, h)) + }) + }) + + t.Run("get range hash", func(t *testing.T) { + rngs := []Range{ + {Offset: 0, Length: 0}, + {Offset: 1, Length: 1}, + } + + salt := testData(t, 10) + + req := newRawRangeHashInfo() + req.setAddress(addr) + req.setRanges(rngs) + req.setSalt(salt) + + t.Run("error", func(t *testing.T) { + rhErr := internal.Error("test error for range hasher") + + s := &localOperationExecutor{ + rngHasher: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0].(Address)) + require.Equal(t, rngs, items[1].([]Range)) + require.Equal(t, salt, items[2].([]byte)) + }, + err: rhErr, + }, + } + + require.EqualError(t, + s.executeOperation(ctx, req, nil), + rhErr.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + hashes := []Hash{ + hash.Sum(testData(t, 10)), + hash.Sum(testData(t, 10)), + } + + h := &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, hashes, items[0].([]Hash)) + }, + } + + s := &localOperationExecutor{ + rngHasher: &testExecutionEntity{ + res: hashes, + }, + } + + require.NoError(t, s.executeOperation(ctx, req, h)) + }) + }) +} + +func Test_coreOperationFinalizer_completeExecution(t *testing.T) { + ctx := context.TODO() + + t.Run("address store failure", func(t *testing.T) { + asErr := internal.Error("test error for address store") + + s := &coreOperationFinalizer{ + interceptorPreparer: &testExecutionEntity{ + err: asErr, + }, + } + + require.EqualError(t, s.completeExecution(ctx, operationParams{ + metaInfo: &transportRequest{serviceRequest: new(object.SearchRequest)}, + }), asErr.Error()) + }) + + t.Run("correct execution construction", func(t *testing.T) { + req := &transportRequest{ + serviceRequest: &object.SearchRequest{ + ContainerID: testObjectAddress(t).CID, + Query: testData(t, 10), + QueryVersion: 1, + }, + timeout: 10 * time.Second, + } + + req.SetTTL(10) + + itemHandler := new(testExecutionEntity) + opParams := operationParams{ + computableParams: computableParams{ + addr: testObjectAddress(t), + stopCount: 2, + allowPartialResult: false, + tryPreviousNetMap: false, + selfForward: true, + maxRecycleCount: 7, + }, + metaInfo: req, + itemHandler: itemHandler, + } + + curPl := new(testExecutionEntity) + prevPl := new(testExecutionEntity) + wp := new(testExecutionEntity) + s := &coreOperationFinalizer{ + curPlacementBuilder: curPl, + prevPlacementBuilder: prevPl, + interceptorPreparer: &testExecutionEntity{ + res: func(context.Context, multiaddr.Multiaddr) bool { return true }, + }, + workerPool: wp, + traverseExec: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct traverse executor params", func(t *testing.T) { + p := items[0].(implementations.TraverseParams) + + require.True(t, p.ExecutionInterceptor(ctx, nil)) + require.Equal(t, req, p.TransportInfo) + require.Equal(t, wp, p.WorkerPool) + + tr := p.Traverser.(*coreTraverser) + require.Equal(t, opParams.addr, tr.addr) + require.Equal(t, opParams.tryPreviousNetMap, tr.tryPrevNM) + require.Equal(t, curPl, tr.curPlacementBuilder) + require.Equal(t, prevPl, tr.prevPlacementBuilder) + require.Equal(t, opParams.maxRecycleCount, tr.maxRecycleCount) + require.Equal(t, opParams.stopCount, tr.stopCount) + + h := p.Handler.(*coreHandler) + require.Equal(t, tr, h.traverser) + require.Equal(t, itemHandler, h.itemHandler) + }) + }, + }, + log: zap.L(), + } + + require.EqualError(t, s.completeExecution(ctx, opParams), errIncompleteOperation.Error()) + }) +} + +func Test_coreInterceptorPreparer_prepareInterceptor(t *testing.T) { + t.Run("address store failure", func(t *testing.T) { + asErr := internal.Error("test error for address store") + + s := &coreInterceptorPreparer{ + addressStore: &testExecutionEntity{ + err: asErr, + }, + } + + res, err := s.prepareInterceptor(interceptorItems{}) + require.EqualError(t, err, asErr.Error()) + require.Nil(t, res) + }) + + t.Run("correct interceptor", func(t *testing.T) { + ctx := context.TODO() + selfAddr := testNode(t, 0) + + t.Run("local node", func(t *testing.T) { + req := new(transportRequest) + itemHandler := new(testExecutionEntity) + + localErr := internal.Error("test error for local executor") + + p := interceptorItems{ + selfForward: true, + handler: &testExecutionEntity{ + f: func(items ...interface{}) { + t.Run("correct local executor params", func(t *testing.T) { + require.Equal(t, selfAddr, items[0].(multiaddr.Multiaddr)) + require.Nil(t, items[1]) + require.EqualError(t, items[2].(error), localErr.Error()) + }) + }, + }, + metaInfo: req, + itemHandler: itemHandler, + } + + s := &coreInterceptorPreparer{ + localExec: &testExecutionEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0].(transport.MetaInfo)) + require.Equal(t, itemHandler, items[1].(responseItemHandler)) + }, + err: localErr, + }, + addressStore: &testExecutionEntity{ + res: selfAddr, + }, + } + + res, err := s.prepareInterceptor(p) + require.NoError(t, err) + require.False(t, res(ctx, selfAddr)) + }) + + t.Run("remote node", func(t *testing.T) { + node := testNode(t, 1) + remoteNode := testNode(t, 2) + + p := interceptorItems{} + + s := &coreInterceptorPreparer{ + addressStore: &testExecutionEntity{ + res: remoteNode, + }, + } + + res, err := s.prepareInterceptor(p) + require.NoError(t, err) + require.False(t, res(ctx, node)) + }) + }) +} + +// testAddrList returns count random object addresses. +func testAddrList(t *testing.T, count int) (res []Address) { + for i := 0; i < count; i++ { + res = append(res, testObjectAddress(t)) + } + return +} diff --git a/services/public/object/filter.go b/services/public/object/filter.go new file mode 100644 index 0000000000..dc8ddc6c9b --- /dev/null +++ b/services/public/object/filter.go @@ -0,0 +1,251 @@ +package object + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/pkg/errors" +) + +type ( + filterParams struct { + sgInfoRecv storagegroup.InfoReceiver + tsPresChecker tombstonePresenceChecker + maxProcSize uint64 + storageCap uint64 + localStore localstore.Localstore + epochRecv EpochReceiver + verifier objutil.Verifier + + maxPayloadSize uint64 + } + + filterConstructor func(p *filterParams) localstore.FilterFunc + + tombstonePresenceChecker interface { + hasLocalTombstone(addr Address) (bool, error) + } + + coreTSPresChecker struct { + localStore localstore.Localstore + } +) + +const ( + ttlValue = "TTL" +) + +const ( + commonObjectFN = "OBJECTS_OVERALL" + storageGroupFN = "STORAGE_GROUP" + tombstoneOverwriteFN = "TOMBSTONE_OVERWRITE" + objSizeFN = "OBJECT_SIZE" + creationEpochFN = "CREATION_EPOCH" + objIntegrityFN = "OBJECT_INTEGRITY" + payloadSizeFN = "PAYLOAD_SIZE" +) + +const ( + errObjectFilter = internal.Error("incoming object has not passed filter") +) + +var ( + _ tombstonePresenceChecker = (*coreTSPresChecker)(nil) +) + +var mFilters = map[string]filterConstructor{ + tombstoneOverwriteFN: tombstoneOverwriteFC, + storageGroupFN: storageGroupFC, + creationEpochFN: creationEpochFC, + objIntegrityFN: objectIntegrityFC, + payloadSizeFN: payloadSizeFC, +} + +var mBasicFilters = map[string]filterConstructor{ + objSizeFN: objectSizeFC, +} + +func newIncomingObjectFilter(p *Params) (Filter, error) { + filter, err := newFilter(p, readyObjectsCheckpointFilterName, mFilters) + if err != nil { + return nil, err + } + + return filter, nil +} + +func newFilter(p *Params, name string, m map[string]filterConstructor) (Filter, error) { + filter := localstore.NewFilter(&localstore.FilterParams{ + Name: name, + FilterFunc: localstore.SkippingFilterFunc, + }) + + fp := &filterParams{ + sgInfoRecv: p.SGInfoReceiver, + tsPresChecker: &coreTSPresChecker{localStore: p.LocalStore}, + maxProcSize: p.MaxProcessingSize, + storageCap: p.StorageCapacity, + localStore: p.LocalStore, + epochRecv: p.EpochReceiver, + verifier: p.Verifier, + + maxPayloadSize: p.MaxPayloadSize, + } + + items := make([]*localstore.FilterParams, 0, len(m)) + for fName, fCons := range m { + items = append(items, &localstore.FilterParams{Name: fName, FilterFunc: fCons(fp)}) + } + + f, err := localstore.AllPassIncludingFilter(commonObjectFN, items...) + if err != nil { + return nil, err + } + + if err := filter.PutSubFilter(localstore.SubFilterParams{ + PriorityFlag: localstore.PriorityValue, + FilterPipeline: f, + OnFail: localstore.CodeFail, + }); err != nil { + return nil, errors.Wrapf(err, "could not put filter %s in pipeline", f.GetName()) + } + + return filter, nil +} + +func (s *coreTSPresChecker) hasLocalTombstone(addr Address) (bool, error) { + m, err := s.localStore.Meta(addr) + if err != nil { + if errors.Is(errors.Cause(err), core.ErrNotFound) { + return false, nil + } + + return false, err + } + + return m.Object.IsTombstone(), nil +} + +func storageGroupFC(p *filterParams) localstore.FilterFunc { + return func(ctx context.Context, meta *Meta) *localstore.FilterResult { + if sgInfo, err := meta.Object.StorageGroup(); err != nil { + return localstore.ResultPass() + } else if group := meta.Object.Group(); len(group) == 0 { + return localstore.ResultFail() + } else if realSGInfo, err := p.sgInfoRecv.GetSGInfo(ctx, meta.Object.SystemHeader.CID, group); err != nil { + return localstore.ResultWithError(localstore.CodeFail, err) + } else if sgInfo.ValidationDataSize != realSGInfo.ValidationDataSize { + return localstore.ResultWithError( + localstore.CodeFail, + &detailedError{ + error: errWrongSGSize, + d: sgSizeDetails(sgInfo.ValidationDataSize, realSGInfo.ValidationDataSize), + }, + ) + } else if !sgInfo.ValidationHash.Equal(realSGInfo.ValidationHash) { + return localstore.ResultWithError( + localstore.CodeFail, + &detailedError{ + error: errWrongSGHash, + d: sgHashDetails(sgInfo.ValidationHash, realSGInfo.ValidationHash), + }, + ) + } + + return localstore.ResultPass() + } +} + +func tombstoneOverwriteFC(p *filterParams) localstore.FilterFunc { + return func(ctx context.Context, meta *Meta) *localstore.FilterResult { + if meta.Object.IsTombstone() { + return localstore.ResultPass() + } else if hasTombstone, err := p.tsPresChecker.hasLocalTombstone(*meta.Object.Address()); err != nil { + return localstore.ResultFail() + } else if hasTombstone { + return localstore.ResultFail() + } + + return localstore.ResultPass() + } +} + +func objectSizeFC(p *filterParams) localstore.FilterFunc { + return func(ctx context.Context, meta *Meta) *localstore.FilterResult { + if need := meta.Object.SystemHeader.PayloadLength; need > p.maxProcSize { + return localstore.ResultWithError( + localstore.CodeFail, + &detailedError{ // // TODO: NSPCC-1048 + error: errProcPayloadSize, + d: maxProcPayloadSizeDetails(p.maxProcSize), + }, + ) + } else if ctx.Value(ttlValue).(uint32) < service.NonForwardingTTL { + if left := p.storageCap - uint64(p.localStore.Size()); need > left { + return localstore.ResultWithError( + localstore.CodeFail, + errLocalStorageOverflow, + ) + } + } + + return localstore.ResultPass() + } +} + +func payloadSizeFC(p *filterParams) localstore.FilterFunc { + return func(ctx context.Context, meta *Meta) *localstore.FilterResult { + if meta.Object.SystemHeader.PayloadLength > p.maxPayloadSize { + return localstore.ResultWithError( + localstore.CodeFail, + &detailedError{ // TODO: NSPCC-1048 + error: errObjectPayloadSize, + d: maxObjectPayloadSizeDetails(p.maxPayloadSize), + }, + ) + } + + return localstore.ResultPass() + } +} + +func creationEpochFC(p *filterParams) localstore.FilterFunc { + return func(_ context.Context, meta *Meta) *localstore.FilterResult { + if current := p.epochRecv.Epoch(); meta.Object.SystemHeader.CreatedAt.Epoch > current { + return localstore.ResultWithError( + localstore.CodeFail, + &detailedError{ // TODO: NSPCC-1048 + error: errObjectFromTheFuture, + d: objectCreationEpochDetails(current), + }, + ) + } + + return localstore.ResultPass() + } +} + +func objectIntegrityFC(p *filterParams) localstore.FilterFunc { + return func(ctx context.Context, meta *Meta) *localstore.FilterResult { + if err := p.verifier.Verify(ctx, meta.Object); err != nil { + return localstore.ResultWithError( + localstore.CodeFail, + &detailedError{ + error: errObjectHeadersVerification, + d: objectHeadersVerificationDetails(err), + }, + ) + } + + return localstore.ResultPass() + } +} + +func basicFilter(p *Params) (Filter, error) { + return newFilter(p, allObjectsCheckpointFilterName, mBasicFilters) +} diff --git a/services/public/object/filter_test.go b/services/public/object/filter_test.go new file mode 100644 index 0000000000..1b4084f059 --- /dev/null +++ b/services/public/object/filter_test.go @@ -0,0 +1,400 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testFilterEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + localstore.Localstore + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } + + testFilterUnit struct { + obj *Object + exp localstore.FilterCode + } +) + +var ( + _ storagegroup.InfoReceiver = (*testFilterEntity)(nil) + _ objutil.Verifier = (*testFilterEntity)(nil) + _ EpochReceiver = (*testFilterEntity)(nil) + _ localstore.Localstore = (*testFilterEntity)(nil) + _ tombstonePresenceChecker = (*testFilterEntity)(nil) +) + +func (s *testFilterEntity) Meta(addr Address) (*Meta, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Meta), nil +} + +func (s *testFilterEntity) GetSGInfo(ctx context.Context, cid CID, group []ID) (*storagegroup.StorageGroup, error) { + if s.f != nil { + s.f(cid, group) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*storagegroup.StorageGroup), nil +} + +func (s *testFilterEntity) hasLocalTombstone(addr Address) (bool, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return false, s.err + } + return s.res.(bool), nil +} + +func (s *testFilterEntity) Size() int64 { return s.res.(int64) } + +func (s *testFilterEntity) Epoch() uint64 { return s.res.(uint64) } + +func (s *testFilterEntity) Verify(_ context.Context, obj *Object) error { + if s.f != nil { + s.f(obj) + } + return s.err +} + +func Test_creationEpochFC(t *testing.T) { + ctx := context.TODO() + localEpoch := uint64(100) + + ff := creationEpochFC(&filterParams{epochRecv: &testFilterEntity{res: localEpoch}}) + + valid := []Object{ + {SystemHeader: SystemHeader{CreatedAt: CreationPoint{Epoch: localEpoch - 1}}}, + {SystemHeader: SystemHeader{CreatedAt: CreationPoint{Epoch: localEpoch}}}, + } + + invalid := []Object{ + {SystemHeader: SystemHeader{CreatedAt: CreationPoint{Epoch: localEpoch + 1}}}, + {SystemHeader: SystemHeader{CreatedAt: CreationPoint{Epoch: localEpoch + 2}}}, + } + + testFilteringObjects(t, ctx, ff, valid, invalid, nil) +} + +func Test_objectSizeFC(t *testing.T) { + maxProcSize := uint64(100) + + t.Run("forwarding TTL", func(t *testing.T) { + var ( + ctx = context.WithValue(context.TODO(), ttlValue, uint32(service.SingleForwardingTTL)) + ff = objectSizeFC(&filterParams{maxProcSize: maxProcSize}) + ) + + valid := []Object{ + {SystemHeader: SystemHeader{PayloadLength: maxProcSize - 1}}, + {SystemHeader: SystemHeader{PayloadLength: maxProcSize}}, + } + + invalid := []Object{ + {SystemHeader: SystemHeader{PayloadLength: maxProcSize + 1}}, + {SystemHeader: SystemHeader{PayloadLength: maxProcSize + 2}}, + } + + testFilteringObjects(t, ctx, ff, valid, invalid, nil) + }) + + t.Run("non-forwarding TTL", func(t *testing.T) { + var ( + ctx = context.WithValue(context.TODO(), ttlValue, uint32(service.NonForwardingTTL-1)) + objSize = maxProcSize / 2 + ls = &testFilterEntity{res: int64(maxProcSize - objSize)} + ) + + ff := objectSizeFC(&filterParams{ + maxProcSize: maxProcSize, + storageCap: maxProcSize, + localStore: ls, + }) + + valid := []Object{{SystemHeader: SystemHeader{PayloadLength: objSize}}} + invalid := []Object{{SystemHeader: SystemHeader{PayloadLength: objSize + 1}}} + + testFilteringObjects(t, ctx, ff, valid, invalid, nil) + }) +} + +func Test_objectIntegrityFC(t *testing.T) { + var ( + ctx = context.TODO() + valid = &Object{SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}} + invalid = &Object{SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}} + ) + valid.Headers = append(valid.Headers, Header{Value: new(object.Header_PayloadChecksum)}) + + ver := new(testFilterEntity) + ver.f = func(items ...interface{}) { + if items[0].(*Object).SystemHeader.ID.Equal(valid.SystemHeader.ID) { + ver.err = nil + } else { + ver.err = internal.Error("") + } + } + + ff := objectIntegrityFC(&filterParams{verifier: ver}) + + testFilterFunc(t, ctx, ff, testFilterUnit{obj: valid, exp: localstore.CodePass}) + testFilterFunc(t, ctx, ff, testFilterUnit{obj: invalid, exp: localstore.CodeFail}) +} + +func Test_tombstoneOverwriteFC(t *testing.T) { + var ( + obj1 = Object{ + SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}, + Headers: []Header{{Value: new(object.Header_Tombstone)}}, + } + obj2 = Object{ + SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}, + } + obj3 = Object{ + SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}, + } + obj4 = Object{ + SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}, + } + ) + + ts := new(testFilterEntity) + ts.f = func(items ...interface{}) { + addr := items[0].(Address) + if addr.ObjectID.Equal(obj2.SystemHeader.ID) { + ts.res, ts.err = nil, internal.Error("") + } else if addr.ObjectID.Equal(obj3.SystemHeader.ID) { + ts.res, ts.err = true, nil + } else { + ts.res, ts.err = false, nil + } + } + + valid := []Object{obj1, obj4} + invalid := []Object{obj2, obj3} + + ff := tombstoneOverwriteFC(&filterParams{tsPresChecker: ts}) + + testFilteringObjects(t, context.TODO(), ff, valid, invalid, nil) +} + +func Test_storageGroupFC(t *testing.T) { + var ( + valid, invalid []Object + cid = testObjectAddress(t).CID + sgSize, sgHash = uint64(10), hash.Sum(testData(t, 10)) + + sg = &storagegroup.StorageGroup{ + ValidationDataSize: sgSize, + ValidationHash: sgHash, + } + + sgHeaders = []Header{ + {Value: &object.Header_StorageGroup{StorageGroup: sg}}, + {Value: &object.Header_Link{Link: &object.Link{Type: object.Link_StorageGroup}}}, + } + ) + + valid = append(valid, Object{ + SystemHeader: SystemHeader{ + CID: cid, + }, + }) + + valid = append(valid, Object{ + SystemHeader: SystemHeader{ + CID: cid, + }, + Headers: sgHeaders, + }) + + invalid = append(invalid, Object{ + SystemHeader: SystemHeader{ + CID: cid, + }, + Headers: sgHeaders[:1], + }) + + invalid = append(invalid, Object{ + SystemHeader: SystemHeader{ + CID: cid, + }, + Headers: []Header{ + { + Value: &object.Header_StorageGroup{ + StorageGroup: &storagegroup.StorageGroup{ + ValidationDataSize: sg.ValidationDataSize + 1, + }, + }, + }, + { + Value: &object.Header_Link{ + Link: &object.Link{ + Type: object.Link_StorageGroup, + }, + }, + }, + }, + }) + + invalid = append(invalid, Object{ + SystemHeader: SystemHeader{ + CID: cid, + }, + Headers: []Header{ + { + Value: &object.Header_StorageGroup{ + StorageGroup: &storagegroup.StorageGroup{ + ValidationDataSize: sg.ValidationDataSize, + ValidationHash: Hash{1, 2, 3}, + }, + }, + }, + { + Value: &object.Header_Link{ + Link: &object.Link{ + Type: object.Link_StorageGroup, + }, + }, + }, + }, + }) + + sr := &testFilterEntity{ + f: func(items ...interface{}) { + require.Equal(t, cid, items[0]) + }, + res: sg, + } + + ff := storageGroupFC(&filterParams{sgInfoRecv: sr}) + + testFilteringObjects(t, context.TODO(), ff, valid, invalid, nil) +} + +func Test_coreTSPresChecker(t *testing.T) { + addr := testObjectAddress(t) + + t.Run("local storage failure", func(t *testing.T) { + ls := &testFilterEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0]) + }, + err: errors.Wrap(core.ErrNotFound, "some message"), + } + + s := &coreTSPresChecker{localStore: ls} + + res, err := s.hasLocalTombstone(addr) + require.NoError(t, err) + require.False(t, res) + + lsErr := internal.Error("test error for local storage") + ls.err = lsErr + + res, err = s.hasLocalTombstone(addr) + require.EqualError(t, err, lsErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + m := &Meta{Object: new(Object)} + + ls := &testFilterEntity{res: m} + + s := &coreTSPresChecker{localStore: ls} + + res, err := s.hasLocalTombstone(addr) + require.NoError(t, err) + require.False(t, res) + + m.Object.AddHeader(&object.Header{Value: new(object.Header_Tombstone)}) + + res, err = s.hasLocalTombstone(addr) + require.NoError(t, err) + require.True(t, res) + }) +} + +func testFilteringObjects(t *testing.T, ctx context.Context, f localstore.FilterFunc, valid, invalid, ignored []Object) { + units := make([]testFilterUnit, 0, len(valid)+len(invalid)+len(ignored)) + + for i := range valid { + units = append(units, testFilterUnit{ + obj: &valid[i], + exp: localstore.CodePass, + }) + } + + for i := range invalid { + units = append(units, testFilterUnit{ + obj: &invalid[i], + exp: localstore.CodeFail, + }) + } + + for i := range ignored { + units = append(units, testFilterUnit{ + obj: &ignored[i], + exp: localstore.CodeIgnore, + }) + } + + testFilterFunc(t, ctx, f, units...) +} + +func testFilterFunc(t *testing.T, ctx context.Context, f localstore.FilterFunc, units ...testFilterUnit) { + for i := range units { + res := f(ctx, &Meta{Object: units[i].obj}) + require.Equal(t, units[i].exp, res.Code()) + } +} + +func Test_payloadSizeFC(t *testing.T) { + maxPayloadSize := uint64(100) + + valid := []Object{ + {SystemHeader: SystemHeader{PayloadLength: maxPayloadSize - 1}}, + {SystemHeader: SystemHeader{PayloadLength: maxPayloadSize}}, + } + + invalid := []Object{ + {SystemHeader: SystemHeader{PayloadLength: maxPayloadSize + 1}}, + {SystemHeader: SystemHeader{PayloadLength: maxPayloadSize + 2}}, + } + + ff := payloadSizeFC(&filterParams{ + maxPayloadSize: maxPayloadSize, + }) + + testFilteringObjects(t, context.TODO(), ff, valid, invalid, nil) +} diff --git a/services/public/object/get.go b/services/public/object/get.go new file mode 100644 index 0000000000..666721283d --- /dev/null +++ b/services/public/object/get.go @@ -0,0 +1,111 @@ +package object + +import ( + "bytes" + "io" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + getServerWriter struct { + req *object.GetRequest + + srv object.Service_GetServer + + respPreparer responsePreparer + } +) + +const ( + maxGetPayloadSize = 3584 * 1024 // 3.5 MiB + + emSendObjectHead = "could not send object head" +) + +var _ io.Writer = (*getServerWriter)(nil) + +func (s *objectService) Get(req *object.GetRequest, server object.Service_GetServer) (err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestGet), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestGet, + e: err, + }) + }() + + var r interface{} + + if r, err = s.requestHandler.handleRequest(server.Context(), handleRequestParams{ + request: req, + executor: s, + }); err != nil { + return err + } + + obj := r.(*objectData) + + var payload []byte + payload, obj.Payload = obj.Payload, nil + + resp := makeGetHeaderResponse(obj.Object) + if err = s.respPreparer.prepareResponse(server.Context(), req, resp); err != nil { + return + } + + if err = server.Send(resp); err != nil { + return errors.Wrap(err, emSendObjectHead) + } + + _, err = io.CopyBuffer( + &getServerWriter{ + req: req, + srv: server, + respPreparer: s.getChunkPreparer, + }, + io.MultiReader(bytes.NewReader(payload), obj.payload), + make([]byte, maxGetPayloadSize)) + + return err +} + +func splitBytes(data []byte, maxSize int) (result [][]byte) { + l := len(data) + if l == 0 { + return nil + } + + for i := 0; i < l; i += maxSize { + last := i + maxSize + if last > l { + last = l + } + + result = append(result, data[i:last]) + } + + return +} + +func (s *getServerWriter) Write(p []byte) (int, error) { + resp := makeGetChunkResponse(p) + if err := s.respPreparer.prepareResponse(s.srv.Context(), s.req, resp); err != nil { + return 0, err + } + + if err := s.srv.Send(resp); err != nil { + return 0, err + } + + return len(p), nil +} diff --git a/services/public/object/get_test.go b/services/public/object/get_test.go new file mode 100644 index 0000000000..a78fde76ce --- /dev/null +++ b/services/public/object/get_test.go @@ -0,0 +1,225 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testGetEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + localstore.Localstore + object.Service_GetServer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ object.Service_GetServer = (*testGetEntity)(nil) + _ requestHandler = (*testGetEntity)(nil) + _ responsePreparer = (*testGetEntity)(nil) +) + +func (s *testGetEntity) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + if s.f != nil { + s.f(req, resp) + } + return s.err +} + +func (s *testGetEntity) Context() context.Context { return context.TODO() } + +func (s *testGetEntity) Send(r *object.GetResponse) error { + if s.f != nil { + s.f(r) + } + return s.err +} + +func (s *testGetEntity) handleRequest(_ context.Context, p handleRequestParams) (interface{}, error) { + if s.f != nil { + s.f(p) + } + return s.res, s.err +} + +func Test_makeGetHeaderResponse(t *testing.T) { + obj := &Object{Payload: testData(t, 10)} + + require.Equal(t, &object.GetResponse{R: &object.GetResponse_Object{Object: obj}}, makeGetHeaderResponse(obj)) +} + +func Test_makeGetChunkResponse(t *testing.T) { + chunk := testData(t, 10) + + require.Equal(t, &object.GetResponse{R: &object.GetResponse_Chunk{Chunk: chunk}}, makeGetChunkResponse(chunk)) +} + +func Test_splitBytes(t *testing.T) { + t.Run("empty data", func(t *testing.T) { + testSplit(t, make([]byte, 0), 0) + testSplit(t, nil, 0) + }) + + t.Run("less size", func(t *testing.T) { + testSplit(t, make([]byte, 10), 20) + }) + + t.Run("equal size", func(t *testing.T) { + testSplit(t, make([]byte, 20), 20) + }) + + t.Run("oversize", func(t *testing.T) { + testSplit(t, make([]byte, 3), 17) + }) +} + +func testSplit(t *testing.T, initData []byte, maxSize int) { + res := splitBytes(initData, maxSize) + restored := make([]byte, 0, len(initData)) + for i := range res { + require.LessOrEqual(t, len(res[i]), maxSize) + restored = append(restored, res[i]...) + } + require.Len(t, restored, len(initData)) + if len(initData) > 0 { + require.Equal(t, initData, restored) + } +} + +func TestObjectService_Get(t *testing.T) { + req := &object.GetRequest{Address: testObjectAddress(t)} + + t.Run("request handler failure", func(t *testing.T) { + hErr := internal.Error("test error for request handler") + + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testGetEntity{ + f: func(items ...interface{}) { + t.Run("correct request handler params", func(t *testing.T) { + p := items[0].(handleRequestParams) + require.Equal(t, req, p.request) + require.Equal(t, s, p.executor) + }) + }, + err: hErr, + } + + require.EqualError(t, s.Get(req, new(testGetEntity)), hErr.Error()) + }) + + t.Run("send object head failure", func(t *testing.T) { + srvErr := internal.Error("test error for get server") + + obj := &Object{ + SystemHeader: SystemHeader{ + ID: testObjectAddress(t).ObjectID, + CID: testObjectAddress(t).CID, + }, + } + + s := objectService{ + requestHandler: &testGetEntity{res: &objectData{Object: obj}}, + respPreparer: &testGetEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, makeGetHeaderResponse(obj), items[1]) + }, + res: new(object.GetResponse), + }, + + statusCalculator: newStatusCalculator(), + } + + require.EqualError(t, s.Get(req, &testGetEntity{err: srvErr}), errors.Wrap(srvErr, emSendObjectHead).Error()) + }) + + t.Run("send chunk failure", func(t *testing.T) { + srvErr := internal.Error("test error for get server") + payload := testData(t, 10) + + obj := &Object{ + SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}, + Headers: []Header{{ + Value: &object.Header_UserHeader{UserHeader: &UserHeader{Key: "key", Value: "value"}}, + }}, + Payload: payload, + } + + headResp := makeGetHeaderResponse(&Object{ + SystemHeader: obj.SystemHeader, + Headers: obj.Headers, + }) + + chunkResp := makeGetChunkResponse(payload) + + callNum := 0 + + respPrep := new(testGetEntity) + respPrep.f = func(items ...interface{}) { + if callNum == 0 { + respPrep.res = headResp + } else { + respPrep.res = chunkResp + } + } + + s := objectService{ + requestHandler: &testGetEntity{res: &objectData{Object: obj}}, + respPreparer: respPrep, + + getChunkPreparer: respPrep, + + statusCalculator: newStatusCalculator(), + } + + srv := new(testGetEntity) + srv.f = func(items ...interface{}) { + t.Run("correct get server params", func(t *testing.T) { + if callNum == 0 { + require.Equal(t, headResp, items[0]) + } else { + require.Equal(t, chunkResp, items[0]) + srv.err = srvErr + } + callNum++ + }) + } + + require.EqualError(t, s.Get(req, srv), srvErr.Error()) + }) + + t.Run("send success", func(t *testing.T) { + s := objectService{ + requestHandler: &testGetEntity{res: &objectData{ + Object: new(Object), + payload: new(emptyReader), + }}, + respPreparer: &testGetEntity{ + res: new(object.GetResponse), + }, + + statusCalculator: newStatusCalculator(), + } + + require.NoError(t, s.Get(req, new(testGetEntity))) + }) +} diff --git a/services/public/object/handler.go b/services/public/object/handler.go new file mode 100644 index 0000000000..9d704239f0 --- /dev/null +++ b/services/public/object/handler.go @@ -0,0 +1,109 @@ +package object + +import ( + "context" + "fmt" + + "github.com/nspcc-dev/neofs-api-go/object" +) + +type ( + // requestHandler is an interface of Object service cross-request handler. + requestHandler interface { + // Handles request by parameter-bound logic. + handleRequest(context.Context, handleRequestParams) (interface{}, error) + } + + handleRequestParams struct { + // Processing request. + request serviceRequest + + // Processing request executor. + executor requestHandleExecutor + } + + // coreRequestHandler is an implementation of requestHandler interface used in Object service production. + coreRequestHandler struct { + // Request preprocessor. + preProc requestPreProcessor + + // Request postprocessor. + postProc requestPostProcessor + } + + // requestHandleExecutor is an interface of universal Object operation executor. + requestHandleExecutor interface { + // Executes actions parameter-bound logic and returns execution result. + executeRequest(context.Context, serviceRequest) (interface{}, error) + } +) + +var _ requestHandler = (*coreRequestHandler)(nil) + +// requestHandler method implementation. +// +// If internal requestPreProcessor returns non-nil error for request argument, it returns. +// Otherwise, requestHandleExecutor argument performs actions. Received error is passed to requestPoistProcessor routine. +// Returned results of requestHandleExecutor are return. +func (s *coreRequestHandler) handleRequest(ctx context.Context, p handleRequestParams) (interface{}, error) { + if err := s.preProc.preProcess(ctx, p.request); err != nil { + return nil, err + } + + res, err := p.executor.executeRequest(ctx, p.request) + + go s.postProc.postProcess(ctx, p.request, err) + + return res, err +} + +// TODO: separate executors for each operation +// requestHandleExecutor method implementation. +func (s *objectService) executeRequest(ctx context.Context, req serviceRequest) (interface{}, error) { + switch r := req.(type) { + case *object.SearchRequest: + return s.objSearcher.searchObjects(ctx, &transportRequest{ + serviceRequest: r, + timeout: s.pSrch.Timeout, + }) + case *putRequest: + addr, err := s.objStorer.putObject(ctx, r) + if err != nil { + return nil, err + } + + resp := makePutResponse(*addr) + if err := s.respPreparer.prepareResponse(ctx, r.PutRequest, resp); err != nil { + return nil, err + } + + return nil, r.srv.SendAndClose(resp) + case *object.DeleteRequest: + return nil, s.objRemover.delete(ctx, &transportRequest{ + serviceRequest: r, + timeout: s.pDel.Timeout, + }) + case *object.GetRequest: + return s.objRecv.getObject(ctx, &transportRequest{ + serviceRequest: r, + timeout: s.pGet.Timeout, + }) + case *object.HeadRequest: + return s.objRecv.getObject(ctx, &transportRequest{ + serviceRequest: r, + timeout: s.pHead.Timeout, + }) + case *GetRangeRequest: + return s.payloadRngRecv.getRangeData(ctx, &transportRequest{ + serviceRequest: r, + timeout: s.pRng.Timeout, + }) + case *object.GetRangeHashRequest: + return s.rngRecv.getRange(ctx, &transportRequest{ + serviceRequest: r, + timeout: s.pRng.Timeout, + }) + default: + panic(fmt.Sprintf(pmWrongRequestType, r)) + } +} diff --git a/services/public/object/handler_test.go b/services/public/object/handler_test.go new file mode 100644 index 0000000000..abc0c7ce08 --- /dev/null +++ b/services/public/object/handler_test.go @@ -0,0 +1,442 @@ +package object + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testHandlerEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + object.Service_PutServer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ requestPreProcessor = (*testHandlerEntity)(nil) + _ requestPostProcessor = (*testHandlerEntity)(nil) + _ requestHandleExecutor = (*testHandlerEntity)(nil) + _ objectSearcher = (*testHandlerEntity)(nil) + _ objectStorer = (*testHandlerEntity)(nil) + _ object.Service_PutServer = (*testHandlerEntity)(nil) + _ objectRemover = (*testHandlerEntity)(nil) + _ objectReceiver = (*testHandlerEntity)(nil) + _ objectRangeReceiver = (*testHandlerEntity)(nil) + _ payloadRangeReceiver = (*testHandlerEntity)(nil) + _ responsePreparer = (*testHandlerEntity)(nil) +) + +func (s *testHandlerEntity) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + if s.f != nil { + s.f(req, resp) + } + return s.err +} + +func (s *testHandlerEntity) getRangeData(_ context.Context, info transport.RangeInfo, l ...Object) (io.Reader, error) { + if s.f != nil { + s.f(info, l) + } + if s.err != nil { + return nil, s.err + } + return s.res.(io.Reader), nil +} + +func (s *testHandlerEntity) getRange(_ context.Context, r rangeTool) (interface{}, error) { + if s.f != nil { + s.f(r) + } + return s.res, s.err +} + +func (s *testHandlerEntity) getObject(_ context.Context, r ...transport.GetInfo) (*objectData, error) { + if s.f != nil { + s.f(r) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*objectData), nil +} + +func (s *testHandlerEntity) delete(_ context.Context, r deleteInfo) error { + if s.f != nil { + s.f(r) + } + return s.err +} + +func (s *testHandlerEntity) SendAndClose(r *object.PutResponse) error { + if s.f != nil { + s.f(r) + } + return s.err +} + +func (s *testHandlerEntity) putObject(_ context.Context, r transport.PutInfo) (*Address, error) { + if s.f != nil { + s.f(r) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Address), nil +} + +func (s *testHandlerEntity) searchObjects(_ context.Context, r transport.SearchInfo) ([]Address, error) { + if s.f != nil { + s.f(r) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]Address), nil +} + +func (s *testHandlerEntity) preProcess(_ context.Context, req serviceRequest) error { + if s.f != nil { + s.f(req) + } + return s.err +} + +func (s *testHandlerEntity) postProcess(_ context.Context, req serviceRequest, e error) { + if s.f != nil { + s.f(req, e) + } +} + +func (s *testHandlerEntity) executeRequest(_ context.Context, req serviceRequest) (interface{}, error) { + if s.f != nil { + s.f(req) + } + return s.res, s.err +} + +func TestCoreRequestHandler_HandleRequest(t *testing.T) { + ctx := context.TODO() + + // create custom serviceRequest + req := new(testHandlerEntity) + + t.Run("pre processor error", func(t *testing.T) { + // create custom error + pErr := internal.Error("test error for pre-processor") + + s := &coreRequestHandler{ + preProc: &testHandlerEntity{ + f: func(items ...interface{}) { + t.Run("correct pre processor params", func(t *testing.T) { + require.Equal(t, req, items[0].(serviceRequest)) + }) + }, + err: pErr, // force requestPreProcessor to return pErr + }, + } + + res, err := s.handleRequest(ctx, handleRequestParams{request: req}) + + // ascertain that error returns as expected + require.EqualError(t, err, pErr.Error()) + + // ascertain that nil result returns as expected + require.Nil(t, res) + }) + + t.Run("correct behavior", func(t *testing.T) { + // create custom error + eErr := internal.Error("test error for request executor") + + // create custom result + eRes := testData(t, 10) + + // create channel for requestPostProcessor + ch := make(chan struct{}) + + executor := &testHandlerEntity{ + f: func(items ...interface{}) { + t.Run("correct executor params", func(t *testing.T) { + require.Equal(t, req, items[0].(serviceRequest)) + }) + }, + res: eRes, // force requestHandleExecutor to return created result + err: eErr, // force requestHandleExecutor to return created error + } + + s := &coreRequestHandler{ + preProc: &testHandlerEntity{ + err: nil, // force requestPreProcessor to return nil error + }, + postProc: &testHandlerEntity{ + f: func(items ...interface{}) { + t.Run("correct pre processor params", func(t *testing.T) { + require.Equal(t, req, items[0].(serviceRequest)) + require.Equal(t, eErr, items[1].(error)) + }) + ch <- struct{}{} // write to channel + }, + }, + } + + res, err := s.handleRequest(ctx, handleRequestParams{ + request: req, + executor: executor, + }) + + // ascertain that results return as expected + require.EqualError(t, err, eErr.Error()) + require.Equal(t, eRes, res) + + <-ch // read from channel + }) +} + +func Test_objectService_executeRequest(t *testing.T) { + ctx := context.TODO() + + t.Run("invalid request", func(t *testing.T) { + req := new(testHandlerEntity) + require.PanicsWithValue(t, fmt.Sprintf(pmWrongRequestType, req), func() { + _, _ = new(objectService).executeRequest(ctx, req) + }) + }) + + t.Run("search request", func(t *testing.T) { + var ( + timeout = 3 * time.Second + req = &object.SearchRequest{ContainerID: testObjectAddress(t).CID} + addrList = testAddrList(t, 3) + ) + + s := &objectService{ + pSrch: OperationParams{Timeout: timeout}, + objSearcher: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, &transportRequest{ + serviceRequest: req, + timeout: timeout, + }, items[0]) + }, + res: addrList, + }, + } + + res, err := s.executeRequest(ctx, req) + require.NoError(t, err) + require.Equal(t, addrList, res) + }) + + t.Run("put request", func(t *testing.T) { + t.Run("storer error", func(t *testing.T) { + sErr := internal.Error("test error for object storer") + + req := &putRequest{ + PutRequest: new(object.PutRequest), + srv: new(testHandlerEntity), + timeout: 3 * time.Second, + } + + s := &objectService{ + objStorer: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + }, + err: sErr, + }, + respPreparer: &testHandlerEntity{ + res: serviceResponse(nil), + }, + } + + _, err := s.executeRequest(ctx, req) + require.EqualError(t, err, sErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + addr := testObjectAddress(t) + + srvErr := internal.Error("test error for stream server") + + resp := &object.PutResponse{Address: addr} + + pReq := new(object.PutRequest) + + s := &objectService{ + objStorer: &testHandlerEntity{ + res: &addr, + }, + respPreparer: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, pReq, items[0]) + require.Equal(t, makePutResponse(addr), items[1]) + }, + res: resp, + }, + } + + req := &putRequest{ + PutRequest: pReq, + srv: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, resp, items[0]) + }, + err: srvErr, + }, + } + + res, err := s.executeRequest(ctx, req) + require.EqualError(t, err, srvErr.Error()) + require.Nil(t, res) + }) + }) + + t.Run("delete request", func(t *testing.T) { + var ( + timeout = 3 * time.Second + dErr = internal.Error("test error for object remover") + req = &object.DeleteRequest{Address: testObjectAddress(t)} + ) + + s := &objectService{ + objRemover: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, &transportRequest{ + serviceRequest: req, + timeout: timeout, + }, items[0]) + }, + err: dErr, + }, + pDel: OperationParams{Timeout: timeout}, + } + + res, err := s.executeRequest(ctx, req) + require.EqualError(t, err, dErr.Error()) + require.Nil(t, res) + }) + + t.Run("get request", func(t *testing.T) { + var ( + timeout = 3 * time.Second + obj = &objectData{Object: &Object{Payload: testData(t, 10)}} + req = &object.GetRequest{Address: testObjectAddress(t)} + ) + + s := &objectService{ + objRecv: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, []transport.GetInfo{&transportRequest{ + serviceRequest: req, + timeout: timeout, + }}, items[0]) + }, + res: obj, + }, + pGet: OperationParams{Timeout: timeout}, + } + + res, err := s.executeRequest(ctx, req) + require.NoError(t, err) + require.Equal(t, obj, res) + }) + + t.Run("head request", func(t *testing.T) { + var ( + timeout = 3 * time.Second + hErr = internal.Error("test error for head receiver") + req = &object.HeadRequest{Address: testObjectAddress(t)} + ) + + s := &objectService{ + objRecv: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, []transport.GetInfo{&transportRequest{ + serviceRequest: req, + timeout: timeout, + }}, items[0]) + }, + err: hErr, + }, + pHead: OperationParams{Timeout: timeout}, + } + + _, err := s.executeRequest(ctx, req) + require.EqualError(t, err, hErr.Error()) + }) + + t.Run("range requests", func(t *testing.T) { + t.Run("data", func(t *testing.T) { + var ( + timeout = 3 * time.Second + rData = testData(t, 10) + req = &GetRangeRequest{Address: testObjectAddress(t)} + ) + + s := &objectService{ + payloadRngRecv: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, &transportRequest{ + serviceRequest: req, + timeout: timeout, + }, items[0]) + require.Empty(t, items[1]) + }, + res: bytes.NewReader(rData), + }, + pRng: OperationParams{Timeout: timeout}, + } + + res, err := s.executeRequest(ctx, req) + require.NoError(t, err) + d, err := ioutil.ReadAll(res.(io.Reader)) + require.NoError(t, err) + require.Equal(t, rData, d) + }) + + t.Run("hashes", func(t *testing.T) { + var ( + timeout = 3 * time.Second + rErr = internal.Error("test error for range receiver") + req = &object.GetRangeHashRequest{Address: testObjectAddress(t)} + ) + + s := &objectService{ + rngRecv: &testHandlerEntity{ + f: func(items ...interface{}) { + require.Equal(t, &transportRequest{ + serviceRequest: req, + timeout: timeout, + }, items[0]) + }, + err: rErr, + }, + pRng: OperationParams{Timeout: timeout}, + } + + _, err := s.executeRequest(ctx, req) + require.EqualError(t, err, rErr.Error()) + }) + }) +} diff --git a/services/public/object/head.go b/services/public/object/head.go new file mode 100644 index 0000000000..2eb89ce433 --- /dev/null +++ b/services/public/object/head.go @@ -0,0 +1,640 @@ +package object + +import ( + "context" + "fmt" + "io" + "sync" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/objio" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/panjf2000/ants/v2" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + objectData struct { + *Object + payload io.Reader + } + + objectReceiver interface { + getObject(context.Context, ...transport.GetInfo) (*objectData, error) + } + + rangeDataReceiver interface { + recvData(context.Context, transport.RangeInfo, io.Writer) error + } + + rangeReaderAccumulator interface { + responseItemHandler + rangeData() io.Reader + } + + rangeRdrAccum struct { + *sync.Once + r io.Reader + } + + straightRangeDataReceiver struct { + executor operationExecutor + } + + coreObjectReceiver struct { + straightObjRecv objectReceiver + childLister objectChildrenLister + ancestralRecv ancestralObjectsReceiver + + log *zap.Logger + } + + straightObjectReceiver struct { + executor operationExecutor + } + + objectRewinder interface { + rewind(context.Context, ...Object) (*Object, error) + } + + payloadPartReceiver interface { + recvPayload(context.Context, []transport.RangeInfo) (io.Reader, error) + } + + corePayloadPartReceiver struct { + rDataRecv rangeDataReceiver + windowController slidingWindowController + } + + slidingWindowController interface { + newWindow() (WorkerPool, error) + } + + simpleWindowController struct { + windowSize int + } + + coreObjectRewinder struct { + transformer transformer.ObjectRestorer + } + + objectAccumulator interface { + responseItemHandler + object() *Object + } + + coreObjAccum struct { + *sync.Once + obj *Object + } + + rawGetInfo struct { + *rawAddrInfo + } + + rawHeadInfo struct { + rawGetInfo + fullHeaders bool + } + + childrenReceiver interface { + getChildren(context.Context, Address, []ID) ([]Object, error) + } + + coreChildrenReceiver struct { + coreObjRecv objectReceiver + timeout time.Duration + } + + payloadRangeReceiver interface { + getRangeData(context.Context, transport.RangeInfo, ...Object) (io.Reader, error) + } + + corePayloadRangeReceiver struct { + chopTable objio.ChopperTable + relRecv objio.RelativeReceiver + payloadRecv payloadPartReceiver + + // Set of errors that won't be converted to errPayloadRangeNotFound + mErr map[error]struct{} + + log *zap.Logger + } + + ancestralObjectsReceiver interface { + getFromChildren(context.Context, Address, []ID, bool) (*objectData, error) + } + + coreAncestralReceiver struct { + childrenRecv childrenReceiver + objRewinder objectRewinder + pRangeRecv payloadRangeReceiver + timeout time.Duration + } + + emptyReader struct{} +) + +const ( + emHeadRecvFail = "could not receive %d of %d object head" + + childrenNotFound = internal.Error("could not find child objects") + errNonAssembly = internal.Error("node is not capable to assemble the object") +) + +var ( + _ objectReceiver = (*straightObjectReceiver)(nil) + _ objectReceiver = (*coreObjectReceiver)(nil) + _ objectRewinder = (*coreObjectRewinder)(nil) + _ objectAccumulator = (*coreObjAccum)(nil) + _ transport.HeadInfo = (*transportRequest)(nil) + _ transport.HeadInfo = (*rawHeadInfo)(nil) + _ transport.GetInfo = (*transportRequest)(nil) + _ transport.GetInfo = (*rawGetInfo)(nil) + + _ payloadPartReceiver = (*corePayloadPartReceiver)(nil) + + _ ancestralObjectsReceiver = (*coreAncestralReceiver)(nil) + + _ childrenReceiver = (*coreChildrenReceiver)(nil) + + _ payloadRangeReceiver = (*corePayloadRangeReceiver)(nil) + + _ rangeDataReceiver = (*straightRangeDataReceiver)(nil) + + _ slidingWindowController = (*simpleWindowController)(nil) + + _ io.Reader = (*emptyReader)(nil) + + _ rangeReaderAccumulator = (*rangeRdrAccum)(nil) +) + +func (s *objectService) Head(ctx context.Context, req *object.HeadRequest) (res *object.HeadResponse, err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestHead), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestHead, + e: err, + }) + }() + + var r interface{} + + if r, err = s.requestHandler.handleRequest(ctx, handleRequestParams{ + request: req, + executor: s, + }); err != nil { + return + } + + obj := r.(*objectData).Object + if !req.FullHeaders { + obj.Headers = nil + } + + res = makeHeadResponse(obj) + err = s.respPreparer.prepareResponse(ctx, req, res) + + return res, err +} + +func (s *coreObjectReceiver) getObject(ctx context.Context, info ...transport.GetInfo) (*objectData, error) { + var ( + childCount int + children []ID + ) + + obj, err := s.straightObjRecv.getObject(ctx, s.sendingRequest(info[0])) + + if info[0].GetRaw() { + return obj, err + } else if err == nil { + children = obj.Links(object.Link_Child) + if childCount = len(children); childCount <= 0 { + return obj, nil + } + } + + if s.ancestralRecv == nil { + return nil, errNonAssembly + } + + ctx = contextWithValues(ctx, + transformer.PublicSessionToken, info[0].GetSessionToken(), + implementations.BearerToken, info[0].GetBearerToken(), + implementations.ExtendedHeaders, info[0].ExtendedHeaders(), + ) + + if childCount <= 0 { + if children = s.childLister.children(ctx, info[0].GetAddress()); len(children) == 0 { + return nil, childrenNotFound + } + } + + res, err := s.ancestralRecv.getFromChildren(ctx, info[0].GetAddress(), children, info[0].Type() == object.RequestHead) + if err != nil { + s.log.Error("could not get object from children", + zap.String("error", err.Error()), + ) + + return nil, errIncompleteOperation + } + + return res, nil +} + +func (s *coreObjectReceiver) sendingRequest(src transport.GetInfo) transport.GetInfo { + if s.ancestralRecv == nil || src.GetRaw() { + return src + } + + getInfo := *newRawGetInfo() + getInfo.setTimeout(src.GetTimeout()) + getInfo.setAddress(src.GetAddress()) + getInfo.setRaw(true) + getInfo.setSessionToken(src.GetSessionToken()) + getInfo.setBearerToken(src.GetBearerToken()) + getInfo.setExtendedHeaders(src.ExtendedHeaders()) + getInfo.setTTL( + maxu32( + src.GetTTL(), + service.NonForwardingTTL, + ), + ) + + if src.Type() == object.RequestHead { + headInfo := newRawHeadInfo() + headInfo.setGetInfo(getInfo) + headInfo.setFullHeaders(true) + + return headInfo + } + + return getInfo +} + +func (s *coreAncestralReceiver) getFromChildren(ctx context.Context, addr Address, children []ID, head bool) (*objectData, error) { + var ( + err error + childObjs []Object + res = new(objectData) + ) + + if childObjs, err = s.childrenRecv.getChildren(ctx, addr, children); err != nil { + return nil, err + } else if res.Object, err = s.objRewinder.rewind(ctx, childObjs...); err != nil { + return nil, err + } + + if head { + return res, nil + } + + rngInfo := newRawRangeInfo() + rngInfo.setTTL(service.NonForwardingTTL) + rngInfo.setTimeout(s.timeout) + rngInfo.setAddress(addr) + rngInfo.setSessionToken(tokenFromContext(ctx)) + rngInfo.setBearerToken(bearerFromContext(ctx)) + rngInfo.setExtendedHeaders(extendedHeadersFromContext(ctx)) + rngInfo.setRange(Range{ + Length: res.SystemHeader.PayloadLength, + }) + + res.payload, err = s.pRangeRecv.getRangeData(ctx, rngInfo, childObjs...) + + return res, err +} + +func (s *corePayloadRangeReceiver) getRangeData(ctx context.Context, info transport.RangeInfo, selection ...Object) (res io.Reader, err error) { + defer func() { + if err != nil { + if _, ok := s.mErr[errors.Cause(err)]; !ok { + s.log.Error("get payload range data failure", + zap.String("error", err.Error()), + ) + + err = errPayloadRangeNotFound + } + } + }() + + var ( + chopper RangeChopper + addr = info.GetAddress() + ) + + chopper, err = s.chopTable.GetChopper(addr, objio.RCCharybdis) + if err != nil || !chopper.Closed() { + if len(selection) == 0 { + if chopper, err = s.chopTable.GetChopper(addr, objio.RCScylla); err != nil { + if chopper, err = objio.NewScylla(&objio.ChopperParams{ + RelativeReceiver: s.relRecv, + Addr: addr, + }); err != nil { + return + } + } + } else { + rs := make([]RangeDescriptor, 0, len(selection)) + for i := range selection { + rs = append(rs, RangeDescriptor{ + Size: int64(selection[i].SystemHeader.PayloadLength), + Addr: *selection[i].Address(), + + LeftBound: i == 0, + RightBound: i == len(selection)-1, + }) + } + + if chopper, err = objio.NewCharybdis(&objio.CharybdisParams{ + Addr: addr, + ReadySelection: rs, + }); err != nil { + return + } + } + } + + _ = s.chopTable.PutChopper(addr, chopper) + + r := info.GetRange() + + ctx = contextWithValues(ctx, + transformer.PublicSessionToken, info.GetSessionToken(), + implementations.BearerToken, info.GetBearerToken(), + implementations.ExtendedHeaders, info.ExtendedHeaders(), + ) + + var rList []RangeDescriptor + + if rList, err = chopper.Chop(ctx, int64(r.Length), int64(r.Offset), true); err != nil { + return + } + + return s.payloadRecv.recvPayload(ctx, newRangeInfoList(info, rList)) +} + +func newRangeInfoList(src transport.RangeInfo, rList []RangeDescriptor) []transport.RangeInfo { + var infoList []transport.RangeInfo + if l := len(rList); l == 1 && src.GetAddress().Equal(&rList[0].Addr) { + infoList = []transport.RangeInfo{src} + } else { + infoList = make([]transport.RangeInfo, 0, l) + for i := range rList { + rngInfo := newRawRangeInfo() + + rngInfo.setTTL(src.GetTTL()) + rngInfo.setTimeout(src.GetTimeout()) + rngInfo.setAddress(rList[i].Addr) + rngInfo.setSessionToken(src.GetSessionToken()) + rngInfo.setBearerToken(src.GetBearerToken()) + rngInfo.setExtendedHeaders(src.ExtendedHeaders()) + rngInfo.setRange(Range{ + Offset: uint64(rList[i].Offset), + Length: uint64(rList[i].Size), + }) + + infoList = append(infoList, rngInfo) + } + } + + return infoList +} + +func (s *corePayloadPartReceiver) recvPayload(ctx context.Context, rList []transport.RangeInfo) (io.Reader, error) { + pool, err := s.windowController.newWindow() + if err != nil { + return nil, err + } + + var ( + readers = make([]io.Reader, 0, len(rList)) + writers = make([]*io.PipeWriter, 0, len(rList)) + ) + + for range rList { + r, w := io.Pipe() + readers = append(readers, r) + writers = append(writers, w) + } + + ctx, cancel := context.WithCancel(ctx) + + go func() { + for i := range rList { + select { + case <-ctx.Done(): + return + default: + } + + rd, w := rList[i], writers[i] + + if err := pool.Submit(func() { + err := s.rDataRecv.recvData(ctx, rd, w) + if err != nil { + cancel() + } + _ = w.CloseWithError(err) + }); err != nil { + _ = w.CloseWithError(err) + + cancel() + + break + } + } + }() + + return io.MultiReader(readers...), nil +} + +func (s *simpleWindowController) newWindow() (WorkerPool, error) { return ants.NewPool(s.windowSize) } + +func (s *straightRangeDataReceiver) recvData(ctx context.Context, info transport.RangeInfo, w io.Writer) error { + rAccum := newRangeReaderAccumulator() + err := s.executor.executeOperation(ctx, info, rAccum) + + if err == nil { + _, err = io.Copy(w, rAccum.rangeData()) + } + + return err +} + +func maxu32(a, b uint32) uint32 { + if a > b { + return a + } + + return b +} + +func (s *straightObjectReceiver) getObject(ctx context.Context, info ...transport.GetInfo) (*objectData, error) { + accum := newObjectAccumulator() + if err := s.executor.executeOperation(ctx, info[0], accum); err != nil { + return nil, err + } + + return &objectData{ + Object: accum.object(), + payload: new(emptyReader), + }, nil +} + +func (s *coreChildrenReceiver) getChildren(ctx context.Context, parent Address, children []ID) ([]Object, error) { + objList := make([]Object, 0, len(children)) + + headInfo := newRawHeadInfo() + headInfo.setTTL(service.NonForwardingTTL) + headInfo.setTimeout(s.timeout) + headInfo.setFullHeaders(true) + headInfo.setSessionToken(tokenFromContext(ctx)) + headInfo.setBearerToken(bearerFromContext(ctx)) + headInfo.setExtendedHeaders(extendedHeadersFromContext(ctx)) + + for i := range children { + headInfo.setAddress(Address{ + ObjectID: children[i], + CID: parent.CID, + }) + + obj, err := s.coreObjRecv.getObject(ctx, headInfo) + if err != nil { + return nil, errors.Errorf(emHeadRecvFail, i+1, len(children)) + } + + objList = append(objList, *obj.Object) + } + + return transformer.GetChain(objList...) +} + +func tokenFromContext(ctx context.Context) service.SessionToken { + if v, ok := ctx.Value(transformer.PublicSessionToken).(service.SessionToken); ok { + return v + } + + return nil +} + +func bearerFromContext(ctx context.Context) service.BearerToken { + if v, ok := ctx.Value(implementations.BearerToken).(service.BearerToken); ok { + return v + } + + return nil +} + +func extendedHeadersFromContext(ctx context.Context) []service.ExtendedHeader { + if v, ok := ctx.Value(implementations.ExtendedHeaders).([]service.ExtendedHeader); ok { + return v + } + + return nil +} + +func (s *coreObjectRewinder) rewind(ctx context.Context, objs ...Object) (*Object, error) { + objList, err := s.transformer.Restore(ctx, objs...) + if err != nil { + return nil, err + } + + return &objList[0], nil +} + +func (s *coreObjAccum) handleItem(v interface{}) { s.Do(func() { s.obj = v.(*Object) }) } + +func (s *coreObjAccum) object() *Object { return s.obj } + +func newObjectAccumulator() objectAccumulator { return &coreObjAccum{Once: new(sync.Once)} } + +func (s *rawGetInfo) getAddrInfo() *rawAddrInfo { + return s.rawAddrInfo +} + +func (s *rawGetInfo) setAddrInfo(v *rawAddrInfo) { + s.rawAddrInfo = v + s.setType(object.RequestGet) +} + +func newRawGetInfo() *rawGetInfo { + res := new(rawGetInfo) + + res.setAddrInfo(newRawAddressInfo()) + + return res +} + +func (s rawHeadInfo) GetFullHeaders() bool { + return s.fullHeaders +} + +func (s *rawHeadInfo) setFullHeaders(v bool) { + s.fullHeaders = v +} + +func (s rawHeadInfo) getGetInfo() rawGetInfo { + return s.rawGetInfo +} + +func (s *rawHeadInfo) setGetInfo(v rawGetInfo) { + s.rawGetInfo = v + s.setType(object.RequestHead) +} + +func newRawHeadInfo() *rawHeadInfo { + res := new(rawHeadInfo) + + res.setGetInfo(*newRawGetInfo()) + + return res +} + +func (s *transportRequest) GetAddress() Address { + switch t := s.serviceRequest.(type) { + case *object.HeadRequest: + return t.Address + case *GetRangeRequest: + return t.Address + case *object.GetRangeHashRequest: + return t.Address + case *object.DeleteRequest: + return t.Address + case *object.GetRequest: + return t.Address + default: + panic(fmt.Sprintf(pmWrongRequestType, t)) + } +} + +func (s *transportRequest) GetFullHeaders() bool { + return s.serviceRequest.(*object.HeadRequest).GetFullHeaders() +} + +func (s *transportRequest) Raw() bool { + return s.serviceRequest.GetRaw() +} + +func (s *emptyReader) Read([]byte) (int, error) { return 0, io.EOF } + +func newRangeReaderAccumulator() rangeReaderAccumulator { return &rangeRdrAccum{Once: new(sync.Once)} } + +func (s *rangeRdrAccum) rangeData() io.Reader { return s.r } + +func (s *rangeRdrAccum) handleItem(r interface{}) { s.Do(func() { s.r = r.(io.Reader) }) } diff --git a/services/public/object/head_test.go b/services/public/object/head_test.go new file mode 100644 index 0000000000..fcf2be7bb2 --- /dev/null +++ b/services/public/object/head_test.go @@ -0,0 +1,595 @@ +package object + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testHeadEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + transformer.ObjectRestorer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ ancestralObjectsReceiver = (*testHeadEntity)(nil) + _ objectChildrenLister = (*testHeadEntity)(nil) + _ objectReceiver = (*testHeadEntity)(nil) + _ requestHandler = (*testHeadEntity)(nil) + _ operationExecutor = (*testHeadEntity)(nil) + _ objectRewinder = (*testHeadEntity)(nil) + _ transformer.ObjectRestorer = (*testHeadEntity)(nil) + _ responsePreparer = (*testHeadEntity)(nil) +) + +func (s *testHeadEntity) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + if s.f != nil { + s.f(req, resp) + } + return s.err +} + +func (s *testHeadEntity) getFromChildren(ctx context.Context, addr Address, ids []ID, h bool) (*objectData, error) { + if s.f != nil { + s.f(addr, ids, h, ctx) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*objectData), nil +} + +func (s *testHeadEntity) Restore(_ context.Context, objs ...Object) ([]Object, error) { + if s.f != nil { + s.f(objs) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]Object), nil +} + +func (s *testHeadEntity) rewind(ctx context.Context, objs ...Object) (*Object, error) { + if s.f != nil { + s.f(objs) + } + return s.res.(*Object), s.err +} + +func (s *testHeadEntity) executeOperation(_ context.Context, i transport.MetaInfo, h responseItemHandler) error { + if s.f != nil { + s.f(i, h) + } + return s.err +} + +func (s *testHeadEntity) children(ctx context.Context, addr Address) []ID { + if s.f != nil { + s.f(addr, ctx) + } + return s.res.([]ID) +} + +func (s *testHeadEntity) getObject(_ context.Context, p ...transport.GetInfo) (*objectData, error) { + if s.f != nil { + s.f(p) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*objectData), nil +} + +func (s *testHeadEntity) handleRequest(_ context.Context, p handleRequestParams) (interface{}, error) { + if s.f != nil { + s.f(p) + } + return s.res, s.err +} + +func Test_transportRequest_HeadInfo(t *testing.T) { + t.Run("address", func(t *testing.T) { + t.Run("valid request", func(t *testing.T) { + addr := testObjectAddress(t) + + reqs := []transportRequest{ + {serviceRequest: &object.HeadRequest{Address: addr}}, + {serviceRequest: &object.GetRequest{Address: addr}}, + {serviceRequest: &GetRangeRequest{Address: addr}}, + {serviceRequest: &object.GetRangeHashRequest{Address: addr}}, + {serviceRequest: &object.DeleteRequest{Address: addr}}, + } + + for i := range reqs { + require.Equal(t, addr, reqs[i].GetAddress()) + } + }) + + t.Run("unknown request", func(t *testing.T) { + req := new(object.SearchRequest) + + r := &transportRequest{ + serviceRequest: req, + } + + require.PanicsWithValue(t, fmt.Sprintf(pmWrongRequestType, req), func() { + _ = r.GetAddress() + }) + }) + }) + + t.Run("full headers", func(t *testing.T) { + r := &transportRequest{ + serviceRequest: &object.HeadRequest{ + FullHeaders: true, + }, + } + + require.True(t, r.GetFullHeaders()) + }) + + t.Run("raw", func(t *testing.T) { + hReq := new(object.HeadRequest) + hReq.SetRaw(true) + + r := &transportRequest{ + serviceRequest: hReq, + } + require.True(t, r.Raw()) + + hReq.SetRaw(false) + require.False(t, r.Raw()) + }) +} + +func Test_rawHeadInfo(t *testing.T) { + t.Run("address", func(t *testing.T) { + addr := testObjectAddress(t) + + r := newRawHeadInfo() + r.setAddress(addr) + + require.Equal(t, addr, r.GetAddress()) + }) + + t.Run("full headers", func(t *testing.T) { + r := newRawHeadInfo() + r.setFullHeaders(true) + + require.True(t, r.GetFullHeaders()) + }) +} + +func Test_coreObjAccum(t *testing.T) { + t.Run("new", func(t *testing.T) { + s := newObjectAccumulator() + v := s.(*coreObjAccum) + require.Nil(t, v.obj) + require.NotNil(t, v.Once) + }) + + t.Run("handle/object", func(t *testing.T) { + obj1 := new(Object) + + s := newObjectAccumulator() + + // add first object + s.handleItem(obj1) + + // ascertain tha object was added + require.Equal(t, obj1, s.object()) + + obj2 := new(Object) + + // add second object + s.handleItem(obj2) + + // ascertain that second object was ignored + require.Equal(t, obj1, s.object()) + }) +} + +func Test_objectService_Head(t *testing.T) { + ctx := context.TODO() + + t.Run("request handler error", func(t *testing.T) { + // create custom error for test + rhErr := internal.Error("test error for request handler") + + // create custom request for test + req := new(object.HeadRequest) + + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct request handler params", func(t *testing.T) { + p := items[0].(handleRequestParams) + require.Equal(t, s, p.executor) + require.Equal(t, req, p.request) + }) + }, + err: rhErr, // force requestHandler to return rhErr + } + + res, err := s.Head(ctx, req) + require.EqualError(t, err, rhErr.Error()) + require.Nil(t, res) + }) + + t.Run("correct resulst", func(t *testing.T) { + obj := &objectData{Object: new(Object)} + + resp := &object.HeadResponse{Object: obj.Object} + + req := new(object.HeadRequest) + + s := &objectService{ + requestHandler: &testHeadEntity{ + res: obj, // force request handler to return obj + }, + respPreparer: &testHeadEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, makeHeadResponse(obj.Object), items[1]) + }, + res: resp, + }, + + statusCalculator: newStatusCalculator(), + } + + res, err := s.Head(ctx, new(object.HeadRequest)) + require.NoError(t, err) + require.Equal(t, resp, res) + }) +} + +func Test_coreHeadReceiver_head(t *testing.T) { + ctx := context.TODO() + + t.Run("raw handling", func(t *testing.T) { + // create custom head info for test + hInfo := newRawHeadInfo() + hInfo.setRaw(true) + + // create custom error for test + srErr := internal.Error("test error for straight object receiver") + + s := &coreObjectReceiver{ + straightObjRecv: &testHeadEntity{ + err: srErr, // force straightObjectReceiver to return srErr + }, + } + + _, err := s.getObject(ctx, hInfo) + // ascertain that straightObjectReceiver result returns in raw case as expected + require.EqualError(t, err, srErr.Error()) + }) + + t.Run("straight receive of non-linking object", func(t *testing.T) { + // create custom head info for test + hInfo := newRawHeadInfo() + + // create object w/o children for test + obj := &objectData{Object: new(Object)} + + s := &coreObjectReceiver{ + straightObjRecv: &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct straight receiver params", func(t *testing.T) { + require.Equal(t, []transport.GetInfo{hInfo}, items[0]) + }) + }, + res: obj, + }, + } + + res, err := s.getObject(ctx, hInfo) + require.NoError(t, err) + require.Equal(t, obj, res) + }) + + t.Run("linking object/non-assembly", func(t *testing.T) { + // create custom head info for test + hInfo := newRawHeadInfo() + + // create object w/ children for test + obj := &objectData{ + Object: &Object{Headers: []Header{{Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Child}}}}}, + } + + s := &coreObjectReceiver{ + straightObjRecv: &testHeadEntity{ + res: obj, // force straightObjectReceiver to return obj + }, + ancestralRecv: nil, // make component to be non-assembly + } + + res, err := s.getObject(ctx, hInfo) + require.EqualError(t, err, errNonAssembly.Error()) + require.Nil(t, res) + }) + + t.Run("children search failure", func(t *testing.T) { + addr := testObjectAddress(t) + + hInfo := newRawHeadInfo() + hInfo.setAddress(addr) + hInfo.setSessionToken(new(service.Token)) + + s := &coreObjectReceiver{ + straightObjRecv: &testHeadEntity{ + err: internal.Error(""), // force straightObjectReceiver to return non-empty error + }, + childLister: &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct child lister params", func(t *testing.T) { + require.Equal(t, addr, items[0]) + require.Equal(t, + hInfo.GetSessionToken(), + items[1].(context.Context).Value(transformer.PublicSessionToken), + ) + }) + }, + res: make([]ID, 0), // force objectChildren lister to return empty list + }, + ancestralRecv: new(testHeadEntity), + } + + res, err := s.getObject(ctx, hInfo) + require.EqualError(t, err, childrenNotFound.Error()) + require.Nil(t, res) + }) + + t.Run("correct result", func(t *testing.T) { + var ( + childCount = 5 + rErr = internal.Error("test error for rewinding receiver") + children = make([]ID, 0, childCount) + ) + + for i := 0; i < childCount; i++ { + id := testObjectAddress(t).ObjectID + children = append(children, id) + } + + // create custom head info + hInfo := newRawHeadInfo() + hInfo.setTTL(5) + hInfo.setTimeout(3 * time.Second) + hInfo.setAddress(testObjectAddress(t)) + hInfo.setSessionToken(new(service.Token)) + + t.Run("error/children from straight receiver", func(t *testing.T) { + obj := &objectData{Object: new(Object)} + + for i := range children { + // add child reference to object + obj.Headers = append(obj.Headers, Header{ + Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Child, ID: children[i]}}, + }) + } + + s := &coreObjectReceiver{ + straightObjRecv: &testHeadEntity{ + res: obj, // force straight receiver to return obj + }, + ancestralRecv: &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct rewinding receiver", func(t *testing.T) { + require.Equal(t, hInfo.GetAddress(), items[0]) + require.Equal(t, children, items[1]) + require.True(t, items[2].(bool)) + require.Equal(t, + hInfo.GetSessionToken(), + items[3].(context.Context).Value(transformer.PublicSessionToken), + ) + }) + }, + err: rErr, // force rewinding receiver to return rErr + }, + log: zap.L(), + } + + res, err := s.getObject(ctx, hInfo) + require.EqualError(t, err, errIncompleteOperation.Error()) + require.Nil(t, res) + }) + + t.Run("success/children from child lister", func(t *testing.T) { + obj := &objectData{Object: new(Object)} + + s := &coreObjectReceiver{ + straightObjRecv: &testHeadEntity{ + err: internal.Error(""), // force straight receiver to return non-nil error + }, + ancestralRecv: &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct rewinding receiver", func(t *testing.T) { + require.Equal(t, hInfo.GetAddress(), items[0]) + require.Equal(t, children, items[1]) + require.True(t, items[2].(bool)) + }) + }, + res: obj, // force rewinding receiver to return obj + }, + childLister: &testHeadEntity{ + res: children, // force objectChildrenLister to return particular list + }, + } + + res, err := s.getObject(ctx, hInfo) + require.NoError(t, err, rErr.Error()) + require.Equal(t, obj, res) + }) + }) +} + +func Test_straightHeadReceiver_head(t *testing.T) { + ctx := context.TODO() + + hInfo := newRawHeadInfo() + hInfo.setFullHeaders(true) + + t.Run("executor error", func(t *testing.T) { + exErr := internal.Error("test error for operation executor") + + s := &straightObjectReceiver{ + executor: &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct operation executor params", func(t *testing.T) { + require.Equal(t, hInfo, items[0]) + _ = items[1].(objectAccumulator) + }) + }, + err: exErr, // force operationExecutor to return exErr + }, + } + + _, err := s.getObject(ctx, hInfo) + require.EqualError(t, err, exErr.Error()) + + hInfo = newRawHeadInfo() + hInfo.setFullHeaders(true) + + _, err = s.getObject(ctx, hInfo) + require.EqualError(t, err, exErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + obj := &objectData{Object: new(Object), payload: new(emptyReader)} + + s := &straightObjectReceiver{ + executor: &testHeadEntity{ + f: func(items ...interface{}) { + items[1].(objectAccumulator).handleItem(obj.Object) + }, + }, + } + + res, err := s.getObject(ctx, hInfo) + require.NoError(t, err) + require.Equal(t, obj, res) + }) +} + +func Test_coreObjectRewinder_rewind(t *testing.T) { + ctx := context.TODO() + + t.Run("transformer failure", func(t *testing.T) { + tErr := internal.Error("test error for object transformer") + objs := []Object{*new(Object), *new(Object)} + + s := &coreObjectRewinder{ + transformer: &testHeadEntity{ + f: func(items ...interface{}) { + t.Run("correct transformer params", func(t *testing.T) { + require.Equal(t, objs, items[0]) + }) + }, + err: tErr, // force transformer to return tErr + }, + } + + res, err := s.rewind(ctx, objs...) + require.EqualError(t, err, tErr.Error()) + require.Empty(t, res) + }) + + t.Run("correct result", func(t *testing.T) { + objs := []Object{ + {SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}}, + {SystemHeader: SystemHeader{ID: testObjectAddress(t).ObjectID}}, + } + + s := &coreObjectRewinder{ + transformer: &testHeadEntity{ + res: objs, // force transformer to return objs + }, + } + + res, err := s.rewind(ctx, objs...) + require.NoError(t, err) + require.Equal(t, &objs[0], res) + }) +} + +func Test_coreObjectReceiver_sendingRequest(t *testing.T) { + t.Run("non-assembly", func(t *testing.T) { + src := &transportRequest{serviceRequest: new(object.GetRequest)} + // ascertain that request not changed if node is non-assembled + require.Equal(t, src, new(coreObjectReceiver).sendingRequest(src)) + }) + + t.Run("assembly", func(t *testing.T) { + s := &coreObjectReceiver{ancestralRecv: new(testHeadEntity)} + + t.Run("raw request", func(t *testing.T) { + src := newRawGetInfo() + src.setRaw(true) + // ascertain that request not changed if request is raw + require.Equal(t, src, s.sendingRequest(src)) + }) + + t.Run("non-raw request", func(t *testing.T) { + getInfo := *newRawGetInfo() + getInfo.setTTL(uint32(5)) + getInfo.setTimeout(3 * time.Second) + getInfo.setAddress(testObjectAddress(t)) + getInfo.setRaw(false) + getInfo.setSessionToken(new(service.Token)) + + t.Run("get", func(t *testing.T) { + res := s.sendingRequest(getInfo) + require.Equal(t, getInfo.GetTimeout(), res.GetTimeout()) + require.Equal(t, getInfo.GetAddress(), res.GetAddress()) + require.Equal(t, getInfo.GetTTL(), res.GetTTL()) + require.Equal(t, getInfo.GetSessionToken(), res.GetSessionToken()) + require.True(t, res.GetRaw()) + + t.Run("zero ttl", func(t *testing.T) { + res := s.sendingRequest(newRawGetInfo()) + require.Equal(t, uint32(service.NonForwardingTTL), res.GetTTL()) + }) + }) + + t.Run("head", func(t *testing.T) { + hInfo := newRawHeadInfo() + hInfo.setGetInfo(getInfo) + hInfo.setFullHeaders(false) + + res := s.sendingRequest(hInfo) + require.Equal(t, getInfo.GetTimeout(), res.GetTimeout()) + require.Equal(t, getInfo.GetAddress(), res.GetAddress()) + require.Equal(t, getInfo.GetTTL(), res.GetTTL()) + require.Equal(t, getInfo.GetSessionToken(), res.GetSessionToken()) + require.True(t, res.GetRaw()) + require.True(t, res.(transport.HeadInfo).GetFullHeaders()) + }) + }) + }) +} diff --git a/services/public/object/implementations.go b/services/public/object/implementations.go new file mode 100644 index 0000000000..2f4b3715a4 --- /dev/null +++ b/services/public/object/implementations.go @@ -0,0 +1,32 @@ +package object + +import ( + "context" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/lib/peers" + "github.com/pkg/errors" +) + +type ( + remoteService struct { + ps peers.Interface + } +) + +// NewRemoteService is a remote service controller's constructor. +func NewRemoteService(ps peers.Interface) RemoteService { + return &remoteService{ + ps: ps, + } +} + +func (rs remoteService) Remote(ctx context.Context, addr multiaddr.Multiaddr) (object.ServiceClient, error) { + con, err := rs.ps.GRPCConnection(ctx, addr, false) + if err != nil { + return nil, errors.Wrapf(err, "remoteService.Remote failed on GRPCConnection to %s", addr) + } + + return object.NewServiceClient(con), nil +} diff --git a/services/public/object/listing.go b/services/public/object/listing.go new file mode 100644 index 0000000000..9967fc543c --- /dev/null +++ b/services/public/object/listing.go @@ -0,0 +1,286 @@ +package object + +import ( + "context" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + v1 "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/objio" + "github.com/nspcc-dev/neofs-node/lib/transport" + "go.uber.org/zap" +) + +type ( + objectChildrenLister interface { + children(context.Context, Address) []ID + } + + coreChildrenLister struct { + queryFn relationQueryFunc + objSearcher objectSearcher + log *zap.Logger + timeout time.Duration + } + + relationQueryFunc func(Address) ([]byte, error) + + rawSearchInfo struct { + *rawMetaInfo + cid CID + query []byte + } + + neighborReceiver struct { + firstChildQueryFn relationQueryFunc + leftNeighborQueryFn relationQueryFunc + rightNeighborQueryFn relationQueryFunc + rangeDescRecv selectiveRangeReceiver + } + + selectiveRangeReceiver interface { + rangeDescriptor(context.Context, Address, relationQueryFunc) (RangeDescriptor, error) + } + + selectiveRangeRecv struct { + executor implementations.SelectiveContainerExecutor + } +) + +const ( + lmQueryMarshalFail = "marshal search query failure" + lmListFail = "searching inside children listing failure" + + errRelationNotFound = internal.Error("relation not found") +) + +var ( + _ relationQueryFunc = coreChildrenQueryFunc + _ transport.SearchInfo = (*rawSearchInfo)(nil) + _ objectChildrenLister = (*coreChildrenLister)(nil) + _ objio.RelativeReceiver = (*neighborReceiver)(nil) + _ selectiveRangeReceiver = (*selectiveRangeRecv)(nil) +) + +func (s *neighborReceiver) Base(ctx context.Context, addr Address) (RangeDescriptor, error) { + if res, err := s.rangeDescRecv.rangeDescriptor(ctx, addr, s.firstChildQueryFn); err == nil { + return res, nil + } + + return s.rangeDescRecv.rangeDescriptor(ctx, addr, nil) +} + +func (s *neighborReceiver) Neighbor(ctx context.Context, addr Address, left bool) (res RangeDescriptor, err error) { + if left { + res, err = s.rangeDescRecv.rangeDescriptor(ctx, addr, s.leftNeighborQueryFn) + } else { + res, err = s.rangeDescRecv.rangeDescriptor(ctx, addr, s.rightNeighborQueryFn) + } + + return +} + +func (s *selectiveRangeRecv) rangeDescriptor(ctx context.Context, addr Address, fn relationQueryFunc) (res RangeDescriptor, err error) { + b := false + + p := &implementations.HeadParams{ + GetParams: implementations.GetParams{ + SelectiveParams: implementations.SelectiveParams{ + CID: addr.CID, + ServeLocal: true, + TTL: service.SingleForwardingTTL, + Token: tokenFromContext(ctx), + Bearer: bearerFromContext(ctx), + + ExtendedHeaders: extendedHeadersFromContext(ctx), + }, + Handler: func(_ multiaddr.Multiaddr, obj *Object) { + res.Addr = *obj.Address() + res.Offset = 0 + res.Size = int64(obj.SystemHeader.PayloadLength) + + sameID := res.Addr.ObjectID.Equal(addr.ObjectID) + bound := boundaryChild(obj) + res.LeftBound = sameID || bound == boundBoth || bound == boundLeft + res.RightBound = sameID || bound == boundBoth || bound == boundRight + + b = true + }, + }, + FullHeaders: true, + } + + if fn != nil { + if p.Query, err = fn(addr); err != nil { + return + } + } else { + p.IDList = []ID{addr.ObjectID} + } + + if err = s.executor.Head(ctx, p); err != nil { + return + } else if !b { + err = errRelationNotFound + } + + return res, err +} + +const ( + boundBoth = iota + boundLeft + boundRight + boundMid +) + +func boundaryChild(obj *Object) (res int) { + splitInd, _ := obj.LastHeader(object.HeaderType(object.TransformHdr)) + if splitInd < 0 { + return + } + + for i := len(obj.Headers) - 1; i > splitInd; i-- { + hVal := obj.Headers[i].GetValue() + if hVal == nil { + continue + } + + hLink, ok := hVal.(*object.Header_Link) + if !ok || hLink == nil || hLink.Link == nil { + continue + } + + linkType := hLink.Link.GetType() + if linkType != object.Link_Previous && linkType != object.Link_Next { + continue + } + + res = boundMid + + if hLink.Link.ID.Empty() { + if linkType == object.Link_Next { + res = boundRight + } else if linkType == object.Link_Previous { + res = boundLeft + } + + return + } + } + + return res +} + +func firstChildQueryFunc(addr Address) ([]byte, error) { + return (&v1.Query{ + Filters: append(parentFilters(addr), QueryFilter{ + Type: v1.Filter_Exact, + Name: KeyPrev, + Value: ID{}.String(), + }), + }).Marshal() +} + +func leftNeighborQueryFunc(addr Address) ([]byte, error) { + return idQueryFunc(KeyNext, addr.ObjectID) +} + +func rightNeighborQueryFunc(addr Address) ([]byte, error) { + return idQueryFunc(KeyPrev, addr.ObjectID) +} + +func idQueryFunc(key string, id ID) ([]byte, error) { + return (&v1.Query{Filters: []QueryFilter{ + { + Type: v1.Filter_Exact, + Name: key, + Value: id.String(), + }, + }}).Marshal() +} + +func coreChildrenQueryFunc(addr Address) ([]byte, error) { + return (&v1.Query{Filters: parentFilters(addr)}).Marshal() +} + +func (s *coreChildrenLister) children(ctx context.Context, parent Address) []ID { + query, err := s.queryFn(parent) + if err != nil { + s.log.Error(lmQueryMarshalFail, zap.Error(err)) + return nil + } + + sInfo := newRawSearchInfo() + sInfo.setTTL(service.NonForwardingTTL) + sInfo.setTimeout(s.timeout) + sInfo.setCID(parent.CID) + sInfo.setQuery(query) + sInfo.setSessionToken(tokenFromContext(ctx)) + sInfo.setBearerToken(bearerFromContext(ctx)) + sInfo.setExtendedHeaders(extendedHeadersFromContext(ctx)) + + children, err := s.objSearcher.searchObjects(ctx, sInfo) + if err != nil { + s.log.Error(lmListFail, zap.Error(err)) + return nil + } + + res := make([]ID, 0, len(children)) + for i := range children { + res = append(res, children[i].ObjectID) + } + + return res +} + +func (s *rawSearchInfo) GetCID() CID { + return s.cid +} + +func (s *rawSearchInfo) setCID(v CID) { + s.cid = v +} + +func (s *rawSearchInfo) GetQuery() []byte { + return s.query +} + +func (s *rawSearchInfo) setQuery(v []byte) { + s.query = v +} + +func (s *rawSearchInfo) getMetaInfo() *rawMetaInfo { + return s.rawMetaInfo +} + +func (s *rawSearchInfo) setMetaInfo(v *rawMetaInfo) { + s.rawMetaInfo = v + s.setType(object.RequestSearch) +} + +func newRawSearchInfo() *rawSearchInfo { + res := new(rawSearchInfo) + + res.setMetaInfo(newRawMetaInfo()) + + return res +} + +func parentFilters(addr Address) []QueryFilter { + return []QueryFilter{ + { + Type: v1.Filter_Exact, + Name: transport.KeyHasParent, + }, + { + Type: v1.Filter_Exact, + Name: transport.KeyParent, + Value: addr.ObjectID.String(), + }, + } +} diff --git a/services/public/object/listing_test.go b/services/public/object/listing_test.go new file mode 100644 index 0000000000..7c27ca7941 --- /dev/null +++ b/services/public/object/listing_test.go @@ -0,0 +1,513 @@ +package object + +import ( + "context" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/query" + v1 "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testListingEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + implementations.SelectiveContainerExecutor + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ objectSearcher = (*testListingEntity)(nil) + _ selectiveRangeReceiver = (*testListingEntity)(nil) + + _ implementations.SelectiveContainerExecutor = (*testListingEntity)(nil) +) + +func (s *testListingEntity) rangeDescriptor(_ context.Context, a Address, f relationQueryFunc) (RangeDescriptor, error) { + if s.f != nil { + s.f(a, f) + } + if s.err != nil { + return RangeDescriptor{}, s.err + } + return s.res.(RangeDescriptor), nil +} + +func (s *testListingEntity) Head(_ context.Context, p *implementations.HeadParams) error { + if s.f != nil { + s.f(p) + } + return s.err +} + +func (s *testListingEntity) searchObjects(ctx context.Context, i transport.SearchInfo) ([]Address, error) { + if s.f != nil { + s.f(i) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]Address), nil +} + +func Test_rawSeachInfo(t *testing.T) { + t.Run("TTL", func(t *testing.T) { + ttl := uint32(3) + + r := newRawSearchInfo() + r.setTTL(ttl) + + require.Equal(t, ttl, r.GetTTL()) + }) + + t.Run("timeout", func(t *testing.T) { + timeout := 3 * time.Second + + r := newRawSearchInfo() + r.setTimeout(timeout) + + require.Equal(t, timeout, r.GetTimeout()) + }) + + t.Run("CID", func(t *testing.T) { + cid := testObjectAddress(t).CID + + r := newRawSearchInfo() + r.setCID(cid) + + require.Equal(t, cid, r.GetCID()) + }) + + t.Run("query", func(t *testing.T) { + query := testData(t, 10) + + r := newRawSearchInfo() + r.setQuery(query) + + require.Equal(t, query, r.GetQuery()) + }) +} + +func Test_coreChildrenQueryFunc(t *testing.T) { + t.Run("correct query composition", func(t *testing.T) { + // create custom address for test + addr := testObjectAddress(t) + + res, err := coreChildrenQueryFunc(addr) + require.NoError(t, err) + + // unmarshal query + q := v1.Query{} + require.NoError(t, q.Unmarshal(res)) + + // ascertain that filter list composed correctly + require.Len(t, q.Filters, 2) + + require.Contains(t, q.Filters, QueryFilter{ + Type: v1.Filter_Exact, + Name: transport.KeyHasParent, + }) + + require.Contains(t, q.Filters, QueryFilter{ + Type: v1.Filter_Exact, + Name: transport.KeyParent, + Value: addr.ObjectID.String(), + }) + }) +} + +func Test_coreChildrenLister_children(t *testing.T) { + ctx := context.TODO() + addr := testObjectAddress(t) + + t.Run("query function failure", func(t *testing.T) { + s := &coreChildrenLister{ + queryFn: func(v Address) ([]byte, error) { + t.Run("correct query function params", func(t *testing.T) { + require.Equal(t, addr, v) + }) + return nil, internal.Error("") // force relationQueryFunc to return some non-nil error + }, + log: test.NewTestLogger(false), + } + + require.Empty(t, s.children(ctx, addr)) + }) + + t.Run("object searcher failure", func(t *testing.T) { + // create custom timeout for test + sErr := internal.Error("test error for object searcher") + // create custom timeout for test + timeout := 3 * time.Second + // create custom query for test + query := testData(t, 10) + + s := &coreChildrenLister{ + queryFn: func(v Address) ([]byte, error) { + return query, nil // force relationQueryFunc to return created query + }, + objSearcher: &testListingEntity{ + f: func(items ...interface{}) { + t.Run("correct object searcher params", func(t *testing.T) { + p := items[0].(transport.SearchInfo) + require.Equal(t, timeout, p.GetTimeout()) + require.Equal(t, query, p.GetQuery()) + require.Equal(t, addr.CID, p.GetCID()) + require.Equal(t, uint32(service.NonForwardingTTL), p.GetTTL()) + }) + }, + err: sErr, // force objectSearcher to return sErr + }, + log: test.NewTestLogger(false), + timeout: timeout, + } + + require.Empty(t, s.children(ctx, addr)) + }) + + t.Run("correct result", func(t *testing.T) { + // create custom child list + addrList := testAddrList(t, 5) + idList := make([]ID, 0, len(addrList)) + for i := range addrList { + idList = append(idList, addrList[i].ObjectID) + } + + s := &coreChildrenLister{ + queryFn: func(address Address) ([]byte, error) { + return nil, nil // force relationQueryFunc to return nil error + }, + objSearcher: &testListingEntity{ + res: addrList, + }, + } + + require.Equal(t, idList, s.children(ctx, addr)) + }) +} + +func Test_queryGenerators(t *testing.T) { + t.Run("object ID", func(t *testing.T) { + var ( + q = new(query.Query) + key = "key for test" + id = testObjectAddress(t).ObjectID + ) + + res, err := idQueryFunc(key, id) + require.NoError(t, err) + + require.NoError(t, q.Unmarshal(res)) + require.Len(t, q.Filters, 1) + + require.Equal(t, query.Filter{ + Type: v1.Filter_Exact, + Name: key, + Value: id.String(), + }, q.Filters[0]) + }) + + t.Run("left neighbor", func(t *testing.T) { + var ( + q = new(query.Query) + addr = testObjectAddress(t) + ) + + res, err := leftNeighborQueryFunc(addr) + require.NoError(t, err) + + require.NoError(t, q.Unmarshal(res)) + require.Len(t, q.Filters, 1) + + require.Equal(t, query.Filter{ + Type: v1.Filter_Exact, + Name: KeyNext, + Value: addr.ObjectID.String(), + }, q.Filters[0]) + }) + + t.Run("right neighbor", func(t *testing.T) { + var ( + q = new(query.Query) + addr = testObjectAddress(t) + ) + + res, err := rightNeighborQueryFunc(addr) + require.NoError(t, err) + + require.NoError(t, q.Unmarshal(res)) + require.Len(t, q.Filters, 1) + + require.Equal(t, query.Filter{ + Type: v1.Filter_Exact, + Name: KeyPrev, + Value: addr.ObjectID.String(), + }, q.Filters[0]) + }) + + t.Run("first child", func(t *testing.T) { + var ( + q = new(query.Query) + addr = testObjectAddress(t) + ) + + res, err := firstChildQueryFunc(addr) + require.NoError(t, err) + + require.NoError(t, q.Unmarshal(res)) + require.Len(t, q.Filters, 3) + + require.Contains(t, q.Filters, query.Filter{ + Type: v1.Filter_Exact, + Name: transport.KeyHasParent, + }) + require.Contains(t, q.Filters, query.Filter{ + Type: v1.Filter_Exact, + Name: transport.KeyParent, + Value: addr.ObjectID.String(), + }) + require.Contains(t, q.Filters, query.Filter{ + Type: v1.Filter_Exact, + Name: KeyPrev, + Value: ID{}.String(), + }) + }) +} + +func Test_selectiveRangeRecv(t *testing.T) { + ctx := context.TODO() + addr := testObjectAddress(t) + + t.Run("query function failure", func(t *testing.T) { + qfErr := internal.Error("test error for query function") + _, err := new(selectiveRangeRecv).rangeDescriptor(ctx, testObjectAddress(t), func(Address) ([]byte, error) { + return nil, qfErr + }) + require.EqualError(t, err, qfErr.Error()) + }) + + t.Run("correct executor params", func(t *testing.T) { + t.Run("w/ query function", func(t *testing.T) { + qBytes := testData(t, 10) + + s := &selectiveRangeRecv{ + executor: &testListingEntity{ + f: func(items ...interface{}) { + p := items[0].(*implementations.HeadParams) + require.Equal(t, addr.CID, p.CID) + require.True(t, p.ServeLocal) + require.Equal(t, uint32(service.SingleForwardingTTL), p.TTL) + require.True(t, p.FullHeaders) + require.Equal(t, qBytes, p.Query) + require.Empty(t, p.IDList) + }, + }, + } + + _, _ = s.rangeDescriptor(ctx, addr, func(Address) ([]byte, error) { return qBytes, nil }) + }) + + t.Run("w/o query function", func(t *testing.T) { + s := &selectiveRangeRecv{ + executor: &testListingEntity{ + f: func(items ...interface{}) { + p := items[0].(*implementations.HeadParams) + require.Equal(t, addr.CID, p.CID) + require.True(t, p.ServeLocal) + require.Equal(t, uint32(service.SingleForwardingTTL), p.TTL) + require.True(t, p.FullHeaders) + require.Empty(t, p.Query) + require.Equal(t, []ID{addr.ObjectID}, p.IDList) + }, + }, + } + + _, _ = s.rangeDescriptor(ctx, addr, nil) + }) + }) + + t.Run("correct result", func(t *testing.T) { + t.Run("failure", func(t *testing.T) { + t.Run("executor failure", func(t *testing.T) { + exErr := internal.Error("test error for executor") + + s := &selectiveRangeRecv{ + executor: &testListingEntity{ + err: exErr, + }, + } + + _, err := s.rangeDescriptor(ctx, addr, nil) + require.EqualError(t, err, exErr.Error()) + }) + + t.Run("not found", func(t *testing.T) { + s := &selectiveRangeRecv{ + executor: new(testListingEntity), + } + + _, err := s.rangeDescriptor(ctx, addr, nil) + require.EqualError(t, err, errRelationNotFound.Error()) + }) + }) + + t.Run("success", func(t *testing.T) { + foundAddr := testObjectAddress(t) + + obj := &Object{ + SystemHeader: SystemHeader{ + PayloadLength: 100, + ID: foundAddr.ObjectID, + CID: foundAddr.CID, + }, + } + + s := &selectiveRangeRecv{ + executor: &testListingEntity{ + SelectiveContainerExecutor: nil, + f: func(items ...interface{}) { + p := items[0].(*implementations.HeadParams) + p.Handler(nil, obj) + }, + }, + } + + res, err := s.rangeDescriptor(ctx, addr, nil) + require.NoError(t, err) + require.Equal(t, RangeDescriptor{ + Size: int64(obj.SystemHeader.PayloadLength), + Offset: 0, + Addr: foundAddr, + + LeftBound: true, + RightBound: true, + }, res) + }) + }) +} + +func Test_neighborReceiver(t *testing.T) { + ctx := context.TODO() + addr := testObjectAddress(t) + + t.Run("neighbor", func(t *testing.T) { + t.Run("correct internal logic", func(t *testing.T) { + rightCalled, leftCalled := false, false + + s := &neighborReceiver{ + leftNeighborQueryFn: func(a Address) ([]byte, error) { + require.Equal(t, addr, a) + leftCalled = true + return nil, nil + }, + rightNeighborQueryFn: func(a Address) ([]byte, error) { + require.Equal(t, addr, a) + rightCalled = true + return nil, nil + }, + rangeDescRecv: &testListingEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0]) + _, _ = items[1].(relationQueryFunc)(addr) + }, + err: internal.Error(""), + }, + } + + _, _ = s.Neighbor(ctx, addr, true) + require.False(t, rightCalled) + require.True(t, leftCalled) + + leftCalled = false + + _, _ = s.Neighbor(ctx, addr, false) + require.False(t, leftCalled) + require.True(t, rightCalled) + }) + + t.Run("correct result", func(t *testing.T) { + rErr := internal.Error("test error for range receiver") + + rngRecv := &testListingEntity{err: rErr} + s := &neighborReceiver{rangeDescRecv: rngRecv} + + _, err := s.Neighbor(ctx, addr, false) + require.EqualError(t, err, rErr.Error()) + + rngRecv.err = errRelationNotFound + + _, err = s.Neighbor(ctx, addr, false) + require.EqualError(t, err, errRelationNotFound.Error()) + + rd := RangeDescriptor{Size: 1, Offset: 2, Addr: addr} + rngRecv.res, rngRecv.err = rd, nil + + res, err := s.Neighbor(ctx, addr, false) + require.NoError(t, err) + require.Equal(t, rd, res) + }) + }) + + t.Run("base", func(t *testing.T) { + rd := RangeDescriptor{Size: 1, Offset: 2, Addr: addr} + + t.Run("first child exists", func(t *testing.T) { + called := false + + s := &neighborReceiver{ + firstChildQueryFn: func(a Address) ([]byte, error) { + require.Equal(t, addr, a) + called = true + return nil, nil + }, + rangeDescRecv: &testListingEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0]) + _, _ = items[1].(relationQueryFunc)(addr) + }, + res: rd, + }, + } + + res, err := s.Base(ctx, addr) + require.NoError(t, err) + require.Equal(t, rd, res) + require.True(t, called) + }) + + t.Run("first child doesn't exist", func(t *testing.T) { + called := false + + recv := &testListingEntity{err: internal.Error("")} + + recv.f = func(...interface{}) { + if called { + recv.res, recv.err = rd, nil + } + called = true + } + + s := &neighborReceiver{rangeDescRecv: recv} + + res, err := s.Base(ctx, addr) + require.NoError(t, err) + require.Equal(t, rd, res) + }) + }) +} diff --git a/services/public/object/postprocessor.go b/services/public/object/postprocessor.go new file mode 100644 index 0000000000..52d474d4c3 --- /dev/null +++ b/services/public/object/postprocessor.go @@ -0,0 +1,47 @@ +package object + +import ( + "context" +) + +type ( + // requestPostProcessor is an interface of RPC call outcome handler. + requestPostProcessor interface { + // Performs actions based on the outcome of request processing. + postProcess(context.Context, serviceRequest, error) + } + + // complexPostProcessor is an implementation of requestPostProcessor interface. + complexPostProcessor struct { + // Sequence of requestPostProcessor instances. + list []requestPostProcessor + } +) + +var _ requestPostProcessor = (*complexPostProcessor)(nil) + +// requestPostProcessor method implementation. +// +// Panics with pmEmptyServiceRequest on nil request argument. +// +// Passes request through the sequence of requestPostProcessor instances. +// +// Warn: adding instance to list itself provoke endless recursion. +func (s *complexPostProcessor) postProcess(ctx context.Context, req serviceRequest, e error) { + if req == nil { + panic(pmEmptyServiceRequest) + } + + for i := range s.list { + s.list[i].postProcess(ctx, req, e) + } +} + +// Creates requestPostProcessor based on Params. +// +// Uses complexPostProcessor instance as a result implementation. +func newPostProcessor() requestPostProcessor { + return &complexPostProcessor{ + list: []requestPostProcessor{}, + } +} diff --git a/services/public/object/postprocessor_test.go b/services/public/object/postprocessor_test.go new file mode 100644 index 0000000000..f114fc982d --- /dev/null +++ b/services/public/object/postprocessor_test.go @@ -0,0 +1,83 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-node/internal" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testPostProcessorEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var _ requestPostProcessor = (*testPostProcessorEntity)(nil) + +func (s *testPostProcessorEntity) postProcess(_ context.Context, req serviceRequest, e error) { + if s.f != nil { + s.f(req, e) + } +} + +func TestComplexPostProcessor_PostProcess(t *testing.T) { + ctx := context.TODO() + + t.Run("empty request argument", func(t *testing.T) { + require.PanicsWithValue(t, pmEmptyServiceRequest, func() { + // ascertain that nil request causes panic + new(complexPostProcessor).postProcess(ctx, nil, nil) + }) + }) + + t.Run("correct behavior", func(t *testing.T) { + // create serviceRequest instance. + req := new(testPostProcessorEntity) + + // create custom error + pErr := internal.Error("test error for post processor") + + // create list of post processors + postProcCount := 10 + postProcessors := make([]requestPostProcessor, 0, postProcCount) + + postProcessorCalls := make([]struct{}, 0, postProcCount) + + for i := 0; i < postProcCount; i++ { + postProcessors = append(postProcessors, &testPostProcessorEntity{ + f: func(items ...interface{}) { + t.Run("correct arguments", func(t *testing.T) { + postProcessorCalls = append(postProcessorCalls, struct{}{}) + }) + }, + }) + } + + s := &complexPostProcessor{list: postProcessors} + + s.postProcess(ctx, req, pErr) + + // ascertain all internal requestPostProcessor instances were called + require.Len(t, postProcessorCalls, postProcCount) + }) +} + +func Test_newPostProcessor(t *testing.T) { + res := newPostProcessor() + + pp := res.(*complexPostProcessor) + require.Len(t, pp.list, 0) +} diff --git a/services/public/object/preprocessor.go b/services/public/object/preprocessor.go new file mode 100644 index 0000000000..620cae5a21 --- /dev/null +++ b/services/public/object/preprocessor.go @@ -0,0 +1,163 @@ +package object + +import ( + "context" + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/service" + "go.uber.org/zap" +) + +type ( + // requestPreProcessor is an interface of Object service request installer. + requestPreProcessor interface { + // Performs preliminary request validation and preparation. + preProcess(context.Context, serviceRequest) error + } + + // complexPreProcessor is an implementation of requestPreProcessor interface. + complexPreProcessor struct { + // Sequence of requestPreProcessor instances. + list []requestPreProcessor + } + + signingPreProcessor struct { + preProc requestPreProcessor + key *ecdsa.PrivateKey + + log *zap.Logger + } +) + +const pmEmptyServiceRequest = "empty service request" + +var ( + _ requestPreProcessor = (*signingPreProcessor)(nil) + _ requestPreProcessor = (*complexPreProcessor)(nil) +) + +// requestPreProcessor method implementation. +// +// Passes request through internal requestPreProcessor. +// If internal requestPreProcessor returns non-nil error, this error returns. +// Returns result of signRequest function. +func (s *signingPreProcessor) preProcess(ctx context.Context, req serviceRequest) (err error) { + if err = s.preProc.preProcess(ctx, req); err != nil { + return + } else if err = signRequest(s.key, req); err != nil { + s.log.Error("could not re-sign request", + zap.Error(err), + ) + err = errReSigning + } + + return +} + +// requestPreProcessor method implementation. +// +// Panics with pmEmptyServiceRequest on nil request argument. +// +// Passes request through the sequence of requestPreProcessor instances. +// Any non-nil error returned by some instance returns. +// +// Warn: adding instance to list itself provoke endless recursion. +func (s *complexPreProcessor) preProcess(ctx context.Context, req serviceRequest) error { + if req == nil { + panic(pmEmptyServiceRequest) + } + + for i := range s.list { + if err := s.list[i].preProcess(ctx, req); err != nil { + return err + } + } + + return nil +} + +// Creates requestPreProcessor based on Params. +// +// Uses complexPreProcessor instance as a result implementation. +// +// Adds to next preprocessors to list: +// * verifyPreProcessor; +// * ttlPreProcessor; +// * epochPreProcessor, if CheckEpochSync flag is set in params. +// * aclPreProcessor, if CheckAcl flag is set in params. +func newPreProcessor(p *Params) requestPreProcessor { + preProcList := make([]requestPreProcessor, 0) + + if p.CheckACL { + preProcList = append(preProcList, &aclPreProcessor{ + log: p.Logger, + + aclInfoReceiver: p.aclInfoReceiver, + + basicChecker: p.BasicACLChecker, + + reqActionCalc: p.requestActionCalculator, + + localStore: p.LocalStore, + + extACLSource: p.ExtendedACLSource, + + bearerVerifier: &complexBearerVerifier{ + items: []bearerTokenVerifier{ + &bearerActualityVerifier{ + epochRecv: p.EpochReceiver, + }, + new(bearerSignatureVerifier), + &bearerOwnershipVerifier{ + cnrOwnerChecker: p.ACLHelper, + }, + }, + }, + }) + } + + preProcList = append(preProcList, + &verifyPreProcessor{ + fVerify: requestVerifyFunc, + }, + + &ttlPreProcessor{ + staticCond: []service.TTLCondition{ + validTTLCondition, + }, + condPreps: []ttlConditionPreparer{ + &coreTTLCondPreparer{ + curAffChecker: &corePlacementUtil{ + prevNetMap: false, + localAddrStore: p.AddressStore, + placementBuilder: p.Placer, + log: p.Logger, + }, + prevAffChecker: &corePlacementUtil{ + prevNetMap: true, + localAddrStore: p.AddressStore, + placementBuilder: p.Placer, + log: p.Logger, + }, + }, + }, + fProc: processTTLConditions, + }, + + &tokenPreProcessor{ + keyVerifier: p.OwnerKeyVerifier, + staticVerifier: newComplexTokenVerifier( + &tokenEpochsVerifier{ + epochRecv: p.EpochReceiver, + }, + ), + }, + + new(decTTLPreProcessor), + ) + + return &signingPreProcessor{ + preProc: &complexPreProcessor{list: preProcList}, + key: p.Key, + } +} diff --git a/services/public/object/preprocessor_test.go b/services/public/object/preprocessor_test.go new file mode 100644 index 0000000000..7a15092855 --- /dev/null +++ b/services/public/object/preprocessor_test.go @@ -0,0 +1,142 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testPreProcessorEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + Placer + implementations.AddressStoreComponent + EpochReceiver + core.OwnerKeyVerifier + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var _ requestPreProcessor = (*testPreProcessorEntity)(nil) + +func (s *testPreProcessorEntity) preProcess(_ context.Context, req serviceRequest) error { + if s.f != nil { + s.f(req) + } + return s.err +} + +func TestSigningPreProcessor_preProcess(t *testing.T) { + ctx := context.TODO() + + req := new(object.SearchRequest) + + t.Run("internal pre-processor error", func(t *testing.T) { + ppErr := internal.Error("test error for pre-processor") + + s := &signingPreProcessor{ + preProc: &testPreProcessorEntity{ + f: func(items ...interface{}) { + t.Run("correct internal pre-processor params", func(t *testing.T) { + require.Equal(t, req, items[0].(serviceRequest)) + }) + }, + err: ppErr, + }, + } + + require.EqualError(t, s.preProcess(ctx, req), ppErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + key := test.DecodeKey(0) + + exp := signRequest(key, req) + + s := &signingPreProcessor{ + preProc: new(testPreProcessorEntity), + key: key, + } + + require.Equal(t, exp, s.preProcess(ctx, req)) + }) +} + +func TestComplexPreProcessor_PreProcess(t *testing.T) { + ctx := context.TODO() + + t.Run("empty request argument", func(t *testing.T) { + require.PanicsWithValue(t, pmEmptyServiceRequest, func() { + // ascertain that nil request causes panic + _ = new(complexPreProcessor).preProcess(ctx, nil) + }) + }) + + // create serviceRequest instance. + req := new(testPreProcessorEntity) + + t.Run("empty list", func(t *testing.T) { + require.NoError(t, new(complexPreProcessor).preProcess(ctx, req)) + }) + + t.Run("non-empty list", func(t *testing.T) { + firstCalled := false + p1 := &testPreProcessorEntity{ + f: func(items ...interface{}) { + t.Run("correct nested pre processor params", func(t *testing.T) { + require.Equal(t, req, items[0].(serviceRequest)) + }) + + firstCalled = true // mark first requestPreProcessor call + }, + err: nil, // force requestPreProcessor to return nil error + } + + // create custom error + pErr := internal.Error("pre processor error for test") + p2 := &testPreProcessorEntity{ + err: pErr, // force second requestPreProcessor to return created error + } + + thirdCalled := false + p3 := &testPreProcessorEntity{ + f: func(_ ...interface{}) { + thirdCalled = true // mark third requestPreProcessor call + }, + err: nil, // force requestPreProcessor to return nil error + } + + // create complex requestPreProcessor + p := &complexPreProcessor{ + list: []requestPreProcessor{p1, p2, p3}, // order is important + } + + // ascertain error returns as expected + require.EqualError(t, + p.preProcess(ctx, req), + pErr.Error(), + ) + + // ascertain first requestPreProcessor was called + require.True(t, firstCalled) + + // ascertain first requestPreProcessor was not called + require.False(t, thirdCalled) + }) +} diff --git a/services/public/object/put.go b/services/public/object/put.go new file mode 100644 index 0000000000..b1cc519ca8 --- /dev/null +++ b/services/public/object/put.go @@ -0,0 +1,437 @@ +package object + +import ( + "context" + "io" + "sync" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + objectStorer interface { + putObject(context.Context, transport.PutInfo) (*Address, error) + } + + bifurcatingObjectStorer struct { + straightStorer objectStorer + tokenStorer objectStorer + } + + receivingObjectStorer struct { + straightStorer objectStorer + vPayload objutil.Verifier + } + + filteringObjectStorer struct { + filter Filter + objStorer objectStorer + } + + tokenObjectStorer struct { + tokenStore session.PrivateTokenStore + objStorer objectStorer + } + + transformingObjectStorer struct { + transformer transformer.Transformer + objStorer objectStorer + + // Set of errors that won't be converted to errTransformer + mErr map[error]struct{} + } + + straightObjectStorer struct { + executor operationExecutor + } + + putRequest struct { + *object.PutRequest + srv object.Service_PutServer + timeout time.Duration + } + + addressAccumulator interface { + responseItemHandler + address() *Address + } + + coreAddrAccum struct { + *sync.Once + addr *Address + } + + rawPutInfo struct { + *rawMetaInfo + obj *Object + r io.Reader + copyNum uint32 + } + + putStreamReader struct { + tail []byte + srv object.Service_PutServer + } +) + +type transformerHandlerErr struct { + error +} + +const ( + errObjectExpected = internal.Error("missing object") + errChunkExpected = internal.Error("empty chunk received") +) + +const ( + errMissingOwnerKeys = internal.Error("missing owner keys") + errBrokenToken = internal.Error("broken token structure") + errNilToken = internal.Error("missing session token") + errWrongTokenAddress = internal.Error("wrong object address in token") +) + +const errTransformer = internal.Error("could not transform the object") + +var ( + _ transport.PutInfo = (*rawPutInfo)(nil) + _ addressAccumulator = (*coreAddrAccum)(nil) + _ objectStorer = (*straightObjectStorer)(nil) + _ transport.PutInfo = (*putRequest)(nil) + _ io.Reader = (*putStreamReader)(nil) + _ objectStorer = (*filteringObjectStorer)(nil) + _ objectStorer = (*transformingObjectStorer)(nil) + _ objectStorer = (*tokenObjectStorer)(nil) + _ objectStorer = (*receivingObjectStorer)(nil) +) + +func (s *objectService) Put(srv object.Service_PutServer) (err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestPut), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestPut, + e: err, + }) + }() + + var req *object.PutRequest + + if req, err = recvPutHeaderMsg(srv); err != nil { + return + } + + _, err = s.requestHandler.handleRequest(srv.Context(), handleRequestParams{ + request: &putRequest{ + PutRequest: req, + srv: srv, + }, + executor: s, + }) + + return err +} + +func (s *bifurcatingObjectStorer) putObject(ctx context.Context, info transport.PutInfo) (*Address, error) { + if withTokenFromOwner(info) { + return s.tokenStorer.putObject(ctx, info) + } + + return s.straightStorer.putObject(ctx, info) +} + +func withTokenFromOwner(src service.SessionTokenSource) bool { + if src == nil { + return false + } + + token := src.GetSessionToken() + if token == nil { + return false + } + + signedReq, ok := src.(service.SignKeyPairSource) + if !ok { + return false + } + + signKeyPairs := signedReq.GetSignKeyPairs() + if len(signKeyPairs) == 0 { + return false + } + + firstKey := signKeyPairs[0].GetPublicKey() + if firstKey == nil { + return false + } + + reqOwner, err := refs.NewOwnerID(firstKey) + if err != nil { + return false + } + + return reqOwner.Equal(token.GetOwnerID()) +} + +func (s *tokenObjectStorer) putObject(ctx context.Context, info transport.PutInfo) (*Address, error) { + token := info.GetSessionToken() + + key := session.PrivateTokenKey{} + key.SetOwnerID(token.GetOwnerID()) + key.SetTokenID(token.GetID()) + + pToken, err := s.tokenStore.Fetch(key) + if err != nil { + return nil, &detailedError{ + error: errTokenRetrieval, + d: privateTokenRecvDetails(token.GetID(), token.GetOwnerID()), + } + } + + return s.objStorer.putObject( + contextWithValues(ctx, + transformer.PrivateSessionToken, pToken, + transformer.PublicSessionToken, token, + implementations.BearerToken, info.GetBearerToken(), + implementations.ExtendedHeaders, info.ExtendedHeaders(), + ), + info, + ) +} + +func (s *filteringObjectStorer) putObject(ctx context.Context, info transport.PutInfo) (*Address, error) { + if res := s.filter.Pass( + contextWithValues(ctx, ttlValue, info.GetTTL()), + &Meta{Object: info.GetHead()}, + ); res.Code() != localstore.CodePass { + if err := res.Err(); err != nil { + return nil, err + } + + return nil, errObjectFilter + } + + return s.objStorer.putObject(ctx, info) +} + +func (s *receivingObjectStorer) putObject(ctx context.Context, src transport.PutInfo) (*Address, error) { + obj := src.GetHead() + obj.Payload = make([]byte, obj.SystemHeader.PayloadLength) + + if _, err := io.ReadFull(src.Payload(), obj.Payload); err != nil && err != io.EOF { + if errors.Is(err, io.ErrUnexpectedEOF) { + err = transformer.ErrPayloadEOF + } + + return nil, err + } else if err = s.vPayload.Verify(ctx, obj); err != nil { + return nil, errPayloadChecksum + } + + putInfo := newRawPutInfo() + putInfo.setTimeout(src.GetTimeout()) + putInfo.setTTL(src.GetTTL()) + putInfo.setCopiesNumber(src.CopiesNumber()) + putInfo.setHead(obj) + putInfo.setSessionToken(src.GetSessionToken()) + putInfo.setBearerToken(src.GetBearerToken()) + putInfo.setExtendedHeaders(src.ExtendedHeaders()) + + return s.straightStorer.putObject(ctx, putInfo) +} + +func (s *transformingObjectStorer) putObject(ctx context.Context, src transport.PutInfo) (res *Address, err error) { + var ( + ttl = src.GetTTL() + timeout = src.GetTimeout() + copyNum = src.CopiesNumber() + token = src.GetSessionToken() + bearer = src.GetBearerToken() + extHdrs = src.ExtendedHeaders() + ) + + err = s.transformer.Transform(ctx, + transformer.ProcUnit{ + Head: src.GetHead(), + Payload: src.Payload(), + }, func(ctx context.Context, unit transformer.ProcUnit) error { + res = unit.Head.Address() + + putInfo := newRawPutInfo() + putInfo.setHead(unit.Head) + putInfo.setPayload(unit.Payload) + putInfo.setTimeout(timeout) + putInfo.setTTL(ttl) + putInfo.setCopiesNumber(copyNum) + putInfo.setSessionToken(token) + putInfo.setBearerToken(bearer) + putInfo.setExtendedHeaders(extHdrs) + + _, err := s.objStorer.putObject(ctx, putInfo) + if err != nil { + err = &transformerHandlerErr{ + error: err, + } + } + return err + }, + ) + + if e := errors.Cause(err); e != nil { + if v, ok := e.(*transformerHandlerErr); ok { + err = v.error + } else if _, ok := s.mErr[e]; !ok { + err = errTransformer + } + } + + return res, err +} + +func (s *putStreamReader) Read(p []byte) (n int, err error) { + if s.srv == nil { + return 0, io.EOF + } + + n += copy(p, s.tail) + if n > 0 { + s.tail = s.tail[n:] + return + } + + var msg *object.PutRequest + + if msg, err = s.srv.Recv(); err != nil { + return + } + + chunk := msg.GetChunk() + if len(chunk) == 0 { + return 0, errChunkExpected + } + + r := copy(p, chunk) + + s.tail = chunk[r:] + + n += r + + return +} + +func (s *straightObjectStorer) putObject(ctx context.Context, pInfo transport.PutInfo) (*Address, error) { + addrAccum := newAddressAccumulator() + if err := s.executor.executeOperation(ctx, pInfo, addrAccum); err != nil { + return nil, err + } + + return addrAccum.address(), nil +} + +func recvPutHeaderMsg(srv object.Service_PutServer) (*object.PutRequest, error) { + req, err := srv.Recv() + if err != nil { + return nil, err + } else if req == nil { + return nil, errHeaderExpected + } else if h := req.GetHeader(); h == nil { + return nil, errHeaderExpected + } else if h.GetObject() == nil { + return nil, errObjectExpected + } + + return req, nil +} + +func contextWithValues(parentCtx context.Context, items ...interface{}) context.Context { + fCtx := parentCtx + for i := 0; i < len(items); i += 2 { + fCtx = context.WithValue(fCtx, items[i], items[i+1]) + } + + return fCtx +} + +func (s *putRequest) GetTimeout() time.Duration { return s.timeout } + +func (s *putRequest) GetHead() *Object { return s.GetHeader().GetObject() } + +func (s *putRequest) CopiesNumber() uint32 { + h := s.GetHeader() + if h == nil { + return 0 + } + + return h.GetCopiesNumber() +} + +func (s *putRequest) Payload() io.Reader { + return &putStreamReader{ + srv: s.srv, + } +} + +func (s *rawPutInfo) GetHead() *Object { + return s.obj +} + +func (s *rawPutInfo) setHead(obj *Object) { + s.obj = obj +} + +func (s *rawPutInfo) Payload() io.Reader { + return s.r +} + +func (s *rawPutInfo) setPayload(r io.Reader) { + s.r = r +} + +func (s *rawPutInfo) CopiesNumber() uint32 { + return s.copyNum +} + +func (s *rawPutInfo) setCopiesNumber(v uint32) { + s.copyNum = v +} + +func (s *rawPutInfo) getMetaInfo() *rawMetaInfo { + return s.rawMetaInfo +} + +func (s *rawPutInfo) setMetaInfo(v *rawMetaInfo) { + s.rawMetaInfo = v + s.setType(object.RequestPut) +} + +func newRawPutInfo() *rawPutInfo { + res := new(rawPutInfo) + + res.setMetaInfo(newRawMetaInfo()) + + return res +} + +func (s *coreAddrAccum) handleItem(item interface{}) { s.Do(func() { s.addr = item.(*Address) }) } + +func (s *coreAddrAccum) address() *Address { return s.addr } + +func newAddressAccumulator() addressAccumulator { return &coreAddrAccum{Once: new(sync.Once)} } diff --git a/services/public/object/put_test.go b/services/public/object/put_test.go new file mode 100644 index 0000000000..80fe338384 --- /dev/null +++ b/services/public/object/put_test.go @@ -0,0 +1,958 @@ +package object + +import ( + "bytes" + "context" + "io" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testPutEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + object.Service_PutServer + transport.PutInfo + Filter + session.PrivateTokenStore + implementations.SelectiveContainerExecutor + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ object.Service_PutServer = (*testPutEntity)(nil) + _ requestHandler = (*testPutEntity)(nil) + _ objectStorer = (*testPutEntity)(nil) + _ transport.PutInfo = (*testPutEntity)(nil) + _ Filter = (*testPutEntity)(nil) + _ operationExecutor = (*testPutEntity)(nil) + _ session.PrivateTokenStore = (*testPutEntity)(nil) + _ EpochReceiver = (*testPutEntity)(nil) + _ transformer.Transformer = (*testPutEntity)(nil) +) + +func (s *testPutEntity) Verify(_ context.Context, obj *Object) error { + if s.f != nil { + s.f(obj) + } + return s.err +} + +func (s *testPutEntity) Transform(_ context.Context, u transformer.ProcUnit, h ...transformer.ProcUnitHandler) error { + if s.f != nil { + s.f(u, h) + } + return s.err +} + +func (s *testPutEntity) verify(_ context.Context, token *session.Token, obj *Object) error { + if s.f != nil { + s.f(token, obj) + } + return s.err +} + +func (s *testPutEntity) Epoch() uint64 { return s.res.(uint64) } + +func (s *testPutEntity) Direct(ctx context.Context, objs ...Object) ([]Object, error) { + if s.f != nil { + s.f(ctx, objs) + } + return s.res.([]Object), s.err +} + +func (s *testPutEntity) Fetch(id session.PrivateTokenKey) (session.PrivateToken, error) { + if s.f != nil { + s.f(id) + } + if s.err != nil { + return nil, s.err + } + return s.res.(session.PrivateToken), nil +} + +func (s *testPutEntity) executeOperation(_ context.Context, m transport.MetaInfo, h responseItemHandler) error { + if s.f != nil { + s.f(m, h) + } + return s.err +} + +func (s *testPutEntity) Pass(ctx context.Context, m *Meta) *localstore.FilterResult { + if s.f != nil { + s.f(ctx, m) + } + items := s.res.([]interface{}) + return items[0].(*localstore.FilterResult) +} + +func (s *testPutEntity) GetTTL() uint32 { return s.res.(uint32) } + +func (s *testPutEntity) GetToken() *session.Token { return s.res.(*session.Token) } + +func (s *testPutEntity) GetHead() *Object { return s.res.(*Object) } + +func (s *testPutEntity) putObject(ctx context.Context, p transport.PutInfo) (*Address, error) { + if s.f != nil { + s.f(p, ctx) + } + if s.err != nil { + return nil, s.err + } + return s.res.(*Address), nil +} + +func (s *testPutEntity) handleRequest(_ context.Context, p handleRequestParams) (interface{}, error) { + if s.f != nil { + s.f(p) + } + return s.res, s.err +} + +func (s *testPutEntity) Recv() (*object.PutRequest, error) { + if s.f != nil { + s.f() + } + if s.err != nil { + return nil, s.err + } else if s.res == nil { + return nil, nil + } + return s.res.(*object.PutRequest), nil +} + +func (s *testPutEntity) Context() context.Context { return context.TODO() } + +func Test_objectService_Put(t *testing.T) { + + t.Run("stream error", func(t *testing.T) { + // create custom error for test + psErr := internal.Error("test error for put stream server") + + s := &testPutEntity{ + err: psErr, // force server to return psErr + } + + srv := &objectService{ + statusCalculator: newStatusCalculator(), + } + + // ascertain that error returns as expected + require.EqualError(t, + srv.Put(s), + psErr.Error(), + ) + }) + + t.Run("request handling", func(t *testing.T) { + // create custom request for test + req := &object.PutRequest{R: &object.PutRequest_Header{ + Header: &object.PutRequest_PutHeader{ + Object: new(Object), + }, + }} + + // create custom error for test + hErr := internal.Error("test error for request handler") + + srv := &testPutEntity{ + res: req, // force server to return req + } + + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct request handler params", func(t *testing.T) { + p := items[0].(handleRequestParams) + require.Equal(t, s, p.executor) + require.Equal(t, &putRequest{ + PutRequest: req, + srv: srv, + }, p.request) + }) + }, + err: hErr, // force requestHandler to return hErr + } + + // ascertain that error returns as expected + require.EqualError(t, + s.Put(srv), + hErr.Error(), + ) + }) +} + +func Test_straightObjectStorer_putObject(t *testing.T) { + ctx := context.TODO() + + t.Run("executor error", func(t *testing.T) { + // create custom error for test + exErr := internal.Error("test error for operation executor") + + // create custom meta info for test + info := new(testPutEntity) + + s := &straightObjectStorer{ + executor: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct operation executor params", func(t *testing.T) { + require.Equal(t, info, items[0]) + acc := items[1].(*coreAddrAccum) + require.NotNil(t, acc.Once) + }) + }, + err: exErr, + }, + } + + _, err := s.putObject(ctx, info) + + // ascertain that error returns as expected + require.EqualError(t, err, exErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + addr := testObjectAddress(t) + + s := &straightObjectStorer{ + executor: &testPutEntity{ + f: func(items ...interface{}) { + // add address to accumulator + items[1].(addressAccumulator).handleItem(&addr) + }, + }, + } + + res, err := s.putObject(ctx, new(testPutEntity)) + require.NoError(t, err) + + // ascertain that result returns as expected + require.Equal(t, &addr, res) + }) +} + +func Test_recvPutHeaderMsg(t *testing.T) { + t.Run("server error", func(t *testing.T) { + // create custom error for test + srvErr := internal.Error("test error for put server") + + srv := &testPutEntity{ + err: srvErr, // force put server to return srvErr + } + + res, err := recvPutHeaderMsg(srv) + + // ascertain that error returns as expected + require.EqualError(t, err, srvErr.Error()) + require.Nil(t, res) + }) + + t.Run("empty message", func(t *testing.T) { + srv := &testPutEntity{ + res: nil, // force put server to return nil, nil + } + + res, err := recvPutHeaderMsg(srv) + + // ascertain that error returns as expected + require.EqualError(t, err, errHeaderExpected.Error()) + require.Nil(t, res) + }) + + t.Run("empty put header in message", func(t *testing.T) { + srv := &testPutEntity{ + res: new(object.PutRequest), // force put server to return message w/o put header + } + + res, err := recvPutHeaderMsg(srv) + + // ascertain that error returns as expected + require.EqualError(t, err, object.ErrHeaderExpected.Error()) + require.Nil(t, res) + }) + + t.Run("empty object in put header", func(t *testing.T) { + srv := &testPutEntity{ + res: object.MakePutRequestHeader(nil), // force put server to return message w/ nil object + } + + res, err := recvPutHeaderMsg(srv) + + // ascertain that error returns as expected + require.EqualError(t, err, errObjectExpected.Error()) + require.Nil(t, res) + }) +} + +func Test_putRequest(t *testing.T) { + t.Run("timeout", func(t *testing.T) { + timeout := 3 * time.Second + + req := &putRequest{ + timeout: timeout, + } + + // ascertain that timeout returns as expected + require.Equal(t, timeout, req.GetTimeout()) + }) + + t.Run("head", func(t *testing.T) { + // create custom object for test + obj := new(Object) + + req := &putRequest{ + PutRequest: object.MakePutRequestHeader(obj), // wrap object to test message + } + + // ascertain that head returns as expected + require.Equal(t, obj, req.GetHead()) + }) + + t.Run("payload", func(t *testing.T) { + req := &putRequest{ + srv: new(testPutEntity), + } + + require.Equal(t, &putStreamReader{srv: req.srv}, req.Payload()) + }) + + t.Run("copies number", func(t *testing.T) { + cn := uint32(5) + + req := &putRequest{ + PutRequest: &object.PutRequest{ + R: &object.PutRequest_Header{ + Header: &object.PutRequest_PutHeader{ + CopiesNumber: cn, + }, + }, + }, + } + + require.Equal(t, cn, req.CopiesNumber()) + }) +} + +func Test_coreAddrAccum(t *testing.T) { + t.Run("new", func(t *testing.T) { + s := newAddressAccumulator() + // ascertain that type is correct and Once entity initialize + require.NotNil(t, s.(*coreAddrAccum).Once) + }) + + t.Run("address", func(t *testing.T) { + addr := testObjectAddress(t) + + s := &coreAddrAccum{addr: &addr} + + // ascertain that address returns as expected + require.Equal(t, &addr, s.address()) + }) + + t.Run("handle", func(t *testing.T) { + addr := testObjectAddress(t) + + s := newAddressAccumulator() + + s.handleItem(&addr) + + // ascertain that address saved + require.Equal(t, &addr, s.address()) + + // create another address for test + addr2 := testObjectAddress(t) + + s.handleItem(&addr2) + + // ascertain that second address is ignored + require.Equal(t, &addr, s.address()) + }) +} + +func Test_rawPutInfo(t *testing.T) { + t.Run("TTL", func(t *testing.T) { + ttl := uint32(3) + + s := newRawPutInfo() + s.setTTL(ttl) + + require.Equal(t, ttl, s.GetTTL()) + }) + + t.Run("head", func(t *testing.T) { + obj := new(Object) + + s := newRawPutInfo() + s.setHead(obj) + + require.Equal(t, obj, s.GetHead()) + }) + + t.Run("payload", func(t *testing.T) { + // ascertain that nil chunk returns as expected + r := bytes.NewBuffer(nil) + + req := newRawPutInfo() + req.setPayload(r) + + require.Equal(t, r, req.Payload()) + }) + + t.Run("token", func(t *testing.T) { + // ascertain that nil token returns as expected + require.Nil(t, newRawPutInfo().GetSessionToken()) + }) + + t.Run("copies number", func(t *testing.T) { + cn := uint32(100) + + s := newRawPutInfo() + s.setCopiesNumber(cn) + + require.Equal(t, cn, s.CopiesNumber()) + }) +} + +func Test_contextWithValues(t *testing.T) { + k1, k2 := "key 1", "key2" + v1, v2 := "value 1", "value 2" + + ctx := contextWithValues(context.TODO(), k1, v1, k2, v2) + + // ascertain that all values added + require.Equal(t, v1, ctx.Value(k1)) + require.Equal(t, v2, ctx.Value(k2)) +} + +func Test_bifurcatingObjectStorer(t *testing.T) { + ctx := context.TODO() + + // create custom error for test + sErr := internal.Error("test error for object storer") + + t.Run("w/ token", func(t *testing.T) { + // create custom request w/ token + sk := test.DecodeKey(0) + + owner, err := refs.NewOwnerID(&sk.PublicKey) + require.NoError(t, err) + + token := new(service.Token) + token.SetOwnerID(owner) + + req := &putRequest{ + PutRequest: object.MakePutRequestHeader(new(Object)), + } + req.SetToken(token) + require.NoError(t, requestSignFunc(sk, req)) + + s := &bifurcatingObjectStorer{ + tokenStorer: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct token storer params", func(t *testing.T) { + require.Equal(t, req, items[0]) + }) + }, + err: sErr, // force token storer to return sErr + }, + } + + _, err = s.putObject(ctx, req) + require.EqualError(t, err, sErr.Error()) + }) + + t.Run("w/o token", func(t *testing.T) { + // create custom request w/o token + req := newRawPutInfo() + require.Nil(t, req.GetSessionToken()) + + s := &bifurcatingObjectStorer{ + straightStorer: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct token storer params", func(t *testing.T) { + require.Equal(t, req, items[0]) + }) + }, + err: sErr, // force token storer to return sErr + }, + } + + _, err := s.putObject(ctx, req) + require.EqualError(t, err, sErr.Error()) + }) +} + +func TestWithTokenFromOwner(t *testing.T) { + // nil request + require.False(t, withTokenFromOwner(nil)) + + // create test request + req := &putRequest{ + PutRequest: new(object.PutRequest), + } + + // w/o session token + require.Nil(t, req.GetSessionToken()) + require.False(t, withTokenFromOwner(req)) + + // create test session token and add it to request + token := new(service.Token) + req.SetToken(token) + + // w/o signatures + require.False(t, withTokenFromOwner(req)) + + // create test public key + pk := &test.DecodeKey(0).PublicKey + + // add key-signature pair + req.AddSignKey(nil, pk) + + // wrong token owner + require.False(t, withTokenFromOwner(req)) + + // set correct token owner + owner, err := refs.NewOwnerID(pk) + require.NoError(t, err) + + token.SetOwnerID(owner) + + require.True(t, withTokenFromOwner(req)) +} + +func Test_tokenObjectStorer(t *testing.T) { + ctx := context.TODO() + + token := new(service.Token) + token.SetID(session.TokenID{1, 2, 3}) + token.SetSignature(testData(t, 10)) + + // create custom request w/ token and object for test + req := newRawPutInfo() + req.setSessionToken(token) + req.setHead(&Object{ + Payload: testData(t, 10), + }) + + t.Run("token store failure", func(t *testing.T) { + s := &tokenObjectStorer{ + tokenStore: &testPutEntity{ + err: internal.Error(""), // force token store to return a non-nil error + }, + } + + _, err := s.putObject(ctx, req) + require.EqualError(t, err, errTokenRetrieval.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + addr := testObjectAddress(t) + + pToken, err := session.NewPrivateToken(0) + require.NoError(t, err) + + s := &tokenObjectStorer{ + tokenStore: &testPutEntity{ + res: pToken, + }, + objStorer: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct object storer params", func(t *testing.T) { + require.Equal(t, req, items[0]) + ctx := items[1].(context.Context) + require.Equal(t, pToken, ctx.Value(transformer.PrivateSessionToken)) + require.Equal(t, token, ctx.Value(transformer.PublicSessionToken)) + }) + }, + res: &addr, + }, + } + + res, err := s.putObject(ctx, req) + require.NoError(t, err) + require.Equal(t, addr, *res) + }) +} + +func Test_filteringObjectStorer(t *testing.T) { + ctx := context.TODO() + + t.Run("filter failure", func(t *testing.T) { + var ( + ttl = uint32(5) + obj = &Object{Payload: testData(t, 10)} + ) + + req := newRawPutInfo() + req.setHead(obj) + req.setTTL(ttl) + + s := &filteringObjectStorer{ + filter: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct filter params", func(t *testing.T) { + require.Equal(t, &Meta{Object: obj}, items[1]) + ctx := items[0].(context.Context) + require.Equal(t, ttl, ctx.Value(ttlValue)) + }) + }, + res: []interface{}{localstore.ResultFail()}, + }, + } + + _, err := s.putObject(ctx, req) + require.EqualError(t, err, errObjectFilter.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + req := newRawPutInfo() + req.setHead(&Object{ + Payload: testData(t, 10), + }) + + addr := testObjectAddress(t) + + s := &filteringObjectStorer{ + filter: &testPutEntity{ + res: []interface{}{localstore.ResultPass()}, + }, + objStorer: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct object storer params", func(t *testing.T) { + require.Equal(t, req, items[0]) + }) + }, + res: &addr, + }, + } + + res, err := s.putObject(ctx, req) + require.NoError(t, err) + require.Equal(t, &addr, res) + }) +} + +func Test_receivingObjectStorer(t *testing.T) { + ctx := context.TODO() + + t.Run("cut payload", func(t *testing.T) { + payload := testData(t, 10) + + req := newRawPutInfo() + req.setHead(&Object{ + SystemHeader: SystemHeader{ + PayloadLength: uint64(len(payload)) + 1, + }, + }) + req.setPayload(bytes.NewBuffer(payload)) + + _, err := new(receivingObjectStorer).putObject(ctx, req) + require.EqualError(t, err, transformer.ErrPayloadEOF.Error()) + }) + + t.Run("payload verification failure", func(t *testing.T) { + vErr := internal.Error("payload verification error for test") + + req := newRawPutInfo() + req.setHead(&Object{ + Payload: testData(t, 10), + }) + + s := &receivingObjectStorer{ + vPayload: &testPutEntity{ + f: func(items ...interface{}) { + require.Equal(t, req.obj, items[0]) + }, + err: vErr, + }, + } + + _, err := s.putObject(ctx, req) + + require.EqualError(t, err, errPayloadChecksum.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + var ( + cn = uint32(10) + ttl = uint32(5) + timeout = 3 * time.Second + payload = testData(t, 10) + addr = testObjectAddress(t) + ) + + obj := &Object{ + SystemHeader: SystemHeader{ + PayloadLength: uint64(len(payload)), + ID: addr.ObjectID, + CID: addr.CID, + }, + } + + req := newRawPutInfo() + req.setHead(obj) + req.setPayload(bytes.NewBuffer(payload)) + req.setTimeout(timeout) + req.setTTL(ttl) + req.setCopiesNumber(cn) + req.setSessionToken(new(service.Token)) + + s := &receivingObjectStorer{ + straightStorer: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct straight storer params", func(t *testing.T) { + exp := newRawPutInfo() + exp.setHead(obj) + exp.setTimeout(timeout) + exp.setTTL(ttl) + exp.setCopiesNumber(cn) + exp.setSessionToken(req.GetSessionToken()) + + require.Equal(t, exp, items[0]) + }) + }, + res: &addr, + }, + vPayload: new(testPutEntity), + } + + res, err := s.putObject(ctx, req) + require.NoError(t, err) + require.Equal(t, &addr, res) + }) +} + +func Test_transformingObjectStorer(t *testing.T) { + ctx := context.TODO() + + t.Run("correct behavior", func(t *testing.T) { + var ( + tErr = internal.Error("test error for transformer") + addr = testObjectAddress(t) + obj = &Object{ + SystemHeader: SystemHeader{ + ID: addr.ObjectID, + CID: addr.CID, + }, + Payload: testData(t, 10), + } + ) + + req := newRawPutInfo() + req.setHead(obj) + req.setPayload(bytes.NewBuffer(obj.Payload)) + req.setTimeout(3 * time.Second) + req.setTTL(5) + req.setCopiesNumber(100) + req.setSessionToken(new(service.Token)) + + tr := &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct transformer params", func(t *testing.T) { + require.Equal(t, transformer.ProcUnit{ + Head: req.obj, + Payload: req.r, + }, items[0]) + fns := items[1].([]transformer.ProcUnitHandler) + require.Len(t, fns, 1) + _ = fns[0](ctx, transformer.ProcUnit{ + Head: req.obj, + Payload: req.r, + }) + }) + }, + } + + s := &transformingObjectStorer{ + transformer: tr, + objStorer: &testPutEntity{ + f: func(items ...interface{}) { + t.Run("correct object storer params", func(t *testing.T) { + exp := newRawPutInfo() + exp.setHead(req.GetHead()) + exp.setPayload(req.Payload()) + exp.setTimeout(req.GetTimeout()) + exp.setTTL(req.GetTTL()) + exp.setCopiesNumber(req.CopiesNumber()) + exp.setSessionToken(req.GetSessionToken()) + + require.Equal(t, exp, items[0]) + }) + }, + err: internal.Error(""), + }, + mErr: map[error]struct{}{ + tErr: {}, + }, + } + + res, err := s.putObject(ctx, req) + require.NoError(t, err) + require.Equal(t, &addr, res) + + tr.err = tErr + + _, err = s.putObject(ctx, req) + require.EqualError(t, err, tErr.Error()) + + tr.err = internal.Error("some other error") + + _, err = s.putObject(ctx, req) + require.EqualError(t, err, errTransformer.Error()) + + e := &transformerHandlerErr{ + error: internal.Error("transformer handler error"), + } + + tr.err = e + + _, err = s.putObject(ctx, req) + require.EqualError(t, err, e.error.Error()) + }) +} + +func Test_putStreamReader(t *testing.T) { + t.Run("empty server", func(t *testing.T) { + s := new(putStreamReader) + n, err := s.Read(make([]byte, 1)) + require.EqualError(t, err, io.EOF.Error()) + require.Zero(t, n) + }) + + t.Run("fail presence", func(t *testing.T) { + initTail := testData(t, 10) + + s := putStreamReader{ + tail: initTail, + srv: new(testPutEntity), + } + + buf := make([]byte, len(s.tail)/2) + + n, err := s.Read(buf) + require.NoError(t, err) + require.Equal(t, len(buf), n) + require.Equal(t, buf, initTail[:n]) + require.Equal(t, initTail[n:], s.tail) + }) + + t.Run("receive message failure", func(t *testing.T) { + t.Run("stream problem", func(t *testing.T) { + srvErr := internal.Error("test error for stream server") + + s := &putStreamReader{ + srv: &testPutEntity{ + err: srvErr, + }, + } + + n, err := s.Read(make([]byte, 1)) + require.EqualError(t, err, srvErr.Error()) + require.Zero(t, n) + }) + + t.Run("incorrect chunk", func(t *testing.T) { + t.Run("empty data", func(t *testing.T) { + s := &putStreamReader{ + srv: &testPutEntity{ + res: object.MakePutRequestChunk(make([]byte, 0)), + }, + } + + n, err := s.Read(make([]byte, 1)) + require.EqualError(t, err, errChunkExpected.Error()) + require.Zero(t, n) + }) + + t.Run("wrong message type", func(t *testing.T) { + s := &putStreamReader{ + srv: &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + }, + } + + n, err := s.Read(make([]byte, 1)) + require.EqualError(t, err, errChunkExpected.Error()) + require.Zero(t, n) + }) + }) + }) + + t.Run("correct read", func(t *testing.T) { + chunk := testData(t, 10) + buf := make([]byte, len(chunk)/2) + + s := &putStreamReader{ + srv: &testPutEntity{ + res: object.MakePutRequestChunk(chunk), + }, + } + + n, err := s.Read(buf) + require.NoError(t, err) + require.Equal(t, chunk[:n], buf) + require.Equal(t, chunk[n:], s.tail) + }) + + t.Run("ful read", func(t *testing.T) { + var ( + callNum = 0 + chunk1, chunk2 = testData(t, 100), testData(t, 88) + ) + + srv := new(testPutEntity) + srv.f = func(items ...interface{}) { + if callNum == 0 { + srv.res = object.MakePutRequestChunk(chunk1) + } else if callNum == 1 { + srv.res = object.MakePutRequestChunk(chunk2) + } else { + srv.res, srv.err = 0, io.EOF + } + callNum++ + } + + s := &putStreamReader{ + srv: srv, + } + + var ( + n int + err error + res = make([]byte, 0) + buf = make([]byte, 10) + ) + + for err != io.EOF { + n, err = s.Read(buf) + res = append(res, buf[:n]...) + } + + require.Equal(t, append(chunk1, chunk2...), res) + }) +} diff --git a/services/public/object/query.go b/services/public/object/query.go new file mode 100644 index 0000000000..79fddde471 --- /dev/null +++ b/services/public/object/query.go @@ -0,0 +1,234 @@ +package object + +import ( + "context" + "fmt" + "regexp" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/transport" + "go.uber.org/zap" +) + +type ( + queryVersionController struct { + m map[int]localQueryImposer + } + + coreQueryImposer struct { + fCreator filterCreator + lsLister localstore.Iterator + + log *zap.Logger + } + + filterCreator interface { + createFilter(query.Query) Filter + } + + coreFilterCreator struct{} +) + +const ( + queryFilterName = "QUERY_FILTER" + + pmUndefinedFilterType = "undefined filter type %d" + + errUnsupportedQueryVersion = internal.Error("unsupported query version number") +) + +const errSearchQueryUnmarshal = internal.Error("query unmarshal failure") + +const errLocalQueryImpose = internal.Error("local query imposing failure") + +var ( + _ filterCreator = (*coreFilterCreator)(nil) + _ localQueryImposer = (*queryVersionController)(nil) + _ localQueryImposer = (*coreQueryImposer)(nil) +) + +func (s *queryVersionController) imposeQuery(ctx context.Context, c CID, d []byte, v int) ([]Address, error) { + imp := s.m[v] + if imp == nil { + return nil, errUnsupportedQueryVersion + } + + return imp.imposeQuery(ctx, c, d, v) +} + +func (s *coreQueryImposer) imposeQuery(ctx context.Context, cid CID, qData []byte, _ int) (res []Address, err error) { + defer func() { + switch err { + case nil, errSearchQueryUnmarshal: + default: + s.log.Error("local query imposing failure", + zap.String("error", err.Error()), + ) + + err = errLocalQueryImpose + } + }() + + var q query.Query + + if err = q.Unmarshal(qData); err != nil { + s.log.Error("could not unmarshal search query", + zap.String("error", err.Error()), + ) + + return nil, errSearchQueryUnmarshal + } else if err = mouldQuery(cid, &q); err != nil { + return + } + + err = s.lsLister.Iterate( + s.fCreator.createFilter(q), + func(meta *Meta) (stop bool) { + res = append(res, Address{ + CID: meta.Object.SystemHeader.CID, + ObjectID: meta.Object.SystemHeader.ID, + }) + return + }, + ) + + return res, err +} + +func (s *coreFilterCreator) createFilter(q query.Query) Filter { + f, err := localstore.AllPassIncludingFilter(queryFilterName, &localstore.FilterParams{ + FilterFunc: func(_ context.Context, o *Meta) *localstore.FilterResult { + if !imposeQuery(q, o.Object) { + return localstore.ResultFail() + } + return localstore.ResultPass() + }, + }) + if err != nil { + panic(err) // TODO: test panic occasion + } + + return f +} + +func mouldQuery(cid CID, q *query.Query) error { + var ( + withCID bool + cidStr = cid.String() + ) + + for i := range q.Filters { + if q.Filters[i].Name == KeyCID { + if q.Filters[i].Value != cidStr { + return errInvalidCIDFilter + } + + withCID = true + } + } + + if !withCID { + q.Filters = append(q.Filters, QueryFilter{ + Type: query.Filter_Exact, + Name: KeyCID, + Value: cidStr, + }) + } + + return nil +} + +func imposeQuery(q query.Query, o *Object) bool { + fs := make(map[string]*QueryFilter) + + for i := range q.Filters { + switch q.Filters[i].Name { + case transport.KeyTombstone: + if !o.IsTombstone() { + return false + } + default: + fs[q.Filters[i].Name] = &q.Filters[i] + } + } + + if !filterSystemHeader(fs, &o.SystemHeader) { + return false + } + + orphan := true + + for i := range o.Headers { + var key, value string + + switch h := o.Headers[i].Value.(type) { + case *object.Header_Link: + switch h.Link.Type { + case object.Link_Parent: + delete(fs, transport.KeyHasParent) + key = transport.KeyParent + orphan = false + case object.Link_Previous: + key = KeyPrev + case object.Link_Next: + key = KeyNext + case object.Link_Child: + if _, ok := fs[transport.KeyNoChildren]; ok { + return false + } + + key = KeyChild + default: + continue + } + + value = h.Link.ID.String() + case *object.Header_UserHeader: + key, value = h.UserHeader.Key, h.UserHeader.Value + case *object.Header_StorageGroup: + key = transport.KeyStorageGroup + default: + continue + } + + if !applyFilter(fs, key, value) { + return false + } + } + + if _, ok := fs[KeyRootObject]; ok && orphan { // we think that object without parents is a root or user's object + delete(fs, KeyRootObject) + } + + delete(fs, transport.KeyNoChildren) + + return len(fs) == 0 +} + +func filterSystemHeader(fs map[string]*QueryFilter, sysHead *SystemHeader) bool { + return applyFilter(fs, KeyID, sysHead.ID.String()) && + applyFilter(fs, KeyCID, sysHead.CID.String()) && + applyFilter(fs, KeyOwnerID, sysHead.OwnerID.String()) +} + +func applyFilter(fs map[string]*QueryFilter, key, value string) bool { + f := fs[key] + if f == nil { + return true + } + + delete(fs, key) + + switch f.Type { + case query.Filter_Exact: + return value == f.Value + case query.Filter_Regex: + regex, err := regexp.Compile(f.Value) + return err == nil && regex.MatchString(value) + default: + panic(fmt.Sprintf(pmUndefinedFilterType, f.Type)) + } +} diff --git a/services/public/object/query_test.go b/services/public/object/query_test.go new file mode 100644 index 0000000000..ee6a5dea35 --- /dev/null +++ b/services/public/object/query_test.go @@ -0,0 +1,828 @@ +package object + +import ( + "context" + "fmt" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testQueryEntity struct { + // Set of interfaces which testQueryEntity must implement, but some methods from those does not call. + Filter + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ filterCreator = (*testQueryEntity)(nil) + _ localQueryImposer = (*testQueryEntity)(nil) +) + +func (s *testQueryEntity) imposeQuery(_ context.Context, c CID, q []byte, v int) ([]Address, error) { + if s.f != nil { + s.f(c, q, v) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]Address), nil +} + +func (s *testQueryEntity) createFilter(p query.Query) Filter { + if s.f != nil { + s.f(p) + } + return s +} + +func (s *testQueryEntity) Iterate(p Filter, h localstore.MetaHandler) error { + if s.f != nil { + s.f(p) + } + if s.err != nil { + return s.err + } + for _, item := range s.res.([]localstore.ListItem) { + h(&item.ObjectMeta) + } + return nil +} + +func Test_queryVersionController_imposeQuery(t *testing.T) { + ctx := context.TODO() + cid := testObjectAddress(t).CID + + t.Run("unsupported version", func(t *testing.T) { + qImp := &queryVersionController{ + m: make(map[int]localQueryImposer), + } + + res, err := qImp.imposeQuery(ctx, cid, nil, 1) + require.EqualError(t, err, errUnsupportedQueryVersion.Error()) + require.Empty(t, res) + }) + + t.Run("correct imposer choose", func(t *testing.T) { + m := make(map[int]localQueryImposer) + qData := testData(t, 10) + + qImp := &queryVersionController{m: m} + + m[0] = &testQueryEntity{ + f: func(items ...interface{}) { + t.Run("correct imposer params", func(t *testing.T) { + require.Equal(t, cid, items[0].(CID)) + require.Equal(t, qData, items[1].([]byte)) + require.Equal(t, 0, items[2].(int)) + }) + }, + err: internal.Error(""), // just to prevent panic + } + + _, _ = qImp.imposeQuery(ctx, cid, qData, 0) + }) + + t.Run("correct imposer result", func(t *testing.T) { + t.Run("error", func(t *testing.T) { + m := make(map[int]localQueryImposer) + qImp := &queryVersionController{m: m} + + impErr := internal.Error("test error for query imposer") + + m[0] = &testQueryEntity{ + err: impErr, // force localQueryImposer to return impErr + } + + res, err := qImp.imposeQuery(ctx, cid, nil, 0) + + // ascertain that error returns as expected + require.EqualError(t, err, impErr.Error()) + // ascertain that result is empty + require.Empty(t, res) + + // create test address list + addrList := testAddrList(t, 5) + + m[1] = &testQueryEntity{ + res: addrList, // force localQueryImposer to return addrList + } + + res, err = qImp.imposeQuery(ctx, cid, nil, 1) + require.NoError(t, err) + + // ascertain that result returns as expected + require.Equal(t, addrList, res) + }) + }) +} + +func Test_coreQueryImposer_imposeQuery(t *testing.T) { + v := 1 + ctx := context.TODO() + cid := testObjectAddress(t).CID + log := zap.L() + + t.Run("query unmarshal failure", func(t *testing.T) { + var ( + qErr error + data []byte + ) + + // create invalid query binary representation + for { + data = testData(t, 1024) + if qErr = new(query.Query).Unmarshal(data); qErr != nil { + break + } + } + + s := &coreQueryImposer{ + log: zap.L(), + } + + // trying to impose invalid query data + res, err := s.imposeQuery(ctx, cid, data, v) + + // ascertain that reached error exactly like in unmarshal + require.EqualError(t, err, errSearchQueryUnmarshal.Error()) + + // ascertain that empty result returned + require.Nil(t, res) + }) + + t.Run("mould query failure", func(t *testing.T) { + // create testQuery with CID filter with value other than cid + testQuery := &query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: KeyCID, Value: cid.String() + "1"}}} + + // try to mould this testQuery + mErr := mouldQuery(cid, testQuery) + + // ascertain that testQuery mould failed + require.Error(t, mErr) + + // ascertain that testQuery marshals normally + d, err := testQuery.Marshal() + require.NoError(t, err) + + s := &coreQueryImposer{ + log: log, + } + + // try to impose testQuery + res, err := s.imposeQuery(ctx, cid, d, v) + + // ascertain that impose fails with same error as mould + require.EqualError(t, err, errLocalQueryImpose.Error()) + + // ascertain that result is empty + require.Nil(t, res) + }) + + t.Run("local store listing", func(t *testing.T) { + // create testQuery and object which matches to it + testQuery, obj := testFullObjectWithQuery(t) + + // ascertain testQuery marshals normally + qBytes, err := testQuery.Marshal() + require.NoError(t, err) + + t.Run("listing error", func(t *testing.T) { + // create new error for test + lsErr := internal.Error("test error of local store listing") + + // create test query imposer with mocked always failing lister + qImposer := &coreQueryImposer{ + fCreator: new(coreFilterCreator), + lsLister: &testQueryEntity{err: lsErr}, + log: log, + } + + // try to impose testQuery + list, err := qImposer.imposeQuery(ctx, obj.SystemHeader.CID, qBytes, v) + + // ascertain that impose fails same error as lister + require.EqualError(t, err, errLocalQueryImpose.Error()) + + // ascertain that result is empty + require.Empty(t, list) + }) + + t.Run("correct parameter", func(t *testing.T) { + // create new mocked filter creator + fc := new(testQueryEntity) + fc.res = fc + + // create testQuery imposer + qImposer := &coreQueryImposer{ + fCreator: fc, + lsLister: &testQueryEntity{ + f: func(p ...interface{}) { + // intercept lister arguments + // ascertain that argument is as expected + require.Equal(t, fc, p[0].(Filter)) + }, + err: internal.Error(""), + }, + log: log, + } + + _, _ = qImposer.imposeQuery(ctx, obj.SystemHeader.CID, qBytes, v) + }) + + t.Run("correct result", func(t *testing.T) { + // create list of random address items + addrList := testAddrList(t, 10) + items := make([]localstore.ListItem, 0, len(addrList)) + for i := range addrList { + items = append(items, localstore.ListItem{ + ObjectMeta: Meta{ + Object: &Object{ + SystemHeader: SystemHeader{ + ID: addrList[i].ObjectID, + CID: addrList[i].CID, + }, + }, + }, + }) + } + + // create imposer with mocked lister + qImposer := &coreQueryImposer{ + fCreator: new(coreFilterCreator), + lsLister: &testQueryEntity{res: items}, + } + + // try to impose testQuery + list, err := qImposer.imposeQuery(ctx, obj.SystemHeader.CID, qBytes, v) + + // ascertain that imposing finished normally + require.NoError(t, err) + + // ascertain that resulting list size as expected + require.Len(t, list, len(addrList)) + + // ascertain that all source items are presented in result + for i := range addrList { + require.Contains(t, list, addrList[i]) + } + }) + }) +} + +func Test_coreFilterCreator_createFilter(t *testing.T) { + ctx := context.TODO() + fCreator := new(coreFilterCreator) + + t.Run("composing correct filter", func(t *testing.T) { + var f Filter + + // ascertain filter creation does not panic + require.NotPanics(t, func() { f = fCreator.createFilter(query.Query{}) }) + + // ascertain that created filter is not empty + require.NotNil(t, f) + + // ascertain that created filter has expected name + require.Equal(t, queryFilterName, f.GetName()) + }) + + t.Run("passage on matching query", func(t *testing.T) { + // create testQuery and object which matches to it + testQuery, obj := testFullObjectWithQuery(t) + + // create filter for testQuery and pass object to it + res := fCreator.createFilter(testQuery).Pass(ctx, &Meta{Object: obj}) + + // ascertain that filter is passed + require.Equal(t, localstore.CodePass, res.Code()) + }) + + t.Run("failure on mismatching query", func(t *testing.T) { + testQuery, obj := testFullObjectWithQuery(t) + obj.SystemHeader.ID[0]++ + require.False(t, imposeQuery(testQuery, obj)) + + res := fCreator.createFilter(testQuery).Pass(ctx, &Meta{Object: obj}) + + require.Equal(t, localstore.CodeFail, res.Code()) + }) +} + +func Test_mouldQuery(t *testing.T) { + cid := testObjectAddress(t).CID + + t.Run("invalid CID filter", func(t *testing.T) { + // create query with CID filter with other than cid value + query := &query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: KeyCID, Value: cid.String() + "1"}}} + + // try to mould this query for cid + err := mouldQuery(cid, query) + + // ascertain wrong CID value is not allowed + require.EqualError(t, err, errInvalidCIDFilter.Error()) + }) + + t.Run("correct CID filter", func(t *testing.T) { + // create testQuery with CID filter with cid value + cidF := QueryFilter{Type: query.Filter_Exact, Name: KeyCID, Value: cid.String()} + testQuery := &query.Query{Filters: []QueryFilter{cidF}} + + // ascertain mould is processed + require.NoError(t, mouldQuery(cid, testQuery)) + + // ascertain filter is still in testQuery + require.Contains(t, testQuery.Filters, cidF) + }) + + t.Run("missing CID filter", func(t *testing.T) { + // create CID filter with cid value + expF := QueryFilter{Type: query.Filter_Exact, Name: KeyCID, Value: cid.String()} + + // create empty testQuery + testQuery := new(query.Query) + + // ascertain mould is processed + require.NoError(t, mouldQuery(cid, testQuery)) + + // ascertain exact CID filter added to testQuery + require.Contains(t, testQuery.Filters, expF) + }) +} + +func Test_applyFilter(t *testing.T) { + k, v := "key", "value" + + t.Run("empty map", func(t *testing.T) { + // ascertain than applyFilter always return true on empty filter map + require.True(t, applyFilter(nil, k, v)) + }) + + t.Run("passage on missing key", func(t *testing.T) { + t.Run("exact", func(t *testing.T) { + require.True(t, applyFilter(map[string]*QueryFilter{k: {Type: query.Filter_Exact, Value: v + "1"}}, k+"1", v)) + }) + + t.Run("regex", func(t *testing.T) { + require.True(t, applyFilter(map[string]*QueryFilter{k: {Type: query.Filter_Regex, Value: v + "1"}}, k+"1", v)) + }) + }) + + t.Run("passage on key presence and matching value", func(t *testing.T) { + t.Run("exact", func(t *testing.T) { + require.True(t, applyFilter(map[string]*QueryFilter{k: {Type: query.Filter_Exact, Value: v}}, k, v)) + }) + + t.Run("regex", func(t *testing.T) { + require.True(t, applyFilter(map[string]*QueryFilter{k: {Type: query.Filter_Regex, Value: v + "|" + v + "1"}}, k, v)) + }) + }) + + t.Run("failure on key presence and mismatching value", func(t *testing.T) { + t.Run("exact", func(t *testing.T) { + require.False(t, applyFilter(map[string]*QueryFilter{k: {Type: query.Filter_Exact, Value: v + "1"}}, k, v)) + }) + + t.Run("regex", func(t *testing.T) { + require.False(t, applyFilter(map[string]*QueryFilter{k: {Type: query.Filter_Regex, Value: v + "&" + v + "1"}}, k, v)) + }) + }) + + t.Run("key removes from filter map", func(t *testing.T) { + // create filter map with several elements + m := map[string]*QueryFilter{ + k: {Type: query.Filter_Exact, Value: v}, + k + "1": {Type: query.Filter_Exact, Value: v}, + } + + // save initial len + initLen := len(m) + + // apply filter with key from filter map + applyFilter(m, k, v) + + // ascertain exactly key was removed from filter map + require.Len(t, m, initLen-1) + + // ascertain this is exactly applyFilter argument + _, ok := m[k] + require.False(t, ok) + }) + + t.Run("panic on unknown filter type", func(t *testing.T) { + // create filter type other than FilterExact and FilterRegex + fType := query.Filter_Exact + query.Filter_Regex + 1 + require.NotEqual(t, query.Filter_Exact, fType) + require.NotEqual(t, query.Filter_Regex, fType) + + // ascertain applyFilter does not process this type but panic + require.PanicsWithValue(t, + fmt.Sprintf(pmUndefinedFilterType, fType), + func() { applyFilter(map[string]*QueryFilter{k: {Type: fType}}, k, v) }, + ) + }) +} + +func Test_imposeQuery(t *testing.T) { + t.Run("tombstone filter", func(t *testing.T) { + // create testQuery with only tombstone filter + testQuery := query.Query{Filters: []QueryFilter{{Name: transport.KeyTombstone}}} + + // create object which is not a tombstone + obj := new(Object) + + testQueryMatch(t, testQuery, obj, func(t *testing.T, obj *Object) { + // adding tombstone header makes object to satisfy tombstone testQuery + obj.Headers = append(obj.Headers, Header{Value: new(object.Header_Tombstone)}) + }) + }) + + t.Run("system header", func(t *testing.T) { + addr := testObjectAddress(t) + cid, oid, ownerID := addr.CID, addr.ObjectID, OwnerID{3} + + // create testQuery with system header filters + testQuery := query.Query{Filters: []QueryFilter{ + {Type: query.Filter_Exact, Name: KeyCID, Value: cid.String()}, + {Type: query.Filter_Exact, Name: KeyID, Value: oid.String()}, + {Type: query.Filter_Exact, Name: KeyOwnerID, Value: ownerID.String()}, + }} + + // fn sets system header fields values to ones from filters + fn := func(t *testing.T, obj *Object) { obj.SystemHeader = SystemHeader{CID: cid, ID: oid, OwnerID: ownerID} } + + // create object with empty system header fields + obj := new(Object) + testQueryMatch(t, testQuery, obj, fn) + + // create object with CID from filters + sysHdr := SystemHeader{CID: cid} + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + + // create object with OID from filters + sysHdr.CID = CID{} + sysHdr.ID = oid + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + + // create object with OwnerID from filters + sysHdr.ID = ID{} + sysHdr.OwnerID = ownerID + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + + // create object with CID and OwnerID from filters + sysHdr.CID = cid + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + + // create object with OID and OwnerID from filters + sysHdr.CID = CID{} + sysHdr.ID = oid + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + + // create object with OID and OwnerID from filters + sysHdr.ID = oid + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + + // create object with CID and OID from filters + sysHdr.CID = cid + sysHdr.OwnerID = OwnerID{} + obj = &Object{SystemHeader: sysHdr} + testQueryMatch(t, testQuery, obj, fn) + }) + + t.Run("no children filter", func(t *testing.T) { + // create testQuery with only orphan filter + testQuery := query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: transport.KeyNoChildren}}} + + // create object with child relation + obj := &Object{Headers: []Header{{Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Child}}}}} + + testQueryMatch(t, testQuery, obj, func(t *testing.T, obj *Object) { + // child relation removal makes object to satisfy orphan testQuery + obj.Headers = nil + }) + }) + + t.Run("has parent filter", func(t *testing.T) { + // create testQuery with parent relation filter + testQuery := query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: transport.KeyHasParent}}} + + // create object w/o parent + obj := new(Object) + + testQueryMatch(t, testQuery, obj, func(t *testing.T, obj *Object) { + // adding parent relation makes object to satisfy parent testQuery + obj.Headers = append(obj.Headers, Header{Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Parent}}}) + }) + }) + + t.Run("root object filter", func(t *testing.T) { + // create testQuery with only root filter + testQuery := query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: KeyRootObject}}} + + // create object with parent relation + obj := &Object{Headers: []Header{{Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Parent}}}}} + + testQueryMatch(t, testQuery, obj, func(t *testing.T, obj *Object) { + // parent removal makes object to satisfy root testQuery + obj.Headers = nil + }) + }) + + t.Run("link value filters", func(t *testing.T) { + t.Run("parent", func(t *testing.T) { + testLinkQuery(t, transport.KeyParent, object.Link_Parent) + }) + + t.Run("child", func(t *testing.T) { + testLinkQuery(t, KeyChild, object.Link_Child) + }) + + t.Run("previous", func(t *testing.T) { + testLinkQuery(t, KeyPrev, object.Link_Previous) + }) + + t.Run("next", func(t *testing.T) { + testLinkQuery(t, KeyNext, object.Link_Next) + }) + + t.Run("other", func(t *testing.T) { + // create not usable link type + linkKey := object.Link_Parent + object.Link_Child + object.Link_Next + object.Link_Previous + + // add some usable link to testQuery + par := ID{1, 2, 3} + testQuery := query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: transport.KeyParent, Value: par.String()}}} + + // ascertain that undefined link type has no affect on testQuery imposing + require.True(t, imposeQuery(testQuery, &Object{ + Headers: []Header{ + {Value: &object.Header_Link{Link: &object.Link{Type: linkKey}}}, + {Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Parent, ID: par}}}, + }, + })) + }) + }) + + t.Run("user header filter", func(t *testing.T) { + // user header key-value pair + k, v := "header", "value" + + // query with user header filter + query := query.Query{Filters: []QueryFilter{{ + Type: query.Filter_Exact, + Name: k, + Value: v, + }}} + + // create user header with same key and different value + hdr := &UserHeader{Key: k, Value: v + "1"} + + // create object with this user header + obj := &Object{Headers: []Header{{Value: &object.Header_UserHeader{UserHeader: hdr}}}} + + testQueryMatch(t, query, obj, func(t *testing.T, obj *Object) { + // correcting value to one from filter makes object to satisfy query + hdr.Value = v + }) + }) + + t.Run("storage group filter", func(t *testing.T) { + // create testQuery with only storage group filter + testQuery := query.Query{Filters: []QueryFilter{{Type: query.Filter_Exact, Name: transport.KeyStorageGroup}}} + + // create object w/o storage group header + obj := new(Object) + + testQueryMatch(t, testQuery, obj, func(t *testing.T, obj *Object) { + // adding storage group headers make object to satisfy testQuery + obj.Headers = append(obj.Headers, Header{Value: &object.Header_StorageGroup{StorageGroup: new(storagegroup.StorageGroup)}}) + }) + }) +} + +func Test_filterSystemHeader(t *testing.T) { + var ( + ownerID1, ownerID2 = OwnerID{1}, OwnerID{2} + addr1, addr2 = testObjectAddress(t), testObjectAddress(t) + cid1, cid2 = addr1.CID, addr2.CID + oid1, oid2 = addr1.ObjectID, addr2.ObjectID + sysHdr = SystemHeader{ID: oid1, OwnerID: ownerID1, CID: cid1} + ) + require.NotEqual(t, ownerID1, ownerID2) + require.NotEqual(t, cid1, cid2) + require.NotEqual(t, oid1, oid2) + + t.Run("empty filter map", func(t *testing.T) { + // ascertain that any system header satisfies to empty (nil) filter map + require.True(t, filterSystemHeader(nil, &sysHdr)) + }) + + t.Run("missing of some of the fields", func(t *testing.T) { + // create filter map for system header + m := sysHeaderFilterMap(sysHdr) + + // copy system header for initial values saving + h := sysHdr + + // change CID + h.CID = cid2 + + // ascertain filter failure + require.False(t, filterSystemHeader(m, &h)) + + // remove CID from filter map + delete(m, KeyCID) + + // ascertain filter passage + require.True(t, filterSystemHeader(m, &h)) + + m = sysHeaderFilterMap(sysHdr) + h = sysHdr + + // change OwnerID + h.OwnerID = ownerID2 + + // ascertain filter failure + require.False(t, filterSystemHeader(m, &h)) + + // remove OwnerID from filter map + delete(m, KeyOwnerID) + + // ascertain filter passage + require.True(t, filterSystemHeader(m, &h)) + + m = sysHeaderFilterMap(sysHdr) + h = sysHdr + + // change ObjectID + h.ID = oid2 + + // ascertain filter failure + require.False(t, filterSystemHeader(m, &h)) + + // remove ObjectID from filter map + delete(m, KeyID) + + // ascertain filter passage + require.True(t, filterSystemHeader(m, &h)) + }) + + t.Run("valid fields passage", func(t *testing.T) { + require.True(t, filterSystemHeader(sysHeaderFilterMap(sysHdr), &sysHdr)) + }) + + t.Run("mismatching values failure", func(t *testing.T) { + h := sysHdr + + // make CID value not matching + h.CID = cid2 + + require.False(t, filterSystemHeader(sysHeaderFilterMap(sysHdr), &h)) + + h = sysHdr + + // make ObjectID value not matching + h.ID = oid2 + + require.False(t, filterSystemHeader(sysHeaderFilterMap(sysHdr), &h)) + + h = sysHdr + + // make OwnerID value not matching + h.OwnerID = ownerID2 + + require.False(t, filterSystemHeader(sysHeaderFilterMap(sysHdr), &h)) + }) +} + +// testQueryMatch imposes passed query to passed object for tests. +// Passed object should not match to passed query. +// Passed function must mutate object so that becomes query matching. +func testQueryMatch(t *testing.T, q query.Query, obj *Object, fn func(*testing.T, *Object)) { + require.False(t, imposeQuery(q, obj)) + fn(t, obj) + require.True(t, imposeQuery(q, obj)) +} + +// testLinkQuery tests correctness of imposing query with link filters. +// Inits object with value different from one from filter. Then uses testQueryMatch with correcting value func. +func testLinkQuery(t *testing.T, key string, lt object.Link_Type) { + // create new relation link + relative, err := refs.NewObjectID() + require.NoError(t, err) + + // create another relation link + wrongRelative := relative + for wrongRelative.Equal(relative) { + wrongRelative, err = refs.NewObjectID() + require.NoError(t, err) + } + + // create query with relation filter + query := query.Query{Filters: []QueryFilter{{ + Type: query.Filter_Exact, + Name: key, + Value: relative.String(), + }}} + + // create link with relation different from one from filter + link := &object.Link{Type: lt, ID: wrongRelative} + // create object with this link + obj := &Object{Headers: []Header{{Value: &object.Header_Link{Link: link}}}} + testQueryMatch(t, query, obj, func(t *testing.T, object *Object) { + // changing link value to one from filter make object to satisfy relation query + link.ID = relative + }) +} + +// sysHeaderFilterMap creates filter map for passed system header. +func sysHeaderFilterMap(hdr SystemHeader) map[string]*QueryFilter { + return map[string]*QueryFilter{ + KeyCID: { + Type: query.Filter_Exact, + Name: KeyCID, + Value: hdr.CID.String(), + }, + KeyOwnerID: { + Type: query.Filter_Exact, + Name: KeyOwnerID, + Value: hdr.OwnerID.String(), + }, + KeyID: { + Type: query.Filter_Exact, + Name: KeyID, + Value: hdr.ID.String(), + }, + } +} + +// testFullObjectWithQuery creates query with set of permissible filters and object matching to this query. +func testFullObjectWithQuery(t *testing.T) (query.Query, *Object) { + addr := testObjectAddress(t) + selfID, cid := addr.ObjectID, addr.CID + + ownerID := OwnerID{} + copy(ownerID[:], testData(t, refs.OwnerIDSize)) + + addrList := testAddrList(t, 4) + + parID, childID, nextID, prevID := addrList[0].ObjectID, addrList[1].ObjectID, addrList[2].ObjectID, addrList[3].ObjectID + + query := query.Query{Filters: []QueryFilter{ + {Type: query.Filter_Exact, Name: transport.KeyParent, Value: parID.String()}, + {Type: query.Filter_Exact, Name: KeyPrev, Value: prevID.String()}, + {Type: query.Filter_Exact, Name: KeyNext, Value: nextID.String()}, + {Type: query.Filter_Exact, Name: KeyChild, Value: childID.String()}, + {Type: query.Filter_Exact, Name: KeyOwnerID, Value: ownerID.String()}, + {Type: query.Filter_Exact, Name: KeyID, Value: selfID.String()}, + {Type: query.Filter_Exact, Name: KeyCID, Value: cid.String()}, + {Type: query.Filter_Exact, Name: transport.KeyStorageGroup}, + {Type: query.Filter_Exact, Name: transport.KeyTombstone}, + {Type: query.Filter_Exact, Name: transport.KeyHasParent}, + }} + + obj := &Object{ + SystemHeader: SystemHeader{ + ID: selfID, + OwnerID: ownerID, + CID: cid, + }, + Headers: []Header{ + {Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Parent, ID: parID}}}, + {Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Previous, ID: prevID}}}, + {Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Next, ID: nextID}}}, + {Value: &object.Header_Link{Link: &object.Link{Type: object.Link_Child, ID: childID}}}, + {Value: &object.Header_StorageGroup{StorageGroup: new(storagegroup.StorageGroup)}}, + {Value: &object.Header_Tombstone{Tombstone: new(object.Tombstone)}}, + }, + } + + require.True(t, imposeQuery(query, obj)) + + return query, obj +} diff --git a/services/public/object/ranges.go b/services/public/object/ranges.go new file mode 100644 index 0000000000..acc05e0cbe --- /dev/null +++ b/services/public/object/ranges.go @@ -0,0 +1,481 @@ +package object + +import ( + "context" + "io" + "sync" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/objio" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // Range is a type alias of + // Range from object package of neofs-api-go. + Range = object.Range + + // RangeDescriptor is a type alias of + // RangeDescriptor from objio package. + RangeDescriptor = objio.RangeDescriptor + + // RangeChopper is a type alias of + // RangeChopper from objio package. + RangeChopper = objio.RangeChopper + + // GetRangeRequest is a type alias of + // GetRangeRequest from object package of neofs-api-go. + GetRangeRequest = object.GetRangeRequest + + // GetRangeResponse is a type alias of + // GetRangeResponse from object package of neofs-api-go. + GetRangeResponse = object.GetRangeResponse + + // GetRangeHashRequest is a type alias of + // GetRangeResponse from object package of neofs-api-go. + GetRangeHashRequest = object.GetRangeHashRequest + + // GetRangeHashResponse is a type alias of + // GetRangeHashResponse from object package of neofs-api-go. + GetRangeHashResponse = object.GetRangeHashResponse + + objectRangeReceiver interface { + getRange(context.Context, rangeTool) (interface{}, error) + } + + rangeTool interface { + transport.RangeHashInfo + budOff(*RangeDescriptor) rangeTool + handler() rangeItemAccumulator + } + + rawRangeInfo struct { + *rawAddrInfo + rng Range + } + + rawRangeHashInfo struct { + *rawAddrInfo + rngList []Range + salt []byte + } + + coreRangeReceiver struct { + rngRevealer rangeRevealer + straightRngRecv objectRangeReceiver + + // Set of errors that won't be converted into errPayloadRangeNotFound + mErr map[error]struct{} + + log *zap.Logger + } + + straightRangeReceiver struct { + executor operationExecutor + } + + singleItemHandler struct { + *sync.Once + item interface{} + } + + rangeItemAccumulator interface { + responseItemHandler + collect() (interface{}, error) + } + + rangeHashAccum struct { + concat bool + h []Hash + } + + rangeRevealer interface { + reveal(context.Context, *RangeDescriptor) ([]RangeDescriptor, error) + } + + coreRngRevealer struct { + relativeRecv objio.RelativeReceiver + chopTable objio.ChopperTable + } + + getRangeServerWriter struct { + req *GetRangeRequest + + srv object.Service_GetRangeServer + + respPreparer responsePreparer + } +) + +const ( + emGetRangeFail = "could get object range #%d part #%d" + emRangeRevealFail = "could not reveal object range #%d" + emRangeCollect = "could not collect result of object range #%d" + + errRangeReveal = internal.Error("could not reveal payload range") +) + +var ( + _ transport.RangeInfo = (*rawRangeInfo)(nil) + _ rangeTool = (*rawRangeHashInfo)(nil) + _ rangeTool = (*transportRequest)(nil) + _ rangeItemAccumulator = (*rangeHashAccum)(nil) + _ rangeItemAccumulator = (*singleItemHandler)(nil) + _ rangeRevealer = (*coreRngRevealer)(nil) + _ objectRangeReceiver = (*coreRangeReceiver)(nil) + _ objectRangeReceiver = (*straightRangeReceiver)(nil) + _ io.Writer = (*getRangeServerWriter)(nil) + _ transport.RangeInfo = (*transportRequest)(nil) +) + +func (s *objectService) GetRange(req *GetRangeRequest, srv object.Service_GetRangeServer) (err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestRange), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestRange, + e: err, + }) + }() + + var r interface{} + + if r, err = s.requestHandler.handleRequest(srv.Context(), handleRequestParams{ + request: req, + executor: s, + }); err == nil { + _, err = io.CopyBuffer( + &getRangeServerWriter{ + req: req, + srv: srv, + respPreparer: s.rangeChunkPreparer, + }, + r.(io.Reader), + make([]byte, maxGetPayloadSize), + ) + } + + return err +} + +func (s *objectService) GetRangeHash(ctx context.Context, req *GetRangeHashRequest) (res *GetRangeHashResponse, err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestRangeHash), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestRangeHash, + e: err, + }) + }() + + var r interface{} + + if r, err = s.requestHandler.handleRequest(ctx, handleRequestParams{ + request: req, + executor: s, + }); err != nil { + return + } + + res = makeRangeHashResponse(r.([]Hash)) + err = s.respPreparer.prepareResponse(ctx, req, res) + + return +} + +func (s *coreRangeReceiver) getRange(ctx context.Context, rt rangeTool) (res interface{}, err error) { + defer func() { + if err != nil { + if _, ok := s.mErr[errors.Cause(err)]; !ok { + s.log.Error("get range failure", + zap.String("error", err.Error()), + ) + + err = errPayloadRangeNotFound + } + } + }() + + var ( + subRngSet []RangeDescriptor + rngSet = rt.GetRanges() + addr = rt.GetAddress() + handler = rt.handler() + ) + + for i := range rngSet { + rd := RangeDescriptor{ + Size: int64(rngSet[i].Length), + Offset: int64(rngSet[i].Offset), + Addr: addr, + } + + if rt.GetTTL() < service.NonForwardingTTL { + subRngSet = []RangeDescriptor{rd} + } else if subRngSet, err = s.rngRevealer.reveal(ctx, &rd); err != nil { + return nil, errors.Wrapf(err, emRangeRevealFail, i+1) + } else if len(subRngSet) == 0 { + return nil, errRangeReveal + } + + subRangeTool := rt.budOff(&rd) + subHandler := subRangeTool.handler() + + for j := range subRngSet { + tool := subRangeTool.budOff(&subRngSet[j]) + + if subRngSet[j].Addr.Equal(&addr) { + res, err = s.straightRngRecv.getRange(ctx, tool) + } else { + res, err = s.getRange(ctx, tool) + } + + if err != nil { + return nil, errors.Wrapf(err, emGetRangeFail, i+1, j+1) + } + + subHandler.handleItem(res) + } + + rngRes, err := subHandler.collect() + if err != nil { + return nil, errors.Wrapf(err, emRangeCollect, i+1) + } + + handler.handleItem(rngRes) + } + + return handler.collect() +} + +func (s *straightRangeReceiver) getRange(ctx context.Context, rt rangeTool) (interface{}, error) { + handler := newSingleItemHandler() + if err := s.executor.executeOperation(ctx, rt, handler); err != nil { + return nil, err + } + + return handler.collect() +} + +func (s *coreRngRevealer) reveal(ctx context.Context, r *RangeDescriptor) ([]RangeDescriptor, error) { + chopper, err := s.getChopper(r.Addr) + if err != nil { + return nil, err + } + + return chopper.Chop(ctx, r.Size, r.Offset, true) +} + +func (s *coreRngRevealer) getChopper(addr Address) (res RangeChopper, err error) { + if res, err = s.chopTable.GetChopper(addr, objio.RCCharybdis); err == nil && res.Closed() { + return + } else if res, err = s.chopTable.GetChopper(addr, objio.RCScylla); err == nil { + return + } else if res, err = objio.NewScylla(&objio.ChopperParams{ + RelativeReceiver: s.relativeRecv, + Addr: addr, + }); err != nil { + return nil, err + } + + _ = s.chopTable.PutChopper(addr, res) + + return +} + +func loopData(data []byte, size, off int64) []byte { + if len(data) == 0 { + return make([]byte, 0) + } + + res := make([]byte, 0, size) + + var ( + cut int64 + tail = data[off%int64(len(data)):] + ) + + for added := int64(0); added < size; added += cut { + cut = min(int64(len(tail)), size-added) + res = append(res, tail[:cut]...) + tail = data + } + + return res +} + +func min(a, b int64) int64 { + if a < b { + return a + } + + return b +} + +func newSingleItemHandler() rangeItemAccumulator { return &singleItemHandler{Once: new(sync.Once)} } + +func (s *singleItemHandler) handleItem(item interface{}) { s.Do(func() { s.item = item }) } + +func (s *singleItemHandler) collect() (interface{}, error) { return s.item, nil } + +func (s *rangeHashAccum) handleItem(h interface{}) { + if v, ok := h.(Hash); ok { + s.h = append(s.h, v) + return + } + + s.h = append(s.h, h.([]Hash)...) +} + +func (s *rangeHashAccum) collect() (interface{}, error) { + if s.concat { + return hash.Concat(s.h) + } + + return s.h, nil +} + +func (s *rawRangeHashInfo) GetRanges() []Range { + return s.rngList +} + +func (s *rawRangeHashInfo) setRanges(v []Range) { + s.rngList = v +} + +func (s *rawRangeHashInfo) GetSalt() []byte { + return s.salt +} + +func (s *rawRangeHashInfo) setSalt(v []byte) { + s.salt = v +} + +func (s *rawRangeHashInfo) getAddrInfo() *rawAddrInfo { + return s.rawAddrInfo +} + +func (s *rawRangeHashInfo) setAddrInfo(v *rawAddrInfo) { + s.rawAddrInfo = v + s.setType(object.RequestRangeHash) +} + +func newRawRangeHashInfo() *rawRangeHashInfo { + res := new(rawRangeHashInfo) + + res.setAddrInfo(newRawAddressInfo()) + + return res +} + +func (s *rawRangeHashInfo) budOff(r *RangeDescriptor) rangeTool { + res := newRawRangeHashInfo() + + res.setMetaInfo(s.getMetaInfo()) + res.setAddress(r.Addr) + res.setRanges([]Range{ + { + Offset: uint64(r.Offset), + Length: uint64(r.Size), + }, + }) + res.setSalt(loopData(s.salt, int64(len(s.salt)), r.Offset)) + res.setSessionToken(s.GetSessionToken()) + res.setBearerToken(s.GetBearerToken()) + res.setExtendedHeaders(s.ExtendedHeaders()) + + return res +} + +func (s *rawRangeHashInfo) handler() rangeItemAccumulator { return &rangeHashAccum{concat: true} } + +func (s *transportRequest) GetRanges() []Range { + return s.serviceRequest.(*object.GetRangeHashRequest).Ranges +} + +func (s *transportRequest) GetSalt() []byte { + return s.serviceRequest.(*object.GetRangeHashRequest).Salt +} + +func (s *transportRequest) budOff(rd *RangeDescriptor) rangeTool { + res := newRawRangeHashInfo() + + res.setTTL(s.GetTTL()) + res.setTimeout(s.GetTimeout()) + res.setAddress(rd.Addr) + res.setRanges([]Range{ + { + Offset: uint64(rd.Offset), + Length: uint64(rd.Size), + }, + }) + res.setSalt(s.serviceRequest.(*object.GetRangeHashRequest).GetSalt()) + res.setSessionToken(s.GetSessionToken()) + res.setBearerToken(s.GetBearerToken()) + res.setExtendedHeaders(s.ExtendedHeaders()) + + return res +} + +func (s *transportRequest) handler() rangeItemAccumulator { return new(rangeHashAccum) } + +func (s *getRangeServerWriter) Write(p []byte) (int, error) { + resp := makeRangeResponse(p) + if err := s.respPreparer.prepareResponse(s.srv.Context(), s.req, resp); err != nil { + return 0, err + } + + if err := s.srv.Send(resp); err != nil { + return 0, err + } + + return len(p), nil +} + +func (s *rawRangeInfo) GetRange() Range { + return s.rng +} + +func (s *rawRangeInfo) setRange(rng Range) { + s.rng = rng +} + +func (s *rawRangeInfo) getAddrInfo() *rawAddrInfo { + return s.rawAddrInfo +} + +func (s *rawRangeInfo) setAddrInfo(v *rawAddrInfo) { + s.rawAddrInfo = v + s.setType(object.RequestRange) +} + +func newRawRangeInfo() *rawRangeInfo { + res := new(rawRangeInfo) + + res.setAddrInfo(newRawAddressInfo()) + + return res +} + +func (s *transportRequest) GetRange() Range { + return s.serviceRequest.(*GetRangeRequest).Range +} diff --git a/services/public/object/ranges_test.go b/services/public/object/ranges_test.go new file mode 100644 index 0000000000..57d6d2e82b --- /dev/null +++ b/services/public/object/ranges_test.go @@ -0,0 +1,778 @@ +package object + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/objio" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testRangeEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + RangeChopper + object.Service_GetRangeServer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ objio.RelativeReceiver = (*testRangeEntity)(nil) + _ RangeChopper = (*testRangeEntity)(nil) + _ operationExecutor = (*testRangeEntity)(nil) + _ requestHandler = (*testRangeEntity)(nil) + _ rangeRevealer = (*testRangeEntity)(nil) + _ objectRangeReceiver = (*testRangeEntity)(nil) + _ object.Service_GetRangeServer = (*testRangeEntity)(nil) + _ responsePreparer = (*testRangeEntity)(nil) +) + +func (s *testRangeEntity) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + if s.f != nil { + s.f(req, resp) + } + return s.err +} + +func (s *testRangeEntity) Context() context.Context { return context.TODO() } + +func (s *testRangeEntity) Send(r *GetRangeResponse) error { + if s.f != nil { + s.f(r) + } + return s.err +} + +func (s *testRangeEntity) getRange(_ context.Context, t rangeTool) (interface{}, error) { + if s.f != nil { + s.f(t) + } + return s.res, s.err +} + +func (s *testRangeEntity) reveal(_ context.Context, r *RangeDescriptor) ([]RangeDescriptor, error) { + if s.f != nil { + s.f(r) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]RangeDescriptor), nil +} + +func (s *testRangeEntity) Base(ctx context.Context, addr Address) (RangeDescriptor, error) { + if s.f != nil { + s.f(addr) + } + if s.err != nil { + return RangeDescriptor{}, s.err + } + return s.res.(RangeDescriptor), nil +} + +func (s *testRangeEntity) Neighbor(ctx context.Context, addr Address, left bool) (RangeDescriptor, error) { + if s.f != nil { + s.f(addr, left) + } + if s.err != nil { + return RangeDescriptor{}, s.err + } + return s.res.(RangeDescriptor), nil +} + +func (s *testRangeEntity) Chop(ctx context.Context, length, offset int64, fromStart bool) ([]RangeDescriptor, error) { + if s.f != nil { + s.f(length, offset, fromStart) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]RangeDescriptor), nil +} + +func (s *testRangeEntity) Closed() bool { return s.res.(bool) } + +func (s *testRangeEntity) PutChopper(addr Address, chopper RangeChopper) error { + if s.f != nil { + s.f(addr, chopper) + } + return s.err +} + +func (s *testRangeEntity) GetChopper(addr Address, rc objio.RCType) (RangeChopper, error) { + if s.f != nil { + s.f(addr, rc) + } + if s.err != nil { + return nil, s.err + } + return s.res.(RangeChopper), nil +} + +func (s *testRangeEntity) executeOperation(_ context.Context, i transport.MetaInfo, h responseItemHandler) error { + if s.f != nil { + s.f(i, h) + } + return s.err +} + +func (s *testRangeEntity) handleRequest(_ context.Context, p handleRequestParams) (interface{}, error) { + if s.f != nil { + s.f(p) + } + return s.res, s.err +} + +func Test_objectService_GetRange(t *testing.T) { + req := &GetRangeRequest{Address: testObjectAddress(t)} + + t.Run("request handler error", func(t *testing.T) { + rhErr := internal.Error("test error for request handler") + + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testRangeEntity{ + f: func(items ...interface{}) { + t.Run("correct request handler params", func(t *testing.T) { + p := items[0].(handleRequestParams) + require.Equal(t, s, p.executor) + require.Equal(t, req, p.request) + }) + }, + err: rhErr, // force requestHandler to return rhErr + } + + // ascertain that error returns as expected + require.EqualError(t, s.GetRange(req, new(testRangeEntity)), rhErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + fragment := testData(t, 10) + + resp := &GetRangeResponse{Fragment: fragment} + + s := objectService{ + requestHandler: &testRangeEntity{ + res: bytes.NewReader(fragment), // force requestHandler to return fragment + }, + rangeChunkPreparer: &testRangeEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, makeRangeResponse(fragment), items[1]) + }, + res: resp, + }, + + statusCalculator: newStatusCalculator(), + } + + srv := &testRangeEntity{ + f: func(items ...interface{}) { + require.Equal(t, resp, items[0]) + }, + } + + require.NoError(t, s.GetRange(req, srv)) + }) +} + +func Test_objectService_GetRangeHash(t *testing.T) { + ctx := context.TODO() + + req := &GetRangeHashRequest{Address: testObjectAddress(t)} + + t.Run("request handler error", func(t *testing.T) { + rhErr := internal.Error("test error for request handler") + + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testRangeEntity{ + f: func(items ...interface{}) { + t.Run("correct request handler params", func(t *testing.T) { + p := items[0].(handleRequestParams) + require.Equal(t, s, p.executor) + require.Equal(t, req, p.request) + }) + }, + err: rhErr, // force requestHandler to return rhErr + } + + // ascertain that error returns as expected + res, err := s.GetRangeHash(ctx, req) + require.EqualError(t, err, rhErr.Error()) + require.Nil(t, res) + }) + + t.Run("correct result", func(t *testing.T) { + hCount := 5 + hashes := make([]Hash, 0, hCount) + + for i := 0; i < hCount; i++ { + hashes = append(hashes, hash.Sum(testData(t, 10))) + } + + s := objectService{ + requestHandler: &testRangeEntity{ + res: hashes, // force requestHandler to return fragments + }, + respPreparer: &testRangeEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, makeRangeHashResponse(hashes), items[1]) + }, + res: &GetRangeHashResponse{Hashes: hashes}, + }, + + statusCalculator: newStatusCalculator(), + } + + res, err := s.GetRangeHash(ctx, req) + require.NoError(t, err) + require.Equal(t, hashes, res.Hashes) + }) +} + +func Test_coreRangeReceiver(t *testing.T) { + ctx := context.TODO() + log := zap.L() + + t.Run("range reveal failure", func(t *testing.T) { + revErr := internal.Error("test error for range revealer") + + rt := newRawRangeHashInfo() + rt.setTTL(service.NonForwardingTTL) + rt.setAddress(testObjectAddress(t)) + rt.setRanges([]Range{ + { + Offset: 1, + Length: 2, + }, + }) + + revealer := &testRangeEntity{ + f: func(items ...interface{}) { + require.Equal(t, &RangeDescriptor{ + Size: int64(rt.rngList[0].Length), + Offset: int64(rt.rngList[0].Offset), + Addr: rt.addr, + }, items[0]) + }, + err: revErr, + } + + s := &coreRangeReceiver{ + rngRevealer: revealer, + log: log, + } + + res, err := s.getRange(ctx, rt) + require.EqualError(t, err, errPayloadRangeNotFound.Error()) + require.Nil(t, res) + + revealer.err = nil + revealer.res = make([]RangeDescriptor, 0) + + res, err = s.getRange(ctx, rt) + require.EqualError(t, err, errPayloadRangeNotFound.Error()) + require.Nil(t, res) + }) + + t.Run("get sub range failure", func(t *testing.T) { + gErr := internal.Error("test error for get range") + + rt := newRawRangeHashInfo() + rt.setTTL(service.NonForwardingTTL) + rt.setAddress(testObjectAddress(t)) + rt.setRanges([]Range{ + { + Offset: 1, + Length: 2, + }, + }) + + revealer := &testRangeEntity{ + res: []RangeDescriptor{{Size: 3, Offset: 4, Addr: testObjectAddress(t)}}, + } + + called := false + revealer.f = func(items ...interface{}) { + if called { + revealer.err = gErr + return + } + called = true + } + + s := &coreRangeReceiver{ + rngRevealer: revealer, + log: log, + } + + res, err := s.getRange(ctx, rt) + require.EqualError(t, err, errPayloadRangeNotFound.Error()) + require.Nil(t, res) + }) + + t.Run("non-forwarding behavior", func(t *testing.T) { + rt := newRawRangeHashInfo() + rt.setTTL(service.NonForwardingTTL - 1) + rt.setAddress(testObjectAddress(t)) + rt.setRanges([]Range{ + { + Offset: 1, + Length: 2, + }, + }) + + rd := RangeDescriptor{ + Size: int64(rt.rngList[0].Length), + Offset: int64(rt.rngList[0].Offset), + Addr: rt.addr, + } + + d := hash.Sum(testData(t, 10)) + + s := &coreRangeReceiver{ + straightRngRecv: &testRangeEntity{ + f: func(items ...interface{}) { + require.Equal(t, rt.budOff(&rd), items[0]) + }, + res: d, + }, + } + + res, err := s.getRange(ctx, rt) + require.NoError(t, err) + require.Equal(t, d, res) + }) + + t.Run("correct result concat", func(t *testing.T) { + rt := newRawRangeHashInfo() + rt.setTTL(service.NonForwardingTTL) + rt.setRanges([]Range{ + {}, + }) + + revealer := new(testRangeEntity) + revCalled := false + revealer.f = func(items ...interface{}) { + if revCalled { + revealer.res = []RangeDescriptor{items[0].(RangeDescriptor)} + } else { + revealer.res = make([]RangeDescriptor, 2) + } + revCalled = true + } + + h1, h2 := hash.Sum(testData(t, 10)), hash.Sum(testData(t, 10)) + + recvCalled := false + receiver := new(testRangeEntity) + receiver.f = func(...interface{}) { + if recvCalled { + receiver.res = h2 + } else { + receiver.res = h1 + } + recvCalled = true + } + + s := &coreRangeReceiver{ + rngRevealer: revealer, + straightRngRecv: receiver, + } + + exp, err := hash.Concat([]Hash{h1, h2}) + require.NoError(t, err) + + res, err := s.getRange(ctx, rt) + require.NoError(t, err) + require.Equal(t, exp, res) + }) +} + +func Test_straightRangeReceiver_getRange(t *testing.T) { + ctx := context.TODO() + + req := new(transportRequest) + + t.Run("executor error", func(t *testing.T) { + exErr := internal.Error("test error for executor") + + s := &straightRangeReceiver{ + executor: &testRangeEntity{ + f: func(items ...interface{}) { + t.Run("correct executor params", func(t *testing.T) { + require.Equal(t, req, items[0]) + require.Equal(t, newSingleItemHandler(), items[1]) + }) + }, + err: exErr, // force operationExecutor to return exErr + }, + } + + res, err := s.getRange(ctx, req) + require.EqualError(t, err, exErr.Error()) + require.Nil(t, res) + }) + + t.Run("correct result", func(t *testing.T) { + v := testData(t, 10) + + s := &straightRangeReceiver{ + executor: &testRangeEntity{ + f: func(items ...interface{}) { + items[1].(rangeItemAccumulator).handleItem(v) + }, + err: nil, // force operationExecutor to return nil error + }, + } + + res, err := s.getRange(ctx, req) + require.NoError(t, err) + require.Equal(t, v, res) + }) +} + +func Test_coreRngRevealer_reveal(t *testing.T) { + ctx := context.TODO() + + rd := RangeDescriptor{ + Size: 5, + Offset: 6, + Addr: testObjectAddress(t), + } + + t.Run("charybdis chopper presence", func(t *testing.T) { + cErr := internal.Error("test error for charybdis") + + s := &coreRngRevealer{ + chopTable: &testRangeEntity{ + f: func(items ...interface{}) { + t.Run("correct chopper table params", func(t *testing.T) { + require.Equal(t, rd.Addr, items[0]) + require.Equal(t, objio.RCCharybdis, items[1]) + }) + }, + res: &testRangeEntity{ + f: func(items ...interface{}) { + t.Run("correct chopper params", func(t *testing.T) { + require.Equal(t, rd.Size, items[0]) + require.Equal(t, rd.Offset, items[1]) + require.True(t, items[2].(bool)) + }) + }, + res: true, // close chopper + err: cErr, // force RangeChopper to return cErr + }, + }, + } + + res, err := s.reveal(ctx, &rd) + require.EqualError(t, err, cErr.Error()) + require.Empty(t, res) + }) + + t.Run("scylla chopper presence", func(t *testing.T) { + scErr := internal.Error("test error for scylla") + + scylla := &testRangeEntity{ + err: scErr, // force RangeChopper to return scErr + } + + ct := new(testRangeEntity) + + ct.f = func(items ...interface{}) { + if items[1].(objio.RCType) == objio.RCCharybdis { + ct.err = internal.Error("") + } else { + ct.res = scylla + ct.err = nil + } + } + + s := &coreRngRevealer{ + chopTable: ct, + } + + res, err := s.reveal(ctx, &rd) + require.EqualError(t, err, scErr.Error()) + require.Empty(t, res) + }) + + t.Run("new scylla", func(t *testing.T) { + t.Run("error", func(t *testing.T) { + s := &coreRngRevealer{ + relativeRecv: nil, // pass empty relation receiver to fail constructor + chopTable: &testRangeEntity{ + err: internal.Error(""), // force ChopperTable to return non-nil error + }, + } + + res, err := s.reveal(ctx, &rd) + require.Error(t, err) + require.Nil(t, res) + }) + + t.Run("success", func(t *testing.T) { + rrErr := internal.Error("test error for relative receiver") + + relRecv := &testRangeEntity{ + err: rrErr, // force relative receiver to return rrErr + } + + scylla, err := objio.NewScylla(&objio.ChopperParams{ + RelativeReceiver: relRecv, + Addr: rd.Addr, + }) + require.NoError(t, err) + + callNum := 0 + + s := &coreRngRevealer{ + relativeRecv: relRecv, + chopTable: &testRangeEntity{ + f: func(items ...interface{}) { + t.Run("correct put chopper params", func(t *testing.T) { + if callNum >= 2 { + require.Equal(t, rd.Addr, items[0]) + require.Equal(t, scylla, items[1]) + } + }) + }, + err: internal.Error(""), // force ChopperTable to return non-nil error + }, + } + + expRes, expErr := scylla.Chop(ctx, rd.Size, rd.Offset, true) + require.Error(t, expErr) + + res, err := s.reveal(ctx, &rd) + require.EqualError(t, err, expErr.Error()) + require.Equal(t, expRes, res) + }) + }) +} + +func Test_transportRequest_rangeTool(t *testing.T) { + t.Run("get ranges", func(t *testing.T) { + rngs := []Range{ + {Offset: 1, Length: 2}, + {Offset: 3, Length: 4}, + } + + reqs := []transportRequest{ + {serviceRequest: &GetRangeHashRequest{Ranges: rngs}}, + } + + for i := range reqs { + require.Equal(t, reqs[i].GetRanges(), rngs) + } + }) + + t.Run("bud off", func(t *testing.T) { + var ( + timeout = 6 * time.Second + ttl = uint32(16) + rd = RangeDescriptor{ + Size: 1, + Offset: 2, + Addr: testObjectAddress(t), + } + ) + + t.Run("get range hash request", func(t *testing.T) { + salt := testData(t, 10) + + r := &GetRangeHashRequest{Salt: salt} + r.SetToken(new(service.Token)) + + req := &transportRequest{ + serviceRequest: r, + timeout: timeout, + } + req.SetTTL(ttl) + + tool := req.budOff(&rd).(transport.RangeHashInfo) + + require.Equal(t, timeout, tool.GetTimeout()) + require.Equal(t, ttl, tool.GetTTL()) + require.Equal(t, rd.Addr, tool.GetAddress()) + require.Equal(t, []Range{{Offset: uint64(rd.Offset), Length: uint64(rd.Size)}}, tool.GetRanges()) + require.Equal(t, salt, tool.GetSalt()) + require.Equal(t, r.GetSessionToken(), tool.GetSessionToken()) + }) + }) + + t.Run("handler", func(t *testing.T) { + t.Run("get range request", func(t *testing.T) { + req := &transportRequest{serviceRequest: new(GetRangeHashRequest)} + handler := req.handler() + require.Equal(t, new(rangeHashAccum), handler) + }) + }) +} + +func Test_rawRangeHashInfo(t *testing.T) { + t.Run("get ranges", func(t *testing.T) { + rngs := []Range{ + {Offset: 1, Length: 2}, + {Offset: 3, Length: 4}, + } + + r := newRawRangeHashInfo() + r.setRanges(rngs) + + require.Equal(t, rngs, r.GetRanges()) + }) + + t.Run("handler", func(t *testing.T) { + require.Equal(t, + &rangeHashAccum{concat: true}, + newRawRangeHashInfo().handler(), + ) + }) + + t.Run("bud off", func(t *testing.T) { + var ( + ttl = uint32(12) + timeout = 7 * time.Hour + ) + + r := newRawRangeHashInfo() + r.setTTL(ttl) + r.setTimeout(timeout) + r.setSalt(testData(t, 20)) + r.setSessionToken(new(service.Token)) + + rd := RangeDescriptor{ + Size: 120, + Offset: 71, + Addr: testObjectAddress(t), + } + + tool := r.budOff(&rd) + + require.Equal(t, ttl, tool.GetTTL()) + require.Equal(t, timeout, tool.GetTimeout()) + require.Equal(t, rd.Addr, tool.GetAddress()) + require.Equal(t, []Range{{Offset: uint64(rd.Offset), Length: uint64(rd.Size)}}, tool.GetRanges()) + require.Equal(t, r.GetSessionToken(), tool.GetSessionToken()) + require.Equal(t, + loopData(r.salt, int64(len(r.salt)), rd.Offset), + tool.(transport.RangeHashInfo).GetSalt(), + ) + }) +} + +func Test_rawRangeInfo(t *testing.T) { + t.Run("get ranges", func(t *testing.T) { + rng := Range{Offset: 1, Length: 2} + + r := newRawRangeInfo() + r.setRange(rng) + + require.Equal(t, rng, r.GetRange()) + }) +} + +func Test_loopSalt(t *testing.T) { + t.Run("empty data", func(t *testing.T) { + require.Empty(t, loopData(nil, 20, 10)) + require.Empty(t, loopData(make([]byte, 0), 20, 10)) + }) + + t.Run("data part", func(t *testing.T) { + var ( + off, size int64 = 10, 20 + d = testData(t, 40) + ) + require.Equal(t, d[off:off+size], loopData(d, size, off)) + }) + + t.Run("with recycle", func(t *testing.T) { + var ( + d = testData(t, 40) + off = int64(len(d) / 2) + size = 2 * off + ) + + require.Equal(t, + append(d[off:], d[:size-off]...), + loopData(d, size, off), + ) + }) +} + +func Test_rangeHashAccum(t *testing.T) { + t.Run("handle item", func(t *testing.T) { + s := &rangeHashAccum{ + h: []Hash{hash.Sum(testData(t, 10))}, + } + + h := hash.Sum(testData(t, 10)) + + exp := append(s.h, h) + + s.handleItem(h) + + require.Equal(t, exp, s.h) + + exp = append(s.h, s.h...) + + s.handleItem(s.h) + + require.Equal(t, exp, s.h) + }) + + t.Run("collect", func(t *testing.T) { + hashes := []Hash{hash.Sum(testData(t, 10)), hash.Sum(testData(t, 10))} + + t.Run("w/ concat", func(t *testing.T) { + s := &rangeHashAccum{ + concat: true, + h: hashes, + } + + expRes, expErr := hash.Concat(hashes) + + res, err := s.collect() + + require.Equal(t, expRes, res) + require.Equal(t, expErr, err) + }) + + t.Run("w/o concat", func(t *testing.T) { + s := &rangeHashAccum{ + concat: false, + h: hashes, + } + + res, err := s.collect() + require.NoError(t, err) + require.Equal(t, hashes, res) + }) + }) +} diff --git a/services/public/object/response.go b/services/public/object/response.go new file mode 100644 index 0000000000..37f086764e --- /dev/null +++ b/services/public/object/response.go @@ -0,0 +1,144 @@ +package object + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/acl" + "github.com/nspcc-dev/neofs-api-go/object" + libacl "github.com/nspcc-dev/neofs-node/lib/acl" +) + +type ( + serviceResponse interface { + SetEpoch(uint64) + } + + responsePreparer interface { + prepareResponse(context.Context, serviceRequest, serviceResponse) error + } + + epochResponsePreparer struct { + epochRecv EpochReceiver + } +) + +type complexResponsePreparer struct { + items []responsePreparer +} + +type aclResponsePreparer struct { + eaclSrc libacl.ExtendedACLSource + + aclInfoReceiver aclInfoReceiver + + reqActCalc requestActionCalculator +} + +type headersFromObject struct { + obj *Object +} + +var ( + _ responsePreparer = (*epochResponsePreparer)(nil) +) + +func (s headersFromObject) getHeaders() (*Object, bool) { + return s.obj, true +} + +func (s complexResponsePreparer) prepareResponse(ctx context.Context, req serviceRequest, resp serviceResponse) error { + for i := range s.items { + if err := s.items[i].prepareResponse(ctx, req, resp); err != nil { + return err + } + } + + return nil +} + +func (s *epochResponsePreparer) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + resp.SetEpoch(s.epochRecv.Epoch()) + + return nil +} + +func (s *aclResponsePreparer) prepareResponse(ctx context.Context, req serviceRequest, resp serviceResponse) error { + aclInfo, err := s.aclInfoReceiver.getACLInfo(ctx, req) + if err != nil { + return errAccessDenied + } else if !aclInfo.checkBearer && !aclInfo.checkExtended { + return nil + } + + var obj *Object + + switch r := resp.(type) { + case *object.GetResponse: + obj = r.GetObject() + case *object.HeadResponse: + obj = r.GetObject() + case interface { + GetObject() *Object + }: + obj = r.GetObject() + } + + if obj == nil { + return nil + } + + // FIXME: do not check request headers. + // At this stage request is already validated, but action calculator will check it again. + p := requestActionParams{ + eaclSrc: s.eaclSrc, + request: req, + objHdrSrc: headersFromObject{ + obj: obj, + }, + target: aclInfo.target, + } + + if aclInfo.checkBearer { + p.eaclSrc = eaclFromBearer{ + bearer: req.GetBearerToken(), + } + } + + if action := s.reqActCalc.calculateRequestAction(ctx, p); action != acl.ActionAllow { + return errAccessDenied + } + + return nil +} + +func makeDeleteResponse() *object.DeleteResponse { + return new(object.DeleteResponse) +} + +func makeRangeHashResponse(v []Hash) *GetRangeHashResponse { + return &GetRangeHashResponse{Hashes: v} +} + +func makeRangeResponse(v []byte) *GetRangeResponse { + return &GetRangeResponse{Fragment: v} +} + +func makeSearchResponse(v []Address) *object.SearchResponse { + return &object.SearchResponse{Addresses: v} +} + +func makeHeadResponse(v *Object) *object.HeadResponse { + return &object.HeadResponse{Object: v} +} + +func makePutResponse(v Address) *object.PutResponse { + return &object.PutResponse{Address: v} +} + +func makeGetHeaderResponse(v *Object) *object.GetResponse { + return &object.GetResponse{R: &object.GetResponse_Object{Object: v}} +} + +func makeGetChunkResponse(v []byte) *object.GetResponse { + return &object.GetResponse{R: &object.GetResponse_Chunk{Chunk: v}} +} diff --git a/services/public/object/response_test.go b/services/public/object/response_test.go new file mode 100644 index 0000000000..5057029ab1 --- /dev/null +++ b/services/public/object/response_test.go @@ -0,0 +1,116 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/stretchr/testify/require" +) + +func TestEpochResponsePreparer(t *testing.T) { + epoch := uint64(33) + + s := &epochResponsePreparer{ + epochRecv: &testPutEntity{res: epoch}, + } + + ctx := context.TODO() + + t.Run("get", func(t *testing.T) { + t.Run("head", func(t *testing.T) { + obj := &Object{ + SystemHeader: SystemHeader{ + ID: testObjectAddress(t).ObjectID, + CID: testObjectAddress(t).CID, + }, + } + + resp := makeGetHeaderResponse(obj) + + require.NoError(t, s.prepareResponse(ctx, new(object.GetRequest), resp)) + + require.Equal(t, obj, resp.GetObject()) + require.Equal(t, epoch, resp.GetEpoch()) + }) + + t.Run("chunk", func(t *testing.T) { + chunk := testData(t, 10) + + resp := makeGetChunkResponse(chunk) + + require.NoError(t, s.prepareResponse(ctx, new(object.GetRequest), resp)) + + require.Equal(t, chunk, resp.GetChunk()) + require.Equal(t, epoch, resp.GetEpoch()) + }) + }) + + t.Run("put", func(t *testing.T) { + addr := testObjectAddress(t) + + resp := makePutResponse(addr) + require.NoError(t, s.prepareResponse(ctx, new(object.PutRequest), resp)) + + require.Equal(t, addr, resp.GetAddress()) + require.Equal(t, epoch, resp.GetEpoch()) + }) + + t.Run("head", func(t *testing.T) { + obj := &Object{ + SystemHeader: SystemHeader{ + PayloadLength: 7, + ID: testObjectAddress(t).ObjectID, + CID: testObjectAddress(t).CID, + }, + } + + resp := makeHeadResponse(obj) + require.NoError(t, s.prepareResponse(ctx, new(object.HeadRequest), resp)) + + require.Equal(t, obj, resp.GetObject()) + require.Equal(t, epoch, resp.GetEpoch()) + }) + + t.Run("search", func(t *testing.T) { + addrList := testAddrList(t, 5) + + resp := makeSearchResponse(addrList) + require.NoError(t, s.prepareResponse(ctx, new(object.SearchRequest), resp)) + + require.Equal(t, addrList, resp.GetAddresses()) + require.Equal(t, epoch, resp.GetEpoch()) + }) + + t.Run("range", func(t *testing.T) { + data := testData(t, 10) + + resp := makeRangeResponse(data) + require.NoError(t, s.prepareResponse(ctx, new(GetRangeRequest), resp)) + + require.Equal(t, data, resp.GetFragment()) + require.Equal(t, epoch, resp.GetEpoch()) + }) + + t.Run("range hash", func(t *testing.T) { + hashes := []Hash{ + hash.Sum(testData(t, 10)), + hash.Sum(testData(t, 10)), + } + + resp := makeRangeHashResponse(hashes) + require.NoError(t, s.prepareResponse(ctx, new(object.GetRangeHashRequest), resp)) + + require.Equal(t, hashes, resp.Hashes) + require.Equal(t, epoch, resp.GetEpoch()) + }) + + t.Run("delete", func(t *testing.T) { + resp := makeDeleteResponse() + require.NoError(t, s.prepareResponse(ctx, new(object.DeleteRequest), resp)) + + require.IsType(t, new(object.DeleteResponse), resp) + require.Equal(t, epoch, resp.GetEpoch()) + }) +} diff --git a/services/public/object/search.go b/services/public/object/search.go new file mode 100644 index 0000000000..39771ddd63 --- /dev/null +++ b/services/public/object/search.go @@ -0,0 +1,169 @@ +package object + +import ( + "context" + "sync" + + "github.com/nspcc-dev/neofs-api-go/object" + v1 "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-node/lib/transport" + "go.uber.org/zap" +) + +// QueryFilter is a type alias of +// Filter from query package of neofs-api-go. +type QueryFilter = v1.Filter + +const ( + // KeyChild is a filter key to child link. + KeyChild = "CHILD" + + // KeyPrev is a filter key to previous link. + KeyPrev = "PREV" + + // KeyNext is a filter key to next link. + KeyNext = "NEXT" + + // KeyID is a filter key to object ID. + KeyID = "ID" + + // KeyCID is a filter key to container ID. + KeyCID = "CID" + + // KeyOwnerID is a filter key to owner ID. + KeyOwnerID = "OWNERID" + + // KeyRootObject is a filter key to objects w/o parent links. + KeyRootObject = "ROOT_OBJECT" +) + +type ( + objectSearcher interface { + searchObjects(context.Context, transport.SearchInfo) ([]Address, error) + } + + coreObjectSearcher struct { + executor operationExecutor + } + + // objectAddressSet is and interface of object address set. + objectAddressSet interface { + responseItemHandler + + // list returns all elements of set. + list() []Address + } + + // coreObjAddrSet is and implementation of objectAddressSet interface used in Object service production. + coreObjAddrSet struct { + // Read-write mutex for race protection. + *sync.RWMutex + + // Storing element of set. + items []Address + } +) + +var addrPerMsg = int64(maxGetPayloadSize / new(Address).Size()) + +var ( + _ transport.SearchInfo = (*transportRequest)(nil) + _ objectSearcher = (*coreObjectSearcher)(nil) + _ objectAddressSet = (*coreObjAddrSet)(nil) +) + +func (s *transportRequest) GetCID() CID { return s.serviceRequest.(*object.SearchRequest).CID() } + +func (s *transportRequest) GetQuery() []byte { + return s.serviceRequest.(*object.SearchRequest).GetQuery() +} + +func (s *objectService) Search(req *object.SearchRequest, srv object.Service_SearchServer) (err error) { + defer func() { + if r := recover(); r != nil { + s.log.Error(panicLogMsg, + zap.Stringer("request", object.RequestSearch), + zap.Any("reason", r), + ) + + err = errServerPanic + } + + err = s.statusCalculator.make(requestError{ + t: object.RequestSearch, + e: err, + }) + }() + + var r interface{} + + if r, err = s.requestHandler.handleRequest(srv.Context(), handleRequestParams{ + request: req, + executor: s, + }); err != nil { + return err + } + + addrList := r.([]Address) + + for { + cut := min(int64(len(addrList)), addrPerMsg) + + resp := makeSearchResponse(addrList[:cut]) + if err = s.respPreparer.prepareResponse(srv.Context(), req, resp); err != nil { + return + } + + if err = srv.Send(resp); err != nil { + return + } + + addrList = addrList[cut:] + if len(addrList) == 0 { + break + } + } + + return err +} + +func (s *coreObjectSearcher) searchObjects(ctx context.Context, sInfo transport.SearchInfo) ([]Address, error) { + addrSet := newUniqueAddressAccumulator() + if err := s.executor.executeOperation(ctx, sInfo, addrSet); err != nil { + return nil, err + } + + return addrSet.list(), nil +} + +func newUniqueAddressAccumulator() objectAddressSet { + return &coreObjAddrSet{ + RWMutex: new(sync.RWMutex), + items: make([]Address, 0, 10), + } +} + +func (s *coreObjAddrSet) handleItem(v interface{}) { + addrList := v.([]Address) + + s.Lock() + +loop: + for i := range addrList { + for j := range s.items { + if s.items[j].Equal(&addrList[i]) { + continue loop + } + } + s.items = append(s.items, addrList[i]) + } + + s.Unlock() +} + +func (s *coreObjAddrSet) list() []Address { + s.RLock() + defer s.RUnlock() + + return s.items +} diff --git a/services/public/object/search_test.go b/services/public/object/search_test.go new file mode 100644 index 0000000000..dc65edef58 --- /dev/null +++ b/services/public/object/search_test.go @@ -0,0 +1,265 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + v1 "github.com/nspcc-dev/neofs-api-go/query" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testSearchEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + object.Service_SearchServer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ requestHandler = (*testSearchEntity)(nil) + _ operationExecutor = (*testSearchEntity)(nil) + _ responsePreparer = (*testSearchEntity)(nil) + + _ object.Service_SearchServer = (*testSearchEntity)(nil) +) + +func (s *testSearchEntity) prepareResponse(_ context.Context, req serviceRequest, resp serviceResponse) error { + if s.f != nil { + s.f(req, resp) + } + return s.err +} + +func (s *testSearchEntity) Send(r *object.SearchResponse) error { + if s.f != nil { + s.f(r) + } + return s.err +} + +func (s *testSearchEntity) Context() context.Context { return context.TODO() } + +func (s *testSearchEntity) executeOperation(_ context.Context, p transport.MetaInfo, h responseItemHandler) error { + if s.f != nil { + s.f(p, h) + } + return s.err +} + +func (s *testSearchEntity) handleRequest(_ context.Context, p handleRequestParams) (interface{}, error) { + if s.f != nil { + s.f(p) + } + return s.res, s.err +} + +func TestSearchVerify(t *testing.T) { + t.Run("KeyNoChildren", func(t *testing.T) { + var ( + q = v1.Query{ + Filters: []QueryFilter{ + { + Type: v1.Filter_Exact, + Name: transport.KeyNoChildren, + }, + }, + } + obj = new(Object) + ) + require.True(t, imposeQuery(q, obj)) + + obj.Headers = append(obj.Headers, Header{Value: &object.Header_Link{ + Link: &object.Link{ + Type: object.Link_Child, + }, + }}) + require.False(t, imposeQuery(q, obj)) + }) +} + +func Test_coreObjAddrSet(t *testing.T) { + // create address accumulator + acc := newUniqueAddressAccumulator() + require.NotNil(t, acc) + + // check type correctness + v, ok := acc.(*coreObjAddrSet) + require.True(t, ok) + + // check fields initialization + require.NotNil(t, v.items) + require.NotNil(t, v.RWMutex) + + t.Run("add/list", func(t *testing.T) { + // ascertain that initial list is empty + require.Empty(t, acc.list()) + + // add first set of addresses + addrList1 := testAddrList(t, 5) + acc.handleItem(addrList1) + + // ascertain that list is equal to added list + require.Equal(t, addrList1, acc.list()) + + // add more addresses + addrList2 := testAddrList(t, 5) + acc.handleItem(addrList2) + + twoLists := append(addrList1, addrList2...) + + // ascertain that list is a concatenation of added lists + require.Equal(t, twoLists, acc.list()) + + // add second list again + acc.handleItem(addrList2) + + // ascertain that list have not changed after adding existing elements + require.Equal(t, twoLists, acc.list()) + }) +} + +func TestObjectService_Search(t *testing.T) { + req := &object.SearchRequest{ + ContainerID: testObjectAddress(t).CID, + Query: testData(t, 10), + } + + addrList := testAddrList(t, int(addrPerMsg)+5) + + t.Run("request handler failure", func(t *testing.T) { + rhErr := internal.Error("test error for request handler") + s := &objectService{ + statusCalculator: newStatusCalculator(), + } + + s.requestHandler = &testSearchEntity{ + f: func(items ...interface{}) { + p := items[0].(handleRequestParams) + require.Equal(t, req, p.request) + require.Equal(t, s, p.executor) + }, + err: rhErr, + } + + require.EqualError(t, s.Search(req, new(testSearchEntity)), rhErr.Error()) + }) + + t.Run("server error", func(t *testing.T) { + srvErr := internal.Error("test error for search server") + + resp := &object.SearchResponse{Addresses: addrList[:addrPerMsg]} + + s := &objectService{ + requestHandler: &testSearchEntity{ + res: addrList, + }, + respPreparer: &testSearchEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, makeSearchResponse(addrList[:addrPerMsg]), items[1]) + }, + res: resp, + }, + + statusCalculator: newStatusCalculator(), + } + + srv := &testSearchEntity{ + f: func(items ...interface{}) { + require.Equal(t, resp, items[0]) + }, + err: srvErr, // force server to return srvErr + } + + require.EqualError(t, s.Search(req, srv), srvErr.Error()) + }) + + t.Run("correct result", func(t *testing.T) { + handler := &testSearchEntity{res: make([]Address, 0)} + + off := 0 + + var resp *object.SearchResponse + + s := &objectService{ + requestHandler: handler, + respPreparer: &testSearchEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + resp = items[1].(*object.SearchResponse) + sz := len(resp.GetAddresses()) + require.Equal(t, makeSearchResponse(addrList[off:off+sz]), items[1]) + off += sz + }, + }, + + statusCalculator: newStatusCalculator(), + } + + srv := &testSearchEntity{ + f: func(items ...interface{}) { + require.Equal(t, resp, items[0]) + }, + } + + require.NoError(t, s.Search(req, srv)) + + handler.res = addrList + + require.NoError(t, s.Search(req, srv)) + }) +} + +func Test_coreObjectSearcher(t *testing.T) { + ctx := context.TODO() + + req := newRawSearchInfo() + req.setQuery(testData(t, 10)) + + t.Run("operation executor failure", func(t *testing.T) { + execErr := internal.Error("test error for operation executor") + + s := &coreObjectSearcher{ + executor: &testSearchEntity{ + f: func(items ...interface{}) { + require.Equal(t, req, items[0]) + require.Equal(t, newUniqueAddressAccumulator(), items[1]) + }, + err: execErr, + }, + } + + res, err := s.searchObjects(ctx, req) + require.EqualError(t, err, execErr.Error()) + require.Empty(t, res) + }) + + t.Run("correct result", func(t *testing.T) { + addrList := testAddrList(t, 5) + + s := &coreObjectSearcher{ + executor: &testSearchEntity{ + f: func(items ...interface{}) { + items[1].(responseItemHandler).handleItem(addrList) + }, + }, + } + + res, err := s.searchObjects(ctx, req) + require.NoError(t, err) + require.Equal(t, addrList, res) + }) +} diff --git a/services/public/object/service.go b/services/public/object/service.go new file mode 100644 index 0000000000..87e1200724 --- /dev/null +++ b/services/public/object/service.go @@ -0,0 +1,680 @@ +package object + +import ( + "context" + "crypto/ecdsa" + "math" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/hash" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-api-go/storagegroup" + "github.com/nspcc-dev/neofs-node/internal" + libacl "github.com/nspcc-dev/neofs-node/lib/acl" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/ir" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/objio" + "github.com/nspcc-dev/neofs-node/lib/objutil" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/nspcc-dev/neofs-node/modules/grpc" + "github.com/panjf2000/ants/v2" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // CID is a type alias of + // CID from refs package of neofs-api-go. + CID = refs.CID + + // Object is a type alias of + // Object from object package of neofs-api-go. + Object = object.Object + + // ID is a type alias of + // ObjectID from refs package of neofs-api-go. + ID = refs.ObjectID + + // OwnerID is a type alias of + // OwnerID from refs package of neofs-api-go. + OwnerID = refs.OwnerID + + // Address is a type alias of + // Address from refs package of neofs-api-go. + Address = refs.Address + + // Hash is a type alias of + // Hash from hash package of neofs-api-go. + Hash = hash.Hash + + // Meta is a type alias of + // ObjectMeta from localstore package. + Meta = localstore.ObjectMeta + + // Filter is a type alias of + // FilterPipeline from localstore package. + Filter = localstore.FilterPipeline + + // Header is a type alias of + // Header from object package of neofs-api-go. + Header = object.Header + + // UserHeader is a type alias of + // UserHeader from object package of neofs-api-go. + UserHeader = object.UserHeader + + // SystemHeader is a type alias of + // SystemHeader from object package of neofs-api-go. + SystemHeader = object.SystemHeader + + // CreationPoint is a type alias of + // CreationPoint from object package of neofs-api-go. + CreationPoint = object.CreationPoint + + // Service is an interface of the server of Object service. + Service interface { + grpc.Service + CapacityMeter + object.ServiceServer + } + + // CapacityMeter is an interface of node storage capacity meter. + CapacityMeter interface { + RelativeAvailableCap() float64 + AbsoluteAvailableCap() uint64 + } + + // EpochReceiver is an interface of the container of epoch number with read access. + EpochReceiver interface { + Epoch() uint64 + } + + // RemoteService is an interface of Object service client constructor. + RemoteService interface { + Remote(context.Context, multiaddr.Multiaddr) (object.ServiceClient, error) + } + + // Placer is an interface of placement component. + Placer interface { + IsContainerNode(ctx context.Context, addr multiaddr.Multiaddr, cid CID, previousNetMap bool) (bool, error) + GetNodes(ctx context.Context, addr Address, usePreviousNetMap bool, excl ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) + } + + // WorkerPool is an interface of go-routing pool. + WorkerPool interface { + Submit(func()) error + } + + // Salitor is a salting slice function. + Salitor func(data []byte, salt []byte) []byte + + serviceRequest interface { + object.Request + service.RequestData + service.SignKeyPairAccumulator + service.SignKeyPairSource + + SetToken(*service.Token) + + SetBearer(*service.BearerTokenMsg) + + SetHeaders([]service.RequestExtendedHeader_KV) + } + + // Params groups the parameters of Object service server's constructor. + Params struct { + CheckACL bool + + Assembly bool + + WindowSize int + + MaxProcessingSize uint64 + StorageCapacity uint64 + PoolSize int + Salitor Salitor + LocalStore localstore.Localstore + Placer Placer + ObjectRestorer transformer.ObjectRestorer + RemoteService RemoteService + AddressStore implementations.AddressStoreComponent + Logger *zap.Logger + TokenStore session.PrivateTokenStore + EpochReceiver EpochReceiver + + implementations.ContainerNodesLister + + DialTimeout time.Duration + + Key *ecdsa.PrivateKey + + PutParams OperationParams + GetParams OperationParams + DeleteParams OperationParams + HeadParams OperationParams + SearchParams OperationParams + RangeParams OperationParams + RangeHashParams OperationParams + + headRecv objectReceiver + + Verifier objutil.Verifier + + Transformer transformer.Transformer + + MaxPayloadSize uint64 + + // ACL pre-processor params + ACLHelper implementations.ACLHelper + BasicACLChecker libacl.BasicChecker + IRStorage ir.Storage + ContainerLister implementations.ContainerNodesLister + + SGInfoReceiver storagegroup.InfoReceiver + + OwnerKeyVerifier core.OwnerKeyVerifier + + ExtendedACLSource libacl.ExtendedACLSource + + requestActionCalculator + + targetFinder RequestTargeter + + aclInfoReceiver aclInfoReceiver + } + + // OperationParams groups the parameters of particular object operation. + OperationParams struct { + Timeout time.Duration + LogErrors bool + } + + objectService struct { + ls localstore.Localstore + storageCap uint64 + + executor implementations.SelectiveContainerExecutor + + pPut OperationParams + pGet OperationParams + pDel OperationParams + pHead OperationParams + pSrch OperationParams + pRng OperationParams + pRngHash OperationParams + + log *zap.Logger + + requestHandler requestHandler + + objSearcher objectSearcher + objRecv objectReceiver + objStorer objectStorer + objRemover objectRemover + rngRecv objectRangeReceiver + + payloadRngRecv payloadRangeReceiver + + respPreparer responsePreparer + + getChunkPreparer responsePreparer + rangeChunkPreparer responsePreparer + + statusCalculator *statusCalculator + } +) + +const ( + defaultDialTimeout = 5 * time.Second + defaultPutTimeout = time.Second + defaultGetTimeout = time.Second + defaultDeleteTimeout = time.Second + defaultHeadTimeout = time.Second + defaultSearchTimeout = time.Second + defaultRangeTimeout = time.Second + defaultRangeHashTimeout = time.Second + + defaultPoolSize = 10 + + readyObjectsCheckpointFilterName = "READY_OBJECTS_PUT_CHECKPOINT" + allObjectsCheckpointFilterName = "ALL_OBJECTS_PUT_CHECKPOINT" + + errEmptyTokenStore = internal.Error("objectService.New failed: key store not provided") + errEmptyPlacer = internal.Error("objectService.New failed: placer not provided") + errEmptyTransformer = internal.Error("objectService.New failed: transformer pipeline not provided") + errEmptyGRPC = internal.Error("objectService.New failed: gRPC connector not provided") + errEmptyAddress = internal.Error("objectService.New failed: address store not provided") + errEmptyLogger = internal.Error("objectService.New failed: logger not provided") + errEmptyEpochReceiver = internal.Error("objectService.New failed: epoch receiver not provided") + errEmptyLocalStore = internal.Error("new local client failed: localstore passed") + errEmptyPrivateKey = internal.Error("objectService.New failed: private key not provided") + errEmptyVerifier = internal.Error("objectService.New failed: object verifier not provided") + errEmptyACLHelper = internal.Error("objectService.New failed: ACL helper not provided") + errEmptyBasicACLChecker = internal.Error("objectService.New failed: basic ACL checker not provided") + errEmptyCnrLister = internal.Error("objectService.New failed: container lister not provided") + errEmptySGInfoRecv = internal.Error("objectService.New failed: SG info receiver not provided") + + errInvalidCIDFilter = internal.Error("invalid CID filter") + + errTokenRetrieval = internal.Error("objectService.Put failed on token retrieval") + + errHeaderExpected = internal.Error("expected header as a first message in stream") +) + +var requestSignFunc = service.SignRequestData + +var requestVerifyFunc = core.VerifyRequestWithSignatures + +// New is an Object service server's constructor. +func New(p *Params) (Service, error) { + if p.PutParams.Timeout <= 0 { + p.PutParams.Timeout = defaultPutTimeout + } + + if p.GetParams.Timeout <= 0 { + p.GetParams.Timeout = defaultGetTimeout + } + + if p.DeleteParams.Timeout <= 0 { + p.DeleteParams.Timeout = defaultDeleteTimeout + } + + if p.HeadParams.Timeout <= 0 { + p.HeadParams.Timeout = defaultHeadTimeout + } + + if p.SearchParams.Timeout <= 0 { + p.SearchParams.Timeout = defaultSearchTimeout + } + + if p.RangeParams.Timeout <= 0 { + p.RangeParams.Timeout = defaultRangeTimeout + } + + if p.RangeHashParams.Timeout <= 0 { + p.RangeHashParams.Timeout = defaultRangeHashTimeout + } + + if p.DialTimeout <= 0 { + p.DialTimeout = defaultDialTimeout + } + + if p.PoolSize <= 0 { + p.PoolSize = defaultPoolSize + } + + switch { + case p.TokenStore == nil: + return nil, errEmptyTokenStore + case p.Placer == nil: + return nil, errEmptyPlacer + case p.LocalStore == nil: + return nil, errEmptyLocalStore + case (p.ObjectRestorer == nil || p.Transformer == nil) && p.Assembly: + return nil, errEmptyTransformer + case p.RemoteService == nil: + return nil, errEmptyGRPC + case p.AddressStore == nil: + return nil, errEmptyAddress + case p.Logger == nil: + return nil, errEmptyLogger + case p.EpochReceiver == nil: + return nil, errEmptyEpochReceiver + case p.Key == nil: + return nil, errEmptyPrivateKey + case p.Verifier == nil: + return nil, errEmptyVerifier + case p.IRStorage == nil: + return nil, ir.ErrNilStorage + case p.ContainerLister == nil: + return nil, errEmptyCnrLister + case p.ACLHelper == nil: + return nil, errEmptyACLHelper + case p.BasicACLChecker == nil: + return nil, errEmptyBasicACLChecker + case p.SGInfoReceiver == nil: + return nil, errEmptySGInfoRecv + case p.OwnerKeyVerifier == nil: + return nil, core.ErrNilOwnerKeyVerifier + case p.ExtendedACLSource == nil: + return nil, libacl.ErrNilBinaryExtendedACLStore + } + + pool, err := ants.NewPool(p.PoolSize) + if err != nil { + return nil, errors.Wrap(err, "objectService.New failed: could not create worker pool") + } + + if p.MaxProcessingSize <= 0 { + p.MaxProcessingSize = math.MaxUint64 + } + + if p.StorageCapacity <= 0 { + p.StorageCapacity = math.MaxUint64 + } + + epochRespPreparer := &epochResponsePreparer{ + epochRecv: p.EpochReceiver, + } + + p.targetFinder = &targetFinder{ + log: p.Logger, + irStorage: p.IRStorage, + cnrLister: p.ContainerLister, + cnrOwnerChecker: p.ACLHelper, + } + + p.requestActionCalculator = &reqActionCalc{ + extACLChecker: libacl.NewExtendedACLChecker(), + + log: p.Logger, + } + + p.aclInfoReceiver = aclInfoReceiver{ + basicACLGetter: p.ACLHelper, + + basicChecker: p.BasicACLChecker, + + targetFinder: p.targetFinder, + } + + srv := &objectService{ + ls: p.LocalStore, + log: p.Logger, + pPut: p.PutParams, + pGet: p.GetParams, + pDel: p.DeleteParams, + pHead: p.HeadParams, + pSrch: p.SearchParams, + pRng: p.RangeParams, + pRngHash: p.RangeHashParams, + storageCap: p.StorageCapacity, + + requestHandler: &coreRequestHandler{ + preProc: newPreProcessor(p), + postProc: newPostProcessor(), + }, + + respPreparer: &complexResponsePreparer{ + items: []responsePreparer{ + epochRespPreparer, + &aclResponsePreparer{ + aclInfoReceiver: p.aclInfoReceiver, + + reqActCalc: p.requestActionCalculator, + + eaclSrc: p.ExtendedACLSource, + }, + }, + }, + + getChunkPreparer: epochRespPreparer, + + rangeChunkPreparer: epochRespPreparer, + + statusCalculator: serviceStatusCalculator(), + } + + tr, err := NewMultiTransport(MultiTransportParams{ + AddressStore: p.AddressStore, + EpochReceiver: p.EpochReceiver, + RemoteService: p.RemoteService, + Logger: p.Logger, + Key: p.Key, + PutTimeout: p.PutParams.Timeout, + GetTimeout: p.GetParams.Timeout, + HeadTimeout: p.HeadParams.Timeout, + SearchTimeout: p.SearchParams.Timeout, + RangeHashTimeout: p.RangeHashParams.Timeout, + DialTimeout: p.DialTimeout, + + PrivateTokenStore: p.TokenStore, + }) + if err != nil { + return nil, err + } + + exec, err := implementations.NewContainerTraverseExecutor(tr) + if err != nil { + return nil, err + } + + srv.executor, err = implementations.NewObjectContainerHandler(implementations.ObjectContainerHandlerParams{ + NodeLister: p.ContainerNodesLister, + Executor: exec, + Logger: p.Logger, + }) + if err != nil { + return nil, err + } + + local := &localStoreExecutor{ + salitor: p.Salitor, + epochRecv: p.EpochReceiver, + localStore: p.LocalStore, + } + + qvc := &queryVersionController{ + m: make(map[int]localQueryImposer), + } + + qvc.m[1] = &coreQueryImposer{ + fCreator: new(coreFilterCreator), + lsLister: p.LocalStore, + log: p.Logger, + } + + localExec := &localOperationExecutor{ + objRecv: local, + headRecv: local, + objStore: local, + queryImp: qvc, + rngReader: local, + rngHasher: local, + } + + opExec := &coreOperationExecutor{ + pre: new(coreExecParamsComp), + fin: &coreOperationFinalizer{ + curPlacementBuilder: &corePlacementUtil{ + prevNetMap: false, + placementBuilder: p.Placer, + log: p.Logger, + }, + prevPlacementBuilder: &corePlacementUtil{ + prevNetMap: true, + placementBuilder: p.Placer, + log: p.Logger, + }, + interceptorPreparer: &coreInterceptorPreparer{ + localExec: localExec, + addressStore: p.AddressStore, + }, + workerPool: pool, + traverseExec: exec, + resLogger: &coreResultLogger{ + mLog: requestLogMap(p), + log: p.Logger, + }, + log: p.Logger, + }, + loc: localExec, + } + + srv.objSearcher = &coreObjectSearcher{ + executor: opExec, + } + + childLister := &coreChildrenLister{ + queryFn: coreChildrenQueryFunc, + objSearcher: srv.objSearcher, + log: p.Logger, + timeout: p.SearchParams.Timeout, + } + + childrenRecv := &coreChildrenReceiver{ + timeout: p.HeadParams.Timeout, + } + + chopperTable := objio.NewChopperTable() + + relRecv := &neighborReceiver{ + firstChildQueryFn: firstChildQueryFunc, + leftNeighborQueryFn: leftNeighborQueryFunc, + rightNeighborQueryFn: rightNeighborQueryFunc, + rangeDescRecv: &selectiveRangeRecv{executor: srv.executor}, + } + + straightObjRecv := &straightObjectReceiver{ + executor: opExec, + } + + rngRecv := &corePayloadRangeReceiver{ + chopTable: chopperTable, + relRecv: relRecv, + payloadRecv: &corePayloadPartReceiver{ + rDataRecv: &straightRangeDataReceiver{ + executor: opExec, + }, + windowController: &simpleWindowController{ + windowSize: p.WindowSize, + }, + }, + mErr: map[error]struct{}{ + localstore.ErrOutOfRange: {}, + }, + log: p.Logger, + } + + coreObjRecv := &coreObjectReceiver{ + straightObjRecv: straightObjRecv, + childLister: childLister, + ancestralRecv: &coreAncestralReceiver{ + childrenRecv: childrenRecv, + objRewinder: &coreObjectRewinder{ + transformer: p.ObjectRestorer, + }, + pRangeRecv: rngRecv, + }, + log: p.Logger, + } + childrenRecv.coreObjRecv = coreObjRecv + srv.objRecv = coreObjRecv + srv.payloadRngRecv = rngRecv + + if !p.Assembly { + coreObjRecv.ancestralRecv, coreObjRecv.childLister = nil, nil + } + + p.headRecv = srv.objRecv + + filter, err := newIncomingObjectFilter(p) + if err != nil { + return nil, err + } + + straightStorer := &straightObjectStorer{ + executor: opExec, + } + + bf, err := basicFilter(p) + if err != nil { + return nil, err + } + + transformerObjStorer := &transformingObjectStorer{ + transformer: p.Transformer, + objStorer: straightStorer, + mErr: map[error]struct{}{ + transformer.ErrInvalidSGLinking: {}, + + implementations.ErrIncompleteSGInfo: {}, + }, + } + + srv.objStorer = &filteringObjectStorer{ + filter: bf, + objStorer: &bifurcatingObjectStorer{ + straightStorer: &filteringObjectStorer{ + filter: filter, + objStorer: &receivingObjectStorer{ + straightStorer: straightStorer, + vPayload: implementations.NewPayloadVerifier(), + }, + }, + tokenStorer: &tokenObjectStorer{ + tokenStore: p.TokenStore, + objStorer: transformerObjStorer, + }, + }, + } + + srv.objRemover = &coreObjRemover{ + delPrep: &coreDelPreparer{ + childLister: childLister, + }, + straightRem: &straightObjRemover{ + tombCreator: new(coreTombCreator), + objStorer: transformerObjStorer, + }, + tokenStore: p.TokenStore, + mErr: map[error]struct{}{}, + log: p.Logger, + } + + srv.rngRecv = &coreRangeReceiver{ + rngRevealer: &coreRngRevealer{ + relativeRecv: relRecv, + chopTable: chopperTable, + }, + straightRngRecv: &straightRangeReceiver{ + executor: opExec, + }, + mErr: map[error]struct{}{ + localstore.ErrOutOfRange: {}, + }, + log: p.Logger, + } + + return srv, nil +} + +func requestLogMap(p *Params) map[object.RequestType]struct{} { + m := make(map[object.RequestType]struct{}) + + if p.PutParams.LogErrors { + m[object.RequestPut] = struct{}{} + } + + if p.GetParams.LogErrors { + m[object.RequestGet] = struct{}{} + } + + if p.HeadParams.LogErrors { + m[object.RequestHead] = struct{}{} + } + + if p.SearchParams.LogErrors { + m[object.RequestSearch] = struct{}{} + } + + if p.RangeParams.LogErrors { + m[object.RequestRange] = struct{}{} + } + + if p.RangeHashParams.LogErrors { + m[object.RequestRangeHash] = struct{}{} + } + + return m +} + +func (s *objectService) Name() string { return "Object Service" } + +func (s *objectService) Register(g *grpc.Server) { object.RegisterServiceServer(g, s) } diff --git a/services/public/object/status.go b/services/public/object/status.go new file mode 100644 index 0000000000..f8389c3707 --- /dev/null +++ b/services/public/object/status.go @@ -0,0 +1,951 @@ +package object + +import ( + "fmt" + "sync" + + "github.com/golang/protobuf/proto" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/pkg/errors" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// group of value for status error construction. +type statusInfo struct { + // status code + c codes.Code + // error message + m string + // error details + d []proto.Message +} + +type requestError struct { + // type of request + t object.RequestType + // request handler error + e error +} + +// error implementation used for details attaching. +type detailedError struct { + error + + d []proto.Message +} + +type statusCalculator struct { + *sync.RWMutex + + common map[error]*statusInfo + + custom map[requestError]*statusInfo +} + +const panicLogMsg = "rpc handler caused panic" + +const ( + msgServerPanic = "panic occurred during request processing" + errServerPanic = internal.Error("panic on call handler") +) + +const ( + msgUnauthenticated = "request does not have valid authentication credentials for the operation" + errUnauthenticated = internal.Error("unauthenticated request") +) + +const ( + msgReSigning = "server could not re-sign request" + errReSigning = internal.Error("could not re-sign request") +) + +const ( + msgInvalidTTL = "invalid TTL value" + errInvalidTTL = internal.Error("invalid TTL value") +) + +const ( + msgNotLocalContainer = "server is not presented in container" + errNotLocalContainer = internal.Error("not local container") + descNotLocalContainer = "server is outside container" +) + +const ( + msgContainerAffiliationProblem = "server could not check container affiliation" + errContainerAffiliationProblem = internal.Error("could not check container affiliation") +) + +const ( + msgContainerNotFound = "container not found" + errContainerNotFound = internal.Error("container not found") + descContainerNotFound = "handling a non-existent container" +) + +const ( + msgPlacementProblem = "there were problems building the placement vector on the server" + errPlacementProblem = internal.Error("could not traverse over container") +) + +const ( + msgOverloaded = "system resource overloaded" + errOverloaded = internal.Error("system resource overloaded") +) + +const ( + msgAccessDenied = "access to requested operation is denied" + errAccessDenied = internal.Error("access denied") +) + +const ( + msgPutMessageProblem = "invalid message type" + msgPutNilObject = "object is null" +) + +const ( + msgCutObjectPayload = "lack of object payload data" +) + +const ( + msgMissingTokenKeys = "missing public keys in token" + msgBrokenToken = "token structure failed verification" + msgTokenObjectID = "missing object ID in token" +) + +const ( + msgProcPayloadSize = "max payload size of processing object overflow" + errProcPayloadSize = internal.Error("max processing object payload size overflow") +) + +const ( + msgObjectCreationEpoch = "invalid creation epoch of object" + errObjectFromTheFuture = internal.Error("object from the future") +) + +const ( + msgObjectPayloadSize = "max object payload size overflow" + errObjectPayloadSize = internal.Error("max object payload size overflow") +) + +const ( + msgLocalStorageOverflow = "not enough space in local storage" + errLocalStorageOverflow = internal.Error("local storage overflow") +) + +const ( + msgPayloadChecksum = "invalid payload checksum" + errPayloadChecksum = internal.Error("invalid payload checksum") +) + +const ( + msgObjectHeadersVerification = "object headers failed verification" + errObjectHeadersVerification = internal.Error("object headers failed verification") +) + +const ( + msgForwardPutObject = "forward object failure" +) + +const ( + msgPutLocalFailure = "local object put failure" + errPutLocal = internal.Error("local object put failure") +) + +const ( + msgPrivateTokenRecv = "private token receive failure" +) + +const ( + msgInvalidSGLinking = "invalid storage group headers" +) + +const ( + msgIncompleteSGInfo = "collect storage group info failure" +) + +const ( + msgTransformationFailure = "object preparation failure" +) + +const ( + msgWrongSGSize = "wrong storage group size" + errWrongSGSize = internal.Error("wrong storage group size") +) + +const ( + msgWrongSGHash = "wrong storage group homomorphic hash" + errWrongSGHash = internal.Error("wrong storage group homomorphic hash") +) + +const ( + msgObjectNotFound = "object not found" +) + +const ( + msgObjectHeaderNotFound = "object header not found" +) + +const ( + msgNonAssembly = "assembly option is not enabled on the server" +) + +const ( + msgPayloadOutOfRange = "range is out of object payload bounds" +) + +const ( + msgPayloadRangeNotFound = "object payload range not found" + errPayloadRangeNotFound = internal.Error("object payload range not found") +) + +const ( + msgMissingToken = "missing token in request" +) + +const ( + msgPutTombstone = "could not store tombstone" +) + +const ( + msgDeletePrepare = "delete information preparation failure" + errDeletePrepare = internal.Error("delete information preparation failure") +) + +const ( + msgQueryVersion = "unsupported query version" +) + +const ( + msgSearchQueryUnmarshal = "query unmarshal failure" +) + +const ( + msgLocalQueryImpose = "local query imposing failure" +) + +var mStatusCommon = map[error]*statusInfo{ + // RPC implementation recovered panic + errServerPanic: { + c: codes.Internal, + m: msgServerPanic, + }, + // Request authentication credentials problem + errUnauthenticated: { + c: codes.Unauthenticated, + m: msgUnauthenticated, + d: requestAuthDetails(), + }, + // Request re-signing problem + errReSigning: { + c: codes.Internal, + m: msgReSigning, + }, + // Invalid request TTL + errInvalidTTL: { + c: codes.InvalidArgument, + m: msgInvalidTTL, + d: invalidTTLDetails(), + }, + // Container affiliation check problem + errContainerAffiliationProblem: { + c: codes.Internal, + m: msgContainerAffiliationProblem, + }, + // Server is outside container + errNotLocalContainer: { + c: codes.FailedPrecondition, + m: msgNotLocalContainer, + d: containerAbsenceDetails(), + }, + // Container not found in storage + errContainerNotFound: { + c: codes.NotFound, + m: msgContainerNotFound, + }, + // Container placement build problem + errPlacementProblem: { + c: codes.Internal, + m: msgPlacementProblem, + }, + // System resource overloaded + errOverloaded: { + c: codes.Unavailable, + m: msgOverloaded, + }, + // Access violations + errAccessDenied: { + c: codes.PermissionDenied, + m: msgAccessDenied, + }, + // Maximum processing payload size overflow + errProcPayloadSize: { + c: codes.FailedPrecondition, + m: msgProcPayloadSize, + d: nil, // TODO: NSPCC-1048 + }, +} + +var mStatusCustom = map[requestError]*statusInfo{ + // Invalid first message in Put client stream + { + t: object.RequestPut, + e: errHeaderExpected, + }: { + c: codes.InvalidArgument, + m: msgPutMessageProblem, + d: putFirstMessageDetails(), + }, + // Nil object in Put request + { + t: object.RequestPut, + e: errObjectExpected, + }: { + c: codes.InvalidArgument, + m: msgPutNilObject, + d: putNilObjectDetails(), + }, + // Lack of object payload data + { + t: object.RequestPut, + e: transformer.ErrPayloadEOF, + }: { + c: codes.InvalidArgument, + m: msgCutObjectPayload, + d: payloadSizeDetails(), + }, + // Lack of public keys in the token + { + t: object.RequestPut, + e: errMissingOwnerKeys, + }: { + c: codes.PermissionDenied, + m: msgMissingTokenKeys, + d: tokenKeysDetails(), + }, + // Broken token structure + { + t: object.RequestPut, + e: errBrokenToken, + }: { + c: codes.PermissionDenied, + m: msgBrokenToken, + }, + // Missing object ID in token + { + t: object.RequestPut, + e: errWrongTokenAddress, + }: { + c: codes.PermissionDenied, + m: msgTokenObjectID, + d: tokenOIDDetails(), + }, + // Invalid after-first message in stream + { + t: object.RequestPut, + e: errChunkExpected, + }: { + c: codes.InvalidArgument, + m: msgPutMessageProblem, + d: putChunkMessageDetails(), + }, + { + t: object.RequestPut, + e: errObjectFromTheFuture, + }: { + c: codes.FailedPrecondition, + m: msgObjectCreationEpoch, + d: nil, // TODO: NSPCC-1048 + }, + { + t: object.RequestPut, + e: errObjectPayloadSize, + }: { + c: codes.FailedPrecondition, + m: msgObjectPayloadSize, + d: nil, // TODO: NSPCC-1048 + }, + { + t: object.RequestPut, + e: errLocalStorageOverflow, + }: { + c: codes.Unavailable, + m: msgLocalStorageOverflow, + d: localStorageOverflowDetails(), + }, + { + t: object.RequestPut, + e: errPayloadChecksum, + }: { + c: codes.InvalidArgument, + m: msgPayloadChecksum, + d: payloadChecksumHeaderDetails(), + }, + { + t: object.RequestPut, + e: errObjectHeadersVerification, + }: { + c: codes.InvalidArgument, + m: msgObjectHeadersVerification, + }, + { + t: object.RequestPut, + e: errIncompleteOperation, + }: { + c: codes.Unavailable, + m: msgForwardPutObject, + }, + { + t: object.RequestPut, + e: errPutLocal, + }: { + c: codes.Internal, + m: msgPutLocalFailure, + }, + { + t: object.RequestPut, + e: errTokenRetrieval, + }: { + c: codes.Aborted, + m: msgPrivateTokenRecv, + }, + { + t: object.RequestPut, + e: transformer.ErrInvalidSGLinking, + }: { + c: codes.InvalidArgument, + m: msgInvalidSGLinking, + d: sgLinkingDetails(), + }, + { + t: object.RequestPut, + e: implementations.ErrIncompleteSGInfo, + }: { + c: codes.NotFound, + m: msgIncompleteSGInfo, + }, + { + t: object.RequestPut, + e: errTransformer, + }: { + c: codes.Internal, + m: msgTransformationFailure, + }, + { + t: object.RequestPut, + e: errWrongSGSize, + }: { + c: codes.InvalidArgument, + m: msgWrongSGSize, + }, + { + t: object.RequestPut, + e: errWrongSGHash, + }: { + c: codes.InvalidArgument, + m: msgWrongSGHash, + }, + { + t: object.RequestGet, + e: errIncompleteOperation, + }: { + c: codes.NotFound, + m: msgObjectNotFound, + }, + { + t: object.RequestHead, + e: errIncompleteOperation, + }: { + c: codes.NotFound, + m: msgObjectHeaderNotFound, + }, + { + t: object.RequestGet, + e: errNonAssembly, + }: { + c: codes.Unimplemented, + m: msgNonAssembly, + }, + { + t: object.RequestHead, + e: errNonAssembly, + }: { + c: codes.Unimplemented, + m: msgNonAssembly, + }, + { + t: object.RequestGet, + e: childrenNotFound, + }: { + c: codes.NotFound, + m: msgObjectNotFound, + }, + { + t: object.RequestHead, + e: childrenNotFound, + }: { + c: codes.NotFound, + m: msgObjectHeaderNotFound, + }, + { + t: object.RequestRange, + e: localstore.ErrOutOfRange, + }: { + c: codes.OutOfRange, + m: msgPayloadOutOfRange, + }, + { + t: object.RequestRange, + e: errPayloadRangeNotFound, + }: { + c: codes.NotFound, + m: msgPayloadRangeNotFound, + }, + { + t: object.RequestDelete, + e: errNilToken, + }: { + c: codes.InvalidArgument, + m: msgMissingToken, + d: missingTokenDetails(), + }, + { + t: object.RequestDelete, + e: errMissingOwnerKeys, + }: { + c: codes.PermissionDenied, + m: msgMissingTokenKeys, + d: tokenKeysDetails(), + }, + { + t: object.RequestDelete, + e: errBrokenToken, + }: { + c: codes.PermissionDenied, + m: msgBrokenToken, + }, + { + t: object.RequestDelete, + e: errWrongTokenAddress, + }: { + c: codes.PermissionDenied, + m: msgTokenObjectID, + d: tokenOIDDetails(), + }, + { + t: object.RequestDelete, + e: errTokenRetrieval, + }: { + c: codes.Aborted, + m: msgPrivateTokenRecv, + }, + { + t: object.RequestDelete, + e: errIncompleteOperation, + }: { + c: codes.Unavailable, + m: msgPutTombstone, + }, + { + t: object.RequestDelete, + e: errDeletePrepare, + }: { + c: codes.Internal, + m: msgDeletePrepare, + }, + { + t: object.RequestSearch, + e: errUnsupportedQueryVersion, + }: { + c: codes.Unimplemented, + m: msgQueryVersion, + }, + { + t: object.RequestSearch, + e: errSearchQueryUnmarshal, + }: { + c: codes.InvalidArgument, + m: msgSearchQueryUnmarshal, + }, + { + t: object.RequestSearch, + e: errLocalQueryImpose, + }: { + c: codes.Internal, + m: msgLocalQueryImpose, + }, + { + t: object.RequestRangeHash, + e: errPayloadRangeNotFound, + }: { + c: codes.NotFound, + m: msgPayloadRangeNotFound, + }, + { + t: object.RequestRangeHash, + e: localstore.ErrOutOfRange, + }: { + c: codes.OutOfRange, + m: msgPayloadOutOfRange, + }, +} + +func serviceStatusCalculator() *statusCalculator { + s := newStatusCalculator() + + for k, v := range mStatusCommon { + s.addCommon(k, v) + } + + for k, v := range mStatusCustom { + s.addCustom(k, v) + } + + return s +} + +func statusError(v *statusInfo) (bool, error) { + st, err := status.New(v.c, v.m).WithDetails(v.d...) + if err != nil { + return false, nil + } + + return true, st.Err() +} + +func (s *statusCalculator) addCommon(k error, v *statusInfo) { + s.Lock() + s.common[k] = v + s.Unlock() +} + +func (s *statusCalculator) addCustom(k requestError, v *statusInfo) { + s.Lock() + s.custom[k] = v + s.Unlock() +} + +func (s *statusCalculator) make(e requestError) error { + s.RLock() + defer s.RUnlock() + + var ( + ok bool + v *statusInfo + d []proto.Message + err = errors.Cause(e.e) + ) + + if v, ok := err.(*detailedError); ok { + d = v.d + err = v.error + } else if v, ok := err.(detailedError); ok { + d = v.d + err = v.error + } + + if v, ok = s.common[err]; !ok { + if v, ok = s.custom[requestError{ + t: e.t, + e: err, + }]; !ok { + return e.e + } + } + + vv := *v + + vv.d = append(vv.d, d...) + + if ok, res := statusError(&vv); ok { + return res + } + + return e.e +} + +func newStatusCalculator() *statusCalculator { + return &statusCalculator{ + RWMutex: new(sync.RWMutex), + common: make(map[error]*statusInfo), + custom: make(map[requestError]*statusInfo), + } +} + +func requestAuthDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "Signatures", + Description: "should be formed according to VerificationHeader signing", + }, + }, + }, + } +} + +func invalidTTLDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "TTL", + Description: "should greater or equal than NonForwardingTTL", + }, + }, + }, + } +} + +func containerAbsenceDetails() []proto.Message { + return []proto.Message{ + &errdetails.PreconditionFailure{ + Violations: []*errdetails.PreconditionFailure_Violation{ + { + Type: "container options", + Subject: "container nodes", + Description: "server node should be presented container", + }, + }, + }, + } +} + +func containerDetails(cid CID, desc string) []proto.Message { + return []proto.Message{ + &errdetails.ResourceInfo{ + ResourceType: "container", + ResourceName: cid.String(), + Description: desc, + }, + } +} + +func putFirstMessageDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R", + Description: "should be PutRequest_Header", + }, + }, + }, + } +} + +func putChunkMessageDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R", + Description: "should be PutRequest_Chunk", + }, + { + Field: "R.Chunk", + Description: "should not be empty", + }, + }, + }, + } +} + +func putNilObjectDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object", + Description: "should not be null", + }, + }, + }, + } +} + +func payloadSizeDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object.SystemHeader.PayloadLength", + Description: "should be equal to the sum of the sizes of the streaming payload chunks", + }, + }, + }, + } +} + +func tokenKeysDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Token.PublicKeys", + Description: "should be non-empty list of marshaled ecdsa public keys", + }, + }, + }, + } +} + +func tokenOIDDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Token.ObjectID", + Description: "should contain requested object", + }, + }, + }, + } +} + +func maxProcPayloadSizeDetails(sz uint64) []proto.Message { + return []proto.Message{ + &errdetails.PreconditionFailure{ + Violations: []*errdetails.PreconditionFailure_Violation{ + { + Type: "object requirements", + Subject: "max processing payload size", + Description: fmt.Sprintf("should not be greater than %d bytes", sz), + }, + }, + }, + } +} + +func objectCreationEpochDetails(e uint64) []proto.Message { + return []proto.Message{ + &errdetails.PreconditionFailure{ + Violations: []*errdetails.PreconditionFailure_Violation{ + { + Type: "object requirements", + Subject: "creation epoch", + Description: fmt.Sprintf("should not be greater than %d", e), + }, + }, + }, + } +} + +func maxObjectPayloadSizeDetails(sz uint64) []proto.Message { + return []proto.Message{ + &errdetails.PreconditionFailure{ + Violations: []*errdetails.PreconditionFailure_Violation{ + { + Type: "object requirements", + Subject: "max object payload size", + Description: fmt.Sprintf("should not be greater than %d bytes", sz), + }, + }, + }, + } +} + +func localStorageOverflowDetails() []proto.Message { + return []proto.Message{ + &errdetails.ResourceInfo{ + ResourceType: "local storage", + ResourceName: "disk storage", + Description: "not enough space", + }, + } +} + +func payloadChecksumHeaderDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object.Headers", + Description: "should contain correct payload checksum header", + }, + }, + }, + } +} + +func objectHeadersVerificationDetails(e error) []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object.Headers", + Description: e.Error(), + }, + }, + }, + } +} + +func privateTokenRecvDetails(id session.TokenID, owner OwnerID) []proto.Message { + return []proto.Message{ + &errdetails.ResourceInfo{ + ResourceType: "private token", + ResourceName: id.String(), + Owner: owner.String(), + Description: "problems with getting a private token", + }, + } +} + +func sgLinkingDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object.Headers", + Description: "should not contain Header_StorageGroup and Link_StorageGroup or should contain both", + }, + }, + }, + } +} + +func sgSizeDetails(exp, act uint64) []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object.Headers", + Description: fmt.Sprintf("wrong storage group size: expected %d, collected %d", exp, act), + }, + }, + }, + } +} + +func sgHashDetails(exp, act Hash) []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "R.Object.Headers", + Description: fmt.Sprintf("wrong storage group hash: expected %s, collected %s", exp, act), + }, + }, + }, + } +} + +func missingTokenDetails() []proto.Message { + return []proto.Message{ + &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{ + { + Field: "Token", + Description: "should not be null", + }, + }, + }, + } +} diff --git a/services/public/object/status_test.go b/services/public/object/status_test.go new file mode 100644 index 0000000000..b076fec83c --- /dev/null +++ b/services/public/object/status_test.go @@ -0,0 +1,1210 @@ +package object + +import ( + "context" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/localstore" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/nspcc-dev/neofs-node/lib/transformer" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type testPanickingHandler struct{} + +func (*testPanickingHandler) handleRequest(context.Context, handleRequestParams) (interface{}, error) { + panic("panicking handler") +} + +func TestStatusCalculator(t *testing.T) { + t.Run("unknown error", func(t *testing.T) { + e := internal.Error("error for test") + + s := newStatusCalculator() + + require.Equal(t, e, s.make(requestError{ + e: e, + })) + }) + + t.Run("common error", func(t *testing.T) { + v := &statusInfo{ + c: codes.Aborted, + m: "test error message", + d: []proto.Message{ + &errdetails.ResourceInfo{ + ResourceType: "type", + ResourceName: "name", + Owner: "owner", + Description: "description", + }, + }, + } + + s := newStatusCalculator() + + e := internal.Error("error for test") + + s.addCommon(e, v) + + ok, err := statusError(v) + require.True(t, ok) + + require.Equal(t, + err, + s.make(requestError{ + e: e, + }), + ) + }) + + t.Run("custom error", func(t *testing.T) { + var ( + c1, c2 = codes.Aborted, codes.AlreadyExists + t1, t2 = object.RequestPut, object.RequestGet + e1, e2 = internal.Error("test error 1"), internal.Error("test error 2") + m1, m2 = "message 1", "message 2" + ) + + s := newStatusCalculator() + + s1 := &statusInfo{ + c: c1, + m: m1, + } + + re1 := requestError{ + t: t1, + e: e1, + } + + s.addCustom(re1, s1) + + s2 := &statusInfo{ + c: c2, + m: m2, + } + + r2 := requestError{ + t: t2, + e: e2, + } + + s.addCustom(r2, s2) + + ok, err1 := statusError(s1) + require.True(t, ok) + + ok, err2 := statusError(s2) + require.True(t, ok) + + require.Equal(t, + err1, + s.make(re1), + ) + + require.Equal(t, + err2, + s.make(r2), + ) + }) +} + +func testStatusCommon(t *testing.T, h requestHandler, c codes.Code, m string, d []interface{}) { + ctx := context.TODO() + + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + errPut := s.Put(&testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + }) + + errGet := s.Get(new(object.GetRequest), new(testGetEntity)) + + _, errHead := s.Head(ctx, new(object.HeadRequest)) + + _, errDelete := s.Head(ctx, new(object.HeadRequest)) + + errRange := s.GetRange(new(GetRangeRequest), new(testRangeEntity)) + + _, errRangeHash := s.GetRangeHash(ctx, new(object.GetRangeHashRequest)) + + errSearch := s.Search(new(object.SearchRequest), new(testSearchEntity)) + + errs := []error{ + errPut, + errGet, + errHead, + errRange, + errRangeHash, + errSearch, + errDelete, + } + + for _, err := range errs { + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, c, st.Code()) + require.Equal(t, m, st.Message()) + require.Equal(t, d, st.Details()) + } +} + +func TestStatusCommon(t *testing.T) { + t.Run("handler panic", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + new(testPanickingHandler), + codes.Internal, + msgServerPanic, + ds, + ) + }) + + t.Run("request authentication", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range requestAuthDetails() { + ds = append(ds, d) + } + + testStatusCommon(t, + &testPutEntity{ + err: errUnauthenticated, + }, + codes.Unauthenticated, + msgUnauthenticated, + ds, + ) + }) + + t.Run("re-signing problem", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + &testPutEntity{ + err: errReSigning, + }, + codes.Internal, + msgReSigning, + ds, + ) + }) + + t.Run("invalid TTL", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range invalidTTLDetails() { + ds = append(ds, d) + } + + testStatusCommon(t, + &testPutEntity{ + err: errInvalidTTL, + }, + codes.InvalidArgument, + msgInvalidTTL, + ds, + ) + }) + + t.Run("container affiliation problem", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + &testPutEntity{ + err: errContainerAffiliationProblem, + }, + codes.Internal, + msgContainerAffiliationProblem, + ds, + ) + }) + + t.Run("container not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + &testPutEntity{ + err: errContainerNotFound, + }, + codes.NotFound, + msgContainerNotFound, + ds, + ) + }) + + t.Run("server is missing in container", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range containerAbsenceDetails() { + ds = append(ds, d) + } + + testStatusCommon(t, + &testPutEntity{ + err: errNotLocalContainer, + }, + codes.FailedPrecondition, + msgNotLocalContainer, + ds, + ) + }) + + t.Run("placement problem", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + &testPutEntity{ + err: errPlacementProblem, + }, + codes.Internal, + msgPlacementProblem, + ds, + ) + }) + + t.Run("system resource overloaded", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + &testPutEntity{ + err: errOverloaded, + }, + codes.Unavailable, + msgOverloaded, + ds, + ) + }) + + t.Run("access denied", func(t *testing.T) { + ds := make([]interface{}, 0) + + testStatusCommon(t, + &testPutEntity{ + err: errAccessDenied, + }, + codes.PermissionDenied, + msgAccessDenied, + ds, + ) + }) + + t.Run("max processing payload size overflow", func(t *testing.T) { + maxSz := uint64(100) + + ds := make([]interface{}, 0) + + for _, d := range maxProcPayloadSizeDetails(maxSz) { + ds = append(ds, d) + } + + testStatusCommon(t, + &testPutEntity{ + err: &detailedError{ + error: errProcPayloadSize, + d: maxProcPayloadSizeDetails(maxSz), + }, + }, + codes.FailedPrecondition, + msgProcPayloadSize, + ds, + ) + }) +} + +func testStatusPut(t *testing.T, h requestHandler, srv object.Service_PutServer, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + err := s.Put(srv) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusPut(t *testing.T) { + t.Run("invalid first message type", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range putFirstMessageDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestChunk(nil), + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgPutMessageProblem, + } + + testStatusPut(t, nil, srv, info, ds) + }) + + t.Run("invalid first message type", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range putNilObjectDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(nil), + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgPutNilObject, + } + + testStatusPut(t, nil, srv, info, ds) + }) + + t.Run("invalid first message type", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range payloadSizeDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: transformer.ErrPayloadEOF, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgCutObjectPayload, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("token w/o public keys", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range tokenKeysDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errMissingOwnerKeys, + } + + info := statusInfo{ + c: codes.PermissionDenied, + m: msgMissingTokenKeys, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("broken token", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errBrokenToken, + } + + info := statusInfo{ + c: codes.PermissionDenied, + m: msgBrokenToken, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("missing object in token", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range tokenOIDDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errWrongTokenAddress, + } + + info := statusInfo{ + c: codes.PermissionDenied, + m: msgTokenObjectID, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("object from future", func(t *testing.T) { + e := uint64(3) + + ds := make([]interface{}, 0) + + for _, d := range objectCreationEpochDetails(e) { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: &detailedError{ + error: errObjectFromTheFuture, + d: objectCreationEpochDetails(e), + }, + } + + info := statusInfo{ + c: codes.FailedPrecondition, + m: msgObjectCreationEpoch, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("max object payload size", func(t *testing.T) { + sz := uint64(3) + + ds := make([]interface{}, 0) + + for _, d := range maxObjectPayloadSizeDetails(sz) { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: &detailedError{ + error: errObjectPayloadSize, + d: maxObjectPayloadSizeDetails(sz), + }, + } + + info := statusInfo{ + c: codes.FailedPrecondition, + m: msgObjectPayloadSize, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("local storage overflow", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range localStorageOverflowDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errLocalStorageOverflow, + } + + info := statusInfo{ + c: codes.Unavailable, + m: msgLocalStorageOverflow, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("invalid payload checksum", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range payloadChecksumHeaderDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errPayloadChecksum, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgPayloadChecksum, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("invalid object header structure", func(t *testing.T) { + e := internal.Error("test error") + + ds := make([]interface{}, 0) + + for _, d := range objectHeadersVerificationDetails(e) { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: &detailedError{ + error: errObjectHeadersVerification, + d: objectHeadersVerificationDetails(e), + }, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgObjectHeadersVerification, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("put generated object failure", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errIncompleteOperation, + } + + info := statusInfo{ + c: codes.Unavailable, + m: msgForwardPutObject, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("private token receive failure", func(t *testing.T) { + owner := OwnerID{1, 2, 3} + tokenID := session.TokenID{4, 5, 6} + + ds := make([]interface{}, 0) + + for _, d := range privateTokenRecvDetails(tokenID, owner) { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: &detailedError{ + error: errTokenRetrieval, + d: privateTokenRecvDetails(tokenID, owner), + }, + } + + info := statusInfo{ + c: codes.Aborted, + m: msgPrivateTokenRecv, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("invalid SG headers", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range sgLinkingDetails() { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: transformer.ErrInvalidSGLinking, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgInvalidSGLinking, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("incomplete SG info", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: implementations.ErrIncompleteSGInfo, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgIncompleteSGInfo, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("object transformation failure", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: errTransformer, + } + + info := statusInfo{ + c: codes.Internal, + m: msgTransformationFailure, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("wrong SG size", func(t *testing.T) { + var exp, act uint64 = 1, 2 + + ds := make([]interface{}, 0) + + for _, d := range sgSizeDetails(exp, act) { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: &detailedError{ + error: errWrongSGSize, + d: sgSizeDetails(exp, act), + }, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgWrongSGSize, + } + + testStatusPut(t, h, srv, info, ds) + }) + + t.Run("wrong SG size", func(t *testing.T) { + var exp, act = Hash{1}, Hash{2} + + ds := make([]interface{}, 0) + + for _, d := range sgHashDetails(exp, act) { + ds = append(ds, d) + } + + srv := &testPutEntity{ + res: object.MakePutRequestHeader(new(Object)), + } + + h := &testPutEntity{ + err: &detailedError{ + error: errWrongSGHash, + d: sgHashDetails(exp, act), + }, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgWrongSGHash, + } + + testStatusPut(t, h, srv, info, ds) + }) +} + +func testStatusGet(t *testing.T, h requestHandler, srv object.Service_GetServer, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + err := s.Get(new(object.GetRequest), srv) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusGet(t *testing.T) { + t.Run("object not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testGetEntity) + + h := &testGetEntity{ + err: errIncompleteOperation, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgObjectNotFound, + } + + testStatusGet(t, h, srv, info, ds) + }) + + t.Run("non-assembly", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testGetEntity) + + h := &testGetEntity{ + err: errNonAssembly, + } + + info := statusInfo{ + c: codes.Unimplemented, + m: msgNonAssembly, + } + + testStatusGet(t, h, srv, info, ds) + }) + + t.Run("children not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testGetEntity) + + h := &testGetEntity{ + err: childrenNotFound, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgObjectNotFound, + } + + testStatusGet(t, h, srv, info, ds) + }) +} + +func testStatusHead(t *testing.T, h requestHandler, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + _, err := s.Head(context.TODO(), new(object.HeadRequest)) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusHead(t *testing.T) { + t.Run("object not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: errIncompleteOperation, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgObjectHeaderNotFound, + } + + testStatusHead(t, h, info, ds) + }) + + t.Run("non-assembly", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: errNonAssembly, + } + + info := statusInfo{ + c: codes.Unimplemented, + m: msgNonAssembly, + } + + testStatusHead(t, h, info, ds) + }) + + t.Run("children not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: childrenNotFound, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgObjectHeaderNotFound, + } + + testStatusHead(t, h, info, ds) + }) +} + +func testStatusGetRange(t *testing.T, h requestHandler, srv object.Service_GetRangeServer, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + err := s.GetRange(new(GetRangeRequest), srv) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusGetRange(t *testing.T) { + t.Run("payload range is out of bounds", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testRangeEntity) + + h := &testRangeEntity{ + err: localstore.ErrOutOfRange, + } + + info := statusInfo{ + c: codes.OutOfRange, + m: msgPayloadOutOfRange, + } + + testStatusGetRange(t, h, srv, info, ds) + }) + + t.Run("payload range not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testRangeEntity) + + h := &testRangeEntity{ + err: errPayloadRangeNotFound, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgPayloadRangeNotFound, + } + + testStatusGetRange(t, h, srv, info, ds) + }) +} + +func testStatusDelete(t *testing.T, h requestHandler, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + _, err := s.Delete(context.TODO(), new(object.DeleteRequest)) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusDelete(t *testing.T) { + t.Run("missing token", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range missingTokenDetails() { + ds = append(ds, d) + } + + h := &testHeadEntity{ + err: errNilToken, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgMissingToken, + } + + testStatusDelete(t, h, info, ds) + }) + + t.Run("missing public keys in token", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range tokenKeysDetails() { + ds = append(ds, d) + } + + h := &testHeadEntity{ + err: errMissingOwnerKeys, + } + + info := statusInfo{ + c: codes.PermissionDenied, + m: msgMissingTokenKeys, + } + + testStatusDelete(t, h, info, ds) + }) + + t.Run("broken token structure", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: errBrokenToken, + } + + info := statusInfo{ + c: codes.PermissionDenied, + m: msgBrokenToken, + } + + testStatusDelete(t, h, info, ds) + }) + + t.Run("missing object ID in token", func(t *testing.T) { + ds := make([]interface{}, 0) + + for _, d := range tokenOIDDetails() { + ds = append(ds, d) + } + + h := &testHeadEntity{ + err: errWrongTokenAddress, + } + + info := statusInfo{ + c: codes.PermissionDenied, + m: msgTokenObjectID, + } + + testStatusDelete(t, h, info, ds) + }) + + t.Run("private token receive", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: errTokenRetrieval, + } + + info := statusInfo{ + c: codes.Aborted, + m: msgPrivateTokenRecv, + } + + testStatusDelete(t, h, info, ds) + }) + + t.Run("incomplete tombstone put", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: errIncompleteOperation, + } + + info := statusInfo{ + c: codes.Unavailable, + m: msgPutTombstone, + } + + testStatusDelete(t, h, info, ds) + }) + + t.Run("delete preparation failure", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testHeadEntity{ + err: errDeletePrepare, + } + + info := statusInfo{ + c: codes.Internal, + m: msgDeletePrepare, + } + + testStatusDelete(t, h, info, ds) + }) +} + +func testStatusSearch(t *testing.T, h requestHandler, srv object.Service_SearchServer, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + err := s.Search(new(object.SearchRequest), srv) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusSearch(t *testing.T) { + t.Run("unsupported query version", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testSearchEntity) + + h := &testSearchEntity{ + err: errUnsupportedQueryVersion, + } + + info := statusInfo{ + c: codes.Unimplemented, + m: msgQueryVersion, + } + + testStatusSearch(t, h, srv, info, ds) + }) + + t.Run("query unmarshal failure", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testSearchEntity) + + h := &testSearchEntity{ + err: errSearchQueryUnmarshal, + } + + info := statusInfo{ + c: codes.InvalidArgument, + m: msgSearchQueryUnmarshal, + } + + testStatusSearch(t, h, srv, info, ds) + }) + + t.Run("query imposing problems", func(t *testing.T) { + ds := make([]interface{}, 0) + + srv := new(testSearchEntity) + + h := &testSearchEntity{ + err: errLocalQueryImpose, + } + + info := statusInfo{ + c: codes.Internal, + m: msgLocalQueryImpose, + } + + testStatusSearch(t, h, srv, info, ds) + }) +} + +func testStatusGetRangeHash(t *testing.T, h requestHandler, info statusInfo, d []interface{}) { + s := &objectService{ + log: test.NewTestLogger(false), + requestHandler: h, + statusCalculator: serviceStatusCalculator(), + } + + _, err := s.GetRangeHash(context.TODO(), new(object.GetRangeHashRequest)) + + st, ok := status.FromError(err) + require.True(t, ok) + + require.Equal(t, info.c, st.Code()) + require.Equal(t, info.m, st.Message()) + require.Equal(t, d, st.Details()) +} + +func TestStatusGetRangeHash(t *testing.T) { + t.Run("payload range not found", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testRangeEntity{ + err: errPayloadRangeNotFound, + } + + info := statusInfo{ + c: codes.NotFound, + m: msgPayloadRangeNotFound, + } + + testStatusGetRangeHash(t, h, info, ds) + }) + + t.Run("range out-of-bounds", func(t *testing.T) { + ds := make([]interface{}, 0) + + h := &testRangeEntity{ + err: localstore.ErrOutOfRange, + } + + info := statusInfo{ + c: codes.OutOfRange, + m: msgPayloadOutOfRange, + } + + testStatusGetRangeHash(t, h, info, ds) + }) +} diff --git a/services/public/object/token.go b/services/public/object/token.go new file mode 100644 index 0000000000..81c5437009 --- /dev/null +++ b/services/public/object/token.go @@ -0,0 +1,107 @@ +package object + +import ( + "context" + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-api-go/service" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" +) + +type sessionTokenVerifier interface { + verifySessionToken(context.Context, service.SessionToken) error +} + +type complexTokenVerifier struct { + verifiers []sessionTokenVerifier +} + +type tokenSignatureVerifier struct { + ownerKeys []*ecdsa.PublicKey +} + +type tokenEpochsVerifier struct { + epochRecv EpochReceiver +} + +type tokenPreProcessor struct { + keyVerifier core.OwnerKeyVerifier + + staticVerifier sessionTokenVerifier +} + +const errCreatedAfterExpiration = internal.Error("creation epoch number is greater than expired one") + +const errTokenExpired = internal.Error("token is expired") + +const errForbiddenSpawn = internal.Error("request spawn is forbidden") + +func (s tokenPreProcessor) preProcess(ctx context.Context, req serviceRequest) error { + token := req.GetSessionToken() + if token == nil { + return nil + } + + if !allowedSpawn(token.GetVerb(), req.Type()) { + return errForbiddenSpawn + } + + if err := s.keyVerifier.VerifyKey(ctx, token); err != nil { + return err + } + + ownerKeyBytes := token.GetOwnerKey() + + verifier := newComplexTokenVerifier( + s.staticVerifier, + &tokenSignatureVerifier{ + ownerKeys: []*ecdsa.PublicKey{ + crypto.UnmarshalPublicKey(ownerKeyBytes), + }, + }, + ) + + return verifier.verifySessionToken(ctx, token) +} + +func newComplexTokenVerifier(verifiers ...sessionTokenVerifier) sessionTokenVerifier { + return &complexTokenVerifier{ + verifiers: verifiers, + } +} + +func (s complexTokenVerifier) verifySessionToken(ctx context.Context, token service.SessionToken) error { + for i := range s.verifiers { + if s.verifiers[i] == nil { + continue + } else if err := s.verifiers[i].verifySessionToken(ctx, token); err != nil { + return err + } + } + + return nil +} + +func (s tokenSignatureVerifier) verifySessionToken(ctx context.Context, token service.SessionToken) error { + verifiedToken := service.NewVerifiedSessionToken(token) + + for i := range s.ownerKeys { + if err := service.VerifySignatureWithKey(s.ownerKeys[i], verifiedToken); err != nil { + return err + } + } + + return nil +} + +func (s tokenEpochsVerifier) verifySessionToken(ctx context.Context, token service.SessionToken) error { + if expired := token.ExpirationEpoch(); token.CreationEpoch() > expired { + return errCreatedAfterExpiration + } else if s.epochRecv.Epoch() > expired { + return errTokenExpired + } + + return nil +} diff --git a/services/public/object/token_test.go b/services/public/object/token_test.go new file mode 100644 index 0000000000..e07ccb858e --- /dev/null +++ b/services/public/object/token_test.go @@ -0,0 +1,156 @@ +package object + +import ( + "context" + "errors" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/stretchr/testify/require" +) + +// Entity for mocking interfaces. +// Implementation of any interface intercepts arguments via f (if not nil). +// If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. +type testTokenEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error +} + +func (s testTokenEntity) VerifyKey(_ context.Context, p core.OwnerKeyContainer) error { + if s.f != nil { + s.f(p) + } + return s.err +} + +func (s testTokenEntity) Epoch() uint64 { + return s.res.(uint64) +} + +func (s testTokenEntity) verifySessionToken(_ context.Context, token service.SessionToken) error { + if s.f != nil { + s.f(token) + } + return s.err +} + +func TestTokenPreProcessor(t *testing.T) { + ctx := context.TODO() + + t.Run("nil token", func(t *testing.T) { + var req serviceRequest = new(object.PutRequest) + require.Nil(t, req.GetSessionToken()) + + s := new(tokenPreProcessor) + + require.NoError(t, s.preProcess(ctx, req)) + }) + + t.Run("forbidden spawn", func(t *testing.T) { + token := new(service.Token) + + req := new(object.PutRequest) + req.SetToken(token) + + token.SetVerb(service.Token_Info_Get) + + s := new(tokenPreProcessor) + + require.EqualError(t, s.preProcess(ctx, req), errForbiddenSpawn.Error()) + }) + + t.Run("owner key verifier failure", func(t *testing.T) { + verifierErr := errors.New("test error for key verifier") + + owner := OwnerID{1, 2, 3} + token := new(service.Token) + token.SetOwnerID(owner) + + req := new(object.PutRequest) + req.SetToken(token) + + s := &tokenPreProcessor{ + keyVerifier: &testTokenEntity{ + f: func(items ...interface{}) { + require.Equal(t, token, items[0]) + }, + err: verifierErr, + }, + } + + require.EqualError(t, s.preProcess(ctx, req), verifierErr.Error()) + }) + + t.Run("static verifier error", func(t *testing.T) { + vErr := errors.New("test error for static verifier") + + owner := OwnerID{1, 2, 3} + token := new(service.Token) + token.SetOwnerID(owner) + + req := new(object.PutRequest) + req.SetToken(token) + + s := &tokenPreProcessor{ + keyVerifier: new(testTokenEntity), + staticVerifier: &testTokenEntity{ + f: func(items ...interface{}) { + require.Equal(t, token, items[0]) + }, + err: vErr, + }, + } + + require.EqualError(t, s.preProcess(ctx, req), vErr.Error()) + }) +} + +func TestTokenEpochsVerifier(t *testing.T) { + ctx := context.TODO() + + t.Run("created after expiration", func(t *testing.T) { + token := new(service.Token) + token.SetExpirationEpoch(1) + token.SetCreationEpoch(token.ExpirationEpoch() + 1) + + s := new(tokenEpochsVerifier) + + require.EqualError(t, s.verifySessionToken(ctx, token), errCreatedAfterExpiration.Error()) + }) + + t.Run("expired token", func(t *testing.T) { + token := new(service.Token) + token.SetExpirationEpoch(1) + + s := &tokenEpochsVerifier{ + epochRecv: &testTokenEntity{ + res: token.ExpirationEpoch() + 1, + }, + } + + require.EqualError(t, s.verifySessionToken(ctx, token), errTokenExpired.Error()) + }) + + t.Run("valid token", func(t *testing.T) { + token := new(service.Token) + token.SetCreationEpoch(1) + token.SetExpirationEpoch(token.CreationEpoch() + 1) + + s := &tokenEpochsVerifier{ + epochRecv: &testTokenEntity{ + res: token.ExpirationEpoch() - 1, + }, + } + + require.NoError(t, s.verifySessionToken(ctx, token)) + }) +} diff --git a/services/public/object/transport_implementations.go b/services/public/object/transport_implementations.go new file mode 100644 index 0000000000..3c85ce057a --- /dev/null +++ b/services/public/object/transport_implementations.go @@ -0,0 +1,743 @@ +package object + +import ( + "bytes" + "context" + "crypto/ecdsa" + "fmt" + "io" + "time" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/session" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/lib/transport" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // MultiTransportParams groups the parameters for object transport component's constructor. + MultiTransportParams struct { + AddressStore implementations.AddressStoreComponent + EpochReceiver EpochReceiver + RemoteService RemoteService + Logger *zap.Logger + Key *ecdsa.PrivateKey + PutTimeout time.Duration + GetTimeout time.Duration + HeadTimeout time.Duration + SearchTimeout time.Duration + RangeHashTimeout time.Duration + DialTimeout time.Duration + + PrivateTokenStore session.PrivateTokenStore + } + + transportComponent struct { + reqSender requestSender + + resTracker resultTracker + + getCaller remoteProcessCaller + putCaller remoteProcessCaller + headCaller remoteProcessCaller + rangeCaller remoteProcessCaller + rangeHashCaller remoteProcessCaller + searchCaller remoteProcessCaller + } + + requestSender interface { + sendRequest(context.Context, sendParams) (interface{}, error) + } + + sendParams struct { + req transport.MetaInfo + node multiaddr.Multiaddr + handler remoteProcessCaller + } + + clientInfo struct { + sc object.ServiceClient + key *ecdsa.PublicKey + } + + remoteProcessCaller interface { + call(context.Context, serviceRequest, *clientInfo) (interface{}, error) + } + + getCaller struct { + } + + putCaller struct { + } + + headCaller struct { + } + + rangeCaller struct { + } + + rangeHashCaller struct { + } + + searchCaller struct { + } + + coreRequestSender struct { + requestPrep transportRequestPreparer + addressStore implementations.AddressStoreComponent + remoteService RemoteService + + putTimeout time.Duration + getTimeout time.Duration + searchTimeout time.Duration + headTimeout time.Duration + rangeHashTimeout time.Duration + dialTimeout time.Duration + } + + signingFunc func(*ecdsa.PrivateKey, service.RequestSignedData) error + + coreRequestPreparer struct { + epochRecv EpochReceiver + key *ecdsa.PrivateKey + signingFunc signingFunc + + privateTokenStore session.PrivateTokenSource + } + + transportRequestPreparer interface { + prepareRequest(transport.MetaInfo) (serviceRequest, error) + } + + transportRequest struct { + serviceRequest + timeout time.Duration + } + + putRequestSequence struct { + *object.PutRequest + chunks []*object.PutRequest + } + + rawMetaInfo struct { + raw bool + ttl uint32 + timeout time.Duration + token service.SessionToken + rt object.RequestType + bearer service.BearerToken + extHdrs []service.ExtendedHeader + } + + rawAddrInfo struct { + *rawMetaInfo + addr Address + } +) + +const ( + minRemoteRequestTimeout = 5 * time.Second + minDialTimeout = 500 * time.Millisecond +) + +const pmWrongRequestType = "unknown type: %T" + +var ( + _ serviceRequest = (*putRequestSequence)(nil) + _ transport.MetaInfo = (*transportRequest)(nil) + _ requestSender = (*coreRequestSender)(nil) + _ transport.ObjectTransport = (*transportComponent)(nil) + _ transportRequestPreparer = (*coreRequestPreparer)(nil) + _ transport.MetaInfo = (*rawMetaInfo)(nil) + _ transport.AddressInfo = (*rawAddrInfo)(nil) + + _ remoteProcessCaller = (*getCaller)(nil) + _ remoteProcessCaller = (*putCaller)(nil) + _ remoteProcessCaller = (*headCaller)(nil) + _ remoteProcessCaller = (*searchCaller)(nil) + _ remoteProcessCaller = (*rangeCaller)(nil) + _ remoteProcessCaller = (*rangeHashCaller)(nil) +) + +func newRawMetaInfo() *rawMetaInfo { + return new(rawMetaInfo) +} + +func (s *rawMetaInfo) GetTTL() uint32 { + return s.ttl +} + +func (s *rawMetaInfo) setTTL(ttl uint32) { + s.ttl = ttl +} + +func (s *rawMetaInfo) GetTimeout() time.Duration { + return s.timeout +} + +func (s *rawMetaInfo) setTimeout(dur time.Duration) { + s.timeout = dur +} + +func (s *rawMetaInfo) GetSessionToken() service.SessionToken { + return s.token +} + +func (s *rawMetaInfo) setSessionToken(token service.SessionToken) { + s.token = token +} + +func (s *rawMetaInfo) GetBearerToken() service.BearerToken { + return s.bearer +} + +func (s *rawMetaInfo) setBearerToken(token service.BearerToken) { + s.bearer = token +} + +func (s *rawMetaInfo) ExtendedHeaders() []service.ExtendedHeader { + return s.extHdrs +} + +func (s *rawMetaInfo) setExtendedHeaders(v []service.ExtendedHeader) { + s.extHdrs = v +} + +func (s *rawMetaInfo) GetRaw() bool { + return s.raw +} + +func (s *rawMetaInfo) setRaw(raw bool) { + s.raw = raw +} + +func (s *rawMetaInfo) Type() object.RequestType { + return s.rt +} + +func (s *rawMetaInfo) setType(rt object.RequestType) { + s.rt = rt +} + +func (s *rawAddrInfo) GetAddress() Address { + return s.addr +} + +func (s *rawAddrInfo) setAddress(addr Address) { + s.addr = addr +} + +func (s *rawAddrInfo) getMetaInfo() *rawMetaInfo { + return s.rawMetaInfo +} + +func (s *rawAddrInfo) setMetaInfo(v *rawMetaInfo) { + s.rawMetaInfo = v +} + +func newRawAddressInfo() *rawAddrInfo { + res := new(rawAddrInfo) + + res.setMetaInfo(newRawMetaInfo()) + + return res +} + +func (s *transportRequest) GetTimeout() time.Duration { return s.timeout } + +func (s *transportComponent) Transport(ctx context.Context, p transport.ObjectTransportParams) { + res, err := s.sendRequest(ctx, p.TransportInfo, p.TargetNode) + p.ResultHandler.HandleResult(ctx, p.TargetNode, res, err) + + go s.resTracker.trackResult(ctx, resultItems{ + requestType: p.TransportInfo.Type(), + node: p.TargetNode, + satisfactory: err == nil, + }) +} + +func (s *transportComponent) sendRequest(ctx context.Context, reqInfo transport.MetaInfo, node multiaddr.Multiaddr) (interface{}, error) { + p := sendParams{ + req: reqInfo, + node: node, + } + + switch reqInfo.Type() { + case object.RequestSearch: + p.handler = s.searchCaller + case object.RequestPut: + p.handler = s.putCaller + case object.RequestHead: + p.handler = s.headCaller + case object.RequestGet: + p.handler = s.getCaller + case object.RequestRangeHash: + p.handler = s.rangeHashCaller + case object.RequestRange: + p.handler = s.rangeCaller + default: + panic(fmt.Sprintf(pmWrongRequestType, reqInfo)) + } + + return s.reqSender.sendRequest(ctx, p) +} + +func (s *searchCaller) call(ctx context.Context, r serviceRequest, c *clientInfo) (interface{}, error) { + cSearch, err := c.sc.Search(ctx, r.(*object.SearchRequest)) + if err != nil { + return nil, err + } + + res := make([]Address, 0) + + for { + r, err := cSearch.Recv() + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + res = append(res, r.Addresses...) + } + + return res, nil +} + +func (s *rangeHashCaller) call(ctx context.Context, r serviceRequest, c *clientInfo) (interface{}, error) { + resp, err := c.sc.GetRangeHash(ctx, r.(*object.GetRangeHashRequest)) + if err != nil { + return nil, err + } + + return resp.Hashes, nil +} + +func (s *rangeCaller) call(ctx context.Context, r serviceRequest, c *clientInfo) (interface{}, error) { + req := r.(*GetRangeRequest) + + resp, err := c.sc.GetRange(ctx, req) + if err != nil { + return nil, err + } + + data := make([]byte, 0, req.Range.Length) + + for { + resp, err := resp.Recv() + if err != nil { + if err == io.EOF { + break + } + + return nil, err + } + + data = append(data, resp.Fragment...) + } + + return bytes.NewReader(data), nil +} + +func (s *headCaller) call(ctx context.Context, r serviceRequest, c *clientInfo) (interface{}, error) { + resp, err := c.sc.Head(ctx, r.(*object.HeadRequest)) + if err != nil { + return nil, err + } + + return resp.Object, nil +} + +func (s *getCaller) call(ctx context.Context, r serviceRequest, c *clientInfo) (interface{}, error) { + getClient, err := c.sc.Get(ctx, r.(*object.GetRequest)) + if err != nil { + return nil, err + } + + resp, err := getClient.Recv() + if err != nil { + return nil, err + } + + obj := resp.GetObject() + + if resp.NotFull() { + obj.Payload = make([]byte, 0, obj.SystemHeader.PayloadLength) + + for { + resp, err := getClient.Recv() + if err != nil { + if err == io.EOF { + break + } + + return nil, errors.Wrap(err, "get object received error") + } + + obj.Payload = append(obj.Payload, resp.GetChunk()...) + } + } + + return obj, nil +} + +func (s *putCaller) call(ctx context.Context, r serviceRequest, c *clientInfo) (interface{}, error) { + putClient, err := c.sc.Put(ctx) + if err != nil { + return nil, err + } + + req := r.(*putRequestSequence) + + if err := putClient.Send(req.PutRequest); err != nil { + return nil, err + } + + for i := range req.chunks { + if err := putClient.Send(req.chunks[i]); err != nil { + return nil, err + } + } + + resp, err := putClient.CloseAndRecv() + if err != nil { + return nil, err + } + + return &resp.Address, nil +} + +func (s *coreRequestPreparer) prepareRequest(req transport.MetaInfo) (serviceRequest, error) { + var ( + signed bool + tr *transportRequest + r serviceRequest + ) + + if tr, signed = req.(*transportRequest); signed { + r = tr.serviceRequest + } else { + switch req.Type() { + case object.RequestSearch: + r = prepareSearchRequest(req.(transport.SearchInfo)) + case object.RequestPut: + r = preparePutRequest(req.(transport.PutInfo)) + case object.RequestGet: + r = prepareGetRequest(req.(transport.GetInfo)) + case object.RequestHead: + r = prepareHeadRequest(req.(transport.HeadInfo)) + case object.RequestRange: + r = prepareRangeRequest(req.(transport.RangeInfo)) + case object.RequestRangeHash: + r = prepareRangeHashRequest(req.(transport.RangeHashInfo)) + default: + panic(fmt.Sprintf(pmWrongRequestType, req)) + } + } + + r.SetTTL(req.GetTTL()) + r.SetEpoch(s.epochRecv.Epoch()) + r.SetRaw(req.GetRaw()) + r.SetBearer( + toBearerMessage( + req.GetBearerToken(), + ), + ) + r.SetHeaders( + toExtendedHeaderMessages( + req.ExtendedHeaders(), + ), + ) + + if signed { + return r, nil + } + + key := s.key + + if token := req.GetSessionToken(); token != nil { + /* FIXME: here we must determine whether the node is trusted, + and if so, sign the request with a session key. + In current implementation trusted node may lose its reputation + in case of sending user requests in a nonexistent session. + */ + r.SetToken(toTokenMessage(token)) + + privateTokenKey := session.PrivateTokenKey{} + privateTokenKey.SetTokenID(token.GetID()) + privateTokenKey.SetOwnerID(token.GetOwnerID()) + + pToken, err := s.privateTokenStore.Fetch(privateTokenKey) + if err == nil { + if err := signRequest(pToken.PrivateKey(), r); err != nil { + return nil, err + } + } + } + + return r, signRequest(key, r) +} + +func toTokenMessage(token service.SessionToken) *service.Token { + if token == nil { + return nil + } else if v, ok := token.(*service.Token); ok { + return v + } + + res := new(service.Token) + + res.SetID(token.GetID()) + res.SetOwnerID(token.GetOwnerID()) + res.SetVerb(token.GetVerb()) + res.SetAddress(token.GetAddress()) + res.SetCreationEpoch(token.CreationEpoch()) + res.SetExpirationEpoch(token.ExpirationEpoch()) + res.SetSessionKey(token.GetSessionKey()) + res.SetSignature(token.GetSignature()) + + return res +} + +func toBearerMessage(token service.BearerToken) *service.BearerTokenMsg { + if token == nil { + return nil + } else if v, ok := token.(*service.BearerTokenMsg); ok { + return v + } + + res := new(service.BearerTokenMsg) + + res.SetACLRules(token.GetACLRules()) + res.SetOwnerID(token.GetOwnerID()) + res.SetExpirationEpoch(token.ExpirationEpoch()) + res.SetOwnerKey(token.GetOwnerKey()) + res.SetSignature(token.GetSignature()) + + return res +} + +func toExtendedHeaderMessages(hs []service.ExtendedHeader) []service.RequestExtendedHeader_KV { + res := make([]service.RequestExtendedHeader_KV, 0, len(hs)) + + for i := range hs { + if hs[i] == nil { + continue + } + + h := service.RequestExtendedHeader_KV{} + h.SetK(hs[i].Key()) + h.SetV(hs[i].Value()) + + res = append(res, h) + } + + return res +} + +func signRequest(key *ecdsa.PrivateKey, req serviceRequest) error { + signKeys := req.GetSignKeyPairs() + ln := len(signKeys) + + // TODO: public key bytes can be stored in struct once + if ln > 0 && bytes.Equal( + crypto.MarshalPublicKey(signKeys[ln-1].GetPublicKey()), + crypto.MarshalPublicKey(&key.PublicKey), + ) { + return nil + } + + return requestSignFunc(key, req) +} + +// TODO: write docs, write tests. +func prepareSearchRequest(req transport.SearchInfo) serviceRequest { + return &object.SearchRequest{ + ContainerID: req.GetCID(), + Query: req.GetQuery(), + QueryVersion: 1, + } +} + +func prepareGetRequest(req transport.GetInfo) serviceRequest { + return &object.GetRequest{ + Address: req.GetAddress(), + } +} + +func prepareHeadRequest(req transport.HeadInfo) serviceRequest { + return &object.HeadRequest{ + Address: req.GetAddress(), + FullHeaders: req.GetFullHeaders(), + } +} + +func preparePutRequest(req transport.PutInfo) serviceRequest { + obj := req.GetHead() + chunks := splitBytes(obj.Payload, maxGetPayloadSize) + + // copy object to save payload of initial object unchanged + nObj := new(Object) + *nObj = *obj + nObj.Payload = nil + + res := &putRequestSequence{ + PutRequest: object.MakePutRequestHeader(nObj), + chunks: make([]*object.PutRequest, 0, len(chunks)), + } + + // TODO: think about chunk messages signing + for i := range chunks { + res.chunks = append(res.chunks, object.MakePutRequestChunk(chunks[i])) + } + + return res +} + +func prepareRangeHashRequest(req transport.RangeHashInfo) serviceRequest { + return &object.GetRangeHashRequest{ + Address: req.GetAddress(), + Ranges: req.GetRanges(), + Salt: req.GetSalt(), + } +} + +func prepareRangeRequest(req transport.RangeInfo) serviceRequest { + return &GetRangeRequest{ + Address: req.GetAddress(), + Range: req.GetRange(), + } +} + +// TODO: write docs, write tests. +func (s *coreRequestSender) defaultTimeout(req transport.MetaInfo) time.Duration { + switch req.Type() { + case object.RequestSearch: + return s.searchTimeout + case object.RequestPut: + return s.putTimeout + case object.RequestGet: + return s.getTimeout + case object.RequestHead: + return s.headTimeout + case object.RequestRangeHash: + return s.rangeHashTimeout + } + + return minRemoteRequestTimeout +} + +// TODO: write docs, write tests. +func (s *coreRequestSender) sendRequest(ctx context.Context, p sendParams) (interface{}, error) { + var err error + + if p.node == nil { + if p.node, err = s.addressStore.SelfAddr(); err != nil { + return nil, err + } + } + + timeout := p.req.GetTimeout() + if timeout <= 0 { + timeout = s.defaultTimeout(p.req) + } + + r, err := s.requestPrep.prepareRequest(p.req) + if err != nil { + return nil, err + } + + dialCtx, cancel := context.WithTimeout(ctx, s.dialTimeout) + + c, err := s.remoteService.Remote(dialCtx, p.node) + + cancel() + + if err != nil { + return nil, err + } + + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + + return p.handler.call(ctx, r, &clientInfo{ + sc: c, + key: s.addressStore.PublicKey(p.node), + }) +} + +// NewMultiTransport is an object transport component's constructor. +func NewMultiTransport(p MultiTransportParams) (transport.ObjectTransport, error) { + switch { + case p.RemoteService == nil: + return nil, errEmptyGRPC + case p.AddressStore == nil: + return nil, errEmptyAddress + case p.Logger == nil: + return nil, errEmptyLogger + case p.EpochReceiver == nil: + return nil, errEmptyEpochReceiver + case p.Key == nil: + return nil, errEmptyPrivateKey + case p.PrivateTokenStore == nil: + return nil, errEmptyTokenStore + } + + if p.PutTimeout <= 0 { + p.PutTimeout = minRemoteRequestTimeout + } + + if p.GetTimeout <= 0 { + p.GetTimeout = minRemoteRequestTimeout + } + + if p.HeadTimeout <= 0 { + p.HeadTimeout = minRemoteRequestTimeout + } + + if p.SearchTimeout <= 0 { + p.SearchTimeout = minRemoteRequestTimeout + } + + if p.RangeHashTimeout <= 0 { + p.RangeHashTimeout = minRemoteRequestTimeout + } + + if p.DialTimeout <= 0 { + p.DialTimeout = minDialTimeout + } + + return &transportComponent{ + reqSender: &coreRequestSender{ + requestPrep: &coreRequestPreparer{ + epochRecv: p.EpochReceiver, + key: p.Key, + signingFunc: requestSignFunc, + + privateTokenStore: p.PrivateTokenStore, + }, + addressStore: p.AddressStore, + remoteService: p.RemoteService, + putTimeout: p.PutTimeout, + getTimeout: p.GetTimeout, + searchTimeout: p.SearchTimeout, + headTimeout: p.HeadTimeout, + rangeHashTimeout: p.RangeHashTimeout, + dialTimeout: p.DialTimeout, + }, + resTracker: &idleResultTracker{}, + getCaller: &getCaller{}, + putCaller: &putCaller{}, + headCaller: &headCaller{}, + rangeCaller: &rangeCaller{}, + rangeHashCaller: &rangeHashCaller{}, + searchCaller: &searchCaller{}, + }, nil +} diff --git a/services/public/object/transport_test.go b/services/public/object/transport_test.go new file mode 100644 index 0000000000..74ae2899af --- /dev/null +++ b/services/public/object/transport_test.go @@ -0,0 +1,76 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testTransportEntity struct { + // Set of interfaces which entity must implement, but some methods from those does not call. + object.ServiceClient + object.Service_PutClient + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ object.ServiceClient = (*testTransportEntity)(nil) + _ object.Service_PutClient = (*testTransportEntity)(nil) +) + +func (s *testTransportEntity) Send(*object.PutRequest) error { return s.err } + +func (s *testTransportEntity) CloseAndRecv() (*object.PutResponse, error) { + if s.err != nil { + return nil, s.err + } + return s.res.(*object.PutResponse), nil +} + +func (s *testTransportEntity) Put(ctx context.Context, opts ...grpc.CallOption) (object.Service_PutClient, error) { + if s.err != nil { + return nil, s.err + } + return s.res.(object.Service_PutClient), nil +} + +func Test_putHandler(t *testing.T) { + ctx := context.TODO() + + t.Run("return type correctness", func(t *testing.T) { + addr := new(Address) + *addr = testObjectAddress(t) + + srvClient := &testTransportEntity{ + res: &testTransportEntity{ + res: &object.PutResponse{ + Address: *addr, + }, + }, + } + + putC := &putCaller{} + + res, err := putC.call(ctx, &putRequestSequence{PutRequest: new(object.PutRequest)}, &clientInfo{ + sc: srvClient, + }) + require.NoError(t, err) + + // ascertain that value returns as expected + require.Equal(t, addr, res) + }) +} diff --git a/services/public/object/traverse.go b/services/public/object/traverse.go new file mode 100644 index 0000000000..38ec9d8b8a --- /dev/null +++ b/services/public/object/traverse.go @@ -0,0 +1,186 @@ +package object + +import ( + "context" + "sync" + + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-node/lib/implementations" + + "github.com/multiformats/go-multiaddr" + "github.com/pkg/errors" +) + +type ( + containerTraverser interface { + implementations.Traverser + add(multiaddr.Multiaddr, bool) + done(multiaddr.Multiaddr) bool + finished() bool + close() + Err() error + } + + placementBuilder interface { + buildPlacement(context.Context, Address, ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) + } + + traverseParams struct { + tryPrevNM bool + addr Address + curPlacementBuilder placementBuilder + prevPlacementBuilder placementBuilder + maxRecycleCount int + stopCount int + } + + coreTraverser struct { + closed bool + + usePrevNM bool + + recycleNum int + + *sync.RWMutex + traverseParams + failed []multiaddr.Multiaddr + mDone map[string]struct{} + err error + } +) + +var ( + _ placementBuilder = (*corePlacementUtil)(nil) + _ containerTraverser = (*coreTraverser)(nil) +) + +func (s *coreTraverser) Next(ctx context.Context) []multiaddr.Multiaddr { + if s.isClosed() || s.finished() { + return nil + } + + s.Lock() + defer s.Unlock() + + return s.next(ctx) +} + +func minInt(a, b int) int { + if a < b { + return a + } + + return b +} + +func (s *coreTraverser) next(ctx context.Context) (nodes []multiaddr.Multiaddr) { + defer func() { + if s.stopCount == 0 { + s.stopCount = len(nodes) + } + + if s.stopCount > 0 { + nodes = nodes[:minInt( + s.stopCount-len(s.mDone), + len(nodes), + )] + } + }() + + var placeBuilder = s.curPlacementBuilder + if s.usePrevNM { + placeBuilder = s.prevPlacementBuilder + } + + nodes, s.err = placeBuilder.buildPlacement(ctx, s.addr, s.failed...) + if errors.Is(errors.Cause(s.err), container.ErrNotFound) { + return + } + + for i := 0; i < len(nodes); i++ { + if _, ok := s.mDone[nodes[i].String()]; ok { + nodes = append(nodes[:i], nodes[i+1:]...) + i-- + } + + continue + } + + if len(nodes) == 0 { + if !s.usePrevNM && s.tryPrevNM { + s.usePrevNM = true + return s.next(ctx) + } + + if s.recycleNum < s.maxRecycleCount { + s.reset() + return s.next(ctx) + } + } + + return nodes +} + +func (s *coreTraverser) reset() { + s.usePrevNM = false + s.failed = s.failed[:0] + s.recycleNum++ +} + +func (s *coreTraverser) add(node multiaddr.Multiaddr, ok bool) { + s.Lock() + if ok { + s.mDone[node.String()] = struct{}{} + } else { + s.failed = append(s.failed, node) + } + s.Unlock() +} + +func (s *coreTraverser) done(node multiaddr.Multiaddr) bool { + s.RLock() + _, ok := s.mDone[node.String()] + s.RUnlock() + + return ok +} + +func (s *coreTraverser) close() { + s.Lock() + s.closed = true + s.Unlock() +} + +func (s *coreTraverser) isClosed() bool { + s.RLock() + defer s.RUnlock() + + return s.closed +} + +func (s *coreTraverser) finished() bool { + s.RLock() + defer s.RUnlock() + + return s.stopCount > 0 && len(s.mDone) >= s.stopCount +} + +func (s *coreTraverser) Err() error { + s.RLock() + defer s.RUnlock() + + return s.err +} + +func newContainerTraverser(p *traverseParams) containerTraverser { + return &coreTraverser{ + RWMutex: new(sync.RWMutex), + traverseParams: *p, + failed: make([]multiaddr.Multiaddr, 0), + mDone: make(map[string]struct{}), + } +} + +func (s *corePlacementUtil) buildPlacement(ctx context.Context, addr Address, excl ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + return s.placementBuilder.GetNodes(ctx, addr, s.prevNetMap, excl...) +} diff --git a/services/public/object/traverse_test.go b/services/public/object/traverse_test.go new file mode 100644 index 0000000000..93462b20be --- /dev/null +++ b/services/public/object/traverse_test.go @@ -0,0 +1,378 @@ +package object + +import ( + "context" + "strconv" + "sync" + "testing" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testTraverseEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + Placer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ Placer = (*testTraverseEntity)(nil) + _ placementBuilder = (*testTraverseEntity)(nil) +) + +func (s *testTraverseEntity) GetNodes(ctx context.Context, a Address, p bool, e ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + if s.f != nil { + s.f(a, p, e) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]multiaddr.Multiaddr), nil +} + +func (s *testTraverseEntity) buildPlacement(_ context.Context, addr Address, excl ...multiaddr.Multiaddr) ([]multiaddr.Multiaddr, error) { + if s.f != nil { + s.f(addr, excl) + } + if s.err != nil { + return nil, s.err + } + return s.res.([]multiaddr.Multiaddr), nil +} + +func Test_coreCnrAffChecker_buildPlacement(t *testing.T) { + ctx := context.TODO() + addr := testObjectAddress(t) + nodes := testNodeList(t, 2) + + t.Run("correct placer params", func(t *testing.T) { + s := &corePlacementUtil{ + prevNetMap: true, + placementBuilder: &testTraverseEntity{ + f: func(items ...interface{}) { + require.Equal(t, addr, items[0].(Address)) + require.True(t, items[1].(bool)) + require.Equal(t, nodes, items[2].([]multiaddr.Multiaddr)) + }, + err: internal.Error(""), // just to prevent panic + }, + log: zap.L(), + } + + s.buildPlacement(ctx, addr, nodes...) + }) + + t.Run("correct result", func(t *testing.T) { + t.Run("placer error", func(t *testing.T) { + s := &corePlacementUtil{ + placementBuilder: &testTraverseEntity{ + err: internal.Error(""), // force Placer to return some error + }, + log: zap.L(), + } + + res, err := s.buildPlacement(ctx, addr) + require.Error(t, err) + require.Empty(t, res) + }) + + t.Run("placer success", func(t *testing.T) { + s := &corePlacementUtil{ + placementBuilder: &testTraverseEntity{ + res: nodes, // force Placer to return nodes + }, + log: zap.L(), + } + + res, err := s.buildPlacement(ctx, addr) + require.NoError(t, err) + require.Equal(t, nodes, res) + }) + }) +} + +func Test_coreTraverser(t *testing.T) { + ctx := context.TODO() + + t.Run("new", func(t *testing.T) { + addr := testObjectAddress(t) + pl := new(testTraverseEntity) + + v := newContainerTraverser(&traverseParams{ + tryPrevNM: true, + addr: addr, + curPlacementBuilder: pl, + prevPlacementBuilder: pl, + maxRecycleCount: 10, + }) + + res := v.(*coreTraverser) + + require.NotNil(t, res.RWMutex) + require.Equal(t, addr, res.addr) + require.True(t, res.tryPrevNM) + require.False(t, res.usePrevNM) + require.NotNil(t, res.mDone) + require.Empty(t, res.mDone) + require.Empty(t, res.failed) + require.Equal(t, 10, res.maxRecycleCount) + require.Equal(t, pl, res.curPlacementBuilder) + require.Equal(t, pl, res.prevPlacementBuilder) + require.Equal(t, 0, res.stopCount) + }) + + t.Run("close", func(t *testing.T) { + v := newContainerTraverser(&traverseParams{ + curPlacementBuilder: &testTraverseEntity{ + res: make([]multiaddr.Multiaddr, 1), + }, + }) + + v.close() + + require.Empty(t, v.Next(ctx)) + require.True(t, v.(*coreTraverser).isClosed()) + }) + + t.Run("done", func(t *testing.T) { + nodes := testNodeList(t, 3) + v := newContainerTraverser(&traverseParams{}) + + v.add(nodes[0], true) + require.True(t, v.done(nodes[0])) + + v.add(nodes[1], false) + require.False(t, v.done(nodes[1])) + + require.False(t, v.done(nodes[2])) + }) + + t.Run("finished", func(t *testing.T) { + + t.Run("zero stop count", func(t *testing.T) { + containerTraverser := &coreTraverser{ + RWMutex: new(sync.RWMutex), + traverseParams: traverseParams{stopCount: 0}, + } + require.False(t, containerTraverser.finished()) + }) + + t.Run("positive stop count", func(t *testing.T) { + containerTraverser := &coreTraverser{ + RWMutex: new(sync.RWMutex), + mDone: make(map[string]struct{}), + traverseParams: traverseParams{stopCount: 3}, + } + + for i := 0; i < containerTraverser.stopCount-1; i++ { + containerTraverser.mDone[strconv.Itoa(i)] = struct{}{} + } + + require.False(t, containerTraverser.finished()) + + containerTraverser.mDone["last node"] = struct{}{} + + require.True(t, containerTraverser.finished()) + }) + }) + + t.Run("add result", func(t *testing.T) { + mAddr := testNode(t, 0) + + containerTraverser := &coreTraverser{ + RWMutex: new(sync.RWMutex), + mDone: make(map[string]struct{}), + } + + containerTraverser.add(mAddr, true) + _, ok := containerTraverser.mDone[mAddr.String()] + require.True(t, ok) + + containerTraverser.add(mAddr, false) + require.Contains(t, containerTraverser.failed, mAddr) + }) + + t.Run("reset", func(t *testing.T) { + initRecycleNum := 1 + + s := &coreTraverser{ + failed: testNodeList(t, 1), + usePrevNM: true, + recycleNum: initRecycleNum, + } + + s.reset() + + require.Empty(t, s.failed) + require.False(t, s.usePrevNM) + require.Equal(t, initRecycleNum+1, s.recycleNum) + }) + + t.Run("next", func(t *testing.T) { + + t.Run("exclude done nodes from result", func(t *testing.T) { + nodes := testNodeList(t, 5) + done := make([]multiaddr.Multiaddr, 2) + copy(done, nodes) + + pl := &testTraverseEntity{res: nodes} + tr := newContainerTraverser(&traverseParams{curPlacementBuilder: pl}) + + for i := range done { + tr.add(done[i], true) + } + + res := tr.Next(ctx) + for i := range done { + require.NotContains(t, res, done[i]) + } + + }) + + t.Run("stop count initialization", func(t *testing.T) { + nodes := testNodeList(t, 5) + + pl := &testTraverseEntity{res: nodes} + + tr := newContainerTraverser(&traverseParams{curPlacementBuilder: pl}) + + _ = tr.Next(ctx) + require.Equal(t, len(nodes), tr.(*coreTraverser).stopCount) + }) + + t.Run("all nodes are done", func(t *testing.T) { + nodes := testNodeList(t, 5) + pl := &testTraverseEntity{res: nodes} + tr := newContainerTraverser(&traverseParams{curPlacementBuilder: pl}) + + require.Equal(t, nodes, tr.Next(ctx)) + + for i := range nodes { + tr.add(nodes[i], true) + } + + require.Empty(t, tr.Next(ctx)) + }) + + t.Run("failed nodes accounting", func(t *testing.T) { + nodes := testNodeList(t, 5) + failed := nodes[:len(nodes)-2] + _ = failed + addr := testObjectAddress(t) + + pl := &testTraverseEntity{ + f: func(items ...interface{}) { + t.Run("correct placer params", func(t *testing.T) { + require.Equal(t, addr, items[0].(Address)) + require.Equal(t, failed, items[1].([]multiaddr.Multiaddr)) + }) + }, + res: nodes, + } + + tr := newContainerTraverser(&traverseParams{ + addr: addr, + curPlacementBuilder: pl, + }) + + for i := range failed { + tr.add(failed[i], false) + } + + _ = tr.Next(ctx) + }) + + t.Run("placement build failure", func(t *testing.T) { + + t.Run("forbid previous network map", func(t *testing.T) { + pl := &testTraverseEntity{res: make([]multiaddr.Multiaddr, 0)} + + tr := newContainerTraverser(&traverseParams{curPlacementBuilder: pl}) + + require.Empty(t, tr.Next(ctx)) + }) + + t.Run("allow previous network map", func(t *testing.T) { + + t.Run("failure", func(t *testing.T) { + pl := &testTraverseEntity{ + res: make([]multiaddr.Multiaddr, 0), + } + + tr := newContainerTraverser(&traverseParams{ + tryPrevNM: true, + curPlacementBuilder: pl, + prevPlacementBuilder: pl, + }) + + require.Empty(t, tr.Next(ctx)) + }) + + t.Run("success", func(t *testing.T) { + nodes := testNodeList(t, 5) + + tr := newContainerTraverser(&traverseParams{ + tryPrevNM: true, + curPlacementBuilder: &testTraverseEntity{ + res: make([]multiaddr.Multiaddr, 0), + }, + prevPlacementBuilder: &testTraverseEntity{ + res: nodes, + }, + }) + + require.Equal(t, nodes, tr.Next(ctx)) + }) + }) + + t.Run("recycle", func(t *testing.T) { + recycleCount := 5 + + curNetMapCallCounter, prevNetMapCallCounter := 0, 0 + + tr := newContainerTraverser(&traverseParams{ + tryPrevNM: true, + curPlacementBuilder: &testTraverseEntity{ + f: func(items ...interface{}) { + curNetMapCallCounter++ + }, + res: make([]multiaddr.Multiaddr, 0), + }, + prevPlacementBuilder: &testTraverseEntity{ + f: func(items ...interface{}) { + prevNetMapCallCounter++ + }, + res: make([]multiaddr.Multiaddr, 0), + }, + maxRecycleCount: recycleCount, + }) + + _ = tr.Next(ctx) + require.Equal(t, recycleCount+1, prevNetMapCallCounter) + require.Equal(t, recycleCount+1, curNetMapCallCounter) + }) + }) + }) +} + +func testNodeList(t *testing.T, count int) (res []multiaddr.Multiaddr) { + for i := 0; i < count; i++ { + res = append(res, testNode(t, i)) + } + return +} diff --git a/services/public/object/ttl.go b/services/public/object/ttl.go new file mode 100644 index 0000000000..cdc8a5748d --- /dev/null +++ b/services/public/object/ttl.go @@ -0,0 +1,211 @@ +package object + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type ( + // ttlPreProcessor is an implementation of requestPreProcessor interface used in Object service production. + ttlPreProcessor struct { + // List of static TTL conditions. + staticCond []service.TTLCondition + + // List of TTL condition constructors. + condPreps []ttlConditionPreparer + + // Processing function. + fProc func(service.TTLSource, ...service.TTLCondition) error + } + + // ttlConditionPreparer is an interface of TTL condition constructor. + ttlConditionPreparer interface { + // prepareTTLCondition creates TTL condition instance based on passed request. + prepareTTLCondition(context.Context, object.Request) service.TTLCondition + } + + // coreTTLCondPreparer is an implementation of ttlConditionPreparer interface used in Object service production. + coreTTLCondPreparer struct { + curAffChecker containerAffiliationChecker + prevAffChecker containerAffiliationChecker + } + + containerAffiliationResult int + + // containerAffiliationChecker is an interface of container membership validator. + containerAffiliationChecker interface { + // Checks local node is affiliated with container with passed ID. + affiliated(context.Context, CID) containerAffiliationResult + } + + // corePlacementUtil is an implementation of containerAffiliationChecker interface used in Object service production. + corePlacementUtil struct { + // Previous network map flag. + prevNetMap bool + + // Local node net address store. + localAddrStore implementations.AddressStore + + // Container nodes membership maintainer. + placementBuilder Placer + + // Logging component. + log *zap.Logger + } +) + +// decTTLPreProcessor is an implementation of requestPreProcessor. +type decTTLPreProcessor struct { +} + +const ( + _ containerAffiliationResult = iota + affUnknown + affNotFound + affPresence + affAbsence +) + +const ( + lmSelfAddrRecvFail = "could not receive local network address" +) + +var ( + _ containerAffiliationChecker = (*corePlacementUtil)(nil) + _ ttlConditionPreparer = (*coreTTLCondPreparer)(nil) + _ requestPreProcessor = (*ttlPreProcessor)(nil) + + _ service.TTLCondition = validTTLCondition + + _ requestPreProcessor = (*decTTLPreProcessor)(nil) +) + +// requestPreProcessor method implementation. +// +// Panics with pmEmptyServiceRequest on empty request. +// +// Constructs set of TTL conditions via internal constructors. +// Returns result of internal TTL conditions processing function. +func (s *ttlPreProcessor) preProcess(ctx context.Context, req serviceRequest) error { + if req == nil { + panic(pmEmptyServiceRequest) + } + + dynamicCond := make([]service.TTLCondition, len(s.condPreps)) + + for i := range s.condPreps { + dynamicCond[i] = s.condPreps[i].prepareTTLCondition(ctx, req) + } + + return s.fProc(req, append(s.staticCond, dynamicCond...)...) +} + +// ttlConditionPreparer method implementation. +// +// Condition returns ErrNotLocalContainer if and only if request is non-forwarding and local node is not presented +// in placement vector corresponding to request. +func (s *coreTTLCondPreparer) prepareTTLCondition(ctx context.Context, req object.Request) service.TTLCondition { + if req == nil { + panic(pmEmptyServiceRequest) + } + + return func(ttl uint32) error { + // check forwarding assumption + if ttl >= service.SingleForwardingTTL { + // container affiliation doesn't matter + return nil + } + + // get target container ID from request body + cid := req.CID() + + // check local node affiliation to container + aff := s.curAffChecker.affiliated(ctx, cid) + + if aff == affAbsence && req.AllowPreviousNetMap() { + // request can be forwarded to container members from previous epoch + aff = s.prevAffChecker.affiliated(ctx, cid) + } + + switch aff { + case affUnknown: + return errContainerAffiliationProblem + case affNotFound: + return &detailedError{ + error: errContainerNotFound, + d: containerDetails(cid, descContainerNotFound), + } + case affAbsence: + return &detailedError{ + error: errNotLocalContainer, + d: containerDetails(cid, descNotLocalContainer), + } + } + + return nil + } +} + +// containerAffiliationChecker method implementation. +// +// If local network address store returns error, logger writes error and affUnknown returns. +// If placement builder returns error +// - caused by ErrNotFound, affNotFound returns; +// - status error with NotFound code, affNotFound returns; +// - any other, affUnknown returns, +// Otherwise, if placement builder returns +// - true, affPresence returns; +// - false, affAbsence returns. +func (s *corePlacementUtil) affiliated(ctx context.Context, cid CID) containerAffiliationResult { + selfAddr, err := s.localAddrStore.SelfAddr() + if err != nil { + s.log.Error(lmSelfAddrRecvFail, zap.Error(err)) + return affUnknown + } + + aff, err := s.placementBuilder.IsContainerNode(ctx, selfAddr, cid, s.prevNetMap) + if err != nil { + if err := errors.Cause(err); errors.Is(err, container.ErrNotFound) { + return affNotFound + } + + return affUnknown + } + + if !aff { + return affAbsence + } + + return affPresence +} + +func processTTLConditions(req service.TTLSource, cs ...service.TTLCondition) error { + ttl := req.GetTTL() + + for i := range cs { + if err := cs[i](ttl); err != nil { + return err + } + } + + return nil +} + +func validTTLCondition(ttl uint32) error { + if ttl < service.NonForwardingTTL { + return errInvalidTTL + } + + return nil +} + +func (s *decTTLPreProcessor) preProcess(_ context.Context, req serviceRequest) error { + req.SetTTL(req.GetTTL() - 1) + return nil +} diff --git a/services/public/object/ttl_test.go b/services/public/object/ttl_test.go new file mode 100644 index 0000000000..073d87951b --- /dev/null +++ b/services/public/object/ttl_test.go @@ -0,0 +1,377 @@ +package object + +import ( + "context" + "math/rand" + "strconv" + "testing" + + "github.com/multiformats/go-multiaddr" + "github.com/nspcc-dev/neofs-api-go/container" + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testTTLEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + Placer + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +var ( + _ ttlConditionPreparer = (*testTTLEntity)(nil) + _ implementations.AddressStore = (*testTTLEntity)(nil) + _ containerAffiliationChecker = (*testTTLEntity)(nil) + _ Placer = (*testTTLEntity)(nil) +) + +func (s *testTTLEntity) SelfAddr() (multiaddr.Multiaddr, error) { + if s.err != nil { + return nil, s.err + } + return s.res.(multiaddr.Multiaddr), nil +} + +func (s *testTTLEntity) IsContainerNode(_ context.Context, m multiaddr.Multiaddr, c CID, b bool) (bool, error) { + if s.f != nil { + s.f(m, c, b) + } + if s.err != nil { + return false, s.err + } + return s.res.(bool), nil +} + +func (s *testTTLEntity) CID() CID { return s.res.([]interface{})[0].(CID) } + +func (s *testTTLEntity) AllowPreviousNetMap() bool { return s.res.([]interface{})[1].(bool) } + +func (s *testTTLEntity) prepareTTLCondition(_ context.Context, req object.Request) service.TTLCondition { + if s.f != nil { + s.f(req) + } + return s.res.(service.TTLCondition) +} + +func (s *testTTLEntity) affiliated(ctx context.Context, cid CID) containerAffiliationResult { + if s.f != nil { + s.f(cid) + } + return s.res.(containerAffiliationResult) +} + +func Test_ttlPreProcessor_preProcess(t *testing.T) { + ctx := context.TODO() + + // create custom request with forwarding TTL + req := &testTTLEntity{res: uint32(service.SingleForwardingTTL)} + + t.Run("empty request", func(t *testing.T) { + require.PanicsWithValue(t, pmEmptyServiceRequest, func() { + // ascertain that nil request causes panic + _ = new(ttlPreProcessor).preProcess(ctx, nil) + }) + }) + + t.Run("correct processing", func(t *testing.T) { + // create custom error + pErr := internal.Error("test error for processing func") + + // create custom ttlConditionPreparer + condPreparer := &testTTLEntity{ + f: func(items ...interface{}) { + t.Run("correct condition preparer params", func(t *testing.T) { + // ascertain that request argument of ttlPreProcessor and ttlConditionPreparer are the same + require.Equal(t, req, items[0].(object.Request)) + }) + }, + res: service.TTLCondition(func(uint32) error { return nil }), + } + + s := &ttlPreProcessor{ + condPreps: []ttlConditionPreparer{condPreparer}, + fProc: func(service.TTLSource, ...service.TTLCondition) error { + return pErr // force processing function to return created error + }, + } + + // ascertain error returns as expected + require.EqualError(t, + s.preProcess(ctx, req), + pErr.Error(), + ) + }) +} + +func Test_coreTTLCondPreparer_prepareTTLCondition(t *testing.T) { + ctx := context.TODO() + + // create container ID + cid := testObjectAddress(t).CID + + // // create network address + // mAddr := testNode(t, 0) + // + // // create custom AddressStore + // as := &testTTLEntity{ + // res: mAddr, // force AddressStore to return created address + // } + + t.Run("empty request", func(t *testing.T) { + require.PanicsWithValue(t, pmEmptyServiceRequest, func() { + // ascertain that nil request causes panic + _ = new(coreTTLCondPreparer).prepareTTLCondition(ctx, nil) + }) + }) + + t.Run("forwarding TTL", func(t *testing.T) { + s := &coreTTLCondPreparer{ + curAffChecker: new(testTTLEntity), + prevAffChecker: new(testTTLEntity), + } + + cond := s.prepareTTLCondition(ctx, new(testTTLEntity)) + + // ascertain that error returns as expected + require.NoError(t, cond(service.SingleForwardingTTL)) + }) + + t.Run("non-forwarding TTL", func(t *testing.T) { + t.Run("container non-affiliation", func(t *testing.T) { + t.Run("disallow previous epoch affiliation", func(t *testing.T) { + // create custom serviceRequest for test + req := &testTTLEntity{res: []interface{}{ + cid, // force serviceRequest to return cid + false, // force serviceRequest to disallow previous network map + }} + + s := &coreTTLCondPreparer{ + curAffChecker: &testTTLEntity{ + f: func(items ...interface{}) { + t.Run("correct current epoch affiliation checker params", func(t *testing.T) { + require.Equal(t, cid, items[0].(CID)) + }) + }, + res: affAbsence, // force current epoch containerAffiliationChecker to return affAbsence + }, + prevAffChecker: &testTTLEntity{ + f: func(items ...interface{}) { + t.Run("correct previous epoch affiliation checker params", func(t *testing.T) { + require.Equal(t, cid, items[0].(CID)) + }) + }, + res: affPresence, // force previous epoch containerAffiliationChecker to return affPresence + }, + } + + cond := s.prepareTTLCondition(ctx, req) + + // ascertain that error returns as expected + require.EqualError(t, + cond(service.SingleForwardingTTL-1), // pass any non-forwarding TTL + errNotLocalContainer.Error(), + ) + }) + + t.Run("allow previous epoch affiliation", func(t *testing.T) { + // create custom serviceRequest for test + req := &testTTLEntity{res: []interface{}{ + cid, // force serviceRequest to return cid + true, // force serviceRequest to allow previous network map + }} + + s := &coreTTLCondPreparer{ + curAffChecker: &testTTLEntity{ + res: affAbsence, // force current epoch containerAffiliationChecker to return affAbsence + }, + prevAffChecker: &testTTLEntity{ + res: affAbsence, // force previous epoch containerAffiliationChecker to return affAbsence + }, + } + + cond := s.prepareTTLCondition(ctx, req) + + // ascertain that error returns as expected + require.EqualError(t, + cond(service.SingleForwardingTTL-1), // pass any non-forwarding TTL + errNotLocalContainer.Error(), + ) + }) + }) + + t.Run("container affiliation", func(t *testing.T) { + t.Run("disallow previous epoch affiliation", func(t *testing.T) { + // create custom serviceRequest for test + req := &testTTLEntity{res: []interface{}{ + cid, // force serviceRequest to return cid + false, // force serviceRequest to disallow previous network map + }} + + s := &coreTTLCondPreparer{ + curAffChecker: &testTTLEntity{ + res: affPresence, // force current epoch containerAffiliationChecker to return affPresence + }, + prevAffChecker: &testTTLEntity{ + res: affAbsence, // force previous epoch containerAffiliationChecker to return affAbsence + }, + } + + cond := s.prepareTTLCondition(ctx, req) + + // ascertain that error returns as expected + require.NoError(t, + cond(service.SingleForwardingTTL-1), // pass any non-forwarding TTL + ) + }) + + t.Run("allow previous epoch affiliation", func(t *testing.T) { + // create custom serviceRequest for test + req := &testTTLEntity{res: []interface{}{ + cid, // force serviceRequest to return cid + true, // force serviceRequest to allow previous network map + }} + + s := &coreTTLCondPreparer{ + curAffChecker: &testTTLEntity{ + res: affAbsence, // force current epoch containerAffiliationChecker to return affAbsence + }, + prevAffChecker: &testTTLEntity{ + res: affPresence, // force previous epoch containerAffiliationChecker to return affPresence + }, + } + + cond := s.prepareTTLCondition(ctx, req) + + // ascertain that error returns as expected + require.NoError(t, + cond(service.SingleForwardingTTL-1), // pass any non-forwarding TTL + ) + }) + }) + }) +} + +func Test_coreCnrAffChecker_affiliated(t *testing.T) { + ctx := context.TODO() + + // create container ID + cid := testObjectAddress(t).CID + + log := zap.L() + + t.Run("local network address store error", func(t *testing.T) { + // create custom error for test + saErr := internal.Error("test error for self addr store") + + s := &corePlacementUtil{ + localAddrStore: &testTTLEntity{ + err: saErr, // force address store to return saErr + }, + log: log, + } + + require.Equal(t, affUnknown, s.affiliated(ctx, cid)) + }) + + t.Run("placement build result", func(t *testing.T) { + // create network address + mAddr := testNode(t, 0) + + // create custom AddressStore + as := &testTTLEntity{ + res: mAddr, // force AddressStore to return created address + } + + t.Run("error", func(t *testing.T) { + pb := &testTTLEntity{ + f: func(items ...interface{}) { + t.Run("correct placement builder params", func(t *testing.T) { + require.Equal(t, mAddr, items[0].(multiaddr.Multiaddr)) + require.Equal(t, cid, items[1].(CID)) + require.Equal(t, true, items[2].(bool)) + }) + }, + } + + pb.err = internal.Error("") // force Placer to return some non-nil error + + s := &corePlacementUtil{ + prevNetMap: true, + localAddrStore: as, + placementBuilder: pb, + log: log, + } + + require.Equal(t, affUnknown, s.affiliated(ctx, cid)) + + pb.err = container.ErrNotFound + + require.Equal(t, affNotFound, s.affiliated(ctx, cid)) + }) + + t.Run("no error", func(t *testing.T) { + t.Run("affiliation", func(t *testing.T) { + s := &corePlacementUtil{ + localAddrStore: as, + placementBuilder: &testTTLEntity{ + res: true, // force Placer to return true, nil + }, + log: log, + } + + require.Equal(t, affPresence, s.affiliated(ctx, cid)) + }) + + t.Run("non-affiliation", func(t *testing.T) { + s := &corePlacementUtil{ + localAddrStore: as, + placementBuilder: &testTTLEntity{ + res: false, // force Placer to return false, nil + }, + log: log, + } + + require.Equal(t, affAbsence, s.affiliated(ctx, cid)) + }) + }) + }) +} + +// testNode returns 0.0.0.0:(8000+num). +func testNode(t *testing.T, num int) multiaddr.Multiaddr { + mAddr, err := multiaddr.NewMultiaddr("/ip4/0.0.0.0/tcp/" + strconv.Itoa(8000+num)) + require.NoError(t, err) + return mAddr +} + +// testObjectAddress returns new random object address. +func testObjectAddress(t *testing.T) Address { + oid, err := refs.NewObjectID() + require.NoError(t, err) + return Address{CID: refs.CIDForBytes(testData(t, refs.CIDSize)), ObjectID: oid} +} + +// testData returns size bytes of random data. +func testData(t *testing.T, size int) []byte { + res := make([]byte, size) + _, err := rand.Read(res) + require.NoError(t, err) + return res +} diff --git a/services/public/object/verb.go b/services/public/object/verb.go new file mode 100644 index 0000000000..8551b91f14 --- /dev/null +++ b/services/public/object/verb.go @@ -0,0 +1,79 @@ +package object + +import ( + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" +) + +// Verb is a type alias of +// Token_Info_Verb from service package of neofs-api-go. +type Verb = service.Token_Info_Verb + +const ( + undefinedVerbDesc uint32 = 1 << iota + putVerbDesc + getVerbDesc + headVerbDesc + deleteVerbDesc + searchVerbDesc + rangeVerbDesc + rangeHashVerbDesc +) + +const ( + headSpawnMask = headVerbDesc | getVerbDesc | putVerbDesc | rangeVerbDesc | rangeHashVerbDesc + rangeHashSpawnMask = rangeHashVerbDesc + rangeSpawnMask = rangeVerbDesc | getVerbDesc + getSpawnMask = getVerbDesc + putSpawnMask = putVerbDesc | deleteVerbDesc + deleteSpawnMask = deleteVerbDesc + searchSpawnMask = searchVerbDesc | getVerbDesc | putVerbDesc | headVerbDesc | rangeVerbDesc | rangeHashVerbDesc | deleteVerbDesc +) + +func toVerbDesc(verb Verb) uint32 { + switch verb { + case service.Token_Info_Put: + return putVerbDesc + case service.Token_Info_Get: + return getVerbDesc + case service.Token_Info_Head: + return headVerbDesc + case service.Token_Info_Delete: + return deleteVerbDesc + case service.Token_Info_Search: + return searchVerbDesc + case service.Token_Info_Range: + return rangeVerbDesc + case service.Token_Info_RangeHash: + return rangeHashVerbDesc + default: + return undefinedVerbDesc + } +} + +func toSpawnMask(rt object.RequestType) uint32 { + switch rt { + case object.RequestPut: + return putSpawnMask + case object.RequestGet: + return getSpawnMask + case object.RequestHead: + return headSpawnMask + case object.RequestDelete: + return deleteSpawnMask + case object.RequestSearch: + return searchSpawnMask + case object.RequestRange: + return rangeSpawnMask + case object.RequestRangeHash: + return rangeHashSpawnMask + default: + return undefinedVerbDesc + } +} + +func allowedSpawn(from Verb, to object.RequestType) bool { + desc := toVerbDesc(from) + + return toSpawnMask(to)&desc == desc +} diff --git a/services/public/object/verb_test.go b/services/public/object/verb_test.go new file mode 100644 index 0000000000..0c01e4bedb --- /dev/null +++ b/services/public/object/verb_test.go @@ -0,0 +1,124 @@ +package object + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/stretchr/testify/require" +) + +func TestAllowedSpawn(t *testing.T) { + items := []struct { + to object.RequestType + ok []Verb + fail []Verb + }{ + { // Put + to: object.RequestPut, + ok: []Verb{ + service.Token_Info_Put, + service.Token_Info_Delete, + }, + fail: []Verb{ + service.Token_Info_Get, + service.Token_Info_Head, + service.Token_Info_Range, + service.Token_Info_RangeHash, + }, + }, + { // Get + to: object.RequestGet, + ok: []Verb{ + service.Token_Info_Get, + }, + fail: []Verb{ + service.Token_Info_Put, + service.Token_Info_Delete, + service.Token_Info_RangeHash, + service.Token_Info_Head, + service.Token_Info_Search, + service.Token_Info_Range, + }, + }, + { // Head + to: object.RequestHead, + ok: []Verb{ + service.Token_Info_Head, + service.Token_Info_Put, + service.Token_Info_Range, + service.Token_Info_Get, + service.Token_Info_RangeHash, + }, + fail: []Verb{ + service.Token_Info_Search, + service.Token_Info_Delete, + }, + }, + { // Delete + to: object.RequestDelete, + ok: []Verb{ + service.Token_Info_Delete, + }, + fail: []Verb{ + service.Token_Info_Get, + service.Token_Info_Head, + service.Token_Info_Range, + service.Token_Info_RangeHash, + service.Token_Info_Put, + service.Token_Info_Search, + }, + }, + { // Search + to: object.RequestSearch, + ok: []Verb{ + service.Token_Info_Put, + service.Token_Info_Get, + service.Token_Info_Head, + service.Token_Info_Delete, + service.Token_Info_Range, + service.Token_Info_RangeHash, + service.Token_Info_Search, + }, + fail: []Verb{}, + }, + { // Range + to: object.RequestRange, + ok: []Verb{ + service.Token_Info_Get, + service.Token_Info_Range, + }, + fail: []Verb{ + service.Token_Info_Put, + service.Token_Info_Delete, + service.Token_Info_RangeHash, + service.Token_Info_Head, + service.Token_Info_Search, + }, + }, + { // RangeHash + to: object.RequestRangeHash, + ok: []Verb{ + service.Token_Info_RangeHash, + }, + fail: []Verb{ + service.Token_Info_Put, + service.Token_Info_Get, + service.Token_Info_Delete, + service.Token_Info_Range, + service.Token_Info_Head, + service.Token_Info_Search, + }, + }, + } + + for _, item := range items { + for _, from := range item.ok { + require.True(t, allowedSpawn(from, item.to)) + } + + for _, from := range item.fail { + require.False(t, allowedSpawn(from, item.to)) + } + } +} diff --git a/services/public/object/verification.go b/services/public/object/verification.go new file mode 100644 index 0000000000..de51365c87 --- /dev/null +++ b/services/public/object/verification.go @@ -0,0 +1,36 @@ +package object + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/service" +) + +type ( + verifyRequestFunc func(token service.RequestVerifyData) error + + // verifyPreProcessor is an implementation of requestPreProcessor interface. + verifyPreProcessor struct { + // Verifying function. + fVerify verifyRequestFunc + } +) + +var _ requestPreProcessor = (*verifyPreProcessor)(nil) + +// requestPreProcessor method implementation. +// +// Panics with pmEmptyServiceRequest on empty request. +// +// Returns result of internal requestVerifyFunc instance. +func (s *verifyPreProcessor) preProcess(_ context.Context, req serviceRequest) (err error) { + if req == nil { + panic(pmEmptyServiceRequest) + } + + if err = s.fVerify(req); err != nil { + err = errUnauthenticated + } + + return +} diff --git a/services/public/object/verification_test.go b/services/public/object/verification_test.go new file mode 100644 index 0000000000..b7c305c084 --- /dev/null +++ b/services/public/object/verification_test.go @@ -0,0 +1,63 @@ +package object + +import ( + "context" + "testing" + + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/stretchr/testify/require" +) + +type ( + // Entity for mocking interfaces. + // Implementation of any interface intercepts arguments via f (if not nil). + // If err is not nil, it returns as it is. Otherwise, casted to needed type res returns w/o error. + testVerificationEntity struct { + // Set of interfaces which testCommonEntity must implement, but some methods from those does not call. + serviceRequest + + // Argument interceptor. Used for ascertain of correct parameter passage between components. + f func(...interface{}) + // Mocked result of any interface. + res interface{} + // Mocked error of any interface. + err error + } +) + +func Test_verifyPreProcessor_preProcess(t *testing.T) { + ctx := context.TODO() + + t.Run("empty request", func(t *testing.T) { + require.PanicsWithValue(t, pmEmptyServiceRequest, func() { + _ = new(verifyPreProcessor).preProcess(ctx, nil) + }) + }) + + t.Run("correct result", func(t *testing.T) { + t.Run("failure", func(t *testing.T) { + // create custom error + vErr := internal.Error("test error for verifying func") + + s := &verifyPreProcessor{ + fVerify: func(service.RequestVerifyData) error { return vErr }, // force requestVerifyFunc to return vErr + } + + // ascertain that error returns as expected + require.EqualError(t, + s.preProcess(ctx, new(testVerificationEntity)), + errUnauthenticated.Error(), + ) + }) + + t.Run("success", func(t *testing.T) { + s := &verifyPreProcessor{ + fVerify: func(service.RequestVerifyData) error { return nil }, // force requestVerifyFunc to return nil + } + + // ascertain that nil error returns as expected + require.NoError(t, s.preProcess(ctx, new(testVerificationEntity))) + }) + }) +} diff --git a/services/public/session/create.go b/services/public/session/create.go new file mode 100644 index 0000000000..85696fbd2b --- /dev/null +++ b/services/public/session/create.go @@ -0,0 +1,53 @@ +package session + +import ( + "context" + "errors" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/session" +) + +var errExpiredSession = errors.New("expired session") + +func (s sessionService) Create(ctx context.Context, req *CreateRequest) (*CreateResponse, error) { + // check lifetime + expired := req.ExpirationEpoch() + if s.epochReceiver.Epoch() > expired { + return nil, errExpiredSession + } + + // generate private token for session + pToken, err := session.NewPrivateToken(expired) + if err != nil { + return nil, err + } + + pkBytes, err := session.PublicSessionToken(pToken) + if err != nil { + return nil, err + } + + // generate token ID + tokenID, err := refs.NewUUID() + if err != nil { + return nil, err + } + + // create private token storage key + pTokenKey := session.PrivateTokenKey{} + pTokenKey.SetOwnerID(req.GetOwnerID()) + pTokenKey.SetTokenID(tokenID) + + // store private token + if err := s.ts.Store(pTokenKey, pToken); err != nil { + return nil, err + } + + // construct response + resp := new(session.CreateResponse) + resp.SetID(tokenID) + resp.SetSessionKey(pkBytes) + + return resp, nil +} diff --git a/services/public/session/service.go b/services/public/session/service.go new file mode 100644 index 0000000000..3accd07964 --- /dev/null +++ b/services/public/session/service.go @@ -0,0 +1,66 @@ +package session + +import ( + "github.com/nspcc-dev/neofs-api-go/session" + "github.com/nspcc-dev/neofs-node/modules/grpc" + "go.uber.org/zap" +) + +type ( + sessionService struct { + ts TokenStore + log *zap.Logger + + epochReceiver EpochReceiver + } + + // Service is an interface of the server of Session service. + Service interface { + grpc.Service + session.SessionServer + } + + // EpochReceiver is an interface of the container of epoch number with read access. + EpochReceiver interface { + Epoch() uint64 + } + + // Params groups the parameters of Session service server's constructor. + Params struct { + TokenStore TokenStore + + Logger *zap.Logger + + EpochReceiver EpochReceiver + } + + // TokenStore is a type alias of + // TokenStore from session package of neofs-api-go. + TokenStore = session.PrivateTokenStore + + // CreateRequest is a type alias of + // CreateRequest from session package of neofs-api-go. + CreateRequest = session.CreateRequest + + // CreateResponse is a type alias of + // CreateResponse from session package of neofs-api-go. + CreateResponse = session.CreateResponse +) + +// New is an Session service server's constructor. +func New(p Params) Service { + return &sessionService{ + ts: p.TokenStore, + log: p.Logger, + + epochReceiver: p.EpochReceiver, + } +} + +func (sessionService) Name() string { + return "Session Server" +} + +func (s sessionService) Register(srv *grpc.Server) { + session.RegisterSessionServer(srv, s) +} diff --git a/services/public/session/service_test.go b/services/public/session/service_test.go new file mode 100644 index 0000000000..82f85fac1b --- /dev/null +++ b/services/public/session/service_test.go @@ -0,0 +1,3 @@ +package session + +// TODO: write tests diff --git a/services/public/state/service.go b/services/public/state/service.go new file mode 100644 index 0000000000..14d19c10f6 --- /dev/null +++ b/services/public/state/service.go @@ -0,0 +1,324 @@ +package state + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "strconv" + + "github.com/nspcc-dev/neofs-api-go/bootstrap" + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/state" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/internal" + "github.com/nspcc-dev/neofs-node/lib/core" + "github.com/nspcc-dev/neofs-node/lib/implementations" + "github.com/nspcc-dev/neofs-node/modules/grpc" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/viper" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type ( + // Service is an interface of the server of State service. + Service interface { + state.StatusServer + grpc.Service + Healthy() error + } + + // HealthChecker is an interface of node healthiness checking tool. + HealthChecker interface { + Name() string + Healthy() bool + } + + // Stater is an interface of the node's network state storage with read access. + Stater interface { + NetworkState() *bootstrap.SpreadMap + } + + // Params groups the parameters of State service server's constructor. + Params struct { + Stater Stater + + Logger *zap.Logger + + Viper *viper.Viper + + Checkers []HealthChecker + + PrivateKey *ecdsa.PrivateKey + + MorphNetmapContract *implementations.MorphNetmapContract + } + + stateService struct { + state Stater + config *viper.Viper + checkers []HealthChecker + private *ecdsa.PrivateKey + owners map[refs.OwnerID]struct{} + + stateUpdater *implementations.MorphNetmapContract + } + + // HealthRequest is a type alias of + // HealthRequest from state package of neofs-api-go. + HealthRequest = state.HealthRequest +) + +const ( + errEmptyViper = internal.Error("empty config") + errEmptyLogger = internal.Error("empty logger") + errEmptyStater = internal.Error("empty stater") + errUnknownChangeState = internal.Error("received unknown state") +) + +const msgMissingRequestInitiator = "missing request initiator" + +var requestVerifyFunc = core.VerifyRequestWithSignatures + +// New is an State service server's constructor. +func New(p Params) (Service, error) { + switch { + case p.Logger == nil: + return nil, errEmptyLogger + case p.Viper == nil: + return nil, errEmptyViper + case p.Stater == nil: + return nil, errEmptyStater + case p.PrivateKey == nil: + return nil, crypto.ErrEmptyPrivateKey + } + + svc := &stateService{ + config: p.Viper, + state: p.Stater, + private: p.PrivateKey, + owners: fetchOwners(p.Logger, p.Viper), + checkers: make([]HealthChecker, 0, len(p.Checkers)), + + stateUpdater: p.MorphNetmapContract, + } + + for i, checker := range p.Checkers { + if checker == nil { + p.Logger.Debug("ignore empty checker", + zap.Int("index", i)) + continue + } + + p.Logger.Info("register health-checker", + zap.String("name", checker.Name())) + + svc.checkers = append(svc.checkers, checker) + } + + return svc, nil +} + +func fetchOwners(l *zap.Logger, v *viper.Viper) map[refs.OwnerID]struct{} { + // if config.yml used: + items := v.GetStringSlice("node.rpc.owners") + + for i := 0; ; i++ { + item := v.GetString("node.rpc.owners." + strconv.Itoa(i)) + + if item == "" { + l.Info("stat: skip empty owner", zap.Int("idx", i)) + break + } + + items = append(items, item) + } + + result := make(map[refs.OwnerID]struct{}, len(items)) + + for i := range items { + var owner refs.OwnerID + + if data, err := hex.DecodeString(items[i]); err != nil { + l.Warn("stat: skip wrong hex data", + zap.Int("idx", i), + zap.String("key", items[i]), + zap.Error(err)) + + continue + } else if key := crypto.UnmarshalPublicKey(data); key == nil { + l.Warn("stat: skip wrong key", + zap.Int("idx", i), + zap.String("key", items[i])) + continue + } else if owner, err = refs.NewOwnerID(key); err != nil { + l.Warn("stat: skip wrong key", + zap.Int("idx", i), + zap.String("key", items[i]), + zap.Error(err)) + continue + } + + result[owner] = struct{}{} + + l.Info("rpc owner added", zap.Stringer("owner", owner)) + } + + return result +} + +func nonForwarding(ttl uint32) error { + if ttl != service.NonForwardingTTL { + return status.Error(codes.InvalidArgument, service.ErrInvalidTTL.Error()) + } + + return nil +} + +func requestInitiator(req service.SignKeyPairSource) *ecdsa.PublicKey { + if signKeys := req.GetSignKeyPairs(); len(signKeys) > 0 { + return signKeys[0].GetPublicKey() + } + + return nil +} + +// ChangeState allows to change current node state of node. +// To permit access, used server config options. +// The request should be signed. +func (s *stateService) ChangeState(ctx context.Context, in *state.ChangeStateRequest) (*state.ChangeStateResponse, error) { + // verify request structure + if err := requestVerifyFunc(in); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + // verify change state permission + if key := requestInitiator(in); key == nil { + return nil, status.Error(codes.InvalidArgument, msgMissingRequestInitiator) + } else if owner, err := refs.NewOwnerID(key); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } else if _, ok := s.owners[owner]; !ok { + return nil, status.Error(codes.PermissionDenied, service.ErrWrongOwner.Error()) + } + + // convert State field to NodeState + if in.GetState() != state.ChangeStateRequest_Offline { + return nil, status.Error(codes.InvalidArgument, errUnknownChangeState.Error()) + } + + // set update state parameters + p := implementations.UpdateStateParams{} + p.SetState(implementations.StateOffline) + p.SetKey( + crypto.MarshalPublicKey(&s.private.PublicKey), + ) + + if err := s.stateUpdater.UpdateState(p); err != nil { + return nil, status.Error(codes.Aborted, err.Error()) + } + + return new(state.ChangeStateResponse), nil +} + +// DumpConfig request allows dumping settings for the current node. +// To permit access, used server config options. +// The request should be signed. +func (s *stateService) DumpConfig(_ context.Context, req *state.DumpRequest) (*state.DumpResponse, error) { + if err := service.ProcessRequestTTL(req, nonForwarding); err != nil { + return nil, err + } else if err = requestVerifyFunc(req); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } else if key := requestInitiator(req); key == nil { + return nil, status.Error(codes.InvalidArgument, msgMissingRequestInitiator) + } else if owner, err := refs.NewOwnerID(key); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } else if _, ok := s.owners[owner]; !ok { + return nil, status.Error(codes.PermissionDenied, service.ErrWrongOwner.Error()) + } + + return state.EncodeConfig(s.config) +} + +// Netmap returns SpreadMap from Stater (IRState / Place-component). +func (s *stateService) Netmap(_ context.Context, req *state.NetmapRequest) (*bootstrap.SpreadMap, error) { + if err := service.ProcessRequestTTL(req); err != nil { + return nil, err + } else if err = requestVerifyFunc(req); err != nil { + return nil, err + } + + if s.state != nil { + return s.state.NetworkState(), nil + } + + return nil, status.New(codes.Unavailable, "service unavailable").Err() +} + +func (s *stateService) healthy() error { + for _, svc := range s.checkers { + if !svc.Healthy() { + return errors.Errorf("service(%s) unhealthy", svc.Name()) + } + } + + return nil +} + +// Healthy returns error as status of service, if nil service healthy. +func (s *stateService) Healthy() error { return s.healthy() } + +// Check that all checkers is healthy. +func (s *stateService) HealthCheck(_ context.Context, req *HealthRequest) (*state.HealthResponse, error) { + if err := service.ProcessRequestTTL(req); err != nil { + return nil, err + } else if err = requestVerifyFunc(req); err != nil { + return nil, err + } + + var ( + err = s.healthy() + resp = &state.HealthResponse{Healthy: true, Status: "OK"} + ) + + if err != nil { + resp.Healthy = false + resp.Status = err.Error() + } + + return resp, nil +} + +func (*stateService) Metrics(_ context.Context, req *state.MetricsRequest) (*state.MetricsResponse, error) { + if err := service.ProcessRequestTTL(req); err != nil { + return nil, err + } else if err = requestVerifyFunc(req); err != nil { + return nil, err + } + + return state.EncodeMetrics(prometheus.DefaultGatherer) +} + +func (s *stateService) DumpVars(_ context.Context, req *state.DumpVarsRequest) (*state.DumpVarsResponse, error) { + if err := service.ProcessRequestTTL(req, nonForwarding); err != nil { + return nil, err + } else if err = requestVerifyFunc(req); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } else if key := requestInitiator(req); key == nil { + return nil, status.Error(codes.InvalidArgument, msgMissingRequestInitiator) + } else if owner, err := refs.NewOwnerID(key); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } else if _, ok := s.owners[owner]; !ok { + return nil, status.Error(codes.PermissionDenied, service.ErrWrongOwner.Error()) + } + + return state.EncodeVariables(), nil +} + +// Name of the service. +func (*stateService) Name() string { return "StatusService" } + +// Register service on gRPC server. +func (s *stateService) Register(g *grpc.Server) { state.RegisterStatusServer(g, s) } diff --git a/services/public/state/service_test.go b/services/public/state/service_test.go new file mode 100644 index 0000000000..b3a279758d --- /dev/null +++ b/services/public/state/service_test.go @@ -0,0 +1,249 @@ +package state + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "expvar" + "os" + "strings" + "testing" + + "github.com/nspcc-dev/neofs-api-go/refs" + "github.com/nspcc-dev/neofs-api-go/service" + "github.com/nspcc-dev/neofs-api-go/state" + crypto "github.com/nspcc-dev/neofs-crypto" + "github.com/nspcc-dev/neofs-node/lib/test" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var requestSignFunc = service.SignRequestData + +func Test_nonForwarding(t *testing.T) { + cases := []struct { + err error + ttl uint32 + name string + }{ + { + name: "ZeroTTL", + ttl: service.ZeroTTL, + err: status.Error(codes.InvalidArgument, service.ErrInvalidTTL.Error()), + }, + { + name: "SingleForwardingTTL", + ttl: service.SingleForwardingTTL, + err: status.Error(codes.InvalidArgument, service.ErrInvalidTTL.Error()), + }, + { + name: "NonForwardingTTL", + ttl: service.NonForwardingTTL, + err: nil, + }, + } + + for i := range cases { + tt := cases[i] + t.Run(tt.name, func(t *testing.T) { + err := nonForwarding(tt.ttl) + switch tt.err { + case nil: + require.NoError(t, err, tt.name) + default: + require.EqualError(t, err, tt.err.Error()) + } + }) + } +} + +func Test_fetchOwners(t *testing.T) { + l := test.NewTestLogger(false) + + t.Run("from config options", func(t *testing.T) { + key0 := test.DecodeKey(0) + require.NotEmpty(t, key0) + + data0 := crypto.MarshalPublicKey(&key0.PublicKey) + hKey0 := hex.EncodeToString(data0) + + owner0, err := refs.NewOwnerID(&key0.PublicKey) + require.NoError(t, err) + + v := viper.New() + v.SetDefault("node.rpc.owners", []string{hKey0}) + + owners := fetchOwners(l, v) + require.Len(t, owners, 1) + require.Contains(t, owners, owner0) + }) + + t.Run("from environment and config options", func(t *testing.T) { + key0 := test.DecodeKey(0) + require.NotEmpty(t, key0) + + data0 := crypto.MarshalPublicKey(&key0.PublicKey) + hKey0 := hex.EncodeToString(data0) + + owner0, err := refs.NewOwnerID(&key0.PublicKey) + require.NoError(t, err) + + key1 := test.DecodeKey(1) + require.NotEmpty(t, key1) + + owner1, err := refs.NewOwnerID(&key1.PublicKey) + require.NoError(t, err) + + data1 := crypto.MarshalPublicKey(&key1.PublicKey) + hKey1 := hex.EncodeToString(data1) + + require.NoError(t, os.Setenv("NEOFS_NODE_RPC_OWNERS_0", hKey1)) + + v := viper.New() + v.AutomaticEnv() + v.SetEnvPrefix("NeoFS") + v.SetConfigType("yaml") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.SetDefault("node.rpc.owners", []string{hKey0}) + + require.NoError(t, v.ReadConfig(strings.NewReader(""))) + + owners := fetchOwners(l, v) + + require.Len(t, owners, 2) + require.Contains(t, owners, owner0) + require.Contains(t, owners, owner1) + }) +} + +func TestStateService_DumpConfig(t *testing.T) { + cases := []struct { + err error + ttl uint32 + name string + key *ecdsa.PrivateKey + }{ + { + err: nil, + name: "allow", + key: test.DecodeKey(0), + ttl: service.NonForwardingTTL, + }, + { + name: "wrong ttl", + key: test.DecodeKey(0), + ttl: service.SingleForwardingTTL, + err: status.Error(codes.InvalidArgument, service.ErrInvalidTTL.Error()), + }, + } + key := test.DecodeKey(0) + require.NotEmpty(t, key) + + owner, err := refs.NewOwnerID(&key.PublicKey) + require.NoError(t, err) + + owners := map[refs.OwnerID]struct{}{ + owner: {}, + } + + viper.SetDefault("test", true) + + svc := stateService{ + owners: owners, + config: viper.GetViper(), + } + + for i := range cases { + tt := cases[i] + t.Run(tt.name, func(t *testing.T) { + req := new(state.DumpRequest) + + req.SetTTL(tt.ttl) + if tt.key != nil { + require.NoError(t, requestSignFunc(tt.key, req)) + } + + res, err := svc.DumpConfig(context.Background(), req) + switch tt.err { + case nil: + require.NoError(t, err, tt.name) + require.NotEmpty(t, res) + require.NotEmpty(t, res.Config) + default: + require.EqualError(t, err, tt.err.Error()) + require.Empty(t, res) + } + }) + } +} + +func TestStateService_DumpVars(t *testing.T) { + cases := []struct { + err error + ttl uint32 + name string + key *ecdsa.PrivateKey + }{ + { + err: nil, + name: "allow", + key: test.DecodeKey(0), + ttl: service.NonForwardingTTL, + }, + { + name: "wrong ttl", + key: test.DecodeKey(0), + ttl: service.SingleForwardingTTL, + err: status.Error(codes.InvalidArgument, service.ErrInvalidTTL.Error()), + }, + } + key := test.DecodeKey(0) + require.NotEmpty(t, key) + + owner, err := refs.NewOwnerID(&key.PublicKey) + require.NoError(t, err) + + owners := map[refs.OwnerID]struct{}{ + owner: {}, + } + + svc := stateService{owners: owners} + + expvar.NewString("test1").Set("test1") + expvar.NewString("test2").Set("test2") + + for i := range cases { + tt := cases[i] + t.Run(tt.name, func(t *testing.T) { + req := new(state.DumpVarsRequest) + + req.SetTTL(tt.ttl) + if tt.key != nil { + require.NoError(t, requestSignFunc(tt.key, req)) + } + + res, err := svc.DumpVars(nil, req) + switch tt.err { + case nil: + require.NoError(t, err, tt.name) + require.NotEmpty(t, res) + require.NotEmpty(t, res.Variables) + + dump := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(res.Variables, &dump)) + + require.Contains(t, dump, "test1") + require.Equal(t, dump["test1"], "test1") + + require.Contains(t, dump, "test2") + require.Equal(t, dump["test2"], "test2") + default: + require.EqualError(t, err, tt.err.Error()) + require.Empty(t, res) + } + }) + } +}