Commands
pytusk models each Walrus HTTP operation as a
WalrusCommand subclass, dispatched through
WalrusClient.execute() rather
than exposing one method per operation — the same pattern pysui uses for
its Unified Client Interface.
The WalrusCommand ABC
WalrusCommand is an abstract, keyword-only
(kw_only=True) dataclass. Each concrete command overrides:
endpoint_role— aClassVar[str]. Values include"aggregator"(reads),"publisher"(writes), and"storage_node"(operations sent directly to an individual storage node). Selects which Walrus endpoint the command is sent to.http_method() -> str— the HTTP verb to use.url_path(base_url: str) -> str— the full request URL.parse_response(response: httpx.Response) -> SuiRpcResult— turns the raw HTTP response into a typed result.
and may optionally override (empty/None by default):
query_params() -> dict[str, Any]request_body() -> bytes | Noneform_files() -> dict[str, bytes] | None
WalrusClient.execute() reads
http_method(), selects the aggregator or publisher base URL per
endpoint_role, builds the URL via url_path(), gathers
query_params()/request_body()/form_files(), makes the HTTP
request, and returns parse_response(response).
Response types
Every command’s parse_response() returns a SuiRpcResult. The
dataclass below is the payload carried on its result_data attribute:
BlobData— raw blob contentBlobSlice— a partial byte range of a blobQuiltPatch— a single patch’s content from a quiltBlobReceipt— object/blob ID, cost, expiry, and deletable flag for a newly stored blobQuiltReceipt— quilt ID, patch keys, cost, expiry, and object ID for a newly stored quiltSliverAck— acknowledgement that a storage node accepted a sliver PUTMetadataAck— acknowledgement that a storage node accepted a blob-metadata PUTSignedConfirmation— a storage node’s signed confirmation that it holds a blob’s sliversBlobStatus— one storage node’s view of a blob’s status. Unlike every entry above, this is a UNION rather than a single dataclass:NonexistentStatus,InvalidStatus,PermanentStatus,DeletableStatus, orUnresolvedStatus. The variants mirror the protocol’s own enum, so fields that are meaningful on only one of them (such asend_epoch, which a deletable registration does not have) cannot be read on a variant that lacks them
Command Reference
Command |
Description |
Notes |
|---|---|---|
Read a blob by its Walrus blob ID. |
Result data: |
|
Read a byte range from a blob. |
Result data: |
|
Read a blob by its Sui object ID. |
Result data: |
|
Read a single patch from a quilt by quilt ID and patch key. |
Result data: |
|
Read a single patch from a quilt directly by its QuiltPatchId. |
Result data: |
|
List the patches contained in a quilt. |
Result data: |
|
Read and concatenate multiple blobs into a single response. |
Result data: |
|
Store a blob on Walrus. |
Result data: |
|
Store a collection of named blobs (a quilt) on Walrus. |
Result data: |
|
Hand an unencoded blob to an upload relay, which fans slivers out on the caller’s behalf. |
Result data: |
|
Store a blob’s Red Stuff metadata at a storage node. |
Result data: |
|
Store a primary or secondary sliver at a storage node. |
Result data: |
|
Fetch a storage node’s signed confirmation for a blob’s slivers. |
Result data: |
|
Ask ONE storage node for its view of a blob’s status. |
Result data: one of the |
|
Fetch a blob’s Red Stuff metadata from a storage node. |
Result data: |
|
Fetch one primary or secondary sliver from a storage node. |
Result data: |
Read Commands
Detail for the read (aggregator) commands.
ReadBlob
Read a blob by its Walrus blob ID.
blob_id: str— Walrus blob identifier.strict_consistency_check: bool = False— force strict integrity verification of the read blob against its metadata.skip_consistency_check: bool = False— skip integrity verification. Only safe when the writer is known and trusted.
Result data: BlobData.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadBlob
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(command=ReadBlob(blob_id="..."))
if result.is_ok():
blob = result.result_data
print(len(blob.content))
asyncio.run(main())
ReadBlobPartial
Read a byte range from a blob.
blob_id: str— Walrus blob identifier.start: int— first byte offset (inclusive).length: int— number of bytes to read.
Result data: BlobSlice.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadBlobPartial
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=ReadBlobPartial(blob_id="...", start=0, length=1024)
)
if result.is_ok():
blob_slice = result.result_data
print(len(blob_slice.content))
asyncio.run(main())
ReadBlobByObjectId
Read a blob by its Sui object ID, rather than its Walrus blob ID.
object_id: str— Sui object ID of the blob.strict_consistency_check: bool = Falseskip_consistency_check: bool = False
Result data: BlobData.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadBlobByObjectId
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=ReadBlobByObjectId(object_id="0x...")
)
if result.is_ok():
blob = result.result_data
print(len(blob.content))
asyncio.run(main())
ReadQuiltPatch
Read a single patch from a quilt by quilt ID and patch key.
quilt_id: str— Walrus quilt identifier.patch_key: str— key identifying the patch within the quilt.
Result data: QuiltPatch.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadQuiltPatch
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=ReadQuiltPatch(quilt_id="...", patch_key="report.pdf")
)
if result.is_ok():
patch = result.result_data
print(len(patch.content))
asyncio.run(main())
ReadQuiltPatchById
Read a single patch from a quilt directly by its QuiltPatchId.
patch_id: str— Walrus QuiltPatchId (URL-safe base64) addressing the patch directly, independent of its containing quilt.
Result data: QuiltPatch.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadQuiltPatchById
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=ReadQuiltPatchById(patch_id="...")
)
if result.is_ok():
patch = result.result_data
print(len(patch.content))
asyncio.run(main())
ReadQuiltPatches
List the patches contained in a quilt.
quilt_id: str— Walrus quilt identifier.
Result data: QuiltPatchListing, a list of
QuiltPatchItem (patch_key, patch_id, tags).
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadQuiltPatches
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=ReadQuiltPatches(quilt_id="...")
)
if result.is_ok():
for patch in result.result_data.patches:
print(patch.patch_key, patch.patch_id)
asyncio.run(main())
ReadConcatBlobs
Read and concatenate multiple blobs into a single response, in the order given.
ids: list[str]— ordered list of Walrus blob IDs to concatenate.strict_consistency_check: bool = Falseskip_consistency_check: bool = False
Result data: BlobData. Uses the Walrus v1alpha API.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, ReadConcatBlobs
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=ReadConcatBlobs(ids=["blob_id_1", "blob_id_2"])
)
if result.is_ok():
blob = result.result_data
print(len(blob.content))
asyncio.run(main())
Write Commands
Detail for the write (publisher) commands.
WriteBlob
Store a blob on Walrus.
data: bytes— raw blob content to store.epochs: int— number of epochs to store the blob for.send_object_to: str— Sui address to receive the created blob object. The publisher creates the blob object under its own wallet unless this is set, so ownership never transfers to the caller otherwise.permanent: bool = False— ifTrue, the blob cannot be deleted before expiry. Blobs are deletable by default (Walrus v1.33+); the publisher’sdeletablequery parameter has been deprecated since v1.35 and has no effect, so this command does not send it —permanentis the only lever that controls persistence.
Result data: BlobReceipt.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, WriteBlob
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=WriteBlob(
data=b"hello walrus",
epochs=5,
send_object_to="0x...",
)
)
if result.is_ok():
receipt = result.result_data
print(receipt.blob_id, receipt.cost)
asyncio.run(main())
WriteQuilt
Store a quilt — a collection of named blobs — on Walrus.
files: dict[str, bytes]— mapping of patch key to raw file content.epochs: int— number of epochs to store the quilt for.send_object_to: str— Sui address to receive the created quilt blob object. The publisher creates the blob object under its own wallet unless this is set, so ownership never transfers to the caller otherwise.permanent: bool = False— ifTrue, the quilt cannot be deleted before expiry. Quilts are deletable by default (Walrus v1.33+), matchingWriteBlob’s persistence semantics.
Result data: QuiltReceipt.
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, WriteQuilt
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=WriteQuilt(
files={"a.txt": b"...", "b.txt": b"..."},
epochs=5,
send_object_to="0x...",
)
)
if result.is_ok():
receipt = result.result_data
print(receipt.quilt_id, receipt.patch_keys)
asyncio.run(main())
WriteRelayBlob
Hand an unencoded blob to an upload relay, which fans slivers out on the
caller’s behalf. Targets a relay endpoint (endpoint_role = "relay"),
not the publisher. The blob must already be registered on chain, and any
tip the relay charges must already be executed and confirmed — posting
early yields HTTP 401, not a retryable error.
blob_id: str— blob ID as URL-safe unpadded base64.data: bytes— the raw UNENCODED blob bytes; the relay performs the encoding. The server caps the body at 1 GiB.register_tip_tx_digest: str | None = None— digest of the transaction that paid the tip. Omit only against ano_tiprelay.nonce: str | None = None— base64url nonce from the authentication package. Omit only against ano_tiprelay.deletable_blob_object: str | None = None— object ID when the blob was registered deletable. Omitted means permanent — the two are distinguished by presence, not by a boolean.encoding_type: int | None = None— omitted means the relay’s default, RS2, the only live variant.
Result data: RelayUploadAck — carries an
RelayUploadOutcome rather than raising on a refused
upload, since this call runs post-spend (tip paid, blob registered).
import asyncio
from pytusk import PytuskConfiguration, WalrusClient, WriteRelayBlob
async def main():
config = PytuskConfiguration(
active_network="testnet",
pysui_group_name="sui_grpc_config",
pysui_profile_name="testnet",
)
async with WalrusClient(pytusk_config=config) as client:
result = await client.execute(
command=WriteRelayBlob(
blob_id="...",
data=b"hello walrus",
register_tip_tx_digest="...",
nonce="...",
)
)
if result.is_ok():
ack = result.result_data
print(ack.outcome, ack.confirmation_certificate)
asyncio.run(main())
Storage Node Commands
Detail for the commands sent directly to an individual storage node. The base URL for these is a specific node’s address resolved from the committee, not an aggregator or publisher daemon.
WriteMetadata
Store a blob’s Red Stuff metadata at a storage node. A node requires this
metadata before it will accept any sliver PUT for the same blob — a node
that has not received it rejects every sliver PUT with HTTP 400
FAILED_PRECONDITION / METADATA_NOT_FOUND.
blob_id: bytes— raw 32-byte blob ID.metadata_bcs: bytes— BCS-encoded WalrusBlobMetadatapayload.
Result data: MetadataAck.
WriteSliver
Store a primary or secondary sliver at a storage node.
blob_id: bytes— raw 32-byte blob ID.sliver_pair_index: int— sliver-pair index to store at.sliver_type: str—"primary"or"secondary", lowercase. These literals are the upstreamAxisserde values; the wire format is case-sensitive and accepts nothing else.data: bytes— raw BCS-encoded sliver bytes.
Result data: SliverAck.
ReadMetadata
Fetch a blob’s Red Stuff metadata from a storage node. The response is the
OUTER BlobMetadataWithId payload as raw BCS bytes with no JSON envelope
— not the inner BlobMetadata that WriteMetadata sends.
The two differ by a leading 32-byte blob ID.
blob_id: bytes— raw 32-byte blob ID.max_bytes: int | None— optional ceiling on the response body. The native read path sets this from the committee’s shard count, so a node cannot answer a metadata request of a few tens of kilobytes with a gigabyte. Callers rarely set it themselves.
Result data: MetadataData.
ReadSliver
Fetch one primary or secondary sliver from a storage node.
blob_id: bytes— raw 32-byte blob ID.sliver_pair_index: int— the SLIVER-PAIR index to fetch. This is not a shard index: the two differ by a per-blob rotation, and passing a shard index fetches the wrong sliver with no error to say so.sliver_type: str—"primary"or"secondary", lowercase.max_bytes: int | None— optional ceiling on the response body, set by the native read path from the blob’s own verified metadata.
Result data: SliverData.
ReadStorageConfirmation
Fetch a storage node’s signed confirmation that it holds a blob’s slivers. The permanent and deletable forms are two separate upstream routes — the object ID travels in the URL path, not as a query parameter.
blob_id: bytes— raw 32-byte blob ID.object_id: str | None = None— Sui object ID of the deletableBlobobject.Noneselects the permanent-blob route.wait_for_registration: bool = True— when True, the node long-polls until it observes the on-chain registration event before responding. This is server-side long-polling that replaces client-side backoff for the registration-propagation race.wait_millis: int | None = None— upper bound in milliseconds for the node’s long-poll wait. Omitted from the request whenNone.
The result’s serialized_message is passed VERBATIM into
certify_blob; the client never reconstructs it.
Result data: SignedConfirmation.
ReadBlobStatus
Ask one storage node what it knows about a blob:
GET /v1/blobs/{blob_id}/status.
This is a per-node OPINION, never a verdict. A single node can be stale,
byzantine, or simply unaware of a recent registration. Establishing a
verdict means fanning this command across the committee and applying a
shard-weight threshold — which is what
fetch_blob_status() does. Reach for that unless you
specifically want one node’s answer.
blob_id: bytes— raw 32-byte blob ID.
Parse failures come back as a failed SuiRpcResult rather than raising,
matching ReadStorageConfirmation: one bad node response must never
abort a committee-wide fan-out.
Result data: one of the BlobStatus variants.
Composing a Native Read
read_blob_native() is the orchestration layer above
ReadMetadata and ReadSliver: it
resolves the committee, fetches and verifies metadata, fans out for slivers
until enough are held to decode, and reconstructs the blob locally. No
aggregator takes part.
Reach for it when you would rather not trust an aggregator to have served the
right bytes, or when none is reachable. It issues many more requests than
ReadBlob and does considerably more work on the client.
import asyncio
from pytusk import (
PytuskConfiguration,
WalrusClient,
blob_id_from_url_base64,
read_blob_native,
)
async def main():
cfg = PytuskConfiguration(
active_network="testnet", pysui_profile_name="testnet"
)
async with WalrusClient(pytusk_config=cfg) as client:
result = await read_blob_native(
client=client,
blob_id=blob_id_from_url_base64(
blob_id="OgrPHsCfZIQm_m3U3fzddQff5ubQOFuSE0RHkMY7sa8"
),
)
print(len(result.content), result.epoch, result.axis)
asyncio.run(main())
NativeReadResult carries the reconstructed content
alongside the epoch its committee came from, the axis it decoded, and
slivers_used — the count of DISTINCT slivers the successful decode was
given, not the number of node responses received.
reconstruct_blob() is the stage beneath it, for a caller that
has already resolved a committee and verified the metadata and wants only the
fetch-and-decode step. read_blob_native() is the entry point
for everything else.
What Verification Buys You
By default the decoder re-derives the blob ID from the reconstructed content
and rejects a mismatch, which is the only thing that detects a forged or
corrupted sliver. Passing verify=False skips it, and that loses ALL
content authentication of the slivers: verifying the metadata proves the
metadata is authentic for the blob you asked for, and says nothing whatever
about the sliver bytes the content was rebuilt from. Reserve it for slivers
whose provenance is already established.
Tolerating a Bad Node
The decoder is all-or-nothing over a batch of slivers, so one unparseable sliver would otherwise discard every good sliver fetched alongside it — and a plain retry fails identically, because the node that served it answers just as promptly the second time. One faulty node out of a thousand could deny a read outright, against a design meant to tolerate roughly a third of them.
A native read handles this with no action from the caller. When a decode fails, each sliver is verified individually, whoever served an unusable one is barred, and the fan-out runs once more without them. This costs nothing on the ordinary path: the per-sliver check runs only after a decode has already failed.
BlobDecodeError therefore means the read failed even after
that recovery round, or that no individual sliver could be blamed for the
failure. SliverFetchError means too few nodes answered to
reach the decoding threshold at all.
Verifying a Single Sliver
verify_sliver() checks one sliver against verified metadata,
raising SliverVerificationError if it does not hold up. It
is the expensive way to establish authenticity — each call re-encodes the
sliver and rebuilds a Merkle tree over it, where one verified decode proves
the same property for an entire blob — so reach for it to identify WHICH node
served a bad sliver, not as a routine precaution.
Note carefully what it does and does not establish. A sliver is bound to the index it declares for ITSELF, never to whichever index some node was asked for. A node answering with a genuine sliver for a different index passes this check, and that is correct: the decoder reads each sliver’s own index and deduplicates on it.