BIP332: Stale Tip Relay - #2241
Conversation
edilmedeiros
left a comment
There was a problem hiding this comment.
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.
|
|
||
| This BIP defines a new [BIP 434][BIP434] feature id ("the `staletip` feature"): | ||
|
|
||
| * `https://github.com/w0xlt/bitcoin/tree/staletip-v4` |
There was a problem hiding this comment.
This can be left to the reference implementation section.
There was a problem hiding this comment.
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.
| 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`. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Actually, this gets confusing here: the feature is called staletip everywhere, but the serialization in BIP434 featureid part of the message should be BIPxxx?
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
Since the feature is versioned, this slack is really needed? I mean, why not accept EXACTLY one byte with the two expected values?
There was a problem hiding this comment.
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.
| - Nodes SHOULD ignore any headers found to be invalid, and SHOULD NOT disconnect | ||
| or otherwise punish peers for relaying invalid headers[^rat-ignoreinvalid]. |
There was a problem hiding this comment.
Can't this open a DoS vector?
There was a problem hiding this comment.
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.
| - 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| [^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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| #### 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). | ||
|
|
There was a problem hiding this comment.
Not sure this is necessary and potentially implies maintaining this code in the specification over time.
There was a problem hiding this comment.
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.
|
Thanks for opening this here. Plan to review! |
5b30d29 to
d055be1
Compare
@edilmedeiros Thank you so much for taking the time to review. Rebased and added a commit (d055be1) to address Edil's feedback. |
| | 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 | |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 blocksThere was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
This was a very good read :) I left a question and a couple of nits.
| | ---- | ----- | ------- | | ||
| | `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 | |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
| * Nodes MAY advertise stale tips only when the stale tip itself has a | ||
| difficulty greater than some higher threshold, for example 1,000,000. |
There was a problem hiding this comment.
Wouldn’t one of the interesting uses for this BIP be to map the many branches of testnet4?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| | 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 | |
There was a problem hiding this comment.
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.
d055be1 to
d0f6080
Compare
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.