From 70ea0e3071ec8786ed9d1342745bcaf863874c8c Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 19 May 2026 19:47:57 +0300 Subject: [PATCH] feat(openapi): document utilization; add utilizedPercentage to postage stamp response --- openapi/SwarmCommon.yaml | 21 ++++++++++ pkg/api/postage.go | 69 +++++++++++++++++---------------- pkg/api/postage_test.go | 46 +++++++++++----------- pkg/postage/stampissuer.go | 8 ++++ pkg/postage/stampissuer_test.go | 33 ++++++++++++++++ 5 files changed, 122 insertions(+), 55 deletions(-) diff --git a/openapi/SwarmCommon.yaml b/openapi/SwarmCommon.yaml index 303245501f9..d477749c432 100644 --- a/openapi/SwarmCommon.yaml +++ b/openapi/SwarmCommon.yaml @@ -527,6 +527,27 @@ components: $ref: "#/components/schemas/BatchID" utilization: type: integer + description: > + Raw batch fullness indicator: the highest write count among the + `2^bucketDepth` collision buckets of the batch. This is **not** a + percentage; one unit corresponds to one chunk written into the + fullest bucket. Total batch capacity is `2^depth` chunks, while + the fullest bucket caps at `2^(depth - bucketDepth)` chunks, so + the fractional usage of the batch is + `utilization / 2^(depth - bucketDepth)` (also exposed directly as + `utilizedPercentage`). When the value reaches + `2^(depth - bucketDepth)` the batch is effectively full and any + further write to the fullest bucket would overflow it. + utilizedPercentage: + type: number + format: double + minimum: 0 + maximum: 1 + description: > + Fractional batch fullness in the range `[0, 1]`, computed as + `utilization / 2^(depth - bucketDepth)`. A value of `1` means the + fullest bucket has reached its capacity and the batch can no + longer accept writes that would land in that bucket. usable: description: Indicates whether the batch was discovered by the Bee node and has received sufficient on-chain confirmations type: boolean diff --git a/pkg/api/postage.go b/pkg/api/postage.go index 7cd74a23e87..21a28d163e2 100644 --- a/pkg/api/postage.go +++ b/pkg/api/postage.go @@ -139,17 +139,18 @@ func (s *Service) postageCreateHandler(w http.ResponseWriter, r *http.Request) { } type postageStampResponse struct { - BatchID hexByte `json:"batchID"` - Utilization uint32 `json:"utilization"` - Usable bool `json:"usable"` - Label string `json:"label"` - Depth uint8 `json:"depth"` - Amount *bigint.BigInt `json:"amount"` - BucketDepth uint8 `json:"bucketDepth"` - BlockNumber uint64 `json:"blockNumber"` - ImmutableFlag bool `json:"immutableFlag"` - Exists bool `json:"exists"` - BatchTTL int64 `json:"batchTTL"` + BatchID hexByte `json:"batchID"` + Utilization uint32 `json:"utilization"` + UtilizedPercentage float64 `json:"utilizedPercentage"` + Usable bool `json:"usable"` + Label string `json:"label"` + Depth uint8 `json:"depth"` + Amount *bigint.BigInt `json:"amount"` + BucketDepth uint8 `json:"bucketDepth"` + BlockNumber uint64 `json:"blockNumber"` + ImmutableFlag bool `json:"immutableFlag"` + Exists bool `json:"exists"` + BatchTTL int64 `json:"batchTTL"` } type postageStampsResponse struct { @@ -205,17 +206,18 @@ func (s *Service) postageGetStampsHandler(w http.ResponseWriter, r *http.Request } resp.Stamps = append(resp.Stamps, postageStampResponse{ - BatchID: v.ID(), - Utilization: v.Utilization(), - Usable: s.post.IssuerUsable(v), - Label: v.Label(), - Depth: v.Depth(), - Amount: bigint.Wrap(v.Amount()), - BucketDepth: v.BucketDepth(), - BlockNumber: v.BlockNumber(), - ImmutableFlag: v.ImmutableFlag(), - Exists: true, - BatchTTL: batchTTL, + BatchID: v.ID(), + Utilization: v.Utilization(), + UtilizedPercentage: v.UtilizationPercentage(), + Usable: s.post.IssuerUsable(v), + Label: v.Label(), + Depth: v.Depth(), + Amount: bigint.Wrap(v.Amount()), + BucketDepth: v.BucketDepth(), + BlockNumber: v.BlockNumber(), + ImmutableFlag: v.ImmutableFlag(), + Exists: true, + BatchTTL: batchTTL, }) } @@ -395,17 +397,18 @@ func (s *Service) postageGetStampHandler(w http.ResponseWriter, r *http.Request) } jsonhttp.OK(w, &postageStampResponse{ - BatchID: paths.BatchID, - Depth: issuer.Depth(), - BucketDepth: issuer.BucketDepth(), - ImmutableFlag: issuer.ImmutableFlag(), - Exists: true, - BatchTTL: batchTTL, - Utilization: issuer.Utilization(), - Usable: s.post.IssuerUsable(issuer), - Label: issuer.Label(), - Amount: bigint.Wrap(issuer.Amount()), - BlockNumber: issuer.BlockNumber(), + BatchID: paths.BatchID, + Depth: issuer.Depth(), + BucketDepth: issuer.BucketDepth(), + ImmutableFlag: issuer.ImmutableFlag(), + Exists: true, + BatchTTL: batchTTL, + Utilization: issuer.Utilization(), + UtilizedPercentage: issuer.UtilizationPercentage(), + Usable: s.post.IssuerUsable(issuer), + Label: issuer.Label(), + Amount: bigint.Wrap(issuer.Amount()), + BlockNumber: issuer.BlockNumber(), }) } diff --git a/pkg/api/postage_test.go b/pkg/api/postage_test.go index c83aceefe6a..6aa7a6b7641 100644 --- a/pkg/api/postage_test.go +++ b/pkg/api/postage_test.go @@ -222,17 +222,18 @@ func TestPostageGetStamps(t *testing.T) { jsonhttptest.WithExpectedJSONResponse(&api.PostageStampsResponse{ Stamps: []api.PostageStampResponse{ { - BatchID: b.ID, - Utilization: si.Utilization(), - Usable: true, - Label: si.Label(), - Depth: si.Depth(), - Amount: bigint.Wrap(si.Amount()), - BucketDepth: si.BucketDepth(), - BlockNumber: si.BlockNumber(), - ImmutableFlag: si.ImmutableFlag(), - Exists: true, - BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. + BatchID: b.ID, + Utilization: si.Utilization(), + UtilizedPercentage: si.UtilizationPercentage(), + Usable: true, + Label: si.Label(), + Depth: si.Depth(), + Amount: bigint.Wrap(si.Amount()), + BucketDepth: si.BucketDepth(), + BlockNumber: si.BlockNumber(), + ImmutableFlag: si.ImmutableFlag(), + Exists: true, + BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. }, }, }), @@ -373,17 +374,18 @@ func TestPostageGetStamp(t *testing.T) { jsonhttptest.Request(t, ts, http.MethodGet, "/stamps/"+hex.EncodeToString(b.ID), http.StatusOK, jsonhttptest.WithExpectedJSONResponse(&api.PostageStampResponse{ - BatchID: b.ID, - Utilization: si.Utilization(), - Usable: true, - Label: si.Label(), - Depth: si.Depth(), - Amount: bigint.Wrap(si.Amount()), - BucketDepth: si.BucketDepth(), - BlockNumber: si.BlockNumber(), - ImmutableFlag: si.ImmutableFlag(), - Exists: true, - BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. + BatchID: b.ID, + Utilization: si.Utilization(), + UtilizedPercentage: si.UtilizationPercentage(), + Usable: true, + Label: si.Label(), + Depth: si.Depth(), + Amount: bigint.Wrap(si.Amount()), + BucketDepth: si.BucketDepth(), + BlockNumber: si.BlockNumber(), + ImmutableFlag: si.ImmutableFlag(), + Exists: true, + BatchTTL: 15, // ((value-totalAmount)/pricePerBlock)*blockTime=((20-5)/2)*2. }), ) }) diff --git a/pkg/postage/stampissuer.go b/pkg/postage/stampissuer.go index 87686f4d5bc..3ee526cf43d 100644 --- a/pkg/postage/stampissuer.go +++ b/pkg/postage/stampissuer.go @@ -222,6 +222,14 @@ func (si *StampIssuer) Utilization() uint32 { return si.data.MaxBucketCount } +// UtilizationPercentage returns the batch fullness as a fraction in the +// range [0, 1], computed as Utilization / 2^(BatchDepth - BucketDepth). +// A value of 1 means the most-filled bucket is full and any further write +// to that bucket would overflow the batch. +func (si *StampIssuer) UtilizationPercentage() float64 { + return float64(si.data.MaxBucketCount) / float64(uint64(1)<<(si.data.BatchDepth-si.data.BucketDepth)) +} + // ID returns the BatchID for this batch. func (si *StampIssuer) ID() []byte { id := make([]byte, len(si.data.BatchID)) diff --git a/pkg/postage/stampissuer_test.go b/pkg/postage/stampissuer_test.go index 05c177360a7..8f427479c6f 100644 --- a/pkg/postage/stampissuer_test.go +++ b/pkg/postage/stampissuer_test.go @@ -251,6 +251,39 @@ func TestUtilization(t *testing.T) { } } +func TestUtilizationPercentage(t *testing.T) { + t.Parallel() + + // depth=17, bucketDepth=16 => fullest bucket caps at 2^(17-16)=2 chunks. + const depth, bucketDepth uint8 = 17, 16 + + sti := postage.NewStampIssuer("label", "keyID", make([]byte, 32), big.NewInt(3), depth, bucketDepth, 0, true) + + if got := sti.UtilizationPercentage(); got != 0 { + t.Fatalf("empty issuer: want 0, got %v", got) + } + + // Fill the issuer until the fullest bucket is full, then check the value + // matches the formula utilization / 2^(depth - bucketDepth). + for { + _, _, err := sti.Increment(swarm.RandAddress(t)) + if errors.Is(err, postage.ErrBucketFull) { + break + } + if err != nil { + t.Fatal(err) + } + } + + want := float64(sti.Utilization()) / math.Pow(2, float64(depth-bucketDepth)) + if got := sti.UtilizationPercentage(); got != want { + t.Fatalf("filled issuer: want %v, got %v", want, got) + } + if got := sti.UtilizationPercentage(); got != 1 { + t.Fatalf("filled issuer should report 1, got %v", got) + } +} + func bytesToIndex(buf []byte) (bucket, index uint32) { index64 := binary.BigEndian.Uint64(buf) bucket = uint32(index64 >> 32)