Skip to content

Commit 39345a0

Browse files
committed
refactor(fibre): BlobId
1 parent c0099f2 commit 39345a0

19 files changed

+348
-229
lines changed

fibre/blob.go

Lines changed: 35 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package fibre
22

33
import (
44
"encoding/binary"
5-
"encoding/hex"
65
"errors"
76
"fmt"
87
"runtime"
@@ -19,42 +18,6 @@ var (
1918
ErrBlobCommitmentMismatch = errors.New("commitment mismatch: reconstructed data doesn't match expected commitment")
2019
)
2120

22-
// Commitment is a commitment to a blob.
23-
// TODO(@Wondertan): merge with rsema1d.Commitment once it has these methods.
24-
type Commitment rsema1d.Commitment
25-
26-
// UnmarshalBinary decodes a [Commitment] from bytes.
27-
func (c *Commitment) UnmarshalBinary(data []byte) error {
28-
if len(data) != 32 {
29-
return fmt.Errorf("commitment must be 32 bytes, got %d", len(data))
30-
}
31-
copy(c[:], data)
32-
return nil
33-
}
34-
35-
// String returns the hex-encoded string representation of the commitment.
36-
func (c Commitment) String() string {
37-
return hex.EncodeToString(c[:])
38-
}
39-
40-
// CommitmentFromString decodes a [Commitment] from a hex-encoded string.
41-
func CommitmentFromString(s string) (Commitment, error) {
42-
data, err := hex.DecodeString(s)
43-
if err != nil {
44-
return Commitment{}, fmt.Errorf("decoding hex: %w", err)
45-
}
46-
var c Commitment
47-
if err := c.UnmarshalBinary(data); err != nil {
48-
return Commitment{}, err
49-
}
50-
return c, nil
51-
}
52-
53-
// Equals returns true if the two commitments are equal.
54-
func (c Commitment) Equals(other Commitment) bool {
55-
return c == other
56-
}
57-
5821
// BlobConfig contains configuration parameters for blob encoding and decoding.
5922
type BlobConfig struct {
6023
// BlobVersion is the version of the row format.
@@ -77,6 +40,17 @@ func DefaultBlobConfigV0() BlobConfig {
7740
return NewBlobConfigFromParams(0, DefaultProtocolParams)
7841
}
7942

43+
// BlobConfigForVersion returns the [BlobConfig] for the given blob version.
44+
// Returns an error if the version is not supported.
45+
func BlobConfigForVersion(version uint8) (BlobConfig, error) {
46+
switch version {
47+
case 0:
48+
return DefaultBlobConfigV0(), nil
49+
default:
50+
return BlobConfig{}, fmt.Errorf("unsupported blob version: %d", version)
51+
}
52+
}
53+
8054
// NewBlobConfigFromParams creates a [BlobConfig] with values derived from the given [ProtocolParams].
8155
// Use this when you need a config with non-default protocol parameters (e.g., for testing).
8256
func NewBlobConfigFromParams(blobVersion uint8, p ProtocolParams) BlobConfig {
@@ -115,7 +89,7 @@ type Blob struct {
11589
cfg BlobConfig
11690

11791
extendedData *rsema1d.ExtendedData
118-
commitment Commitment
92+
id BlobID
11993
rlcCoeffs []field.GF128
12094

12195
// holds meta fields about the blob
@@ -146,7 +120,8 @@ func NewBlob(data []byte, cfg BlobConfig) (d *Blob, err error) {
146120
}
147121

148122
rows := d.header.encodeToRows(data, cfg)
149-
d.extendedData, d.commitment, d.rlcCoeffs, err = rsema1d.Encode(rows, &rsema1d.Config{
123+
var rsemaCommitment rsema1d.Commitment
124+
d.extendedData, rsemaCommitment, d.rlcCoeffs, err = rsema1d.Encode(rows, &rsema1d.Config{
150125
K: cfg.OriginalRows,
151126
N: cfg.ParityRows,
152127
RowSize: len(rows[0]),
@@ -155,23 +130,32 @@ func NewBlob(data []byte, cfg BlobConfig) (d *Blob, err error) {
155130
if err != nil {
156131
return nil, fmt.Errorf("encoding data: %w", err)
157132
}
133+
d.id = NewBlobID(cfg.BlobVersion, rsemaCommitment)
158134

159135
return d, nil
160136
}
161137

162138
// NewEmptyBlob creates a new [Blob] instance for receiving and reconstructing data.
163-
func NewEmptyBlob(cfg BlobConfig, commitment Commitment) *Blob {
139+
// Returns an error if the BlobID is invalid or the blob version is not supported.
140+
func NewEmptyBlob(id BlobID) (*Blob, error) {
141+
if err := id.Validate(); err != nil {
142+
return nil, fmt.Errorf("invalid blob ID: %w", err)
143+
}
144+
cfg, err := BlobConfigForVersion(id.Version())
145+
if err != nil {
146+
return nil, err
147+
}
164148
totalRows := cfg.OriginalRows + cfg.ParityRows
165149
return &Blob{
166-
cfg: cfg,
167-
commitment: commitment,
168-
rows: make([][]byte, totalRows),
169-
}
150+
cfg: cfg,
151+
id: id,
152+
rows: make([][]byte, totalRows),
153+
}, nil
170154
}
171155

172-
// Commitment returns the commitment to the blob.
173-
func (d *Blob) Commitment() Commitment {
174-
return d.commitment
156+
// ID returns the BlobID of this blob.
157+
func (d *Blob) ID() BlobID {
158+
return d.id
175159
}
176160

177161
// Config returns the blob's configuration.
@@ -220,14 +204,13 @@ func (d *Blob) Row(index int) (*rsema1d.RowInclusionProof, error) {
220204
// SetRow adds and verifies [*rsema1d.RowInclusionProof] to the blob.
221205
// It is safe to call this method concurrently only for disjoint indices.
222206
func (d *Blob) SetRow(row *rsema1d.RowInclusionProof) error {
223-
// verify the inclusion proof
224207
config := &rsema1d.Config{
225208
K: d.cfg.OriginalRows,
226209
N: d.cfg.ParityRows,
227210
RowSize: len(row.Row),
228211
WorkerCount: d.cfg.CodingWorkers,
229212
}
230-
err := rsema1d.VerifyRowInclusionProof(row, rsema1d.Commitment(d.commitment), config)
213+
err := rsema1d.VerifyRowInclusionProof(row, d.id.Commitment(), config)
231214
if err != nil {
232215
return fmt.Errorf("verifying row %d: %w", row.Index, err)
233216
}
@@ -271,9 +254,9 @@ func (d *Blob) Reconstruct() error {
271254
}
272255

273256
// verify commitment matches
274-
if !d.commitment.Equals(Commitment(reconstructedCommitment)) {
275-
return fmt.Errorf("%w: expected %s, got %s",
276-
ErrBlobCommitmentMismatch, d.commitment.String(), Commitment(reconstructedCommitment).String())
257+
if d.id.Commitment() != reconstructedCommitment {
258+
return fmt.Errorf("%w: expected %x, got %x",
259+
ErrBlobCommitmentMismatch, d.id.Commitment(), reconstructedCommitment[:])
277260
}
278261

279262
// decode header and extract original data from the first K rows, then cache it

fibre/blob_id.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package fibre
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"fmt"
7+
8+
"github.com/celestiaorg/rsema1d"
9+
)
10+
11+
// CommitmentSize is the size of a Commitment in bytes.
12+
const CommitmentSize = 32
13+
14+
// Commitment is a 32-byte blob commitment used for protocol messages and storage indexing.
15+
// TODO: merge with rsema1d.Commitment once that package stabilizes.
16+
type Commitment rsema1d.Commitment
17+
18+
// String returns the hex-encoded string representation.
19+
func (c Commitment) String() string {
20+
return hex.EncodeToString(c[:])
21+
}
22+
23+
// UnmarshalBinary decodes a Commitment from bytes.
24+
func (c *Commitment) UnmarshalBinary(data []byte) error {
25+
if len(data) != CommitmentSize {
26+
return fmt.Errorf("commitment must be %d bytes, got %d", CommitmentSize, len(data))
27+
}
28+
copy(c[:], data)
29+
return nil
30+
}
31+
32+
// CommitmentFromString decodes a Commitment from a hex-encoded string.
33+
func CommitmentFromString(s string) (Commitment, error) {
34+
data, err := hex.DecodeString(s)
35+
if err != nil {
36+
return Commitment{}, fmt.Errorf("decoding hex: %w", err)
37+
}
38+
if len(data) != CommitmentSize {
39+
return Commitment{}, fmt.Errorf("commitment must be %d bytes, got %d", CommitmentSize, len(data))
40+
}
41+
var c Commitment
42+
copy(c[:], data)
43+
return c, nil
44+
}
45+
46+
// BlobIDSize is the size of a BlobID in bytes.
47+
// Format: [version (1 byte) | Commitment (32 bytes)]
48+
const BlobIDSize = 33
49+
50+
// BlobID uniquely identifies a blob by combining version and commitment.
51+
// The first byte encodes the blob version, followed by 32 bytes of commitment.
52+
// This makes BlobIDs self-describing, allowing clients to know the blob format before downloading.
53+
type BlobID []byte
54+
55+
// NewBlobID creates a BlobID from version and commitment.
56+
func NewBlobID(version uint8, commitment Commitment) BlobID {
57+
id := make(BlobID, BlobIDSize)
58+
id[0] = version
59+
copy(id[1:], commitment[:])
60+
return id
61+
}
62+
63+
// Version returns the blob version encoded in this BlobID.
64+
// Panics if the BlobID is invalid (empty or wrong length).
65+
func (id BlobID) Version() uint8 {
66+
return id[0]
67+
}
68+
69+
// Validate returns an error if the BlobID is invalid.
70+
func (id BlobID) Validate() error {
71+
if len(id) != BlobIDSize {
72+
return fmt.Errorf("blob ID must be %d bytes, got %d", BlobIDSize, len(id))
73+
}
74+
return nil
75+
}
76+
77+
// Commitment returns the commitment (without version prefix).
78+
func (id BlobID) Commitment() Commitment {
79+
var c Commitment
80+
copy(c[:], id[1:])
81+
return c
82+
}
83+
84+
// UnmarshalBinary decodes a [BlobID] from bytes.
85+
func (id *BlobID) UnmarshalBinary(data []byte) error {
86+
if len(data) != BlobIDSize {
87+
return fmt.Errorf("blob ID must be %d bytes, got %d", BlobIDSize, len(data))
88+
}
89+
*id = make(BlobID, BlobIDSize)
90+
copy(*id, data)
91+
return nil
92+
}
93+
94+
// String returns the hex-encoded string representation of the BlobID.
95+
func (id BlobID) String() string {
96+
return hex.EncodeToString(id)
97+
}
98+
99+
// BlobIDFromString decodes a [BlobID] from a hex-encoded string.
100+
func BlobIDFromString(s string) (BlobID, error) {
101+
data, err := hex.DecodeString(s)
102+
if err != nil {
103+
return nil, fmt.Errorf("decoding hex: %w", err)
104+
}
105+
var id BlobID
106+
if err := id.UnmarshalBinary(data); err != nil {
107+
return nil, err
108+
}
109+
return id, nil
110+
}
111+
112+
// Equals returns true if the two BlobIDs are equal.
113+
func (id BlobID) Equals(other BlobID) bool {
114+
return bytes.Equal(id, other)
115+
}

fibre/blob_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ func TestBlob_Reconstruct(t *testing.T) {
8989
}
9090

9191
testReconstruct := func(t *testing.T, rows []*rsema1d.RowInclusionProof) {
92-
reconstructBlob := NewEmptyBlob(cfg, blob.Commitment())
92+
reconstructBlob, err := NewEmptyBlob(blob.ID())
93+
require.NoError(t, err)
9394

9495
for _, row := range rows {
9596
err = reconstructBlob.SetRow(row)

fibre/client_download.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ var (
3232
// Errors:
3333
// - [ErrNotFound]: no shard was retrieved for the blob
3434
// - [ErrNotEnoughShards]: not enough shards were retrieved to reconstruct the original data
35-
// - [ErrInvalidCommitment]: the commitment doesn't match the reconstructed blob
36-
func (c *Client) Download(ctx context.Context, commitment Commitment) (*Blob, error) {
35+
// - [ErrBlobCommitmentMismatch]: the commitment doesn't match the reconstructed blob
36+
func (c *Client) Download(ctx context.Context, id BlobID) (*Blob, error) {
3737
if c.closed.Load() {
3838
return nil, ErrClientClosed
3939
}
4040

4141
ctx, span := c.tracer.Start(ctx, "fibre.Client.Download",
42-
trace.WithAttributes(attribute.String("blob_commitment", commitment.String())),
42+
trace.WithAttributes(attribute.String("blob_commitment", id.Commitment().String())),
4343
)
4444
defer span.End()
4545

46-
c.log.DebugContext(ctx, "initiating blob download", "blob_commitment", commitment)
46+
c.log.DebugContext(ctx, "initiating blob download", "blob_commitment", id.Commitment())
4747

4848
// get validator set
4949
// TODO(@Wondertan): If we don't want to pass height here, we should at least ensure we handle the case
@@ -59,7 +59,7 @@ func (c *Client) Download(ctx context.Context, commitment Commitment) (*Blob, er
5959
attribute.Int64("validator_set_height", int64(valSet.Height)),
6060
))
6161

62-
blob, err := c.downloadBlob(ctx, valSet, commitment)
62+
blob, err := c.downloadBlob(ctx, valSet, id)
6363
if err != nil {
6464
span.RecordError(err)
6565
span.SetStatus(codes.Error, "failed to download")
@@ -74,7 +74,7 @@ func (c *Client) Download(ctx context.Context, commitment Commitment) (*Blob, er
7474
}
7575

7676
c.log.DebugContext(ctx, "blob download completed successfully",
77-
"blob_commitment", commitment,
77+
"blob_commitment", id.Commitment(),
7878
"upload_size", blob.UploadSize(),
7979
"data_size", blob.DataSize(),
8080
"row_size", blob.RowSize(),
@@ -87,14 +87,13 @@ func (c *Client) Download(ctx context.Context, commitment Commitment) (*Blob, er
8787
return blob, nil
8888
}
8989

90-
// downloadFrom downloads a shard for a commitment from a single validator and applies its rows to the blob.
90+
// downloadFrom downloads a shard for a blob from a single validator and applies its rows to the blob.
9191
func (c *Client) downloadFrom(
9292
ctx context.Context,
9393
val *core.Validator,
9494
blob *Blob,
9595
) error {
96-
commitment := blob.Commitment()
97-
log := c.log.With("validator", val.Address.String(), "blob_commitment", commitment)
96+
log := c.log.With("validator", val.Address.String(), "blob_commitment", blob.ID().Commitment())
9897

9998
ctx, span := c.tracer.Start(ctx, "download_from",
10099
trace.WithAttributes(attribute.String("validator_address", val.Address.String())),
@@ -114,7 +113,7 @@ func (c *Client) downloadFrom(
114113
}
115114
span.AddEvent("client_acquired")
116115

117-
resp, err := client.DownloadShard(ctx, &types.DownloadShardRequest{Commitment: commitment[:]})
116+
resp, err := client.DownloadShard(ctx, &types.DownloadShardRequest{BlobId: blob.ID()})
118117
if err != nil {
119118
if context.Cause(ctx) == errDownloaded {
120119
span.SetStatus(codes.Ok, "")
@@ -173,12 +172,15 @@ func (c *Client) downloadFrom(
173172
func (c *Client) downloadBlob(
174173
ctx context.Context,
175174
valSet validator.Set,
176-
commitment Commitment,
175+
id BlobID,
177176
) (*Blob, error) {
178177
ctx, cancel := context.WithCancelCause(ctx)
179178
defer cancel(errDownloaded)
180179

181-
blob := NewEmptyBlob(DefaultBlobConfigV0(), commitment)
180+
blob, err := NewEmptyBlob(id)
181+
if err != nil {
182+
return nil, fmt.Errorf("creating empty blob: %w", err)
183+
}
182184

183185
var (
184186
responses atomic.Uint32 // tracks finished responses

0 commit comments

Comments
 (0)