Fibre Client

This document describes the Fibre client as implemented in the fibre package. It is a v0 shard upload/download client backed by a celestia-app state client and validator-operated Fibre servers.

0) Glossary

  • Fibre server / FSP: validator-operated gRPC service that stores and serves blob shards.
  • State client: client dependency used to fetch chain ID, validator sets, validator Fibre hosts, and payment-promise validation state.
  • Blob: encoded data plus a small v0 header, Reed-Solomon parity rows, row proofs, RLC vector, and commitment.
  • Commitment: 32-byte rsema1d commitment over the row root and RLC root.
  • BlobID: 33 bytes: blob_version || commitment.
  • PaymentPromise (PP): promise signed by the escrow owner and endorsed by validators after successful shard upload.
  • ShardMap: deterministic mapping from (commitment, validator set) to row indices per validator.

1) Construction & Config

The client is constructed with a Cosmos SDK keyring and ClientConfig.

func NewClient(kr keyring.Keyring, cfg ClientConfig) (*Client, error)

A nil keyring allows downloads only; Upload and Put then return ErrNoKeyring. If a keyring is provided, the configured key must exist. Start(ctx) must be called before Upload, Download, or package-level Put.

client, err := fibre.NewClient(kr, fibre.DefaultClientConfig())
if err != nil {
    return err
}
if err := client.Start(ctx); err != nil {
    return err
}
defer client.Stop(ctx)

ClientConfig

type ClientConfig struct {
    DefaultKeyName string
    StateAddress   string

    SafetyThreshold     cmtmath.Fraction
    LivenessThreshold   cmtmath.Fraction
    MinRowsPerValidator int
    MaxMessageSize      int
    RPCTimeout          time.Duration
    HostRefreshInterval time.Duration
    DetachBackgroundUploads bool

    StateClientFn func() (state.Client, error)
    NewClientFn   fibregrpc.NewClientFn

    Log    *slog.Logger
    Tracer trace.Tracer
    Meter  metric.Meter
    Clock  clock.Clock

    Escrow EscrowConfig
}

HostRefreshInterval throttles how often the default state client re-queries a validator's on-chain fibre host (defaults to the expected block time). Escrow configures the client-side escrow auto-funding described in section 11.

Defaults come from DefaultProtocolParams:

  • DefaultKeyName = "default-fibre"
  • StateAddress = "127.0.0.1:9090"
  • SafetyThreshold = 2/3
  • LivenessThreshold = 1/3
  • RPCTimeout = 15s
  • DetachBackgroundUploads = true
  • MaxBlobSize = 128 MiB
  • original rows K = 4096
  • total rows K + N = 16384
  • supported blob version: 0

If StateClientFn is nil, the client builds a gRPC app state client from StateAddress. If NewClientFn is nil, the client builds Fibre gRPC clients from validator hosts returned by the state client's host registry.

There are no send_workers or read_workers options in the current implementation. Upload starts one goroutine per assigned validator. Download dynamically starts validator requests until enough rows are in flight or retrieved.

2) Public API

Client Lifecycle

func (c *Client) Start(ctx context.Context) error
func (c *Client) Stop(ctx context.Context) error
func (c *Client) Await()
func (c *Client) ChainID() string

Stop marks the client closed, waits for in-flight upload/download work unless its context is canceled, and closes cached Fibre gRPC connections. Await waits for upload/download goroutines without closing the client.

Blob API

func NewBlob(data []byte, cfg BlobConfig) (*Blob, error)
func DefaultBlobConfigV0() BlobConfig

func (b *Blob) ID() BlobID
func (b *Blob) Data() []byte
func (b *Blob) DataSize() int
func (b *Blob) UploadSize() int
func (b *Blob) RowSize() int
func (b *Blob) Free()

NewBlob requires non-empty data. It returns ErrBlobTooLarge if len(data) exceeds BlobConfig.MaxDataSize (128 MiB - 5 for the default v0 header). The returned blob owns pooled storage and must be released with Free.

Upload

type UploadOption func(*uploadOptions)

func WithKeyName(keyName string) UploadOption
func WithAwaitAllSignatures() UploadOption

func (c *Client) Upload(
    ctx context.Context,
    ns share.Namespace,
    blob *Blob,
    opts ...UploadOption,
) (SignedPaymentPromise, error)

Upload signs a payment promise with the configured key, uploads assigned row shards to validators, verifies validator signatures, and returns a SignedPaymentPromise.

