Guides
Timelock
Seal data anyone can open at a known future time — nobody, including you, can open it early.
Timelock encryption binds data to a future round of the drand quicknet randomness beacon. Once that round's signature is published — every 3 seconds, worldwide, no keys or accounts involved — anyone can decrypt. Before then, nobody can, including the author. This is the same mechanism the chain uses for commit-reveal weights, exposed directly so you can timelock anything.
The reveal moment is wall-clock time, not chain blocks: a round happens every
3 seconds regardless of block speed, so reveal_in="1h" means one hour on any
network, including fast-blocks local chains. Encryption is pure local
cryptography — no network, no keys, nothing to keep secret afterwards except
the plaintext. Only decryption fetches the round signature from the drand
HTTP API.
Python
from bittensor import timelock
sealed = timelock.encrypt("the answer is 42", reveal_in="1h")
sealed.reveal_at # datetime of the reveal, UTC
sealed.remaining # timedelta until then; zero once passed
sealed.revealed # False until the round is published
sealed.hex() # portable ciphertext — publish it anywhereThe hex is all anyone needs — the reveal round is embedded in the ciphertext,
so there is no key or metadata to carry alongside it. Decrypt from the hex, a
Timelocked, or raw bytes:
timelock.decrypt(sealed_hex) # raises TimelockNotReady if early
timelock.decrypt(sealed_hex, wait=True) # sleeps until the reveal, then opens
sealed.decrypt(wait=True) # same, as a methodTimelockNotReady carries .reveal_round, .reveal_at, and .remaining,
so callers can schedule a retry instead of polling. A timeout= caps the
total wait in seconds, and timelock.decrypt_async does the same on the
event loop.
Give the reveal moment exactly one way:
timelock.encrypt(data, reveal_in="15m") # duration: "30s", "1h30m", "2d", seconds, timedelta
timelock.encrypt(data, reveal_at="2026-07-08T12:00Z") # absolute: datetime or ISO-8601, no offset = UTC
timelock.encrypt(data, reveal_round=30212846) # explicit drand roundRound arithmetic is exact local math from the quicknet genesis (3 seconds per round), available directly:
timelock.current_round() # latest published round, computed locally
timelock.round_at("2026-07-08T12:00Z") # first round at or after a moment
timelock.reveal_time(30212846) # UTC datetime a round is publishedCLI
btcli timelock encrypt "the answer is 42" --in 1h # prints ciphertext hex
btcli timelock encrypt --file plan.pdf --in 2d --out plan.sealed
btcli timelock encrypt "..." --at 2026-07-08T12:00Z # or --round 30212846Hex goes to stdout (pipeable); the reveal note goes to stderr. --out writes
raw ciphertext bytes to a file instead of printing hex.
btcli timelock decrypt <hex> # fails with the reveal time if early
btcli timelock decrypt plan.sealed --wait --timeout 600
btcli timelock show <hex> # when it unlocks, without decryptingdecrypt and show take the hex or a file path as the argument (or
--file); files may hold raw bytes or hex text. decrypt --out writes the
plaintext bytes to a file — useful for binary payloads.
Publish a timelocked commitment on chain
The Commitments pallet accepts the inner TLE ciphertext and reveal round as a
TimelockEncrypted field. Sign this call with the registered hotkey:
import bittensor as bt
sealed = bt.timelock.encrypt("embargoed result", reveal_in="1h")
call = bt.calls.Commitments.set_commitment(
netuid=42,
info={
"fields": [[{
"TimelockEncrypted": {
"encrypted": sealed.encrypted,
"reveal_round": sealed.reveal_round,
}
}]]
},
)
result = await client.submit_call(call, wallet, signer="hotkey")sealed.encrypted is the pallet-native inner ciphertext. The portable
bytes(sealed) / sealed.hex() envelope includes the reveal-round trailer;
v446 accepts either representation on chain.
Once the pulse exists, the chain attempts the reveal automatically. A
successful plaintext is written to RevealedCommitments and emits
CommitmentRevealed. A commitment that is still waiting for a current or
future pulse remains TimelockEncrypted and stays pending.
V446 makes terminal reveal failures explicit. Corrupt ciphertext, a round
mismatch, an invalid quicknet point, or a pulse that has already expired is
rewritten in place as TimelockRevealFailed and emits
CommitmentRevealFailed once. The failed field remains in CommitmentOf for
audit, but is removed from the reveal index and is not retried. Users cannot
submit TimelockRevealFailed directly; it is a runtime-only state.
Inspect all states with:
btcli query commitments --netuid 42 --jsonUse cases
- Sealed challenge answers — a subnet mechanism publishes the answer timelocked, so miners provably cannot have seen it early.
- Embargoed announcements — publish now, readable at the embargo time, with no trusted party holding the key.
- Sealed-bid auctions — all bids open simultaneously at the deadline.
- Delayed configuration — ship a config that activates itself at a known moment.
Relationship to weight commit-reveal
Validator weights use this machinery automatically: the chain timelocks
committed weights and reveals them on schedule, with no action needed from
you — see commit-weights and the
validating guide. This page is for timelocking
your own data.
Mechanics
- The ciphertext carries its reveal round in a trailer, so no separate bookkeeping is needed — parsing the hex recovers everything.
- Round arithmetic never touches the network: rounds map to timestamps by exact local math from the quicknet genesis.
- After the reveal time passes, the beacon or HTTP relay may lag by a few seconds; decryption retries within a short grace window before giving up.