DA Oracle — Post a Blob, then Verify It On-Chain
Post a blob to the data-availability layer, obtain its NMT commitment, and verify inclusion on-chain from a contract.
5 min read 1,115 words
View docs/tutorials/DA_ORACLE.md on GitHub
Source: docs/tutorials/DA_ORACLE.md — this page mirrors the repository documentation.
Naming note. Older repository documents call the signature scheme “Dilithium3”; that is the lineage name for ML-DSA-65 (FIPS 204, scheme id
0x1003), the only signature scheme mainnet accepts for new transactions. Where SPHINCS+ is mentioned as a backup scheme, note that on mainnet it is legacy and consensus-stranded: it cannot sign transactions.
This tutorial shows a practical pattern to:
- Post a blob to the Data Availability (DA) layer and obtain its commitment (NMT root).
- Verify the same content on-chain by recomputing the commitment inside a contract using the deterministic
blob_pin(ns, data)syscall, then storing and checking it later.
The DA layer handles chunking, erasure coding, NMT commitments, and availability proofs. On-chain we store only the commitment (not the blob), and we verify content by recomputing the commitment from bytes. Availability proofs (DAS) are meant for light/off-chain verification.
0) Prereqs
- Devnet node + DA retrieval service running (see
da/retrieval/and Dev Quickstart). - Python SDK installed (
sdk/python), plus VM CLI (vm_py/cli). - A sample file to post (e.g.
da/fixtures/blob_small.bin).
1) Post a Blob to DA (off-chain)
You can use the Python SDK DA client or the DA CLI. Here we’ll show the SDK way.
# post_blob.py — post bytes to the DA layer; print commitment (NMT root)
from omni_sdk.config import Config
from omni_sdk.da.client import DAClient
RPC_URL = "http://127.0.0.1:8545" # or DA service URL if separate
CHAIN_ID = 1
cfg = Config(rpc_url=RPC_URL, chain_id=CHAIN_ID)
da = DAClient(cfg)
# Choose a namespace (uint32/uint64 domain per your network policy)
NAMESPACE = 24
data = open("da/fixtures/blob_small.bin", "rb").read()
# Post returns a commitment (NMT root) and a receipt
resp = da.post_blob(namespace=NAMESPACE, data=data)
print("namespace:", NAMESPACE)
print("commitment:", resp["commitment"]) # 0x… NMT root
print("size_bytes:", resp["size"])
print("receipt_id:", resp["receipt"]["id"])
Alternative (CLI):
python -m da.cli.put_blob --ns 24 da/fixtures/blob_small.bin
# prints commitment & receipt JSON
Keep the commitment handy; the on-chain contract will recompute the same root from the exact same bytes to verify authenticity.
2) Contract: da_oracle.py
The contract exposes:
- register(ns: u64, data: bytes) -> bytes Computes the DA commitment deterministically via syscalls.blob_pin(ns, data), stores it, and emits an event.
- verify(ns: u64, data: bytes) -> bool Recomputes and checks equality against the stored commitment.
- get_commitment() -> bytes Returns the last stored commitment.
# da_oracle.py — minimal on-chain DA oracle using deterministic blob commitment
# Public API:
# - register(ns: u64, data: bytes) -> bytes
# - verify(ns: u64, data: bytes) -> bool
# - get_commitment() -> bytes
from stdlib import storage, events, abi, syscalls
def _k_commit() -> bytes: return b"da:commit"
def _k_ns() -> bytes: return b"da:ns"
def _k_size() -> bytes: return b"da:size"
def register(ns: int, data: bytes) -> bytes:
"""
Deterministically compute the blob commitment (NMT root) from (ns, data)
using the host-provided syscall. Store it for future verification.
"""
abi.require(ns >= 0 and ns <= (1<<32)-1, b"ns out of range")
abi.require(len(data) > 0 and len(data) <= 2_000_000, b"data size out of bounds")
# blob_pin returns a deterministic commitment for (ns, data).
# The host may enqueue/persist off-chain work; the return value is consensus-safe.
commit = syscalls.blob_pin(ns, data) # bytes (NMT root)
abi.require(len(commit) > 0, b"invalid commitment")
storage.set(_k_commit(), commit)
storage.set(_k_ns(), ns.to_bytes(8, "big"))
storage.set(_k_size(), len(data).to_bytes(8, "big"))
events.emit(b"DARegistered", {b"ns": ns, b"size": len(data), b"commit": commit})
return commit
def verify(ns: int, data: bytes) -> bool:
"""
Recompute the commitment from (ns, data) and compare to the stored one.
"""
stored = storage.get(_k_commit()) or b""
if len(stored) == 0:
return False
current = syscalls.blob_pin(ns, data)
return current == stored
def get_commitment() -> bytes:
return storage.get(_k_commit()) or b""
Why blob_pin?
- It is a deterministic syscall that returns the canonical NMT root
for (namespace, data) under chain rules.
- It avoids re-implementing NMT/erasure logic in the VM and prevents gas abuse.
- The host can bridge to da/ to persist the blob if policy permits (devnet),
while the return value remains deterministic for consensus.
3) Compile & Deploy
python -m vm_py.cli.compile da_oracle.py --out /tmp/da_oracle.ir
python -m vm_py.cli.inspect_ir /tmp/da_oracle.ir
# deploy_and_register.py — deploy contract, then register & verify
from omni_sdk.config import Config
from omni_sdk.rpc.http import HttpRpc
from omni_sdk.wallet.mnemonic import new_mnemonic
from omni_sdk.wallet.signer import Dilithium3Signer
from omni_sdk.address import address_from_pubkey
from omni_sdk.tx.build import build_deploy_tx, build_call_tx
from omni_sdk.tx.send import send_and_await_receipt
from omni_sdk.contracts.client import ContractClient
RPC_URL = "http://127.0.0.1:8545"
CHAIN_ID = 1
cfg = Config(rpc_url=RPC_URL, chain_id=CHAIN_ID)
rpc = HttpRpc(cfg)
mn = new_mnemonic()
signer = Dilithium3Signer.from_mnemonic(mn)
addr = address_from_pubkey("dilithium3", signer.public_key())
ir = open("/tmp/da_oracle.ir","rb").read()
abi = {
"functions":[
{"name":"register","inputs":[{"name":"ns","type":"u64"},{"name":"data","type":"bytes"}],"returns":"bytes","mutates":True},
{"name":"verify","inputs":[{"name":"ns","type":"u64"},{"name":"data","type":"bytes"}],"returns":"bool"},
{"name":"get_commitment","inputs":[],"returns":"bytes"}
],
"events":[{"name":"DARegistered","inputs":[
{"name":"ns","type":"u64"},{"name":"size","type":"u64"},{"name":"commit","type":"bytes"}
]}]
}
# 1) Deploy
tx0 = build_deploy_tx(from_address=addr, manifest={"abi": abi}, code=ir,
gas_price=1, gas_limit=900_000, nonce=0)
rcpt0 = send_and_await_receipt(rpc, signer.sign_tx(tx0, CHAIN_ID), 30)
assert rcpt0["status"] == "SUCCESS", rcpt0
contract = rcpt0["contractAddress"]
print("DA Oracle deployed at:", contract)
client = ContractClient(rpc=rpc, address=contract, abi=abi)
# 2) Use the same bytes you posted to DA in step (1)
NAMESPACE = 24
data = open("da/fixtures/blob_small.bin","rb").read()
# Register (compute & store commitment on-chain)
tx1 = build_call_tx(from_address=addr, to_address=contract, abi=abi,
function="register", args=[NAMESPACE, data],
gas_price=1, gas_limit=500_000, nonce=1)
rcpt1 = send_and_await_receipt(rpc, signer.sign_tx(tx1, CHAIN_ID), 30)
print("register status:", rcpt1["status"])
# 3) Read commitment + verify equality by recomputing
commit = client.call("get_commitment", [])
print("stored commitment:", commit.hex())
ok = client.call("verify", [NAMESPACE, data])
print("verify:", ok)
Expected:
- register succeeds and emits DARegistered(ns, size, commit)
- get_commitment() returns the same commitment that DA printed in step (1)
- verify(ns, data) returns true
4) Best Practices
- Store only commitments on-chain. Put blob metadata (MIME, filename) and retrieval hints off-chain (e.g. studio-services/artifacts or your app DB).
- Namespace hygiene. Define per-app namespaces to avoid collisions and to enable policy routing/quotas on DA.
- Large blobs. Respect size caps from network policy; the blob_pin syscall will enforce limits and charge gas for commitment work.
- Prove availability off-chain. Use the DA light client verifier to check sampling proofs (da/sampling/light_client.py). On-chain full DAS is intentionally not supported (cost prohibitive).
5) Troubleshooting
- Mismatched commitment: Ensure the exact same data bytes are used both off-chain (DA post) and on-chain (register/verify). Any difference (line endings, encoding) changes the root.
- Syscall not enabled: If blob_pin is feature-gated on your network, the contract call will revert. Enable capabilities per capabilities/config.py.
- Blob too large: The syscall rejects over-limit inputs; chunk and pin multiple blobs, then store their merkle of commitments on-chain.
6) What about “availability proofs” on-chain?
The chain header already binds the DA root for the block. Availability sampling and full proofs are designed for light clients and off-chain agents, not for contracts. Contracts should trust the header binding but can require submitters to present the exact bytes and recompute the commitment as shown here.
See also:
- docs/da/OVERVIEW.md
- docs/da/RETRIEVAL.md
- da/sampling/light_client.py
- capabilities/host/blob.py & capabilities/adapters/da.py
This page mirrors a file in the animicaorg/all repository. If the repository and this page ever disagree, the repository is authoritative. For long-form explainers written for newcomers, see Learn.