Skip to content

Documentation / @etherfold/core / streamDigestOf

Function: streamDigestOf() ​

streamDigestOf<ABI>(source, streamConfig): string

Defined in: packages/core/src/stream/identity.ts:88

The digest that IDENTIFIES a stream, over the DEDUPLICATED streamHash values SORTED BY THEMSELVES, plus the resolved stream config.

Why sorted by THEMSELVES, and not taken in the entry list's order ​

sourceHashesOf returns its entries sorted by (startBlock, hash), and hash covers the DECODING shape. So renaming a non-indexed parameter -- the exact case the two-digest split exists for -- REORDERS that list while every streamHash in it is unchanged. A digest rolled up over the list in that order would move, fork a new stream, re-fetch the whole history and orphan the old one, silently and with no error. Taking the streamHash values alone, deduplicating them and sorting them by themselves is what makes the digest a function of the SET of filter facts. hash and legacyHash are excluded for the same reason: they are the fold's identity, not the stream's.

Note what the set already contains: the block-0 SKELETON entry, whose streamHash covers chainId, genesisHash, and each contract's address and startBlock. That is why chainId is not an address level of its own -- it is in here.

Why the CONFIG is in it ​

The filter is not the only thing that decides what a stream CONTAINS. parse.filters narrows which events are parsed and kept at all, so it changes WHAT IS STORED, and sourceInvalidationOf already invalidates the STREAM half from block 0 whenever the config hash moves. Keyed on the filter alone, two different configs would map to ONE stream and a generation would adopt logs the verdict has already declared invalid -- with the only existing remedy (clear the stream) destroying the stream the live generation is still answering from.

It takes the RESOLVED config (resolveStreamConfig), so an unset finality and the default written out are one stream, exactly as they are one config everywhere else. The config's canonical BYTES go in rather than its simple_hash: that digest is 32 bits, and here a collision is not a missed invalidation but two configs sharing one stream.

Why this hash ​

viem's sha256, truncated to 128 bits. It is already a direct dependency and it is SYNCHRONOUS, which crypto.subtle is not (it also needs a secure context). ONE implementation, never a native fast path beside a pure-JS fallback: two implementations that must agree byte for byte would give different stream ADDRESSES on different browsers, which is the silent history-orphaning this digest exists to prevent. 128 bits because this is a KEY and not a change DETECTOR: simple_hash's 32 bits are a coin-flip collision around 65,000 distinct filters, and a collision here means one generation silently adopting another's stream under a filter that does not match it, so logs are missing and nothing reports it.

Rendered as FIXED-LENGTH lowercase hex, with no 0x: every substrate has to carry it as a KEY ELEMENT (a string on IndexedDB, a column on SQL), and a fixed length is what stops one digest's rendering being read as another's.

Type Parameters ​

ABI ​

ABI extends Abi

Parameters ​

source ​

IndexingSource<ABI>

streamConfig ​

UsedStreamConfig

Returns ​

string