By default, Upload returns after the safety threshold by voting power is reached. Remaining validator uploads continue in background and are tracked by Await/Stop. With DetachBackgroundUploads (on by default) they keep running when the caller cancels its context after Upload returns; without it, that cancel drops them. Cancelling before the threshold aborts the upload either way. WithAwaitAllSignatures changes the threshold to all validator voting power and waits for all successful signatures.

type SignedPaymentPromise struct {
    *PaymentPromise
    ValidatorSignatures [][]byte
}

Put

type PutResult struct {
    BlobID              BlobID
    ValidatorSignatures [][]byte
    TTL                 time.Time
    TxHash              string
    Height              uint64
}

func Put(
    ctx context.Context,
    c *Client,
    txClient *user.TxClient,
    ns share.Namespace,
    data []byte,
) (PutResult, error)

Put is a package-level convenience helper. It creates a v0 blob, calls Client.Upload to upload assigned shards to validators and collect validator signatures, builds MsgPayForFibre, broadcasts it through the supplied user.TxClient, and waits for transaction confirmation.

Put does not use a DFSP relay client. The caller supplies the transaction client, so transaction endpoint selection, account configuration, fees, fee grants, and signing setup are determined by that user.TxClient. Put submits one MsgPayForFibre for the blob; callers that need custom transaction flow should call Client.Upload directly and submit payments themselves. A PFF holds one promise and must be the only message in its transaction; batching PFF messages in one transaction is rejected. TTL is currently present in PutResult but is not populated.

Upload retries

For a validator returning ResourceExhausted, the upload worker makes up to three retries after the initial attempt. It honors positive RetryInfo delays, capped at two minutes, or defaults to one second without a usable hint. Caller cancellation or client shutdown interrupts the wait. This is separate from the connection cache refreshing a failed host.

Download

type DownloadOption func(*downloadOptions)

func WithHeight(height uint64) DownloadOption

func (c *Client) Download(
    ctx context.Context,
    id BlobID,
    opts ...DownloadOption,
) (*Blob, error)

Download fetches and reconstructs a blob by BlobID. If WithHeight is provided, the client uses the validator set at that height; otherwise it uses the current head validator set. For later retrieval, use the payment promise height. PutResult.Height is the settlement height and may differ. Downloads verify content against the supplied commitment; they do not verify on-chain inclusion or settlement. The returned blob owns pooled storage and must be released with Free.

The current API does not expose Get(ctx, namespace, commitment) ([]byte, error). Callers use Download(ctx, NewBlobID(version, commitment)) and then read blob.Data().

3) Payment Promise and Sign Bytes

The implemented PaymentPromise is v0-oriented and uses a secp256k1 public key to identify the escrow owner.

type PaymentPromise struct {
    SignerKey         *secp256k1.PubKey
    ChainID           string
    Namespace         share.Namespace
    UploadSize        uint32
    BlobVersion       uint32
    Commitment        Commitment
    CreationTimestamp time.Time
    Signature         []byte
    Height            uint64
}

The protobuf form is:

message PaymentPromise {
  string chain_id = 1;
  int64 height = 2;
  bytes namespace = 3;
  uint32 blob_size = 4;
  uint32 blob_version = 5;
  bytes commitment = 6;
  google.protobuf.Timestamp creation_timestamp = 7;
  cosmos.crypto.secp256k1.PubKey signer_public_key = 8;
  bytes signature = 9;
}

The signed payload is:

stripped =
  signer_public_key_compressed_33 ||
  namespace_29 ||
  upload_size_u32be ||
  commitment_32 ||
  blob_version_u32be ||
  height_u64be ||
  creation_timestamp_go_binary_utc

SignBytes = CometBFT RawBytesMessageSignBytes(
  chain_id,
  "fibre/pp:v0",
  stripped,
)

The escrow-owner signature is a 64-byte secp256k1 signature over SignBytes. Validator signatures are ed25519 signatures over the same SignBytes.

PaymentPromise.Hash() is:

SHA256(SignBytes || escrow_owner_signature)

4) Blob Encoding

Only blob version 0 is supported.

The encoded data starts with a five-byte header:

version_u8 || original_data_size_u32be

Rows are produced with rsema1d. Default protocol parameters:

  • original rows: 4096
  • parity rows: 12288
  • total rows: 16384
  • encoding ratio: 0.25
  • maximum blob size, including header: 128 MiB
  • minimum row-size alignment: 64 bytes

UploadSize is the padded original-row size only:

UploadSize = row_size * original_rows

It excludes parity rows but includes padding and the v0 header.

5) Assignment

validator.Set.Assign maps row indices to validators using voting power and the configured liveness threshold.

Inputs:

  • commitment
  • totalRows
  • originalRows
  • minRows
  • livenessThreshold
  • validator set at the payment promise height

For each validator:

rows = ceil(originalRows * validator_power * liveness_denominator /
            (total_voting_power * liveness_numerator))
rows = min(max(rows, minRows), originalRows)

Then:

  1. Seed a ChaCha8 RNG with the commitment.
  2. Shuffle all row indices 0..totalRows-1 with Fisher-Yates.
  3. Walk validators in CometBFT validator-set order.
  4. Give each validator the next rows shuffled row indices.
  5. If the sum of assigned rows exceeds totalRows, wrap around modulo totalRows, which can assign the same row to multiple validators.

This is not an equal, non-overlapping permutation assignment. Overlap can occur when minimum-row guarantees over-assign rows.

6) State Client

The client depends on state.Client:

type Client interface {
    validator.SetGetter
    validator.HostRegistry

    ChainID() string
    VerifyPromise(context.Context, *types.PaymentPromise) (VerifiedPromise, error)
    FullStakeStorageBudget(context.Context) (int64, error)

    Start(context.Context) error
    Stop(context.Context) error
}

The default implementation is the gRPC AppClient. On Start, it detects the chain ID from the app node and pulls validator Fibre provider hosts from x/valaddr.

Validator sets are fetched through CometBFT's gRPC Block API:

Head(ctx) (validator.Set, error)
GetByHeight(ctx, height uint64) (validator.Set, error)

There is no embedded light-node-backed client constructor in the current implementation.

7) gRPC Transport

The implemented Fibre service is shard-oriented:

message BlobRow {
  uint32 index = 1;
  bytes data = 2;
  repeated bytes proof = 3;
}

message BlobShard {
  repeated BlobRow rows = 1;
  bytes rlcs = 2;
}

message UploadShardRequest {
  PaymentPromise promise = 1;
  BlobShard shard = 2;
}

message UploadShardResponse {
  bytes validator_signature = 1;
}

message DownloadShardRequest {
  bytes blob_id = 1;
}

message DownloadShardResponse {
  BlobShard shard = 1;
}

service Fibre {
  rpc UploadShard(UploadShardRequest) returns (UploadShardResponse);
  rpc DownloadShard(DownloadShardRequest) returns (DownloadShardResponse);
}

BlobShard.rlcs contains the serialized full original-row RLC vector, not only coefficients for the returned rows.

The default Fibre gRPC client resolves a validator host from the state client's host registry and uses TLS with validator consensus-key identity verification, as specified in Fibre TLS Identity.

8) Upload Flow

  1. Require Start to have completed and the client not to be closed.
  2. Retain the blob's pooled storage for the upload lifetime.
  3. Fetch the current validator set with state.Head(ctx).
  4. Build and sign a PaymentPromise using the selected keyring key.
  5. Compute PaymentPromise.Hash() for logging/storage identity.
  6. Compute the ShardMap from the blob commitment and validator set.
  7. Build one UploadShardRequest envelope per validator with the shared promise and serialized RLC vector.
  8. Start one goroutine per validator.
  9. For each validator, build row data and proofs for its assigned row indices, call UploadShard with RPCTimeout, parse the validator signature, and add it to the SignatureSet.
  10. Return when all validators have responded or the configured voting-power threshold is reached.
  11. Return SignedPaymentPromise.

Signature collection currently enforces voting-power threshold only. It does not enforce a separate count threshold.

9) Put Flow

Put is a convenience wrapper around upload plus app transaction submission:

  1. Create a v0 Blob from data.
  2. Call Client.Upload using WithKeyName(txClient.DefaultAccountName()) to upload assigned shards to validators and collect validator signatures.
  3. Convert the signed promise to proto.
  4. Build x/fibre MsgPayForFibre with validator signatures.
  5. Broadcast through txClient.BroadcastTx. A missing historical validator set rejection is retried up to four total attempts, separated by 500 ms, to allow the app to catch up with the observed head height.
  6. Wait for inclusion with txClient.ConfirmTx.
  7. Return PutResult.

