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 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 the Batch.Commit() concurrency. 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
// 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
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 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 the index entry for k. 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 resolves the blob ID for k in Postgres and fetches the bytes from the Walrus aggregator. Returns ds.ErrNotFound when k is unknown to the index.
func (*WalrusDatastore) GetSize ¶
GetSize returns the stored size for k, answered entirely from Postgres.
func (*WalrusDatastore) Put ¶
Put uploads value to Walrus and records the resulting blob ID and metadata in Postgres. The Walrus upload happens first: if the index write then fails, the blob exists but is unreferenced (a recoverable leak), which is strictly safer than an index row pointing at a blob that was never stored.