From bc3cdbaad4bdf75676ef1f94ce2648eea92e18e1 Mon Sep 17 00:00:00 2001 From: nugaon Date: Wed, 17 Jul 2024 17:41:46 +0200 Subject: [PATCH] feat: envelope (#4723) --- openapi/Swarm.yaml | 26 ++++++++++++ openapi/SwarmCommon.yaml | 24 +++++++++++ pkg/api/envelope.go | 91 ++++++++++++++++++++++++++++++++++++++++ pkg/api/envelope_test.go | 66 +++++++++++++++++++++++++++++ pkg/api/router.go | 4 ++ 5 files changed, 211 insertions(+) create mode 100644 pkg/api/envelope.go create mode 100644 pkg/api/envelope_test.go diff --git a/openapi/Swarm.yaml b/openapi/Swarm.yaml index 0abe50fc2e4..de8614c879f 100644 --- a/openapi/Swarm.yaml +++ b/openapi/Swarm.yaml @@ -1125,6 +1125,32 @@ paths: default: description: Default response + "/envelope/{address}": + post: + summary: "Create postage stamp signature against given chunk address" + tags: + - Envelope + parameters: + - in: header + schema: + $ref: "SwarmCommon.yaml#/components/parameters/SwarmPostageBatchId" + required: true + responses: + "201": + description: Ok + content: + application/json: + schema: + $ref: "SwarmCommon.yaml#/components/schemas/PostEnvelopeResponse" + "400": + $ref: "SwarmCommon.yaml#/components/responses/400" + "402": + $ref: "SwarmCommon.yaml#/components/responses/402" + "500": + $ref: "SwarmCommon.yaml#/components/responses/500" + default: + description: Default response + "/connect/{multiAddress}": post: summary: Connect to address diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index 6e32e455970..1900ca2805b 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -242,6 +242,18 @@ components: pattern: "^([A-Fa-f0-9]+)$" example: "cf880b8eeac5093fa27b0825906c600685" + Hex8Bytes: + description: Hexadecimal string representation of 8 bytes + type: string + pattern: "^([0-9a-fA-F]{16})$" + example: "1a2b3c4d5e6f7a8b" + + Signature: + description: Hexadecimal string representation of cryptographic signature + type: string + pattern: "^([0-9a-fA-F]{130})$" + example: "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e" + MultiAddress: type: string @@ -367,6 +379,18 @@ components: reference: $ref: "#/components/schemas/SwarmReference" + PostEnvelopeResponse: + type: object + properties: + issuer: + $ref: "#/components/schemas/EthereumAddress" + index: + $ref: "#/components/schemas/Hex8Bytes" + timestamp: + $ref: "#/components/schemas/Hex8Bytes" + signature: + $ref: "#/components/schemas/Signature" + DebugPostageBatchesResponse: type: object properties: diff --git a/pkg/api/envelope.go b/pkg/api/envelope.go new file mode 100644 index 00000000000..6a99b98f61c --- /dev/null +++ b/pkg/api/envelope.go @@ -0,0 +1,91 @@ +// Copyright 2024 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api + +import ( + "encoding/hex" + "errors" + "net/http" + + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/postage" + "github.com/ethersphere/bee/v2/pkg/swarm" + "github.com/gorilla/mux" +) + +type postEnvelopeResponse struct { + Issuer string `json:"issuer"` // Ethereum address of the postage batch owner + Index string `json:"index"` // used index of the Postage Batch + Timestamp string `json:"timestamp"` // timestamp of the postage stamp + Signature string `json:"signature"` // postage stamp signature +} + +// envelopePostHandler generates new postage stamp for requested chunk address +func (s *Service) envelopePostHandler(w http.ResponseWriter, r *http.Request) { + logger := s.logger.WithName("post_envelope").Build() + + headers := struct { + BatchID []byte `map:"Swarm-Postage-Batch-Id" validate:"required"` + }{} + if response := s.mapStructure(r.Header, &headers); response != nil { + response("invalid header params", logger, w) + return + } + + paths := struct { + Address swarm.Address `map:"address" validate:"required"` + }{} + if response := s.mapStructure(mux.Vars(r), &paths); response != nil { + response("invalid path params", logger, w) + return + } + + stamper, save, err := s.getStamper(headers.BatchID) + if err != nil { + logger.Debug("get stamper failed", "error", err) + logger.Error(err, "get stamper failed") + switch { + case errors.Is(err, errBatchUnusable) || errors.Is(err, postage.ErrNotUsable): + jsonhttp.UnprocessableEntity(w, "batch not usable yet or does not exist") + case errors.Is(err, postage.ErrNotFound): + jsonhttp.NotFound(w, "batch with id not found") + case errors.Is(err, errInvalidPostageBatch): + jsonhttp.BadRequest(w, "invalid batch id") + default: + jsonhttp.InternalServerError(w, nil) + } + return + } + + stamp, err := stamper.Stamp(paths.Address) + if err != nil { + logger.Debug("split write all failed", "error", err) + logger.Error(nil, "split write all failed") + switch { + case errors.Is(err, postage.ErrBucketFull): + jsonhttp.PaymentRequired(w, "batch is overissued") + default: + jsonhttp.InternalServerError(w, "stamping failed") + } + return + } + err = save() + if err != nil { + jsonhttp.InternalServerError(w, "failed to save stamp issuer") + return + } + + issuer, err := s.signer.EthereumAddress() + if err != nil { + jsonhttp.InternalServerError(w, "signer ethereum address") + return + } + jsonhttp.Created(w, postEnvelopeResponse{ + Issuer: issuer.Hex(), + Index: hex.EncodeToString(stamp.Index()), + Timestamp: hex.EncodeToString(stamp.Timestamp()), + Signature: hex.EncodeToString(stamp.Sig()), + }) +} diff --git a/pkg/api/envelope_test.go b/pkg/api/envelope_test.go new file mode 100644 index 00000000000..ede77e63875 --- /dev/null +++ b/pkg/api/envelope_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Swarm Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package api_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/ethersphere/bee/v2/pkg/api" + "github.com/ethersphere/bee/v2/pkg/jsonhttp" + "github.com/ethersphere/bee/v2/pkg/jsonhttp/jsonhttptest" + mockbatchstore "github.com/ethersphere/bee/v2/pkg/postage/batchstore/mock" + mockpost "github.com/ethersphere/bee/v2/pkg/postage/mock" +) + +func TestPostEnvelope(t *testing.T) { + t.Parallel() + + zeroHex := "0000000000000000000000000000000000000000000000000000000000000000" + envelopeEndpoint := func(chunkAddress string) string { return fmt.Sprintf("/envelope/%s", chunkAddress) } + client, _, _, _ := newTestServer(t, testServerOptions{ + Post: mockpost.New(mockpost.WithAcceptAll()), + }) + + t.Run("ok", func(t *testing.T) { + t.Parallel() + + jsonhttptest.Request(t, client, http.MethodPost, envelopeEndpoint(zeroHex), http.StatusCreated, + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + ) + }) + + t.Run("wrong chunk address", func(t *testing.T) { + t.Parallel() + + jsonhttptest.Request(t, client, http.MethodPost, envelopeEndpoint("invalid"), http.StatusBadRequest, + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + ) + }) + + t.Run("postage does not exist", func(t *testing.T) { + t.Parallel() + client, _, _, _ := newTestServer(t, testServerOptions{}) + + jsonhttptest.Request(t, client, http.MethodPost, envelopeEndpoint(zeroHex), http.StatusNotFound, + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, zeroHex), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{Message: "batch with id not found", Code: http.StatusNotFound}), + ) + }) + + t.Run("batch unusable", func(t *testing.T) { + t.Parallel() + client, _, _, _ := newTestServer(t, testServerOptions{ + Post: mockpost.New(mockpost.WithAcceptAll()), + BatchStore: mockbatchstore.New(), + }) + + jsonhttptest.Request(t, client, http.MethodPost, envelopeEndpoint(zeroHex), http.StatusUnprocessableEntity, + jsonhttptest.WithRequestHeader(api.SwarmPostageBatchIdHeader, batchOkStr), + jsonhttptest.WithExpectedJSONResponse(jsonhttp.StatusResponse{Message: "batch not usable yet or does not exist", Code: http.StatusUnprocessableEntity}), + ) + }) +} diff --git a/pkg/api/router.go b/pkg/api/router.go index e07f406bec7..b4dcb5423df 100644 --- a/pkg/api/router.go +++ b/pkg/api/router.go @@ -234,6 +234,10 @@ func (s *Service) mountAPI() { "HEAD": http.HandlerFunc(s.hasChunkHandler), }) + handle("/envelope/{address}", jsonhttp.MethodHandler{ + "POST": http.HandlerFunc(s.envelopePostHandler), + }) + handle("/soc/{owner}/{id}", jsonhttp.MethodHandler{ "POST": web.ChainHandlers( jsonhttp.NewMaxBodyBytesHandler(swarm.ChunkWithSpanSize),