Logs work exactly as on Ethereum. The thing to know is not about logs, it is about the state window behind the endpoint you are reading them from.
Reading logs
const logs = await provider.getLogs({
address: '0x…',
topics: [id('Bumped(address,uint256)')],
fromBlock: head - 5000,
toBlock: head,
});
eth_getLogs, bloom filters, indexed topics and receipt logs are standard. Contract events are emitted, indexed and filtered the way any Ethereum tool expects.
The 512-block state window
The public RPC keeps state for 512 blocks. Beyond that window:
eth_callat an old block answers from a pruned state.eth_getTransactionCountreturns a false0x0. Not an error. A zero.
At half-second blocks, 512 blocks is about four minutes. Anything older is outside the window.
A false zero is worse than an error, because it looks like a measurement. A tool that reads a nonce at an old block and concludes "this account has never sent a transaction" is wrong and nothing tells it so.
Prove facts about accounts with eth_getProof, which either answers with a proof or fails. Never with a nonce read at an old height.
Historical logs
For history beyond the window, use the indexer rather than the RPC. It holds indexed transactions per address and per block. See Indexer API.
If you need full history under your own control, run an archive node. The package and the exact options are published; see Run a node.
Log volume and empty blocks
The chain produces a block every half second whether or not there are transactions, so most blocks are empty and carry no logs. A subscriber that treats a run of empty blocks as a stall will raise false alarms; chain liveness is the block number advancing, not the transaction count.
A note on log-filter caches
If you run a node, the log-filter cache can grow to several gigabytes while being almost entirely zeros. It is a computed index, not history: it can be rebuilt, so clearing it loses no chain data. Headers are not in that category and must never be treated the same way.