Skip to content

BIP332: Stale Tip Relay - #2241

Open
pseudoramdom wants to merge 1 commit into
bitcoin:masterfrom
pseudoramdom:staletip-bip
Open

BIP332: Stale Tip Relay#2241
pseudoramdom wants to merge 1 commit into
bitcoin:masterfrom
pseudoramdom:staletip-bip

Conversation

@pseudoramdom

Copy link
Copy Markdown

This BIP is a specification for a new an opt-in P2P message,staletip, for relaying recent stale tips between peers.
A proof-of-concept implementation is available at https://github.com/w0xlt/bitcoin/tree/staletip-v4.

Mailing list post - https://gnusha.org/pi/bitcoindev/d92f1615-368b-4406-b326-a1799c72a555n@googlegroups.com/
Discussion - https://groups.google.com/g/bitcoindev/c/AwOPNxF15mU

Feedback is welcome.

LLM Disclosure: LLMs were used to help develop the proposal’s initial outline and to rephrase portions of the draft. The proposal’s technical content was reviewed and finalized by the authors.

@edilmedeiros edilmedeiros left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for submitting your proposal. Gave a round of review, some comments are about the technique itself, but others are more editorial to try to improve and clarify the specification.

Comment thread bip-staletip.md Outdated

This BIP defines a new [BIP 434][BIP434] feature id ("the `staletip` feature"):

* `https://github.com/w0xlt/bitcoin/tree/staletip-v4`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be left to the reference implementation section.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL here is the temporary experimental featureid, not just a reference implementation link. BIP 434 requires an unassigned experimental feature to use a globally unique identifier, such as its repository URL. I’ll clarify that distinction and move the standalone implementation link to the Reference Implementation section.

For features published as a BIP, the featureid SHOULD be the assigned BIP number, eg "BIP434", or be based on the BIP number (eg, "BIP434v2" where the "v2" suffix covers versioning, or "BIP434.3" where the ".3" suffix covers part 3 of the BIP). For experimental features that do not (yet) have a BIP number assigned, some other unique identifier MUST be chosen, such as a URL to the repository where development is taking place, or the sha256 digest of some longer reference.

Comment thread bip-staletip.md Outdated
Comment on lines +82 to +84
If this specification is assigned a BIP number, the feature id SHOULD be updated
to a BIP-number based identifier as recommended by BIP 434, for example
`BIPxxx` or `BIPxxxv1`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird to expect an specification proposal might not get a number… Why not specify BIPxxx for now an update when a number is assigned, without the conditional?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this gets confusing here: the feature is called staletip everywhere, but the serialization in BIP434 featureid part of the message should be BIPxxx?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

staletip is the descriptive feature name and P2P message command. It isn’t the serialized BIP 434 featureid. The experimental implementation currently uses the repository URL as its globally unique featureid, and the published specification will the BIP number. I’ll make that distinction explicit.

Comment thread bip-staletip.md Outdated
Comment on lines +86 to +101
The `staletip` feature data MUST contain at least one byte. The first byte is a
boolean, `prefers_blocks`, indicating whether the node advertising the feature
prefers to collect the full block data associated with stale tips:

* `\x00`: the node prefers header-only announcements and does not request that
announcements be delayed until block data is available.
* `\x01`: the node prefers announcements that include availability of block
data where practical.

Nodes receiving an empty `staletip` feature data field, or a first byte other
than `\x00` or `\x01`, MUST ignore that peer's `staletip` feature
advertisement. They SHOULD NOT disconnect solely because the feature data is not
understood.

For future compatibility, nodes MUST ignore any additional feature data bytes
after the first byte.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the feature is versioned, this slack is really needed? I mean, why not accept EXACTLY one byte with the two expected values?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point :). BIP 434’s versioned feature IDs give us a clean way to make future changes. I’ll update the specification to require one byte with value 0x00 or 0x01.

Comment thread bip-staletip.md Outdated
Comment thread bip-staletip.md Outdated
Comment thread bip-staletip.md Outdated
Comment on lines +257 to +258
- Nodes SHOULD ignore any headers found to be invalid, and SHOULD NOT disconnect
or otherwise punish peers for relaying invalid headers[^rat-ignoreinvalid].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this open a DoS vector?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DoS vectors for p2p are for processing time or memory/disk usage or amplification attacks where we receive a small amount of garbage data and then send a large amount of garbage data to other peers as a result. The bandwidth it takes to send the message isn't a DoS vector, because there's a million ways to send data to a p2p node, and you can't prevent all of them.

Comment thread bip-0332.md
- Nodes SHOULD ignore any headers found to be invalid, and SHOULD NOT disconnect
or otherwise punish peers for relaying invalid headers[^rat-ignoreinvalid].
- If `have_block` is `true`, nodes that prefer to collect the full block data
SHOULD request missing block data for the announced stale branch in the normal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD request missing block data for the announced stale branch in the normal

Sounds that MAY would work better here. What if I already have that specific stale block, SHOULD I request it again?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I already have that specific stale block

Spec says "SHOULD request missing block data". If the node already has that stale block, its data is not missing and should not be requested again.

