Tailstorm
Tailstorm is the block production protocol in effect after Hard Fork 2 activates.
It replaces each single block with a group of k smaller proof-of-work subblocks that are tied together by a summary block, where k is the per-network consensus parameter tailstorm_k (TAILSTORM_K = 120 on mainnet).
The chain of summary blocks is a traditional blockchain, so "block" continues to mean a summary block and the 2 minute average settlement cadence is preserved.
Between summary blocks, subblocks are produced on average every 2 minutes / k, or roughly once per second on mainnet.
The protocol is based on the Tailstorm protocol introduced in 2023 by Patrik Keller, Ben Glickenhaus, George Bissias and Gregory Griffith in Tailstorm: A Secure and Fair Blockchain for Cash Transactions [1]; Nexa's deviations are listed in Differences from the Tailstorm Paper.
Subblocks serve two purposes:
- Low-variance mining: a summary block is backed by
kindependent proof-of-work solutions rather than one, reducing the variance of block discovery and of mining revenue [3]. - Fast commitment intervals: each subblock announces a partially worked block, giving transactions a probabilistic, energy-backed indication of inclusion in the next summary block within about a second instead of minutes.
The subblock transactions are only a probabilistic indicator. They do not change the ledger state; all chain properties are upheld by the summary blockchain exactly as they would be if subblocks did not exist.
Block Types
Subblocks and summary blocks use the standard block header format.
They are distinguished by the version of the previously-unused minerData header field:
| minerData version | Block type | Contents |
|---|---|---|
| 0 | legacy block | Empty vector. Not valid once Hard Fork 2 activates. |
| 1 | subblock | Hashes of the previous subblocks this subblock was mined on top of. |
| 2 | summary block | Proof-of-work solutions for the k - 1 other subblocks it summarizes. |
A subblock is simply a block that does not carry enough proof of work to be a summary block.
The final subblock of each group is the summary block: its creator provides the summary block's own proof of work and references the other k - 1 solutions in minerData.
Requiring the summary block itself to be proof-of-work committed avoids defining an unambiguous proof-of-work-free algorithm for converting a set of subblocks into a summary block, which an attacker could otherwise exploit by spamming competing valid summary blocks.
Subblocks are transient.
They are held in memory only and are never stored on disk; after a summary block is buried deeper than tailstormEnforceDepth blocks, only the proof-of-work solutions retained in the summary block's minerData field are needed (see Subblock Consistency).
Consequently the disk space required is only marginally larger than for a blockchain without subblocks.
The Subblock DAG
The set of subblocks built on top of one summary block forms a directed acyclic graph (DAG) called a grove, in the manner of the DAG-style refinement of parallel proof of work [2]. A subblock may have multiple parents within the grove, and conflicting (double spend) subblocks may coexist within the grove. A node tracks all current groves, one per candidate chain tip.
graph RL
S0[["summary block N-1"]]
S1[["summary block N"]] --> S0
A(["subblock a"]) --> S1
B(["subblock b"]) --> A
C(["subblock c"]) --> A
D(["subblock d"]) --> B
D --> C
S2[["summary block N+1"]] --> D
Arrows point from child to parent.
Summary block N+1 is the final subblock of the grove built on summary block N; its minerData field carries the proof-of-work solutions of subblocks a through d.
Subblock Linking
A subblock's previous block hash header field MUST contain the hash of the summary block its grove is built on.
Its position in the DAG is instead committed by its minerData field (version 1), which lists the block hashes of the subblock DAG branch tips it was mined on top of.
The first subblock in a grove carries an empty list and descends directly from the summary block.
Because every subblock commits to all of the branch tips it builds upon, branch tips cannot be silently pruned from the DAG or swapped for double spend subblocks: any such substitution changes the hashes referenced by every later subblock.
The minerData Field
Subblock Format (version 1)
| Field | Length | Format | Description |
|---|---|---|---|
| version | 1 byte | uint8 (MUST BE 1) | The minerData version. |
| previous subblock hashes | variable | vector of block hashes | The hashes of the subblock DAG branch tips this subblock was mined on top of. Empty for the first subblock in a grove. |
Summary Block Format (version 2)
| Field | Length | Format | Description |
|---|---|---|---|
| version | 1 byte | uint8 (MUST BE 2) | The minerData version. |
| prevOfprevhash | 32 bytes | block hash | The grandparent summary block hash, i.e. the parent of the uncles' grove. MUST be zero at heights below 2, and MUST be the grandparent hash if any uncles are included. When there are no uncles it MUST be either zero or the grandparent hash. |
| nUncles | 1 byte | uint8 | The number of uncle proofs in vSubblockProofs. |
| nBitsUncle | 4 bytes | compressed target | The target shared by all included uncles. This differs from the current subblocks' target because the difficulty changes every summary block. |
| nSubblocks | 1 byte | uint8 | The number of subblock proofs in vSubblockProofs. |
| nBitsSubblock | 4 bytes | compressed target | The target shared by all included subblocks. This may differ from the summary block's own target field because the summary block compensates for low-work uncles (see Summary Block Target). |
| vSubblockProofs | variable | vector of (32 byte hash, byte vector) pairs | One proof-of-work solution per referenced subblock: the subblock's mining header commitment (its candidate hash, see NexaPOW) and its nonce. Uncle proofs MUST appear first, followed by subblock proofs. |
The number of entries in vSubblockProofs MUST equal either tailstorm_k - 1 or nUncles + nSubblocks, and is normally exactly tailstorm_k - 1 (the summary block itself supplies the kth solution).
If nUncles is zero then nBitsUncle MUST be zero, and if nSubblocks is zero then nBitsSubblock MUST be zero.
Each proof is the minimum data needed to verify that the subblock's proof-of-work puzzle was solved; the referenced subblocks do not need to be available to validate the summary block's work.
Uncle Subblocks
An uncle is a subblock that was mined for the previous summary block's grove but was not included in that summary block, for example because it propagated too slowly. Uncles may be included in the next summary block so their work is not wasted.
The proof of work of an uncle is verified against the grandparent summary block hash (prevOfprevhash) and the target declared in nBitsUncle, while regular subblock proofs are verified against the summary block's own previous block hash and nBitsSubblock.
A summary block that includes uncles MUST provide the correct, non-zero prevOfprevhash.
Referenced subblocks MUST have the same height as the summary block, and uncles MUST have a height of exactly one less.
Proof of Work
Binding to the Parent Summary Block
For all Tailstorm blocks, the proof-of-work computation binds the solution to the parent summary block.
Where legacy NexaPOW signs h1 = sha256(miningHash), Tailstorm blocks instead sign:
h1 = sha256(sha256(miningHash || parentSummaryBlockHash))
where parentSummaryBlockHash is the block's previous block hash (or prevOfprevhash when verifying an uncle proof).
Injecting the parent hash at the start of the computation makes it impossible to reuse solutions, or intermediate work, from old blocks or from other forks, and is also what distinguishes an uncle's solution from a current subblock's solution.
Subblock Target
The target (nBits) field of a subblock is the actual work needed to solve that subblock.
It is derived from the summary blockchain: the ASERT difficulty adjustment algorithm is evaluated over the chain of summary blocks exactly as before, and the resulting target is multiplied by tailstorm_k, making each subblock k times easier to solve:
subblock_target = ASERT(...) * tailstorm_k
The result is clamped to the proof-of-work limit; no subblock or summary block may be easier to solve than the limit.
Because ASERT is not a function of subblocks, all subblocks building on the same summary block have the same target value.
Networks with retargeting disabled or with minimum-difficulty allowances (regtest, testnet) follow their usual exceptions to these rules.
Mining infrastructure is unchanged; miners simply solve easier blocks at a higher rate.
Summary Block Target
A summary block must meet its own proof of work as a subblock, and must additionally reference enough solutions to meet the summary block work requirement.
The expected total work of a summary block is k times the work of the default subblock target.
The work already contributed by the referenced proofs is counted exactly: nUncles solutions at nBitsUncle plus nSubblocks solutions at nBitsSubblock.
The summary block's own required work is the default subblock work, hardened to make up any deficit:
expected_work = k * work(subblock_target)
referenced_work = nUncles * work(nBitsUncle) + nSubblocks * work(nBitsSubblock)
summary_work = max(work(subblock_target), expected_work - referenced_work)
The summary block's target field MUST be the hardest compact-encoded target whose work is at least summary_work, clamped to the proof-of-work limit.
This compensates for low-work uncles: a summary block that includes uncles solved at an easier prior target must contribute correspondingly more of its own work, so every summary block represents at least the same expected total work, and its own work is never below the default subblock work.
Chain Work
The chain work header field of a summary block MUST equal the parent's chain work plus the work of every solution the block represents:
chainWork = parent.chainWork
+ nUncles * work(nBitsUncle)
+ nSubblocks * work(nBitsSubblock)
+ work(target)
Reorganizations
Reorganizations operate at two levels: between summary blocks (as in a traditional blockchain) and between competing subblock trees within a grove.
Scoring Competing Forks
Fork choice remains most-accumulated-work, where a candidate tip's work is its summary chain work plus the work of the known subblocks in the grove building on it.
When counting a challenger grove's subblock work, at most tailstorm_k - 1 subblocks are counted per grove; the currently active tip's grove is counted in full.
This cap ensures that a fork padded with extra subblocks can never claim more work than a fork with the correct number of subblocks plus a summary block.
Double Spends Within a Grove
Conflicting (double spend) subblocks may coexist within a grove, but a subblock whose transactions conflict with those of its own ancestor subblocks is rejected. When the summary block's final transaction set is assembled, conflicting transactions across the grove are resolved by an exclusion score and the losing transactions are dropped.
DAG Regeneration
After a reorganization to a different summary block or grove completes, the DAG bookkeeping for the newly active grove (its coins view and transaction maps) is regenerated from the surviving subblocks, so that unprocessed subblocks received during the reorganization are incorporated correctly.
Subblock Consistency and Emergent Consensus
A subblock's consistency is defined in the context of a block that references it. To be consistent, a subblock MUST:
- pass the standard block field checks (previous block hash, merkle root, proof of work, etc.),
- have the subblock
targetrequired for its parent summary block, - have the height of the referencing summary block (or exactly one less for an uncle), and
- have its transactions, minus the grove's double spend exclusions, included in the referencing summary block.
Consistency is only enforced near the chain tip.
A summary block buried deeper than tailstormEnforceDepth blocks (5) does not need consistent subblocks; only the proof-of-work solutions in its minerData field are required.
This "tip consistency" rule is what allows subblocks to be forgotten after that depth.
Deep inconsistency is harmless because subblock transactions carry no ledger state: once the summary block exists, every probabilistic inclusion signal has collapsed into the boolean fact of inclusion, and the retained proofs mean inconsistent subblocks give an attacker no work advantage.
Snap To Chain
A fork containing inconsistent subblocks within tailstormEnforceDepth of its tip is ignored temporarily, not permanently.
Nodes continue mining their own fork, but if the other fork stays ahead until its inconsistencies are buried deeper than tailstormEnforceDepth, nodes "snap" to it as the chain with the most work, in the manner of emergent consensus.
Conversely, if the consistent fork pulls ahead, the inconsistent fork is abandoned by all nodes.
Either way the network converges, so an attacker cannot cause a permanent split by timing the release of headers such that only some nodes evaluate a block's consistency.
A node that cannot obtain the subblocks for a fork may temporarily mine an "illegal" fork, but the effect is no worse than the attacker having slightly more hash power.
Block Sizes
Once Hard Fork 2 is pending or activated, the next maximum block size has a floor of tailstorm_k * 100KB (12MB with mainnet tailstorm_k = 120).
The maximum size of a subblock is the next maximum block size divided by tailstorm_k.
Together with the floor, this guarantees a subblock can always hold the largest possible transaction, since a transaction may be as large as the 100KB minimum block size.
A summary block is limited by the next maximum block size directly, like any block.
Differences from the Tailstorm Paper
Nexa's Tailstorm implementation differs from the protocol described in the paper [1] in the following ways:
- Subblocks form a multi-parent DAG rather than a tree, following [2].
- The summary block is itself a proof-of-work solution (see Block Types); in [1] summaries are assembled from the subblock tree without proof of work of their own.
- Stale subblock work is recovered through uncle inclusion, a mechanism not present in [1].
- The reward discounting of [1] and [2], which reduces rewards in proportion to the subblock tree structure to punish withholding, is not adopted; the incentive-layer analyses of those papers therefore do not directly carry over.
Network Parameters
| Network | tailstorm_k |
tailstormEnforceDepth |
|---|---|---|
| mainnet | 120 | 5 |
| testnet | 120 | 5 |
| stormtest | 40 | 5 |
| regtest | 4 | 5 |
tailstorm_k MUST be at least 2.
Activation
Blocks carrying subblock data are only valid once Hard Fork 2 is pending or activated, and once it is activated, legacy (minerData version 0) blocks are no longer valid.
Because subblocks for the first post-fork grove may arrive while a node is still processing the last legacy block, a get_dag peer-to-peer message allows subblock recovery.
A get_dag message carries the set of subblock hashes the requester already has, and the peer responds with the requester's missing subblocks from the tip grove and its parent grove.
Nodes broadcast a get_dag with an empty hash set (requesting everything) to all peers once when the fork becomes pending, and while Tailstorm is active send one to each peer when headers are first received from it.
References
- Patrik Keller, Ben Glickenhaus, George Bissias, Gregory Griffith. 2023. Tailstorm: A Secure and Fair Blockchain for Cash Transactions. https://arxiv.org/abs/2306.12206
- Patrik Keller. 2023. Parallel Proof-of-Work with DAG-Style Voting and Targeted Reward Discounting. https://arxiv.org/abs/2312.03111
- George Bissias, Brian Neil Levine. 2017. Bobtail: A Proof-of-Work Target that Minimizes Blockchain Mining Variance. https://arxiv.org/abs/1709.08750