The implementation does not submit PFF through a Fibre payment relay service and does not implement DFSP fallback.

10) Download Flow

  1. Require Start to have completed and the client not to be closed.
  2. Validate BlobID.
  3. Fetch a validator set:
    • GetByHeight(ctx, height) when WithHeight(height) is used.
    • Head(ctx) otherwise.
  4. Select validators with validator.Set.Select, shuffled by stake for load balancing.
  5. Start download workers while the reconstructor still wants rows and row reservations are available.
  6. Each worker calls DownloadShard with RPCTimeout.
  7. Parse rows, row proofs, and RLC vector from BlobShard.
  8. Add the shard to the rsema1d.Reconstructor, which verifies proofs and the commitment/RLC relationship.
  9. Stop dispatching after enough unique rows are collected or all selected validators have been tried.
  10. Reconstruct and decode the v0 blob header.
  11. Return a Blob.

RLC verification is for client-side recovery. An attacker can publish a commitment built from tampered rows, so Merkle proofs still pass. But those rows may not be a valid Reed-Solomon encoding. RLC lets the downloader spot the bad rows, skip them, and recover from enough honest rows. It does not prevent a bad commitment from being accepted on-chain if enough validators sign it; punishment or banning is separate.

Note: The on-chain commitment in this attack is the tampered one, so it differs from the commitment for the correctly encoded data.

Errors are classified as:

  • ErrNotFound: no rows were retrieved.
  • ErrNotEnoughShards: some rows were retrieved, but not enough to reconstruct.
  • reconstruction/verification/decode errors for invalid shards or invalid data.
  • context cancellation/deadline errors from the caller or per-RPC timeout.

11) Account and Escrow APIs

The Fibre client package does not currently expose Account() or an AccountClient.

Escrow state and transactions are available through the app's x/fibre query and msg services:

  • Query.EscrowAccount
  • Query.Withdrawals
  • Msg.DepositToEscrow
  • Msg.RequestWithdrawal
  • Msg.PayForFibre
  • Msg.PaymentPromiseTimeout

Callers that need deposits, withdrawals, escrow queries, or PFF transaction submission use the normal app gRPC query clients and transaction clients.

Escrow ledger accounting and limits

Escrow admission and auto-funding run in Put only when Escrow.AutoFund is enabled. Direct calls to Client.Upload bypass them. Each signer has an in-memory ledger seeded from the on-chain AvailableBalance, minus reservations for this client's subsequent Put calls.

Before dispatch, Put reserves the payment amount. It returns that reservation if it fails before the signed promise can leave the client. Once dispatch starts, the reservation stays debited even if upload or settlement fails: anyone holding the signed promise may submit a timeout settlement. Confirmed deposits add to the ledger. Failed uploads whose promises never settle can therefore leave the ledger understating the on-chain balance; an abort before dispatch is credited back.

This accounting only coordinates calls using this ledger. It does not reserve funds on chain or coordinate another process, direct Upload calls, or withdrawals made elsewhere with the same account.

StartupGracePeriod delays seeding, admission, and deposits after ledger creation. It defaults to zero. It is a waiting mechanism, not evidence that old promises have settled. Waiting only PaymentPromiseTimeout is insufficient: expired promises can still be charged through MsgPaymentPromiseTimeout until they fail the freshness check. A restart policy must account for all outstanding promises and the full remaining settlement window, including allowed clock skew and parameter changes; see stateful validation.

12) Errors

Important client-side errors include:

  • ErrClientClosed
  • ErrKeyNotFound
  • ErrNoKeyring
  • ErrBlobTooLarge
  • ErrNotFound
  • ErrNotEnoughShards
  • validator.NotEnoughSignaturesError

Other errors are returned as wrapped errors from keyring operations, state lookups, gRPC calls, row proof generation, reconstruction, decoding, or transaction broadcasting/confirmation.

There is no dedicated client error mapping for insufficient escrow balance, invalid namespace, PFF submission, or RLC mismatch.

13) Metrics

The current client records OpenTelemetry metrics for:

  • upload in-flight count and duration
  • uploaded padded bytes, original data bytes, and network bytes
  • validator signatures collected
  • per-validator upload duration and RPC latency
  • download in-flight count, duration, and bytes
  • per-validator download duration and RPC latency

It does not currently expose separate metrics for encode latency, chosen row size, quorum time, PFF submission/inclusion, balance cache age, or insufficient proof handling.