Documentation
¶
Overview ¶
Package walrusds implements a Kubo (IPFS) datastore backed by Walrus, a decentralized storage network built on Sui, using a shared Postgres database as the durable key -> blob index.
Walrus is content-addressed (blobs are addressed by a content-derived blob ID, not by an arbitrary key), exposes no list/query API, and treats blobs as immutable with a finite, epoch-based lifetime. To present it as an IPFS datastore we keep the bytes on Walrus and the mapping
ds.Key -> { blobId, size, deletable, endEpoch, expiresAt }
in Postgres. Postgres is the source of truth for what the node "knows": it is shared across upload and retrieval nodes, survives local disk loss, and supports point-in-time recovery. Has/GetSize/Query are answered purely from Postgres so they never incur a Walrus round-trip; only Get touches the Walrus aggregator.
Index ¶
- Constants
- Variables
- func EncodedBlobLength(unencodedLength int64, nShards int) int64
- func EncodedStorageUnits(encodedSize int64) int64
- type Client
- func (c *Client) Read(ctx context.Context, blobID string) ([]byte, error)
- func (c *Client) ReadQuiltPatch(ctx context.Context, patchID string) ([]byte, error)
- func (c *Client) ReadRange(ctx context.Context, blobID string, offset, length int64) ([]byte, error)
- func (c *Client) Store(ctx context.Context, value []byte, epochs int, deletable bool) (StoreResult, error)
- func (c *Client) StoreQuilt(ctx context.Context, parts []QuiltPart, epochs int, deletable bool) (QuiltStoreResult, error)
- type ClientConfig
- type Config
- type Index
- type KeyRecord
- type ListItem
- type QuiltPart
- type QuiltPatch
- type QuiltStoreResult
- type Record
- type RenewItem
- type StageEntry
- type StageStats
- type StoreResult
- type WalrusDatastore
- func (w *WalrusDatastore) Batch(_ context.Context) (ds.Batch, error)
- func (w *WalrusDatastore) Close() error
- func (w *WalrusDatastore) Delete(ctx context.Context, k ds.Key) error
- func (w *WalrusDatastore) Get(ctx context.Context, k ds.Key) ([]byte, error)
- func (w *WalrusDatastore) GetSize(ctx context.Context, k ds.Key) (int, error)
- func (w *WalrusDatastore) Has(ctx context.Context, k ds.Key) (bool, error)
- func (w *WalrusDatastore) Put(ctx context.Context, k ds.Key, value []byte) error
- func (w *WalrusDatastore) Query(ctx context.Context, q dsq.Query) (dsq.Results, error)
- func (w *WalrusDatastore) Sync(ctx context.Context, prefix ds.Key) error
Constants ¶
const DefaultNShards = 1000
DefaultNShards is the Walrus mainnet committee shard count. The encoded (billed) size of a blob depends on its unencoded size and this shard count.
Variables ¶
var ErrBlobNotFound = errors.New("walrusds: blob not found")
ErrBlobNotFound is returned by the Walrus client when the aggregator has no blob for the requested blob ID.
Functions ¶
func EncodedBlobLength ¶ added in v0.4.0
EncodedBlobLength returns the Walrus *encoded* size in bytes of a blob whose unencoded length is unencodedLength, for a committee of nShards shards using the RS2 (Reed-Solomon) encoding used on Walrus mainnet. This is the figure Walrus uses for WAL storage billing ("datacap").
It mirrors MystenLabs' reference implementation exactly (RS2 decoding safety limit = 0): for a 17-byte blob on 1000 shards it returns 66,034,000, matching the value documented for the publisher's storage.storageSize. A value <= 0 for nShards falls back to DefaultNShards.
func EncodedStorageUnits ¶ added in v0.4.0
EncodedStorageUnits returns the number of whole 1 MiB storage units billed for an encoded size. WAL cost is (units × epochs × price-per-unit-epoch).
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a small, context-aware HTTP client for the Walrus publisher (write) and aggregator (read) HTTP APIs. It supports multiple endpoints for failover and retries transient failures with exponential backoff.
We deliberately implement this directly instead of depending on a third-party SDK so that every request honours the caller's context (cancellation/timeouts) and so retry/failover behaviour is under our control.
func NewClient ¶
func NewClient(conf ClientConfig) *Client
NewClient builds a Walrus client from the supplied configuration.
func (*Client) Read ¶
Read fetches the bytes of the blob identified by blobID from a Walrus aggregator. It returns ErrBlobNotFound if the aggregator returns 404.
func (*Client) ReadQuiltPatch ¶ added in v0.2.0
ReadQuiltPatch fetches a single quilt member by its QuiltPatchID via the aggregator's GET /v1/blobs/by-quilt-patch-id/{id} endpoint. Only that member's bytes travel over the wire (the aggregator decodes just the patch's slivers), so packed blocks stay cheap to read. Results are cached in the byte-bounded LRU keyed by patch ID.
func (*Client) ReadRange ¶
func (c *Client) ReadRange(ctx context.Context, blobID string, offset, length int64) ([]byte, error)
ReadRange fetches the bytes [offset, offset+length) of the blob identified by blobID. It first issues an HTTP Range request so only the requested block travels over the wire. If the aggregator ignores the Range header and returns the whole blob (HTTP 200), the full body is cached and sliced locally, so packed blocks remain cheap to read even without range support.
func (*Client) Store ¶
func (c *Client) Store(ctx context.Context, value []byte, epochs int, deletable bool) (StoreResult, error)
Store uploads value to a Walrus publisher, keeping it alive for the given number of epochs. When deletable is true the blob is registered as deletable so it can later be removed on-chain.
func (*Client) StoreQuilt ¶ added in v0.2.0
func (c *Client) StoreQuilt(ctx context.Context, parts []QuiltPart, epochs int, deletable bool) (QuiltStoreResult, error)
StoreQuilt batches many small blobs into a single Walrus quilt via the publisher's PUT /v1/quilts endpoint (multipart/form-data, one file part per member keyed by its identifier). A quilt shares one Walrus blob's overhead (Sui storage object, gas, erasure-coding metadata) across all its members, which is dramatically cheaper than one blob per small block. The returned QuiltStoreResult carries the quilt's blob ID plus each member's QuiltPatchID for later individual retrieval.
type ClientConfig ¶
type ClientConfig struct {
// PublisherURLs are Walrus publisher (write) base URLs. At least one is
// required for Put to work.
PublisherURLs []string
// AggregatorURLs are Walrus aggregator (read) base URLs. At least one is
// required for Get to work.
AggregatorURLs []string
// RequestTimeout bounds a single HTTP attempt. Zero means no per-attempt
// timeout beyond the caller's context.
RequestTimeout time.Duration
// MaxRetries is the number of additional attempts (per endpoint set) on
// transient failures. Zero means a single attempt.
MaxRetries int
// BlobCacheBytes is the byte budget for the in-memory LRU of whole blobs
// used to satisfy range reads of packed blocks. Zero disables the cache.
BlobCacheBytes int64
}
ClientConfig configures a Walrus Client.
type Config ¶
type Config struct {
// PublisherURLs are Walrus publisher (write) base URLs (comma-separated
// values are split by the plugin). At least one is required.
PublisherURLs []string
// AggregatorURLs are Walrus aggregator (read) base URLs. At least one is
// required.
AggregatorURLs []string
// PostgresURL is the database/sql connection string for the shared index,
// e.g. "postgres://user:pass@host:5432/db?sslmode=require".
PostgresURL string
// Table is the index table name. Defaults to "walrus_index".
Table string
// Epochs is how many storage epochs new blobs are paid for. Defaults to 1.
Epochs int
// NShards is the Walrus committee shard count, used only to compute a
// blob's encoded ("datacap") size when the publisher response omits it
// (e.g. a deduplicated/already-certified blob). Defaults to DefaultNShards
// (1000, Walrus mainnet). It does not affect what is stored on Walrus.
NShards int
// Deletable registers blobs as deletable on Walrus. Defaults to false.
Deletable bool
// Workers is how many packs the flusher uploads to Walrus in parallel when
// the staging buffer holds more than one pack's worth of blocks. Defaults
// to defaultWorkers. Peak upload memory is roughly Workers ×
// PackTargetSize, so raise both Workers and host RAM together.
Workers int
// MaxOpenConns bounds the Postgres connection pool. Defaults to
// defaultMaxOpenConns. Keep it >= Workers so committing packs does not
// starve on connections; a value <= 0 applies the default.
MaxOpenConns int
// PackTargetSize is the target size (in bytes) of a packed Walrus blob.
// During Batch.Commit, blocks are grouped into packs up to this size and
// uploaded as a single Walrus quilt (or, with DisableQuilt, a concatenated
// blob), amortizing the per-blob Walrus cost (Sui gas + WAL minimums) across
// many IPFS blocks. A block larger than this gets its own blob. Defaults to
// defaultPackTargetSize.
PackTargetSize int64
// DisableQuilt reverts batch packing to the legacy scheme: blocks are
// concatenated into one opaque blob and read back by byte range. By default
// (false) batches are stored as Walrus quilts, which share blob overhead
// natively and are read back per-member by QuiltPatchID. Existing rows
// written under either scheme keep working regardless of this setting.
DisableQuilt bool
// PackMaxAge bounds how long a block may wait in the Postgres staging
// buffer for a pack to fill before it is flushed to Walrus anyway.
// Blocks are durable from the moment they are staged; this only trades
// slightly longer Postgres-served reads for fuller (cheaper) packs.
// Defaults to defaultPackMaxAge.
PackMaxAge time.Duration
// PackIdleFlush flushes the partial tail of the staging buffer once no new
// blocks have been staged for this long — i.e. the ingest has finished —
// so an upload's last pack reaches Walrus promptly instead of waiting out
// PackMaxAge. Uploads with pauses longer than this flush a pack per pause;
// raise it if your ingest is bursty and packing density matters more than
// tail latency. Defaults to defaultPackIdleFlush.
PackIdleFlush time.Duration
// PackFlushInterval is how often the background flusher checks the staging
// buffer (it is additionally kicked after every Commit). Defaults to
// defaultPackFlushInterval.
PackFlushInterval time.Duration
// BlobCacheBytes is the byte budget for the in-memory LRU of whole blobs
// used to serve range reads of packed blocks. Defaults to
// defaultBlobCacheBytes; a negative value disables the cache.
BlobCacheBytes int64
// RequestTimeout bounds a single Walrus HTTP attempt. Defaults to 60s.
RequestTimeout time.Duration
// MaxRetries is the number of retries per Walrus request. Defaults to 3.
MaxRetries int
// EpochDuration is the wall-clock length of one Walrus storage epoch. When
// non-zero (together with RenewInterval) it enables the renewal worker,
// which re-uploads blobs before their paid storage expires. Operators set
// this to match the target network (e.g. ~14 days on mainnet).
EpochDuration time.Duration
// RenewInterval is how often the renewal worker scans for expiring blobs.
// Zero disables renewal.
RenewInterval time.Duration
// RenewLead is how far ahead of expiry a blob is renewed. Defaults to one
// EpochDuration when zero and renewal is enabled.
RenewLead time.Duration
}
Config holds everything needed to construct a WalrusDatastore.
type Index ¶
type Index interface {
Put(ctx context.Context, key string, rec Record) error
PutMany(ctx context.Context, recs []KeyRecord) error
Get(ctx context.Context, key string) (Record, error)
Delete(ctx context.Context, key string) error
DeleteMany(ctx context.Context, keys []string) error
List(ctx context.Context, prefix string, limit, offset int) ([]ListItem, error)
DueForRenewal(ctx context.Context, before time.Time, limit int) ([]RenewItem, error)
UpdateBlobAfterRenewal(ctx context.Context, oldBlobID, newBlobID, newObjectID string, encodedSize int64, cost uint64, endEpoch uint64, expiresAt sql.NullTime) error
// Staging buffer: durable cross-commit accumulation of blocks so the
// flusher can build full-size packs. See StageEntry.
StagePutMany(ctx context.Context, entries []StageEntry) error
StageGet(ctx context.Context, key string) ([]byte, error)
StageGetSize(ctx context.Context, key string) (int64, error)
StageStats(ctx context.Context) (StageStats, error)
// StageClaim leases up to maxCount unleased staged blocks whose cumulative
// size stays within maxBytes (always at least one row when any is
// claimable), so concurrent flushers on different nodes never build packs
// from the same blocks. The lease expires automatically, making a crashed
// flusher's claim recoverable; the upload is idempotent (Walrus blob IDs
// are content-derived), so a re-claim after a crash is safe.
StageClaim(ctx context.Context, maxCount int, maxBytes int64, lease time.Duration) ([]StageEntry, error)
// StageRelease returns claimed rows to the claimable pool after a failed
// upload so the next flush retries them.
StageRelease(ctx context.Context, keys []string) error
// PromoteStaged atomically moves blocks from staging to the index: rows are
// inserted only for keys still present in staging (a concurrent Delete
// wins), and the staged bytes are removed, in one transaction.
PromoteStaged(ctx context.Context, recs []KeyRecord) error
Close() error
}
Index is the durable key -> blob mapping. It is intentionally an interface so the backend (Postgres today, DynamoDB/SQLite later) can be swapped without touching the datastore logic.
type QuiltPart ¶ added in v0.2.0
QuiltPart is a single small blob (an IPFS block) to be batched into a quilt. Identifier is the unique name of the part within the quilt; the store response echoes it back paired with the part's QuiltPatchID.
type QuiltPatch ¶ added in v0.2.0
QuiltPatch is one stored member of a quilt: the identifier we supplied and the QuiltPatchID assigned by Walrus, which is how the part is read back.
type QuiltStoreResult ¶ added in v0.2.0
type QuiltStoreResult struct {
QuiltID string
ObjectID string
EndEpoch uint64
// EncodedSize is the whole quilt's Walrus encoded size in bytes (the billed
// datacap shared by all members). Zero if the quilt was already certified.
EncodedSize int64
// Cost is the WAL paid for the quilt store, in FROST. Zero if already
// certified.
Cost uint64
Patches []QuiltPatch
}
QuiltStoreResult is the parsed result of a store-quilt call: the quilt's own blob ID (used for renewal/extension, since the quilt is itself one Walrus blob), the Sui object ID of that quilt blob (for in-place `walrus extend`), the epoch its paid storage ends, and the per-part patch IDs.
type Record ¶
type Record struct {
BlobID string
// ObjectID is the Sui object ID of the Walrus Blob object backing this row
// (for a quilt, the quilt blob's object). It is what external renewal needs
// to extend the blob in place via `walrus extend` instead of re-uploading.
// It may be empty for legacy rows written before object IDs were tracked,
// or when the publisher returned an already-certified blob (no owned object).
ObjectID string
PatchID string
Offset int64
Size int64
// EncodedSize is the Walrus *encoded* size (datacap, in bytes) of the whole
// backing blob/quilt this row lives in — the billed figure, which includes
// erasure-coding expansion and per-blob metadata. It is a blob-level value:
// every row sharing a blob_id (all members of a quilt, all blocks of a
// concat pack) records the same number, so per-file datacap is summed over
// DISTINCT blob_id. Zero on legacy rows written before billing was tracked.
EncodedSize int64
// Cost is the WAL actually paid for the backing blob, in FROST (1 WAL = 1e9
// FROST), also blob-level. Zero when the blob was deduplicated
// (already-certified) so nothing was paid, or on legacy rows.
Cost uint64
Deletable bool
EndEpoch uint64
ExpiresAt sql.NullTime
}
Record is the metadata we persist in the shared index for each IPFS key. It is everything needed to (a) locate the block's bytes within a Walrus blob, (b) answer Has/GetSize without touching Walrus, and (c) drive epoch renewal.
Several keys can share a single Walrus blob. There are two packing schemes, distinguished by PatchID:
- Quilt rows (PatchID != ""): the block is a member ("patch") of a Walrus quilt. BlobID is the quilt's own blob ID (used for renewal grouping) and PatchID is the QuiltPatchID used to read the member back. Offset is unused.
- Concat/plain rows (PatchID == ""): the block lives at the byte range [Offset, Offset+Size) inside the blob BlobID. Unpacked blocks and all legacy rows are this shape with Offset == 0 and Size == the block length.
type RenewItem ¶
type RenewItem struct {
BlobID string
}
RenewItem identifies a Walrus blob whose paid storage is approaching expiry. Renewal operates per blob (not per key) so a packed blob holding many blocks is re-uploaded exactly once.
type StageEntry ¶ added in v0.5.0
StageEntry is one block buffered durably in the Postgres staging table, waiting to be packed into a full-size Walrus quilt. Staging exists because a single Batch.Commit rarely carries enough blocks to fill a pack: without it every commit became its own under-filled Walrus blob, each paying the fixed ~64 MB per-blob metadata overhead. Blocks are durable the moment they are staged (Postgres is the durable index store), so the datastore's commit contract holds while the flusher accumulates a full pack across commits.
type StageStats ¶ added in v0.5.0
StageStats summarizes the claimable (unleased) rows of the staging table so the flusher can decide whether a pack is worth uploading yet. Oldest drives the age-based flush (no block waits forever); Newest drives the idle-based flush (ingest has gone quiet, so the tail of an upload flushes immediately instead of waiting out the full age window).
type StoreResult ¶
type StoreResult struct {
BlobID string
ObjectID string
EndEpoch uint64
// EncodedSize is the blob's Walrus *encoded* size in bytes (the billed
// "datacap": erasure-coded size plus per-blob metadata), as reported by the
// publisher via storage.storageSize / resourceOperation.encodedLength. It is
// zero for an already-certified (deduplicated) blob, whose response omits
// it; callers can fill it in with EncodedBlobLength.
EncodedSize int64
// Cost is the WAL actually paid for this store, in FROST (1 WAL = 1e9
// FROST). It is zero for an already-certified blob (nothing was paid because
// the content already existed on Walrus).
Cost uint64
}
StoreResult is the subset of a Walrus publisher "store" response that we care about: the resulting blob ID, the Sui object ID of the created Blob object, and the epoch at which the blob's paid storage ends.
ObjectID is the on-chain Sui object that owns the blob's storage; it is what `walrus extend` needs to renew the blob in place (the blob ID alone is not enough). It is only present when the publisher newly created the blob object (the "newlyCreated" response); for an "alreadyCertified" response no new owned object is returned and ObjectID is empty.
type WalrusDatastore ¶
type WalrusDatastore struct {
// contains filtered or unexported fields
}
WalrusDatastore stores values on Walrus and keeps the durable key -> blob mapping in Postgres.
func NewWalrusDatastore ¶
func NewWalrusDatastore(conf Config) (*WalrusDatastore, error)
NewWalrusDatastore validates the configuration, connects to Postgres (creating the index table if needed), prepares the Walrus client and, if configured, starts the background renewal worker.
func (*WalrusDatastore) Batch ¶
Batch buffers Put/Delete operations and applies them concurrently on Commit.
func (*WalrusDatastore) Close ¶
func (w *WalrusDatastore) Close() error
Close stops the renewal worker and closes the Postgres connection.
func (*WalrusDatastore) Delete ¶
Delete removes k from the index and from the staging buffer (so an in-flight pack upload cannot resurrect it). It does not delete the underlying Walrus blob: on-chain deletion requires a Sui key and is out of scope for this datastore. The blob becomes unreferenced and eventually expires. Delete is idempotent.
func (*WalrusDatastore) Get ¶
Get serves k from the Postgres staging buffer if it has not been packed onto Walrus yet; otherwise it resolves the blob ID in Postgres and fetches the bytes from the Walrus aggregator. Returns ds.ErrNotFound when k is unknown.
Staging is checked first deliberately: promotion moves a key atomically from staging to the index, so probing in that order can never miss a block mid-promote (the reverse order could miss both sides of the move).
func (*WalrusDatastore) GetSize ¶
GetSize returns the stored size for k, answered entirely from Postgres.
func (*WalrusDatastore) Has ¶
Has reports whether k is present (staged or indexed), answered entirely from Postgres. Staging is probed first for the same mid-promote reason as Get.
func (*WalrusDatastore) Put ¶
Put durably stages value in Postgres and returns; the background flusher packs staged blocks from many Puts/Commits into full-size Walrus blobs. Staging is what lets packs fill across commits: uploading synchronously per Put/Commit produced one under-filled blob per commit, each paying Walrus's fixed ~64 MB per-blob metadata overhead. A value too large to ever share a pack skips staging and goes straight to Walrus as its own blob.