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 ¶
- Variables
- type Client
- type ClientConfig
- type Config
- type Index
- type KeyRecord
- type ListItem
- 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 ¶
This section is empty.
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 ¶
This section is empty.
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) 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.
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
// Deletable registers blobs as deletable on Walrus. Defaults to false.
Deletable bool
// Workers is the Batch.Commit() concurrency. Defaults to defaultWorkers.
Workers int
// PackTargetSize is the target size (in bytes) of a packed Walrus blob.
// During Batch.Commit, blocks are concatenated into packfiles up to this
// size and uploaded as a single 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
// 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 string, 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 Record ¶
type Record struct {
BlobID string
Offset int64
Size int64
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.
With block-packing, several keys can share a single Walrus blob: each row records the BlobID plus the byte range [Offset, Offset+Size) of the block inside that blob. Unpacked blocks (and all legacy rows) simply have Offset == 0 and Size == the whole blob 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 ¶
StoreResult is the subset of a Walrus publisher "store" response that we care about: the resulting blob ID and the epoch at which the blob's paid storage ends.
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.