Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(debugapi, postage): topup batch handling #2401

Merged
merged 13 commits into from
Aug 23, 2021
37 changes: 37 additions & 0 deletions openapi/SwarmDebug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -871,3 +871,40 @@ paths:
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response

"/stamps/topup/{id}/{amount}":
patch:
summary: Top up an existing postage batch.
description: Be aware, this endpoint creates on-chain transactions and transfers BZZ from the node's Ethereum account and hence directly manipulates the wallet balance!
tags:
- Postage Stamps
parameters:
- in: path
name: id
schema:
$ref: "SwarmCommon.yaml#/components/schemas/BatchID"
required: true
description: Batch ID to top up
- in: path
name: amount
schema:
type: integer
required: true
description: Amount of BZZ per chunk to top up to an existing postage batch.
responses:
"202":
description: Returns the postage batch ID that was topped up
content:
application/json:
schema:
$ref: "SwarmCommon.yaml#/components/schemas/BatchIDResponse"
"400":
$ref: "SwarmCommon.yaml#/components/responses/400"
"429":
$ref: "SwarmCommon.yaml#/components/responses/429"
"402":
$ref: "SwarmCommon.yaml#/components/responses/402"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
default:
description: Default response
60 changes: 60 additions & 0 deletions pkg/debugapi/postage.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,63 @@ func (s *Service) estimateBatchTTL(id []byte) (int64, error) {

return ttl.Int64(), nil
}

func (s *Service) postageTopUpHandler(w http.ResponseWriter, r *http.Request) {
idStr := mux.Vars(r)["id"]
if idStr == "" || len(idStr) != 64 {
s.logger.Error("topup batch: invalid batchID")
jsonhttp.BadRequest(w, "invalid batchID")
return
}
id, err := hex.DecodeString(idStr)
if err != nil {
s.logger.Debugf("topup batch: invalid batchID: %v", err)
s.logger.Error("topup batch: invalid batchID")
jsonhttp.BadRequest(w, "invalid batchID")
return
}

amount, ok := big.NewInt(0).SetString(mux.Vars(r)["amount"], 10)
if !ok {
s.logger.Error("topup batch: invalid amount")
jsonhttp.BadRequest(w, "invalid postage amount")
return
}

ctx := r.Context()
if price, ok := r.Header[gasPriceHeader]; ok {
p, ok := big.NewInt(0).SetString(price[0], 10)
if !ok {
s.logger.Error("topup batch: bad gas price")
jsonhttp.BadRequest(w, errBadGasPrice)
return
}
ctx = sctx.SetGasPrice(ctx, p)
}

if !s.postageCreateSem.TryAcquire(1) {
aloknerurkar marked this conversation as resolved.
Show resolved Hide resolved
s.logger.Debug("topup batch: simultaneous on-chain operations not supported")
s.logger.Error("topup batch: simultaneous on-chain operations not supported")
jsonhttp.TooManyRequests(w, "simultaneous on-chain operations not supported")
return
}
defer s.postageCreateSem.Release(1)

err = s.postageContract.TopUpBatch(ctx, id, amount)
if err != nil {
if errors.Is(err, postagecontract.ErrInsufficientFunds) {
s.logger.Debugf("topup batch: out of funds: %v", err)
s.logger.Error("topup batch: out of funds")
jsonhttp.PaymentRequired(w, "out of funds")
return
}
s.logger.Debugf("topup batch: failed to create: %v", err)
s.logger.Error("topup batch: failed to create")
jsonhttp.InternalServerError(w, "cannot topup batch")
return
}

jsonhttp.Accepted(w, &postageCreateResponse{
BatchID: id,
})
}
118 changes: 118 additions & 0 deletions pkg/debugapi/postage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package debugapi_test

import (
"bytes"
"context"
"encoding/hex"
"errors"
Expand Down Expand Up @@ -368,3 +369,120 @@ func TestChainState(t *testing.T) {
)
})
}

func TestPostageTopUpStamp(t *testing.T) {
topupAmount := int64(1000)
topupBatch := func(id string, amount int64) string {
return fmt.Sprintf("/stamps/topup/%s/%d", id, amount)
}

t.Run("ok", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
if !bytes.Equal(id, batchOk) {
return errors.New("incorrect batch ID in call")
}
if ib.Cmp(big.NewInt(topupAmount)) != 0 {
return fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib)
}
return nil
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})

jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
jsonhttptest.WithExpectedJSONResponse(&debugapi.PostageCreateResponse{
BatchID: batchOk,
}),
)
})

t.Run("with-custom-gas", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
if !bytes.Equal(id, batchOk) {
return errors.New("incorrect batch ID in call")
}
if ib.Cmp(big.NewInt(topupAmount)) != 0 {
return fmt.Errorf("called with wrong topup amount. wanted %d, got %d", topupAmount, ib)
}
if sctx.GetGasPrice(ctx).Cmp(big.NewInt(10000)) != 0 {
return fmt.Errorf("called with wrong gas price. wanted %d, got %d", 10000, sctx.GetGasPrice(ctx))
}
return nil
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})

jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusAccepted,
jsonhttptest.WithRequestHeader("Gas-Price", "10000"),
jsonhttptest.WithExpectedJSONResponse(&debugapi.PostageCreateResponse{
BatchID: batchOk,
}),
)
})

t.Run("with-error", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
return errors.New("err")
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})

jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusInternalServerError,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusInternalServerError,
Message: "cannot topup batch",
}),
)
})

t.Run("out-of-funds", func(t *testing.T) {
contract := contractMock.New(
contractMock.WithTopUpBatchFunc(func(ctx context.Context, id []byte, ib *big.Int) error {
return postagecontract.ErrInsufficientFunds
}),
)
ts := newTestServer(t, testServerOptions{
PostageContract: contract,
})

jsonhttptest.Request(t, ts.Client, http.MethodPatch, topupBatch(batchOkStr, topupAmount), http.StatusPaymentRequired,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusPaymentRequired,
Message: "out of funds",
}),
)
})

t.Run("invalid batch id", func(t *testing.T) {
ts := newTestServer(t, testServerOptions{})

jsonhttptest.Request(t, ts.Client, http.MethodPatch, "/stamps/topup/abcd/2", http.StatusBadRequest,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid batchID",
}),
)
})

t.Run("invalid amount", func(t *testing.T) {
ts := newTestServer(t, testServerOptions{})

wrongURL := fmt.Sprintf("/stamps/topup/%s/amount", batchOkStr)

jsonhttptest.Request(t, ts.Client, http.MethodPatch, wrongURL, http.StatusBadRequest,
jsonhttptest.WithExpectedJSONResponse(&jsonhttp.StatusResponse{
Code: http.StatusBadRequest,
Message: "invalid postage amount",
}),
)
})
}
6 changes: 6 additions & 0 deletions pkg/debugapi/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ func (s *Service) newRouter() *mux.Router {
})),
)

router.Handle("/stamps/topup/{id}/{amount}", web.ChainHandlers(
web.FinalHandler(jsonhttp.MethodHandler{
"PATCH": http.HandlerFunc(s.postageTopUpHandler),
})),
)

return router
}

Expand Down
68 changes: 46 additions & 22 deletions pkg/node/devnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,28 +200,52 @@ func NewDevBee(logger logging.Logger, o *DevOptions) (b *DevBee, err error) {
}

post := mockPost.New()
postageContract := mockPostContract.New(mockPostContract.WithCreateBatchFunc(
func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) {
id := postagetesting.MustNewID()
b := &postage.Batch{
ID: id,
Owner: overlayEthAddress.Bytes(),
Value: big.NewInt(0),
Depth: depth,
Immutable: immutable,
}

err := batchStore.Put(b, initialBalance, depth)
if err != nil {
return nil, err
}

stampIssuer := postage.NewStampIssuer(label, string(overlayEthAddress.Bytes()), id, initialBalance, depth, 0, 0, immutable)
post.Add(stampIssuer)

return id, nil
},
))
postageContract := mockPostContract.New(
mockPostContract.WithCreateBatchFunc(
func(ctx context.Context, initialBalance *big.Int, depth uint8, immutable bool, label string) ([]byte, error) {
id := postagetesting.MustNewID()
b := &postage.Batch{
ID: id,
Owner: overlayEthAddress.Bytes(),
Value: big.NewInt(0),
Depth: depth,
Immutable: immutable,
}

totalAmount := big.NewInt(0).Mul(initialBalance, big.NewInt(int64(1<<depth)))

err := batchStore.Put(b, totalAmount, depth)
if err != nil {
return nil, err
}

stampIssuer := postage.NewStampIssuer(label, string(overlayEthAddress.Bytes()), id, totalAmount, depth, 0, 0, immutable)
post.Add(stampIssuer)

return id, nil
},
),
mockPostContract.WithTopUpBatchFunc(
func(ctx context.Context, batchID []byte, topupAmount *big.Int) error {
batch, err := batchStore.Get(batchID)
if err != nil {
return err
}

totalAmount := big.NewInt(0).Mul(topupAmount, big.NewInt(int64(1<<batch.Depth)))

newBalance := big.NewInt(0).Add(totalAmount, batch.Value)

err = batchStore.Put(batch, newBalance, batch.Depth)
if err != nil {
return err
}

post.HandleTopUp(batch.ID, newBalance)
return nil
},
),
)

feedFactory := factory.New(storer)

Expand Down
1 change: 1 addition & 0 deletions pkg/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ func NewBee(addr string, publicKey *ecdsa.PublicKey, signer crypto.Signer, netwo
erc20Address,
transactionService,
post,
batchStore,
)

if natManager := p2ps.NATManager(); natManager != nil {
Expand Down
12 changes: 9 additions & 3 deletions pkg/postage/batchservice/batchservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type batchService struct {
logger logging.Logger
listener postage.Listener
owner []byte
batchListener postage.BatchCreationListener
batchListener postage.BatchEventListener

checksum hash.Hash // checksum hasher
}
Expand All @@ -45,7 +45,7 @@ func New(
logger logging.Logger,
listener postage.Listener,
owner []byte,
batchListener postage.BatchCreationListener,
batchListener postage.BatchEventListener,
checksumFunc func() hash.Hash,
) (Interface, error) {
if checksumFunc == nil {
Expand Down Expand Up @@ -96,8 +96,9 @@ func (svc *batchService) Create(id, owner []byte, normalisedBalance *big.Int, de
}

if bytes.Equal(svc.owner, owner) && svc.batchListener != nil {
svc.batchListener.Handle(b)
svc.batchListener.HandleCreate(b)
}

cs, err := svc.updateChecksum(txHash)
if err != nil {
return fmt.Errorf("update checksum: %w", err)
Expand All @@ -119,6 +120,11 @@ func (svc *batchService) TopUp(id []byte, normalisedBalance *big.Int, txHash []b
if err != nil {
return fmt.Errorf("put: %w", err)
}

if bytes.Equal(svc.owner, b.Owner) && svc.batchListener != nil {
svc.batchListener.HandleTopUp(id, normalisedBalance)
}

cs, err := svc.updateChecksum(txHash)
if err != nil {
return fmt.Errorf("update checksum: %w", err)
Expand Down
Loading