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 }