Learn how to use Polkadot's Statement Store to submit and receive signed off-chain messages, manage topics, channels, and expiry, and connect through RPC or a light client.
Excerpt: Learn how to use Polkadot's Statement Store to submit and receive signed off-chain messages, manage topics, channels, and expiry, and connect through RPC or a light client.
The first article described what the Statement Store is and why a developer might need it. This article explains how to use the Statement Store in practice: how to submit a statement, how it propagates between nodes, how applications receive statements, and how to connect through RPC or a light client.
A node with the Statement Store enabled exposes a set of RPC methods. To submit a statement, we construct it, SCALE-encode it, and send it to the node with statement_submit.
To construct a statement, we combine the fields from the first article: a payload of around 512 bytes (the recommended size), up to four topics, an expiry, and an optional channel. Finally, we sign the statement and include the cryptographic signature as proof.
Expiry holds the Unix timestamp in the high 32 bits, and the priority in the low 32 bits. For a statement that should live for a day, it is (now + 86_400) << 32. If the developer wants to override the statement later, they should set a channel on the current statement. Then the new statement should include the same channel and a new expiry with the same timestamp but a higher priority: old_expiry | priority.
When the statement arrives, the node answers two questions before storing it: is it well-formed (valid), and can this node accept it (accepted)?.
Validation is the same on every node. The statement must carry a cryptographic proof: if it has none, or the proof does not match the author, the statement is invalid. It is also dropped if it is too large to distribute to other nodes, currently more than 1 MiB. If it has already expired, there is also no point in holding it.
Acceptance depends on the current node. The first limit is the node's own storage: when it is full, the node accepts no further statements. Then the node checks the user's allowance. If the account has no allowance, the node rejects the statement. If the account is at its limit, the outcome depends on expiry. A new statement with a higher expiry evicts the lowest one.
But if the new statement has an expiry lower than everything the account already holds, it is rejected. If the new statement has a channel, the same rule applies within the channel: it rejects a lower expiry. Both cases are rare in practice and usually mean a mistake in how the expiry was computed.
The node answers right away with one of four results. New: the statement is stored. Known: the node already had it. Invalid: the statement failed validation. Rejected: the statement was valid but failed acceptance on that node. Once stored, the statement should be propagated further.
Once a statement is submitted, it is immediately available on that node. The distribution phase lets it propagate beyond the submitting node so subscribers elsewhere in the network can receive it. The node propagates accepted statements to relevant peers through the Statement Store's peer-to-peer gossip protocol.
When another node receives our statement, it does not save it blindly. The statement goes through the same checks it passed on submit, so nothing enters a store without examination. As before, every node returns the same validation answer. Rejection is up to the node: it may be a node with no capacity left, or a node behind on chain sync that does not know the user's latest allowance.
At the time of writing, testing and tuning have focused on parachain-sized networks, with a target propagation latency of no more than ten seconds. The real number depends on network size and load; take it as a rough guide, not a guarantee.
Once the statement has arrived at another node, a user there can open a subscription with statement_subscribeStatement. To answer the request, the node first sends the matching statements it already holds, then every new one as it arrives.
In the subscription, the user defines a topic filter to narrow down the set of statements. Any delivers every statement the node has and will have. It is expensive for both sides and suits debugging tools, not user-facing apps. MatchAny takes up to 128 topics and delivers statements that carry at least one. MatchAll takes up to four topics and delivers only statements that carry all of them.
By design, nodes can hold different views at the same moment. Two users on two nodes with the same subscription may receive different statements. The network is weakly coherent, and there is no guarantee about when a statement will become visible across all the nodes.
Using a single RPC node creates a single point of failure and trust. A pool reduces that dependency, but the application still relies on RPC infrastructure: individual nodes can disappear, come under attack, or fall behind the chain. That is why we suggest RPC only for development or as a backup plan.
A light node reduces that reliance on RPC infrastructure. A Statement Store light node runs part of the statement protocol and is designed for constrained environments such as browsers and mobile apps. It receives statements from full nodes and submits user statements to the network, but does not route or store statements on behalf of others. It connects over P2P to multiple full nodes, reducing the application's dependence on a single RPC operator.
The Statement Store light-client implementation builds on smoldot, the existing Polkadot light client, rather than introducing a separate binary. We get an existing networking stack, and applications get statement distribution without extra dependencies. The light node speaks the same wire protocol as polkadot-sdk full nodes, and its JSON-RPC methods aim to be a drop-in replacement for the full node's. It passes statements through to the application with per-subscription deduplication. Nothing is stored inside the node.
That last point is also the main caveat: a light node can return a result before a full node has accepted a statement. For example, it has no store and does not check allowance, so it may return New even if the statement is later rejected or dropped elsewhere in the network. These gaps remain in the current implementation; until then, an app on a light node should treat New as "sent", not "accepted".
We've covered the basics: the statement structure, the distribution model, and how to talk to the store. That should be enough to understand the mechanism and to debug it when something goes wrong. If the store were meant to be used on its own, we could end here with a link to the documentation and wish the reader luck choosing the right structure and connection method. But there is one more thing.
The Statement Store can also be explored alongside the other parts of Trinity, the common-interface concept introduced in the first article. One prototype exploring how those capabilities can be exposed through a common interface is TrUAPI.
TrUAPI is the interface that a host, such as a desktop browser or a mobile app, exposes to the applications running inside it. The host mediates access to signing, allowances, and transport, whether through an RPC connection or an embedded light client.
The application can call higher-level methods without handling those underlying protocols directly. TrUAPI remains a prototype, showing one approach to exposing these capabilities through a common host interface.