Comment thread bip-0332.md
Comment on lines +662 to +668
[^rat-compressedheader]: Omitting the previous block hash from each header saves
32 bytes per header (40%), as this field can be reconstructed from the
preceding headers in the message. This does not apply to the first header,
which is why the fork point must be included explicitly. This BIP does not
attempt to omit `nBits` or compress `nTime` or `nVersion`, because
reconstructing those fields is significantly more complicated for
comparatively much less potential gain.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It assumes there's only one stale branch, which is probably not true in testnet and may even happen in mainnet. The design can deal with more than one competing branch?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each staletip message describes one linear branch. If there are multiple competing branches, the sender will have to send a separate message for each one. I’ll clarify this in the spec.

Comment thread bip-0332.md
Comment on lines +296 to +319
#### Reconstructing Headers

Headers may be reconstructed from a `staletip` message via the following
algorithm:

```
std::vector<CBlockHeader> headers;
uint256 prev_hash = staletip_msg.fork_point;
for (const auto& ch : staletip_msg.headers) {
headers.emplace_back({
.nVersion = ch.version,
.hashPrevBlock = prev_hash,
.hashMerkleRoot = ch.merkle_root,
.nTime = ch.time,
.nBits = ch.bits,
.nNonce = ch.nonce,
});
prev_hash = headers.back().GetHash();
}
```

Note that headers are reconstructed in order, from oldest (closest to the
`fork_point`) to newest (the stale tip itself).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is necessary and potentially implies maintaining this code in the specification over time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having the reconstruction algorithm in the specification is helpful. That said, it doesn’t need to be C++, so I’ve replaced it with pseudocode.

Comment thread bip-staletip.md Outdated
@0xB10C

0xB10C commented Aug 12, 2026

Copy link
Copy Markdown

Thanks for opening this here. Plan to review!

@pseudoramdom

Copy link
Copy Markdown
Author

Thanks for submitting your proposal. Gave a round of review, some comments are about the technique itself, but others are more editorial to try to improve and clarify the specification.

@edilmedeiros Thank you so much for taking the time to review.


Rebased and added a commit (d055be1) to address Edil's feedback.
The question of whether to reserve a one-byte BIP 324 message ID remains open.

Comment thread bip-0332.md
| Name | Value | Meaning |
| ---- | ----- | ------- |
| `MAX_STALETIP_HEADERS` | 20 | Recommended maximum number of `CompressedHeader` entries in one `staletip` message |
| `STALETIP_RECENT_WINDOW` | 1000 blocks | Recommended maximum distance from the receiver's active tip |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the reason for choosing 1000 blocks when Bitcoin Core, as the most used implementation, would reject any header more than ~144 blocks old (GetAntiDoSWorkThreshold)? With the current implementation, it seems that these older headers would be sent out but would not be accepted by the peer by default. Or does the BIP intend to recommend that participating peers accept headers to their database within STALETIP_RECENT_WINDOW, basically recommending to lower the 144 block threshold?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a round number; more inspired by the average interval between stale blocks on mainnet (so that there's a decent chance a new node using this feature would see data fairly quickly) than anything else.

@mzumsande mzumsande Aug 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but I find it not ideal to ask for stale headers and then immediately discard them because they are too old to satisfy GetAntiDoSWorkThreshold.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STALETIP_RECENT_WINDOW number is arbitrary. 1000 blocks gives monitoring nodes roughly seven days to discover staletip data.

With the current implementation, it seems that these older headers would be sent out but would not be accepted by the peer by default.

Good catch, this might've been an oversight in the PoC implementation. Staletip relay path should probably define a separate bounded acceptance policy.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but I find it not ideal to ask for stale headers and then immediately discard them because they are too old to satisfy GetAntiDoSWorkThreshold.

Right, but that's a problem with the implementation, not the spec? (Beyond the spec noting the interaction to help implementers avoid pitfalls, anyway)

I think 48 hours / 288 blocks hours is likely enough for a stale tip to propagate to 99% of interested nodes just via extra-block-relay-only connections if only 1% of listening nodes support stale tip relay, and 24 hours / 144 blocks is likely enough if ~2.5% of listening nodes support the feature. Those numbers are worse if the extra-block-relay-only connections fail frequently due to addrman poisoning, or better if there are enough stale tip relay nodes that they form connected components in the p2p network. Also better if non-listening nodes are participating. So to me, that says changing this to recommend 144 wouldn't be overly concerning, but I'd still lean towards 1000 to maximise propagation chance to anyone interested.

def propogate(lnodes, support, blks):
    probes = blks*2 # 10min blks, 5min extra-b-r-o-conn
    have = [False]*int(lnodes*support)
    t = len(have)
    assert t > 1
    have[0] = True
    for _ in range(probes):
        for i in range(t): # each node makes a probe
            k = int(random.random() * lnodes) # who does i connect to?
            if k < t and (have[i] or have[k]): # staletip relay?
                have[i] = have[k] = True
    return sum(have)/t
    
