github.com/ethersphere/bee/v2@v2.2.0/pkg/api/postage.go (about)

     1  // Copyright 2021 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package api
     6  
     7  import (
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"math/big"
    13  	"net/http"
    14  	"time"
    15  
    16  	"github.com/ethersphere/bee/v2/pkg/bigint"
    17  	"github.com/ethersphere/bee/v2/pkg/jsonhttp"
    18  	"github.com/ethersphere/bee/v2/pkg/postage"
    19  	"github.com/ethersphere/bee/v2/pkg/postage/postagecontract"
    20  	"github.com/ethersphere/bee/v2/pkg/storage"
    21  	"github.com/ethersphere/bee/v2/pkg/tracing"
    22  	"github.com/gorilla/mux"
    23  )
    24  
    25  var defaultImmutable = true
    26  
    27  func (s *Service) postageAccessHandler(h http.Handler) http.Handler {
    28  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    29  		if !s.postageSem.TryAcquire(1) {
    30  			s.logger.Debug("postage access: simultaneous on-chain operations not supported")
    31  			s.logger.Error(nil, "postage access: simultaneous on-chain operations not supported")
    32  			jsonhttp.TooManyRequests(w, "simultaneous on-chain operations not supported")
    33  			return
    34  		}
    35  		defer s.postageSem.Release(1)
    36  
    37  		h.ServeHTTP(w, r)
    38  	})
    39  }
    40  
    41  func (s *Service) postageSyncStatusCheckHandler(h http.Handler) http.Handler {
    42  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    43  		done, err := s.syncStatus()
    44  		if err != nil {
    45  			s.logger.Debug("postage access: syncing failed", "error", err)
    46  			s.logger.Error(nil, "postage access: syncing failed")
    47  			jsonhttp.ServiceUnavailable(w, "syncing failed")
    48  			return
    49  		}
    50  		if !done {
    51  			s.logger.Debug("postage access: syncing in progress")
    52  			s.logger.Error(nil, "postage access: syncing in progress")
    53  			jsonhttp.ServiceUnavailable(w, "syncing in progress")
    54  			return
    55  		}
    56  
    57  		h.ServeHTTP(w, r)
    58  	})
    59  }
    60  
    61  // hexByte takes care that a byte slice gets correctly
    62  // marshaled by the json serializer.
    63  type hexByte []byte
    64  
    65  func (b hexByte) MarshalJSON() ([]byte, error) {
    66  	return json.Marshal(hex.EncodeToString(b))
    67  }
    68  
    69  type postageCreateResponse struct {
    70  	BatchID hexByte `json:"batchID"`
    71  	TxHash  string  `json:"txHash"`
    72  }
    73  
    74  func (s *Service) postageCreateHandler(w http.ResponseWriter, r *http.Request) {
    75  	logger := s.logger.WithName("post_stamp").Build()
    76  
    77  	paths := struct {
    78  		Amount *big.Int `map:"amount" validate:"required"`
    79  		Depth  uint8    `map:"depth" validate:"required,min=17"`
    80  	}{}
    81  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
    82  		response("invalid path params", logger, w)
    83  		return
    84  	}
    85  
    86  	headers := struct {
    87  		Immutable *bool `map:"Immutable"`
    88  	}{}
    89  	if response := s.mapStructure(r.Header, &headers); response != nil {
    90  		response("invalid header params", logger, w)
    91  		return
    92  	}
    93  	if headers.Immutable == nil {
    94  		headers.Immutable = &defaultImmutable // Set the default.
    95  	}
    96  
    97  	txHash, batchID, err := s.postageContract.CreateBatch(
    98  		r.Context(),
    99  		paths.Amount,
   100  		paths.Depth,
   101  		*headers.Immutable,
   102  		r.URL.Query().Get("label"),
   103  	)
   104  	if err != nil {
   105  		if errors.Is(err, postagecontract.ErrChainDisabled) {
   106  			logger.Debug("create batch: no chain backend", "error", err)
   107  			logger.Error(nil, "create batch: no chain backend")
   108  			jsonhttp.MethodNotAllowed(w, "no chain backend")
   109  			return
   110  		}
   111  		if errors.Is(err, postagecontract.ErrInsufficientFunds) {
   112  			logger.Debug("create batch: out of funds", "error", err)
   113  			logger.Error(nil, "create batch: out of funds")
   114  			jsonhttp.BadRequest(w, "out of funds")
   115  			return
   116  		}
   117  		if errors.Is(err, postagecontract.ErrInvalidDepth) {
   118  			logger.Debug("create batch: invalid depth", "error", err)
   119  			logger.Error(nil, "create batch: invalid depth")
   120  			jsonhttp.BadRequest(w, "invalid depth")
   121  			return
   122  		}
   123  		if errors.Is(err, postagecontract.ErrInsufficientValidity) {
   124  			logger.Debug("create batch: insufficient validity", "error", err)
   125  			logger.Error(nil, "create batch: insufficient validity")
   126  			jsonhttp.BadRequest(w, "insufficient amount for 24h minimum validity")
   127  			return
   128  		}
   129  		logger.Debug("create batch: create failed", "error", err)
   130  		logger.Error(nil, "create batch: create failed")
   131  		jsonhttp.InternalServerError(w, "cannot create batch")
   132  		return
   133  	}
   134  
   135  	jsonhttp.Created(w, &postageCreateResponse{
   136  		BatchID: batchID,
   137  		TxHash:  txHash.String(),
   138  	})
   139  }
   140  
   141  type postageStampResponse struct {
   142  	BatchID       hexByte        `json:"batchID"`
   143  	Utilization   uint32         `json:"utilization"`
   144  	Usable        bool           `json:"usable"`
   145  	Label         string         `json:"label"`
   146  	Depth         uint8          `json:"depth"`
   147  	Amount        *bigint.BigInt `json:"amount"`
   148  	BucketDepth   uint8          `json:"bucketDepth"`
   149  	BlockNumber   uint64         `json:"blockNumber"`
   150  	ImmutableFlag bool           `json:"immutableFlag"`
   151  	Exists        bool           `json:"exists"`
   152  	BatchTTL      int64          `json:"batchTTL"`
   153  }
   154  
   155  type postageStampsResponse struct {
   156  	Stamps []postageStampResponse `json:"stamps"`
   157  }
   158  
   159  type postageBatchResponse struct {
   160  	BatchID     hexByte        `json:"batchID"`
   161  	Value       *bigint.BigInt `json:"value"`
   162  	Start       uint64         `json:"start"`
   163  	Owner       hexByte        `json:"owner"`
   164  	Depth       uint8          `json:"depth"`
   165  	BucketDepth uint8          `json:"bucketDepth"`
   166  	Immutable   bool           `json:"immutable"`
   167  	BatchTTL    int64          `json:"batchTTL"`
   168  }
   169  
   170  type postageStampBucketsResponse struct {
   171  	Depth            uint8        `json:"depth"`
   172  	BucketDepth      uint8        `json:"bucketDepth"`
   173  	BucketUpperBound uint32       `json:"bucketUpperBound"`
   174  	Buckets          []bucketData `json:"buckets"`
   175  }
   176  
   177  type bucketData struct {
   178  	BucketID   uint32 `json:"bucketID"`
   179  	Collisions uint32 `json:"collisions"`
   180  }
   181  
   182  func (s *Service) postageGetStampsHandler(w http.ResponseWriter, r *http.Request) {
   183  	logger := s.logger.WithName("get_stamps").Build()
   184  
   185  	resp := postageStampsResponse{}
   186  	stampIssuers := s.post.StampIssuers()
   187  	resp.Stamps = make([]postageStampResponse, 0, len(stampIssuers))
   188  	for _, v := range stampIssuers {
   189  
   190  		exists, err := s.batchStore.Exists(v.ID())
   191  		if err != nil {
   192  			logger.Error(err, "get stamp issuer: check batch failed", "batch_id", hex.EncodeToString(v.ID()))
   193  			jsonhttp.InternalServerError(w, "unable to check batch")
   194  			return
   195  		}
   196  		if !exists {
   197  			continue
   198  		}
   199  
   200  		batchTTL, err := s.estimateBatchTTLFromID(v.ID())
   201  		if err != nil {
   202  			logger.Error(err, "get stamp issuer: estimate batch expiration failed", "batch_id", hex.EncodeToString(v.ID()))
   203  			jsonhttp.InternalServerError(w, "unable to estimate batch expiration")
   204  			return
   205  		}
   206  
   207  		resp.Stamps = append(resp.Stamps, postageStampResponse{
   208  			BatchID:       v.ID(),
   209  			Utilization:   v.Utilization(),
   210  			Usable:        s.post.IssuerUsable(v),
   211  			Label:         v.Label(),
   212  			Depth:         v.Depth(),
   213  			Amount:        bigint.Wrap(v.Amount()),
   214  			BucketDepth:   v.BucketDepth(),
   215  			BlockNumber:   v.BlockNumber(),
   216  			ImmutableFlag: v.ImmutableFlag(),
   217  			Exists:        true,
   218  			BatchTTL:      batchTTL,
   219  		})
   220  	}
   221  
   222  	jsonhttp.OK(w, resp)
   223  }
   224  
   225  func (s *Service) postageGetAllBatchesHandler(w http.ResponseWriter, _ *http.Request) {
   226  	logger := s.logger.WithName("get_batches").Build()
   227  
   228  	batches := make([]postageBatchResponse, 0)
   229  	err := s.batchStore.Iterate(func(b *postage.Batch) (bool, error) {
   230  		batchTTL, err := s.estimateBatchTTL(b)
   231  		if err != nil {
   232  			return false, fmt.Errorf("estimate batch ttl: %w", err)
   233  		}
   234  
   235  		batches = append(batches, postageBatchResponse{
   236  			BatchID:     b.ID,
   237  			Value:       bigint.Wrap(b.Value),
   238  			Start:       b.Start,
   239  			Owner:       b.Owner,
   240  			Depth:       b.Depth,
   241  			BucketDepth: b.BucketDepth,
   242  			Immutable:   b.Immutable,
   243  			BatchTTL:    batchTTL,
   244  		})
   245  		return false, nil
   246  	})
   247  	if err != nil {
   248  		logger.Debug("iterate batches: iteration failed", "error", err)
   249  		logger.Error(nil, "iterate batches: iteration failed")
   250  		jsonhttp.InternalServerError(w, "unable to iterate all batches")
   251  		return
   252  	}
   253  
   254  	batchesRes := struct {
   255  		Batches []postageBatchResponse `json:"batches"`
   256  	}{
   257  		Batches: batches,
   258  	}
   259  
   260  	jsonhttp.OK(w, batchesRes)
   261  }
   262  
   263  func (s *Service) postageGetStampBucketsHandler(w http.ResponseWriter, r *http.Request) {
   264  	logger := s.logger.WithName("get_stamp_bucket").Build()
   265  
   266  	paths := struct {
   267  		BatchID []byte `map:"batch_id" validate:"required,len=32"`
   268  	}{}
   269  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   270  		response("invalid path params", logger, w)
   271  		return
   272  	}
   273  	hexBatchID := hex.EncodeToString(paths.BatchID)
   274  
   275  	issuer, _, err := s.post.GetStampIssuer(paths.BatchID)
   276  	if err != nil {
   277  		logger.Debug("get stamp issuer: get issuer failed", "batch_id", hexBatchID, "error", err)
   278  		logger.Error(nil, "get stamp issuer: get issuer failed")
   279  		switch {
   280  		case errors.Is(err, postage.ErrNotUsable):
   281  			jsonhttp.BadRequest(w, "batch not usable")
   282  		case errors.Is(err, postage.ErrNotFound):
   283  			jsonhttp.NotFound(w, "issuer does not exist")
   284  		default:
   285  			jsonhttp.InternalServerError(w, "get issuer failed")
   286  		}
   287  		return
   288  	}
   289  
   290  	b := issuer.Buckets()
   291  	resp := postageStampBucketsResponse{
   292  		Depth:            issuer.Depth(),
   293  		BucketDepth:      issuer.BucketDepth(),
   294  		BucketUpperBound: issuer.BucketUpperBound(),
   295  		Buckets:          make([]bucketData, len(b)),
   296  	}
   297  
   298  	for i, v := range b {
   299  		resp.Buckets[i] = bucketData{BucketID: uint32(i), Collisions: v}
   300  	}
   301  
   302  	jsonhttp.OK(w, resp)
   303  }
   304  
   305  func (s *Service) postageGetStampHandler(w http.ResponseWriter, r *http.Request) {
   306  	logger := s.logger.WithName("get_stamp").Build()
   307  
   308  	paths := struct {
   309  		BatchID []byte `map:"batch_id" validate:"required,len=32"`
   310  	}{}
   311  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   312  		response("invalid path params", logger, w)
   313  		return
   314  	}
   315  	hexBatchID := hex.EncodeToString(paths.BatchID)
   316  
   317  	issuer, _, err := s.post.GetStampIssuer(paths.BatchID)
   318  	if err != nil {
   319  		logger.Debug("get stamp issuer: get issuer failed", "batch_id", hexBatchID, "error", err)
   320  		logger.Error(nil, "get stamp issuer: get issuer failed")
   321  		switch {
   322  		case errors.Is(err, postage.ErrNotUsable):
   323  			jsonhttp.BadRequest(w, "batch not usable")
   324  		case errors.Is(err, postage.ErrNotFound):
   325  			jsonhttp.NotFound(w, "issuer does not exist")
   326  		default:
   327  			jsonhttp.InternalServerError(w, "get issuer failed")
   328  		}
   329  		return
   330  	}
   331  
   332  	exists, err := s.batchStore.Exists(paths.BatchID)
   333  	if err != nil {
   334  		logger.Debug("exist check failed", "batch_id", hexBatchID, "error", err)
   335  		logger.Error(nil, "exist check failed")
   336  		jsonhttp.InternalServerError(w, "unable to check batch")
   337  		return
   338  	}
   339  	if !exists {
   340  		logger.Debug("batch does not exists", "batch_id", hexBatchID)
   341  		jsonhttp.NotFound(w, "issuer does not exist")
   342  		return
   343  	}
   344  
   345  	batchTTL, err := s.estimateBatchTTLFromID(paths.BatchID)
   346  	if err != nil {
   347  		logger.Debug("estimate batch expiration failed", "batch_id", hexBatchID, "error", err)
   348  		logger.Error(nil, "estimate batch expiration failed")
   349  		jsonhttp.InternalServerError(w, "unable to estimate batch expiration")
   350  		return
   351  	}
   352  
   353  	jsonhttp.OK(w, &postageStampResponse{
   354  		BatchID:       paths.BatchID,
   355  		Depth:         issuer.Depth(),
   356  		BucketDepth:   issuer.BucketDepth(),
   357  		ImmutableFlag: issuer.ImmutableFlag(),
   358  		Exists:        true,
   359  		BatchTTL:      batchTTL,
   360  		Utilization:   issuer.Utilization(),
   361  		Usable:        s.post.IssuerUsable(issuer),
   362  		Label:         issuer.Label(),
   363  		Amount:        bigint.Wrap(issuer.Amount()),
   364  		BlockNumber:   issuer.BlockNumber(),
   365  	})
   366  }
   367  
   368  type reserveStateResponse struct {
   369  	Radius        uint8  `json:"radius"`
   370  	StorageRadius uint8  `json:"storageRadius"`
   371  	Commitment    uint64 `json:"commitment"`
   372  }
   373  
   374  type chainStateResponse struct {
   375  	ChainTip     uint64         `json:"chainTip"`     // ChainTip (block height).
   376  	Block        uint64         `json:"block"`        // The block number of the last postage event.
   377  	TotalAmount  *bigint.BigInt `json:"totalAmount"`  // Cumulative amount paid per stamp.
   378  	CurrentPrice *bigint.BigInt `json:"currentPrice"` // Bzz/chunk/block normalised price.
   379  }
   380  
   381  func (s *Service) reserveStateHandler(w http.ResponseWriter, _ *http.Request) {
   382  	logger := s.logger.WithName("get_reservestate").Build()
   383  
   384  	commitment, err := s.batchStore.Commitment()
   385  	if err != nil {
   386  		logger.Debug("batch store commitment calculation failed", "error", err)
   387  		logger.Error(nil, "batch store commitment calculation failed")
   388  		jsonhttp.InternalServerError(w, "unable to calculate commitment")
   389  		return
   390  	}
   391  
   392  	jsonhttp.OK(w, reserveStateResponse{
   393  		Radius:        s.batchStore.Radius(),
   394  		StorageRadius: s.storer.StorageRadius(),
   395  		Commitment:    commitment,
   396  	})
   397  }
   398  
   399  // chainStateHandler returns the current chain state.
   400  func (s *Service) chainStateHandler(w http.ResponseWriter, r *http.Request) {
   401  	logger := tracing.NewLoggerWithTraceID(r.Context(), s.logger.WithName("get_chainstate").Build())
   402  
   403  	state := s.batchStore.GetChainState()
   404  	chainTip, err := s.chainBackend.BlockNumber(r.Context())
   405  	if err != nil {
   406  		logger.Debug("get block number failed", "error", err)
   407  		logger.Error(nil, "get block number failed")
   408  		jsonhttp.InternalServerError(w, "block number unavailable")
   409  		return
   410  	}
   411  	jsonhttp.OK(w, chainStateResponse{
   412  		ChainTip:     chainTip,
   413  		Block:        state.Block,
   414  		TotalAmount:  bigint.Wrap(state.TotalAmount),
   415  		CurrentPrice: bigint.Wrap(state.CurrentPrice),
   416  	})
   417  }
   418  
   419  // estimateBatchTTL estimates the time remaining until the batch expires.
   420  // The -1 signals that the batch never expires.
   421  func (s *Service) estimateBatchTTLFromID(id []byte) (int64, error) {
   422  	batch, err := s.batchStore.Get(id)
   423  	switch {
   424  	case errors.Is(err, storage.ErrNotFound):
   425  		return -1, nil
   426  	case err != nil:
   427  		return 0, err
   428  	}
   429  
   430  	return s.estimateBatchTTL(batch)
   431  }
   432  
   433  // estimateBatchTTL estimates the time remaining until the batch expires.
   434  // The -1 signals that the batch never expires.
   435  func (s *Service) estimateBatchTTL(batch *postage.Batch) (int64, error) {
   436  	state := s.batchStore.GetChainState()
   437  	if len(state.CurrentPrice.Bits()) == 0 {
   438  		return -1, nil
   439  	}
   440  
   441  	var (
   442  		normalizedBalance = batch.Value
   443  		cumulativePayout  = state.TotalAmount
   444  		pricePerBlock     = state.CurrentPrice
   445  	)
   446  	ttl := new(big.Int).Sub(normalizedBalance, cumulativePayout)
   447  	ttl = ttl.Mul(ttl, big.NewInt(int64(s.blockTime/time.Second)))
   448  	ttl = ttl.Div(ttl, pricePerBlock)
   449  
   450  	return ttl.Int64(), nil
   451  }
   452  
   453  func (s *Service) postageTopUpHandler(w http.ResponseWriter, r *http.Request) {
   454  	logger := s.logger.WithName("patch_stamp_topup").Build()
   455  
   456  	paths := struct {
   457  		BatchID []byte   `map:"batch_id" validate:"required,len=32"`
   458  		Amount  *big.Int `map:"amount" validate:"required"`
   459  	}{}
   460  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   461  		response("invalid path params", logger, w)
   462  		return
   463  	}
   464  	hexBatchID := hex.EncodeToString(paths.BatchID)
   465  
   466  	txHash, err := s.postageContract.TopUpBatch(r.Context(), paths.BatchID, paths.Amount)
   467  	if err != nil {
   468  		if errors.Is(err, postagecontract.ErrInsufficientFunds) {
   469  			logger.Debug("topup batch: out of funds", "batch_id", hexBatchID, "amount", paths.Amount, "error", err)
   470  			logger.Error(nil, "topup batch: out of funds")
   471  			jsonhttp.PaymentRequired(w, "out of funds")
   472  			return
   473  		}
   474  		if errors.Is(err, postagecontract.ErrNotImplemented) {
   475  			logger.Debug("topup batch: not implemented", "error", err)
   476  			logger.Error(nil, "topup batch: not implemented")
   477  			jsonhttp.NotImplemented(w, nil)
   478  			return
   479  		}
   480  		logger.Debug("topup batch: topup failed", "batch_id", hexBatchID, "amount", paths.Amount, "error", err)
   481  		logger.Error(nil, "topup batch: topup failed")
   482  		jsonhttp.InternalServerError(w, "cannot topup batch")
   483  		return
   484  	}
   485  
   486  	jsonhttp.Accepted(w, &postageCreateResponse{
   487  		BatchID: paths.BatchID,
   488  		TxHash:  txHash.String(),
   489  	})
   490  }
   491  
   492  func (s *Service) postageDiluteHandler(w http.ResponseWriter, r *http.Request) {
   493  	logger := s.logger.WithName("patch_stamp_dilute").Build()
   494  
   495  	paths := struct {
   496  		BatchID []byte `map:"batch_id" validate:"required,len=32"`
   497  		Depth   uint8  `map:"depth" validate:"required"`
   498  	}{}
   499  	if response := s.mapStructure(mux.Vars(r), &paths); response != nil {
   500  		response("invalid path params", logger, w)
   501  		return
   502  	}
   503  	hexBatchID := hex.EncodeToString(paths.BatchID)
   504  
   505  	txHash, err := s.postageContract.DiluteBatch(r.Context(), paths.BatchID, paths.Depth)
   506  	if err != nil {
   507  		if errors.Is(err, postagecontract.ErrInvalidDepth) {
   508  			logger.Debug("dilute batch: invalid depth", "error", err)
   509  			logger.Error(nil, "dilute batch: invalid depth")
   510  			jsonhttp.BadRequest(w, "invalid depth")
   511  			return
   512  		}
   513  		if errors.Is(err, postagecontract.ErrNotImplemented) {
   514  			logger.Debug("dilute batch: not implemented", "error")
   515  			logger.Error(nil, "dilute batch: not implemented")
   516  			jsonhttp.NotImplemented(w, nil)
   517  			return
   518  		}
   519  		logger.Debug("dilute batch: dilute failed", "batch_id", hexBatchID, "depth", paths.Depth, "error", err)
   520  		logger.Error(nil, "dilute batch: dilute failed")
   521  		jsonhttp.InternalServerError(w, "cannot dilute batch")
   522  		return
   523  	}
   524  
   525  	jsonhttp.Accepted(w, &postageCreateResponse{
   526  		BatchID: paths.BatchID,
   527  		TxHash:  txHash.String(),
   528  	})
   529  }