diff --git a/cmd/neofs-node/config/tree/config.go b/cmd/neofs-node/config/tree/config.go new file mode 100644 index 00000000..8fdc1101 --- /dev/null +++ b/cmd/neofs-node/config/tree/config.go @@ -0,0 +1,39 @@ +package treeconfig + +import ( + "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config" +) + +// GRPCConfig is a wrapper over "grpc" config section which provides access +// to gRPC configuration of control service. +type GRPCConfig struct { + cfg *config.Config +} + +const ( + subsection = "tree" + grpcSubsection = "grpc" + + // GRPCEndpointDefault is a default endpoint of gRPC Control service. + GRPCEndpointDefault = "" +) + +// GRPC returns structure that provides access to "grpc" subsection of +// "tree" section. +func GRPC(c *config.Config) GRPCConfig { + return GRPCConfig{ + c.Sub(subsection).Sub(grpcSubsection), + } +} + +// Endpoint returns value of "endpoint" config parameter. +// +// Returns GRPCEndpointDefault if value is not a non-empty string. +func (g GRPCConfig) Endpoint() string { + v := config.String(g.cfg, "endpoint") + if v != "" { + return v + } + + return GRPCEndpointDefault +} diff --git a/cmd/neofs-node/config/tree/config_test.go b/cmd/neofs-node/config/tree/config_test.go new file mode 100644 index 00000000..9ac92fe6 --- /dev/null +++ b/cmd/neofs-node/config/tree/config_test.go @@ -0,0 +1,30 @@ +package treeconfig_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config" + configtest "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/test" + treeconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/tree" + "github.com/stretchr/testify/require" +) + +func TestControlSection(t *testing.T) { + t.Run("defaults", func(t *testing.T) { + empty := configtest.EmptyConfig() + + require.Equal(t, treeconfig.GRPCEndpointDefault, treeconfig.GRPC(empty).Endpoint()) + }) + + const path = "../../../../config/example/node" + + var fileConfigTest = func(c *config.Config) { + require.Equal(t, "127.0.0.1:8091", treeconfig.GRPC(c).Endpoint()) + } + + configtest.ForEachFileType(path, fileConfigTest) + + t.Run("ENV", func(t *testing.T) { + configtest.ForEnvFileType(path, fileConfigTest) + }) +} diff --git a/cmd/neofs-node/main.go b/cmd/neofs-node/main.go index 807dce4c..67aca281 100644 --- a/cmd/neofs-node/main.go +++ b/cmd/neofs-node/main.go @@ -81,6 +81,7 @@ func initApp(c *cfg) { initAndLog(c, "pprof", initProfiler) initAndLog(c, "prometheus", initMetrics) initAndLog(c, "control", initControlService) + initAndLog(c, "tree", initTreeService) initAndLog(c, "storage engine", func(c *cfg) { fatalOnErr(c.cfgObject.cfgLocalStorage.localStorage.Open()) diff --git a/cmd/neofs-node/tree.go b/cmd/neofs-node/tree.go new file mode 100644 index 00000000..37dd331e --- /dev/null +++ b/cmd/neofs-node/tree.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "net" + + treeconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/tree" + "github.com/nspcc-dev/neofs-node/pkg/services/tree" + "google.golang.org/grpc" +) + +func initTreeService(c *cfg) { + endpoint := treeconfig.GRPC(c.appCfg).Endpoint() + if endpoint == treeconfig.GRPCEndpointDefault { + return + } + + treeSvc := tree.New( + tree.WithContainerSource(c.cfgObject.cnrSource), + tree.WithNetmapSource(c.netMapSource), + tree.WithPrivateKey(&c.key.PrivateKey), + tree.WithLogger(c.log), + tree.WithStorage(c.cfgObject.cfgLocalStorage.localStorage)) + + treeServer := grpc.NewServer() + + c.onShutdown(func() { + stopGRPC("NeoFS Tree Service API", treeServer, c.log) + treeSvc.Shutdown() + }) + + lis, err := net.Listen("tcp", endpoint) + fatalOnErr(err) + + tree.RegisterTreeServiceServer(treeServer, treeSvc) + + c.workers = append(c.workers, newWorkerFromFunc(func(ctx context.Context) { + treeSvc.Start(ctx) + fatalOnErr(treeServer.Serve(lis)) + })) +} diff --git a/config/example/node.env b/config/example/node.env index 3c674ee7..1c31501f 100644 --- a/config/example/node.env +++ b/config/example/node.env @@ -46,6 +46,9 @@ NEOFS_GRPC_1_TLS_ENABLED=false NEOFS_CONTROL_AUTHORIZED_KEYS="035839e45d472a3b7769a2a1bd7d54c4ccd4943c3b40f547870e83a8fcbfb3ce11 028f42cfcb74499d7b15b35d9bff260a1c8d27de4f446a627406a382d8961486d6" NEOFS_CONTROL_GRPC_ENDPOINT=localhost:8090 +# Tree service section +NEOFS_TREE_GRPC_ENDPOINT=127.0.0.1:8091 + # Contracts section NEOFS_CONTRACTS_BALANCE=5263abba1abedbf79bb57f3e40b50b4425d2d6cd NEOFS_CONTRACTS_CONTAINER=5d084790d7aa36cea7b53fe897380dab11d2cd3c diff --git a/config/example/node.json b/config/example/node.json index 569aff46..b3ae7850 100644 --- a/config/example/node.json +++ b/config/example/node.json @@ -84,6 +84,11 @@ "endpoint": "localhost:8090" } }, + "tree": { + "grpc": { + "endpoint": "127.0.0.1:8091" + } + }, "contracts": { "balance": "5263abba1abedbf79bb57f3e40b50b4425d2d6cd", "container": "5d084790d7aa36cea7b53fe897380dab11d2cd3c", diff --git a/config/example/node.yaml b/config/example/node.yaml index b3eec638..3465eb07 100644 --- a/config/example/node.yaml +++ b/config/example/node.yaml @@ -66,6 +66,10 @@ control: grpc: endpoint: localhost:8090 # endpoint that is listened by the Control Service +tree: + grpc: + endpoint: 127.0.0.1:8091 # endpoint that is listened by the Tree Service + contracts: # side chain NEOFS contract script hashes; optional, override values retrieved from NNS contract balance: 5263abba1abedbf79bb57f3e40b50b4425d2d6cd container: 5d084790d7aa36cea7b53fe897380dab11d2cd3c diff --git a/go.mod b/go.mod index a6ccbecc..46a96c06 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require google.golang.org/api v0.44.0 + require ( + cloud.google.com/go v0.81.0 // indirect github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210521073959-f0d4d129b7f1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd v0.22.0-beta // indirect @@ -47,8 +50,10 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.3 // indirect + github.com/google/go-cmp v0.5.6 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -87,12 +92,15 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect github.com/twmb/murmur3 v1.1.5 // indirect github.com/urfave/cli v1.22.5 // indirect + go.opencensus.io v0.23.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 1c82bb69..c0f0a43c 100644 Binary files a/go.sum and b/go.sum differ diff --git a/pkg/services/tree/options.go b/pkg/services/tree/options.go new file mode 100644 index 00000000..2cf2bdf4 --- /dev/null +++ b/pkg/services/tree/options.go @@ -0,0 +1,59 @@ +package tree + +import ( + "crypto/ecdsa" + + "github.com/nspcc-dev/neofs-node/pkg/core/container" + "github.com/nspcc-dev/neofs-node/pkg/core/netmap" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" + "go.uber.org/zap" +) + +type cfg struct { + log *zap.Logger + key *ecdsa.PrivateKey + nmSource netmap.Source + cnrSource container.Source + forest pilorama.Forest +} + +// Option represents configuration option for a tree service. +type Option func(*cfg) + +// WithContainerSource sets a container source for a tree service. +// This option is required. +func WithContainerSource(src container.Source) Option { + return func(c *cfg) { + c.cnrSource = src + } +} + +// WithNetmapSource sets a netmap source for a tree service. +// This option is required. +func WithNetmapSource(src netmap.Source) Option { + return func(c *cfg) { + c.nmSource = src + } +} + +// WithPrivateKey sets a netmap source for a tree service. +// This option is required. +func WithPrivateKey(key *ecdsa.PrivateKey) Option { + return func(c *cfg) { + c.key = key + } +} + +// WithLogger sets logger for a tree service. +func WithLogger(log *zap.Logger) Option { + return func(c *cfg) { + c.log = log + } +} + +// WithStorage sets tree storage for a service. +func WithStorage(s pilorama.Forest) Option { + return func(c *cfg) { + c.forest = s + } +} diff --git a/pkg/services/tree/replicator.go b/pkg/services/tree/replicator.go new file mode 100644 index 00000000..87cced5b --- /dev/null +++ b/pkg/services/tree/replicator.go @@ -0,0 +1,145 @@ +package tree + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "time" + + clientcore "github.com/nspcc-dev/neofs-node/pkg/core/client" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" + "github.com/nspcc-dev/neofs-node/pkg/network" + "github.com/nspcc-dev/neofs-node/pkg/services/object_manager/placement" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + netmapSDK "github.com/nspcc-dev/neofs-sdk-go/netmap" + "go.uber.org/zap" + "google.golang.org/api/option" + "google.golang.org/api/transport/grpc" +) + +type movePair struct { + cid cidSDK.ID + treeID string + op *pilorama.LogMove +} + +const ( + defaultReplicatorCapacity = 64 + defaultReplicatorTimeout = time.Second * 2 +) + +func (s *Service) replicateLoop(ctx context.Context) { + for { + select { + case <-s.closeCh: + case <-ctx.Done(): + return + case op := <-s.replicateCh: + ctx, cancel := context.WithTimeout(ctx, defaultReplicatorTimeout) + err := s.replicate(ctx, op) + cancel() + + if err != nil { + s.log.Error("error during replication", + zap.String("err", err.Error()), + zap.Stringer("cid", op.cid), + zap.String("treeID", op.treeID)) + } + } + } +} + +func (s *Service) replicate(ctx context.Context, op movePair) error { + req := newApplyRequest(&op) + // TODO(@fyrchik): #1328 access control + //err := signature.SignDataWithHandler(s.key, req, func(key, sign []byte) { + // req.Signature = &Signature{ + // Key: key, + // Sign: sign, + // } + //}) + //if err != nil { + // return fmt.Errorf("can't sign data: %w", err) + //} + + nodes, err := s.getContainerNodes(op.cid) + if err != nil { + return fmt.Errorf("can't get container nodes: %w", err) + } + + var node clientcore.NodeInfo + for _, n := range nodes { + var lastErr error + + n.IterateNetworkEndpoints(func(addr string) bool { + cc, err := grpc.Dial(ctx, option.WithEndpoint(addr)) + if err != nil { + lastErr = err + return false + } + + // TODO cache clients + c := NewTreeServiceClient(cc) + + _, lastErr = c.Apply(ctx, req) + return lastErr == nil + }) + + if lastErr != nil { + s.log.Warn("failed to sent update to the node", + zap.String("last_error", lastErr.Error()), + zap.String("address", network.StringifyGroup(node.AddressGroup())), + zap.String("key", base64.StdEncoding.EncodeToString(node.PublicKey()))) + } + } + return nil +} + +func (s *Service) pushToQueue(cid cidSDK.ID, treeID string, op *pilorama.LogMove) { + s.replicateCh <- movePair{ + cid: cid, + treeID: treeID, + op: op, + } +} + +func (s *Service) getContainerNodes(cid cidSDK.ID) ([]netmapSDK.NodeInfo, error) { + nm, err := s.nmSource.GetNetMap(0) + if err != nil { + return nil, fmt.Errorf("can't get netmap: %w", err) + } + + cnr, err := s.cnrSource.Get(cid) + if err != nil { + return nil, fmt.Errorf("can't get container: %w", err) + } + + policy := cnr.Value.PlacementPolicy() + rawCID := make([]byte, sha256.Size) + cid.Encode(rawCID) + + nodes, err := nm.ContainerNodes(policy, rawCID) + if err != nil { + return nil, err + } + + return placement.FlattenNodes(nodes), nil +} + +func newApplyRequest(op *movePair) *ApplyRequest { + rawCID := make([]byte, sha256.Size) + op.cid.Encode(rawCID) + + return &ApplyRequest{ + Body: &ApplyRequest_Body{ + ContainerId: rawCID, + TreeId: op.treeID, + Operation: &LogMove{ + ParentId: op.op.Parent, + Meta: op.op.Meta.Bytes(), + ChildId: op.op.Child, + }, + }, + } +} diff --git a/pkg/services/tree/service.go b/pkg/services/tree/service.go new file mode 100644 index 00000000..00de22b8 --- /dev/null +++ b/pkg/services/tree/service.go @@ -0,0 +1,294 @@ +package tree + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/nspcc-dev/neofs-api-go/v2/signature" + "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/pilorama" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + "go.uber.org/zap" +) + +// Service represents tree-service capable of working with multiple +// instances of CRDT trees. +type Service struct { + cfg + + replicateCh chan movePair + closeCh chan struct{} +} + +var _ TreeServiceServer = (*Service)(nil) + +// New creates new tree service instance. +func New(opts ...Option) *Service { + var s Service + for i := range opts { + opts[i](&s.cfg) + } + + if s.log == nil { + s.log = zap.NewNop() + } + + s.closeCh = make(chan struct{}) + s.replicateCh = make(chan movePair, defaultReplicatorCapacity) + + return &s +} + +// Start starts the service. +func (s *Service) Start(ctx context.Context) { + go s.replicateLoop(ctx) +} + +// Shutdown shutdowns the service. +func (s *Service) Shutdown() { + close(s.closeCh) +} + +func (s *Service) Add(_ context.Context, req *AddRequest) (*AddResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + log, err := s.forest.TreeMove(cid, b.GetTreeId(), &pilorama.Move{ + Parent: b.GetParentId(), + Child: pilorama.RootID, + Meta: pilorama.Meta{Items: constructMeta(b.GetMeta())}, + }) + if err != nil { + return nil, err + } + + s.pushToQueue(cid, b.GetTreeId(), log) + return &AddResponse{ + Body: &AddResponse_Body{ + NodeId: log.Child, + }, + }, nil +} + +func (s *Service) AddByPath(_ context.Context, req *AddByPathRequest) (*AddByPathResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + meta := constructMeta(b.GetMeta()) + + attr := b.GetPathAttribute() + if len(attr) == 0 { + attr = pilorama.AttributeFilename + } + + logs, err := s.forest.TreeAddByPath(cid, b.GetTreeId(), attr, b.GetPath(), meta) + if err != nil { + return nil, err + } + + for i := range logs { + s.pushToQueue(cid, b.GetTreeId(), &logs[i]) + } + + nodes := make([]uint64, len(logs)) + nodes[0] = logs[len(logs)-1].Child + for i, l := range logs[:len(logs)-1] { + nodes[i+1] = l.Child + } + + return &AddByPathResponse{ + Body: &AddByPathResponse_Body{ + Nodes: nodes, + }, + }, nil +} + +func (s *Service) Remove(_ context.Context, req *RemoveRequest) (*RemoveResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + if b.GetNodeId() == pilorama.RootID { + return nil, fmt.Errorf("node with ID %d is root and can't be removed", b.GetNodeId()) + } + + log, err := s.forest.TreeMove(cid, b.GetTreeId(), &pilorama.Move{ + Parent: pilorama.TrashID, + Child: b.GetNodeId(), + }) + if err != nil { + return nil, err + } + + s.pushToQueue(cid, b.GetTreeId(), log) + return new(RemoveResponse), nil +} + +// Move applies client operation to the specified tree and pushes in queue +// for replication on other nodes. +func (s *Service) Move(_ context.Context, req *MoveRequest) (*MoveResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + err := s.verifyClient(req, cid, req.GetSignature().GetKey()) + if err != nil { + return nil, err + } + + if b.GetNodeId() == pilorama.RootID { + return nil, fmt.Errorf("node with ID %d is root and can't be moved", b.GetNodeId()) + } + + log, err := s.forest.TreeMove(cid, b.GetTreeId(), &pilorama.Move{ + Parent: b.GetParentId(), + Child: b.GetNodeId(), + Meta: pilorama.Meta{Items: constructMeta(b.GetMeta())}, + }) + if err != nil { + return nil, err + } + + s.pushToQueue(cid, b.GetTreeId(), log) + return new(MoveResponse), nil +} + +func (s *Service) GetNodeByPath(_ context.Context, req *GetNodeByPathRequest) (*GetNodeByPathResponse, error) { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(b.GetContainerId()); err != nil { + return nil, err + } + + attr := b.GetPathAttribute() + if len(attr) == 0 { + attr = pilorama.AttributeFilename + } + + nodes, err := s.forest.TreeGetByPath(cid, b.GetTreeId(), attr, b.GetPath(), b.GetLatestOnly()) + if err != nil { + return nil, err + } + + info := make([]*GetNodeByPathResponse_Info, 0, len(nodes)) + for _, node := range nodes { + m, err := s.forest.TreeGetMeta(cid, b.GetTreeId(), node) + if err != nil { + return nil, err + } + + var x GetNodeByPathResponse_Info + x.NodeId = node + x.Timestamp = m.Time + for _, kv := range m.Items { + needAttr := b.AllAttributes + if !needAttr { + for _, attr := range b.GetAttributes() { + if kv.Key == attr { + needAttr = true + break + } + } + } + + if needAttr { + x.Meta = append(x.Meta, &KeyValue{ + Key: kv.Key, + Value: kv.Value, + }) + } + } + info = append(info, &x) + } + + return &GetNodeByPathResponse{ + Body: &GetNodeByPathResponse_Body{ + Nodes: info, + }, + }, nil +} + +func (s *Service) GetSubTree(_ context.Context, req *GetSubTreeRequest) (*GetSubTreeResponse, error) { + return nil, errors.New("GetSubTree is unimplemented") +} + +// Apply locally applies operation from the remote node to the tree. +func (s *Service) Apply(_ context.Context, req *ApplyRequest) (*ApplyResponse, error) { + err := signature.VerifyServiceMessage(req) + if err != nil { + return nil, err + } + + var cid cidSDK.ID + if err := cid.Decode(req.GetBody().GetContainerId()); err != nil { + return nil, err + } + + found := false + key := req.GetSignature().GetKey() + nodes, _ := s.getContainerNodes(cid) + +loop: + for _, n := range nodes { + if bytes.Equal(key, n.PublicKey()) { + found = true + break loop + } + } + if !found { + return nil, errors.New("`Apply` request must be signed by a container node") + } + + op := req.GetBody().GetOperation() + + var meta pilorama.Meta + if err := meta.FromBytes(op.GetMeta()); err != nil { + return nil, fmt.Errorf("can't parse meta-information: %w", err) + } + + return nil, s.forest.TreeApply(cid, req.GetBody().GetTreeId(), &pilorama.Move{ + Parent: op.GetParentId(), + Child: op.GetChildId(), + Meta: meta, + }) +} + +func constructMeta(arr []*KeyValue) []pilorama.KeyValue { + meta := make([]pilorama.KeyValue, len(arr)) + for i, kv := range arr { + meta[i].Key = kv.Key + meta[i].Value = kv.Value + } + return meta +} diff --git a/pkg/services/tree/service.pb.go b/pkg/services/tree/service.pb.go new file mode 100644 index 00000000..bf1f2690 Binary files /dev/null and b/pkg/services/tree/service.pb.go differ diff --git a/pkg/services/tree/service.proto b/pkg/services/tree/service.proto new file mode 100644 index 00000000..f18b835f --- /dev/null +++ b/pkg/services/tree/service.proto @@ -0,0 +1,194 @@ +syntax = "proto3"; + +package tree; + +import "pkg/services/tree/types.proto"; + +option go_package = "github.com/nspcc-dev/neofs-node/pkg/services/tree"; + +// `TreeService` provides an interface for working with distributed tree. +service TreeService { + /* Client API */ + + // Add adds new node to the tree. Invoked by a client. + rpc Add (AddRequest) returns (AddResponse); + // AddByPath adds new node to the tree by path. Invoked by a client. + rpc AddByPath (AddByPathRequest) returns (AddByPathResponse); + // Remove removes node from the tree. Invoked by a client. + rpc Remove (RemoveRequest) returns (RemoveResponse); + // Move moves node from one parent to another. Invoked by a client. + rpc Move (MoveRequest) returns (MoveResponse); + // GetNodeByPath returns list of IDs corresponding to a specific filepath. + rpc GetNodeByPath (GetNodeByPathRequest) returns (GetNodeByPathResponse); + // GetSubTree returns tree corresponding to a specific node. + rpc GetSubTree (GetSubTreeRequest) returns (GetSubTreeResponse); + + /* Synchronization API */ + + // Apply pushes log operation from another node to the current. + // The request must be signed by a container node. + rpc Apply (ApplyRequest) returns (ApplyResponse); +} + +message AddRequest { + message Body { + bytes container_id = 1; + string tree_id = 2; + uint64 parent_id = 3; + repeated KeyValue meta = 4; + } + + Body body = 1; + Signature signature = 2; +} + +message AddResponse { + message Body { + uint64 node_id = 1; + } + + Body body = 1; + Signature signature = 2; +}; + + +message AddByPathRequest { + message Body { + bytes container_id = 1; + string tree_id = 2; + string path_attribute = 3; + repeated string path = 4; + repeated KeyValue meta = 5; + } + + Body body = 1; + Signature signature = 2; +} + +message AddByPathResponse { + message Body { + repeated uint64 nodes = 1; + uint64 parent_id = 2; + } + + Body body = 1; + Signature signature = 2; +}; + + +message RemoveRequest { + message Body { + bytes container_id = 1; + string tree_id = 2; + uint64 node_id = 3; + } + + Body body = 1; + Signature signature = 2; +} + +message RemoveResponse { + message Body { + } + + Body body = 1; + Signature signature = 2; +}; + + +message MoveRequest { + message Body { + // TODO import neo.fs.v2.refs.ContainerID directly. + bytes container_id = 1; + string tree_id = 2; + uint64 parent_id = 3; + uint64 node_id = 4; + repeated KeyValue meta = 5; + } + + Body body = 1; + Signature signature = 2; +} + +message MoveResponse { + message Body { + } + + Body body = 1; + Signature signature = 2; +}; + + +message GetNodeByPathRequest { + message Body { + bytes container_id = 1; + string tree_id = 2; + string path_attribute = 3; + repeated string path = 4; + repeated string attributes = 5; + bool latest_only = 6; + bool all_attributes = 7; + } + + Body body = 1; + Signature signature = 2; +} + +message GetNodeByPathResponse { + message Info { + uint64 node_id = 1; + uint64 timestamp = 2; + repeated KeyValue meta = 3; + } + message Body { + repeated Info nodes = 1; + } + + Body body = 1; + Signature signature = 2; +}; + + +message GetSubTreeRequest { + message Body { + bytes container_id = 1; + string tree_id = 2; + repeated uint64 nodes = 3; + } + + Body body = 1; + Signature signature = 2; +} + +message GetSubTreeResponse { + message Info { + uint64 node_id = 1; + repeated KeyValue meta = 2; + } + message Body { + repeated Info info = 1; + } + + Body body = 1; + Signature signature = 2; +}; + + +message ApplyRequest { + message Body { + bytes container_id = 1; + string tree_id = 2; + LogMove operation = 3; + } + + Body body = 1; + Signature signature = 2; +} + +message ApplyResponse { + message Body { + } + + Body body = 1; + Signature signature = 2; +}; diff --git a/pkg/services/tree/service_grpc.pb.go b/pkg/services/tree/service_grpc.pb.go new file mode 100644 index 00000000..47aac0f7 Binary files /dev/null and b/pkg/services/tree/service_grpc.pb.go differ diff --git a/pkg/services/tree/signature.go b/pkg/services/tree/signature.go new file mode 100644 index 00000000..299f3ad3 --- /dev/null +++ b/pkg/services/tree/signature.go @@ -0,0 +1,44 @@ +package tree + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/user" +) + +func (s *Service) verifyClient(req interface{}, cid cidSDK.ID, rawKey []byte) error { + // TODO(@fyrchik): #1328 access control + return nil + //nolint:govet + err := signature.VerifyServiceMessage(req) + if err != nil { + return err + } + + cnr, err := s.cnrSource.Get(cid) + if err != nil { + return fmt.Errorf("can't get container %s: %w", cid, err) + } + + ownerID := cnr.Value.Owner() + + pub, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256()) + if err != nil { + return fmt.Errorf("invalid public key: %w", err) + } + + var actualID user.ID + user.IDFromKey(&actualID, (ecdsa.PublicKey)(*pub)) + + if !actualID.Equals(ownerID) { + return errors.New("`Move` request must be signed by a container owner") + } + + return nil +} diff --git a/pkg/services/tree/types.pb.go b/pkg/services/tree/types.pb.go new file mode 100644 index 00000000..d72370c6 Binary files /dev/null and b/pkg/services/tree/types.pb.go differ diff --git a/pkg/services/tree/types.proto b/pkg/services/tree/types.proto new file mode 100644 index 00000000..420fa4df --- /dev/null +++ b/pkg/services/tree/types.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package tree; + +option go_package = "github.com/nspcc-dev/neofs-node/pkg/services/tree"; + +// KeyValue represents key-value pair attached to an object. +message KeyValue { + string key = 1 [json_name = "key"]; + bytes value = 2 [json_name = "value"]; +} + +// LogMove represents log-entry for a single move operation. +message LogMove { + // ID of the parent node. + uint64 parent_id = 2 [json_name = "parentID"]; + // Node meta information, including operation timestamp. + bytes meta = 3 [json_name = "meta"]; + // ID of the node to move. + uint64 child_id = 4 [json_name = "childID"]; +} + +// Signature of a message. +message Signature { + bytes key = 1 [json_name = "key"]; + bytes sign = 2 [json_name = "signature"]; +}