sum(propogate(20000, 0.01, 144) for _ in range(20))/20
# 0.542 -- 54% of staletip relay nodes see the stale tip with 1% sat over 144 blocks
sum(propogate(20000, 0.01, 288) for _ in range(20))/20
# 0.9894999999999999 -- 99% of staletip relay nodes see the stale tip with 1% sat over 288 blocks
sum(propogate(20000, 0.025, 144) for _ in range(20))/20
# 0.9994999999999999 -- 99% of staletip relay nodes see the stale tip with 2.5% sat over 144 blocks
sum(propogate(20000, 0.003, 1000) for _ in range(20))/20
# 0.9991666666666668 -- 99% of staletip relay nodes see the stale tip with 0.3% sat over 1000 blocks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just thinking that in the context of the increased peer limit of Bitcoin Core, this could be even more reliable if blocks-only peers optionally participate in stale tip relay. Perhaps this BIP could make a recommendation whether stale tips should be announced on blocks-only connections or not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, keeping 1000 blocks in the spec gives stale tips a better chance to propagate while adoption is low. Implementations can still apply stricter local limits.
I’ll add a note that implementations reusing their normal header-processing path should ensure its admission policy is compatible with its staletip recency window.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocks-only peers optionally participate in stale tip relay

I don't think block-relay-only mode is documented in a BIP anywhere (treating it as a node configuration details rather than a standard), so it seems slightly weird to introduce it as a BIP-worthy concept here?

I think stale tip relay/announcements should be done on block-relay-only connections, because that allows the extra-block-relay-only connections to make the p2p graph dynamic for the purposes of this message, making it more likely to achieve wide distribution.

@danielabrozzoni danielabrozzoni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a very good read :) I left a question and a couple of nits.

Comment thread bip-staletip.md Outdated
Comment thread bip-staletip.md Outdated
Comment thread bip-0332.md
| ---- | ----- | ------- |
| `MAX_STALETIP_HEADERS` | 20 | Recommended maximum number of `CompressedHeader` entries in one `staletip` message |
| `STALETIP_RECENT_WINDOW` | 1000 blocks | Recommended maximum distance from the receiver's active tip |
| `MAX_RETAINED_STALETIPS` | 10 | Recommended maximum number of stale tips retained for later relay |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when a node already has MAX_RETAINED_STALETIPS stale tips stored and receives a new one? Should it ignore the new tip, or evict an existing one?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be called "ADVERTISED" (or similar) rather than "RETAINED" -- (except for pruning) core retains all the stale tips it has, eg. Advertising the 10 stale tips with most accumulated work, and in the case of a tie, dropping the oldest is probably reasonable for ensuring the tips most likely to be involved in a reorg are available.

Choosing randomly would probably maximise the number of tips available to the network, particularly if it were re-randomized per connection.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it ignore the new tip, or evict an existing one?

I think the exact policy can remain local to the implementation. Agree with AJ, a reasonable policy would be to keep the tips with the most cumulative proof of work.

  • A new tip with more work would replace the lowest-work entry.
  • Among tips with equal work, the most recently learned tips would be retained.
  • A new tip with less work than every existing entry could be ignored.

Should probably be called "ADVERTISED" (or similar) rather than "RETAINED"

MAX_ADVERTISED_STALETIPS sounds more like a lifetime or per-peer announcement limit. How about MAX_STALETIP_RELAY_SET_SIZE?

I can also include a line when expanding on that constant -

A node's stale-tip relay set is the set of stale tips currently selected for later announcement.

@murchandamus murchandamus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough work, this proposal was an excellent read and looks already very mature.

I’m assigning BIP332 to this proposal. Please add a README entry, incorporate the BIP number into the preamble and set the Assigned header to 2026-08-21.

Comment thread bip-0332.md
Comment on lines +484 to +485
* Nodes MAY advertise stale tips only when the stale tip itself has a
difficulty greater than some higher threshold, for example 1,000,000.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn’t one of the interesting uses for this BIP be to map the many branches of testnet4?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I can see mapping competing branches on testnet4 being useful.
Do you think the BIP should avoid prescribing a difficulty threshold for testnet3 and testnet4 and leave it entirely to local policy? We could drop these two recommendations and rely on the general proof-of-work, rate-limiting, and resource-bound requirements instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Difficulty one blocks can be created at a rate of ~200/s per TH/s, so a map that includes those doesn't seem very useful, and likely opens you up to a disk filling attack: make your diff-1 alternative blocks contain 1MvB worth of txs that conflict with the main chain. They're only listed as MAY and SHOULD requirements, so you could still do this while complying with the bip, though.

Comment thread bip-0332.md
| Name | Value | Meaning |
| ---- | ----- | ------- |
| `MAX_STALETIP_HEADERS` | 20 | Recommended maximum number of `CompressedHeader` entries in one `staletip` message |
| `STALETIP_RECENT_WINDOW` | 1000 blocks | Recommended maximum distance from the receiver's active tip |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just thinking that in the context of the increased peer limit of Bitcoin Core, this could be even more reliable if blocks-only peers optionally participate in stale tip relay. Perhaps this BIP could make a recommendation whether stale tips should be announced on blocks-only connections or not.

@murchandamus murchandamus changed the title BIP Draft: Stale Tip Relay BIP332: Stale Tip Relay Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants