Skip to content

The Polkadot Bulletin Chain is for storing raw data and files on-chain — images, HTML, JSON, documents, and more. You get back a CID (Content Identifier) and the data is directly retrievable. Think of it as decentralized file hosting built on a Polkadot SDK-based blockchain with IPFS-compatible content addressing. The data is only kept for a specific retention period and needs to be renewed to be kept for longer. There are no ownership semantics — it's just storage.

The Bulletin Chain is designed as a utility chain — it has no native token balances. Instead, access is managed through an authorization system that grants accounts or preimages the ability to store data. For more details, see the Polkadot Bulletin Chain repository (will be moving to the fellowship runtimes repository in the future).

The Bulletin Chain is suited for any scenario where you need decentralized, content-addressable storage on Polkadot. Common use cases include:

  • Static sites: Host a static website entirely on-chain, served via IPFS-compatible gateways.
  • Images and media assets: Store images, icons, or other media files with permanent, verifiable CIDs.
  • Application data: Persist configuration files, metadata, or other structured data that needs to be publicly accessible and tamper-proof.
  • Document storage: Store documents, certificates, or records that benefit from on-chain verifiability.
  • No balances: The chain does not use token balances for transaction fees. Authorization is required to store data.
  • Authorization-based access: Accounts must be authorized before they can submit storage transactions.
  • Content Identifiers (CIDs): Stored data is addressable via IPFS-compatible CIDs, enabling interoperability with IPFS tooling.
  • Retention periods: Stored data is retained for a limited period (~2 weeks on Polkadot TestNet) and must be renewed to persist beyond that.
  • Chunked uploads: Files larger than 2 MiB must be split into chunks, each stored as a separate transaction (max 2 MiB per transaction).

The Bulletin Chain has no token balances — there are no fees to pay. Instead, storage access is controlled through an authorization system that prevents spam and ensures fair usage. Only privileged origins can grant authorizations, as described in Who Grants Authorization.

Authorization is not a token transfer — it's a permission entry recorded in the chain's storage that specifies:

  • The account allowed to store data
  • The number of transactions and bytes the account can use
  • An expiration block after which the authorization becomes invalid, set a fixed number of blocks after the authorization is granted (14 days on Polkadot TestNet)

Authorization is consumed as you store data — each storage transaction decrements your remaining transaction count and byte allowance. Once your allocation is used up or the expiration block is reached, you need a new authorization.

The authorize_account and authorize_preimage extrinsics require the authorizer origin. On Polkadot TestNet, the authorizer origin accepts Root, sibling parachains that are allowed to manage authorizations, and accounts that Root registers with add_authorizer. Each registered account has a budget that limits how much authorization it can grant.

How you get authorization depends on the network:

NetworkAuthorization Source
Polkadot TestNetThe Console UI Faucet grants authorization for testing
Local DevelopmentThe //Alice account has sudo access for local testing
Polkadot MainnetThe authorization model is being finalized. Check the Bulletin Chain repository for the latest updates

Account authorization grants a specific account the ability to store any data on-chain, up to the allocated transaction and byte limits. This is the most common authorization type for active users.

  • Granted via the authorize_account extrinsic (authorizer origin)
  • Can be refreshed with refresh_account_authorization
  • Expired authorizations can be cleaned up with remove_expired_account_authorization

Preimage authorization grants storage access to anyone who can provide the preimage of a specific hash. This is useful for sponsored uploads where a third party authorizes the storage of specific, known content without needing to know which account will submit it.

  • Granted via the authorize_preimage extrinsic (authorizer origin)
  • Can be refreshed with refresh_preimage_authorization
  • Expired authorizations can be cleaned up with remove_expired_preimage_authorization

When requesting authorization, estimate how many transactions and bytes you need. For simple uploads, a single file of up to 2 MiB requires one transaction. For chunked uploads (files over 2 MiB), each chunk requires a separate transaction, plus one additional transaction for the manifest. With the Bulletin SDKs' default chunk size of 1 MiB, a 10 MiB file requires 11 transactions.

The TransactionStorage pallet is the core module for interacting with the Bulletin Chain. It provides the following extrinsics:

ExtrinsicDescriptionOrigin
store(data)Store data on-chain. Emits Stored with the transaction index, content hash, and CIDAuthorized account, Root, or unsigned with a matching preimage authorization
store_with_cid_config(cid, data)Store data with an explicit CID configuration (codec and hashing algorithm)Authorized account, Root, or unsigned with a matching preimage authorization
authorize_account(who, transactions, bytes)Authorize an account to store up to bytes bytes in transactions transactions. Calling it again before the authorization expires adds to the existing allowanceAuthorizer
authorize_preimage(content_hash, max_size)Authorize anyone to store data with the given content hash, up to max_size bytesAuthorizer
refresh_account_authorization(who)Extend the expiration of an existing account authorization. The allowance is unchangedAuthorizer
refresh_preimage_authorization(content_hash)Extend the expiration of an existing preimage authorization. The allowance is unchangedAuthorizer
remove_expired_account_authorization(who)Remove an expired account authorization from storageAny account
remove_expired_preimage_authorization(content_hash)Remove an expired preimage authorization from storageAny account
add_authorizer(who, budget)Allow an account to grant authorizations, within the given budgetRoot
remove_authorizer(who)Remove an account from the allowed authorizersRoot
remove_exhausted_authorizer(who)Remove an authorizer whose budget is used up or whose validity period has endedAny account
apply_block_inherents(proof)Submit the block's storage proofNone (inherent)

Authorizations expire after a fixed number of blocks (14 days on Polkadot TestNet).

The pallet also exposes the following storage items:

Storage ItemTypeDescription
AuthorizationsMap<AuthorizationScope, Authorization>Active authorizations, keyed by account (Account) or content hash (Preimage)
AllowedAuthorizersMap<AccountId, AuthorizerBudget>Accounts allowed to grant authorizations, with their remaining budget
TransactionsMap<BlockNumber, Vec<TransactionInfo>>Metadata of the transactions stored in each block
TransactionByContentHashMap<Hash, (BlockNumber, u32)>Most recent block number and transaction index for each content hash. Updated on every renewal
BlockTransactionsVec<TransactionInfo>Transactions stored in the current block
ProofCheckedboolWhether the storage proof was checked in the current block
ByteFeeBalanceThe fee charged per byte of stored data
EntryFeeBalanceThe base fee charged per storage transaction
RetentionPeriodBlockNumberThe number of blocks for which stored data is retained

The pallet emits the following events:

EventDescription
StoredEmitted when data is stored. Contains the transaction index, content hash, and CID
ProofCheckedEmitted when a storage proof is successfully verified
AccountAuthorizedEmitted when an account is granted storage authorization
AccountAuthorizationRefreshedEmitted when an account authorization is refreshed
PreimageAuthorizedEmitted when a content hash is granted storage authorization
PreimageAuthorizationRefreshedEmitted when a preimage authorization is refreshed
ExpiredAccountAuthorizationRemovedEmitted when an expired account authorization is removed
ExpiredPreimageAuthorizationRemovedEmitted when an expired preimage authorization is removed
AuthorizerAddedEmitted when an account is added to the allowed authorizers
AuthorizerRemovedEmitted when Root removes an account from the allowed authorizers
ExhaustedAuthorizerRemovedEmitted when an authorizer is removed because its budget is used up or its validity period has ended

Stored data is removed when its retention period ends. The DataRenewal pallet extends the retention of data stored through the TransactionStorage pallet. Renewal calls identify the data in one of two ways:

  • Position { block, index }: The block number and transaction index of the most recent store or renewal of the data.
  • ContentHash(hash): The content hash of the data. The chain maps each content hash to its most recent position, so this form keeps working after the data is renewed.

The pallet provides the following extrinsics:

ExtrinsicDescriptionOrigin
renew(entry)Schedule a one-time renewal. It takes effect when the retention period ends, and the registration is then removed. The renewal is paid from your authorization when you register itAuthorized account
force_renew(entry)Renew the data immediatelyAuthorized account, Root, or unsigned with a matching preimage authorization
enable_auto_renew(content_hash)Renew the data at the end of every retention period. The first renewal is paid when you register; later renewals are charged to your authorization, and the registration is dropped if your authorization cannot cover oneAuthorized account
disable_auto_renew(content_hash)Cancel recurring renewal. The owner can disable it only after the prepaid first renewal has taken effectAuthorized account that owns the registration, or Root
process_pending_renewals()Process the renewals due in the current blockNone (inherent)

Each content hash can have only one registered renewal at a time, across all accounts: renew and enable_auto_renew fail with RenewalAlreadyEnabled while a registration exists.

Renewed bytes are tracked separately from stored bytes. A renewal is rejected when the bytes already renewed under the authorization, plus the size of the data, would exceed the authorization's byte allowance (InvalidTransaction::Custom(5)), or when it would exceed the chain-wide cap on renewed storage, MaxPermanentStorageSize (InvalidTransaction::Custom(6)). To check whether a renewal would be accepted, call the BulletinTransactionStorageApi.can_renew(account, entry) runtime API.

The pallet exposes the following storage items:

Storage ItemTypeDescription
RenewalsMap<Hash, RenewalData>Renewal registrations, keyed by content hash
PendingRenewalsVec<(Hash, TransactionInfo, RenewalData)>Renewals to process in the current block
PermanentStorageUsedu64Total bytes of renewed data on chain, across all accounts

The pallet emits the following events:

EventDescription
RenewedEmitted when data is renewed immediately with force_renew. Contains the new transaction index and the content hash
RenewalEnabledEmitted when a renewal is registered. recurring is true for enable_auto_renew
AutoRenewalDisabledEmitted when recurring renewal is disabled
DataRenewedEmitted when a registered renewal takes effect. Contains the new transaction index, the content hash, and the account
RenewalFailedEmitted when a registered renewal cannot be paid from the account's authorization. The registration is dropped and the data expires
PermanentStorageUsedUpdatedEmitted when the total of renewed storage changes
PermanentStorageNearCapEmitted when the total of renewed storage crosses 80% of MaxPermanentStorageSize

The Bulletin Chain follows a "write-to-chain, read-from-network" architecture. Data is stored via on-chain transactions and retrieved directly from collator nodes using the CID. The chain is designed to work seamlessly with the InterPlanetary File System (IPFS) — Bulletin Chain nodes are themselves IPFS participants.

When you store data on the Bulletin Chain, the following happens:

  1. CID generation: The chain computes an IPFS-compatible Content Identifier (CID) from your data using a cryptographic hash. This CID uniquely identifies the content regardless of where it is stored.
  2. Bitswap serving: Bulletin Chain collator nodes implement the IPFS Bitswap wire protocol, which is the standard way IPFS nodes exchange data blocks. IPFS clients can request data directly from collator nodes using the CID.
MethodStatusDescription
Console UIAvailableDownload data via the Bulletin Chain Console UI using P2P or the IPFS gateway. The simplest option — no code required
Direct P2P (Helia)AvailableConnect to collator nodes via libp2p and fetch data using the CID. The recommended decentralized approach for production applications
Bulletin Chain IPFS GatewayAvailableAccess data via https://paseo-bulletin-next-ipfs.polkadot.io/ipfs/<CID> for Polkadot TestNet. Can also be used programmatically via fetch()
Smoldot Light ClientComing SoonFully decentralized retrieval via the bitswap_block RPC, enabling trustless data access without connecting to a full node

The typical lifecycle of data on the Bulletin Chain follows these steps:

  1. Authorization: Obtain authorization to store data through the faucet (Polkadot TestNet) or from a privileged account (MainNet).
  2. Storage: Submit a store or store_with_cid_config transaction with your data payload.
  3. Retrieval: Fetch data from collator nodes using the CID via the Console UI, Direct P2P (Helia), the Bulletin Chain IPFS gateway, or (coming soon) the Smoldot light client.
  4. Renewal: Before the retention period ends, register a renewal with the DataRenewal pallet: renew for a one-time renewal or enable_auto_renew for recurring renewal. Use force_renew to renew immediately.
ParameterValue
Max transaction size2 MiB
Default chunk size (Bulletin SDKs)1 MiB
Max file size per chunking operation (Bulletin SDKs)64 MiB
Retention period (Polkadot TestNet)~2 weeks

For files larger than the max transaction size, data must be split into chunks using the DAG-PB (UnixFS) standard. Each chunk is stored as a separate transaction. A final manifest transaction is uploaded last, containing the CIDs of all chunks. The manifest's CID becomes the root CID for the complete file. IPFS clients can then retrieve and reassemble the file automatically using the manifest.

The Bulletin SDKs chunk at most 64 MiB in a single operation. To store a larger file, split it into segments of at most 64 MiB and chunk each segment separately.

NetworkEndpoint
Bulletin Chain RPC (Polkadot TestNet)wss://paseo-bulletin-next-rpc.polkadot.io
IPFS Gateway (Polkadot TestNet)https://paseo-bulletin-next-ipfs.polkadot.io
  • Store Data on the Bulletin Chain


    Learn how to authorize your account, store an image, retrieve it, and manage renewals using the Console UI or PAPI.

    → Get Started

Last updated: