Two block reorg at height 941880

And erin, a node that got the Foundry blocks quickly after the 941883 Foundry block was mined:

erin
Time Block Msg/Action From Peer To Peer Log message
15:49:54.446727 Antpool1 GETDATA 916529 2026-03-23T15:49:54.446727Z [msghand] [net] Requesting block 00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 from peer=916529
15:49:54.544777 Antpool1 GETBLOCKTXN 33316 2026-03-23T15:49:54.543863Z [msghand] [cmpctblock] Initialized PartiallyDownloadedBlock for block 00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 using a cmpctblock of size 27910 \n 2026-03-23T15:49:54.544777Z [msghand] [net] sending getblocktxn (93 bytes) peer=33316
15:49:54.831280 Antpool1 BLOCKTXN 33316 2026-03-23T15:49:54.831280Z [msghand] [net] received: blocktxn (21507 bytes) peer=33316
15:49:54.893326 Antpool1 CMPCTBLOCK Reconstructed 33316 2026-03-23T15:49:54.893326Z [msghand] [cmpctblock] Successfully reconstructed block 00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 with 1 txn prefilled, 4510 txn from mempool (incl at least 0 from extra pool) and 56 txn requested
15:49:55.981257 Antpool1 Update Tip 2026-03-23T15:49:55.981257Z [msghand] UpdateTip: new best=00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 height=941881 version=0x3092c000 log2_work=96.137382 tx=1327000957 date=‘2026-03-23T15:49:35Z’ progress=1.000000 cache=243.8MiB(1624562txo)
15:49:55.981503 Antpool1 UpdateTip 33316 2026-03-23T15:49:55.981503Z [msghand] [validation] Enqueuing UpdatedBlockTip: new block hash=00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 fork block hash=0000000000000000000199a739b635c5707a2bda57231b8abe846216ca0cc989 (in IBD=false)
15:51:47.098370 Antpool2 GETDATA 1030531 2026-03-23T15:51:47.098370Z [msghand] [net] Requesting block 00000000000000000000c81cbf94a12ca498e72eb8530f7061c8746cf9687b2e from peer=1030531
15:51:47.160858 Antpool2 CMPCTBLOCK Reconstructed 1030531 2026-03-23T15:51:47.160858Z [msghand] [cmpctblock] Successfully reconstructed block 00000000000000000000c81cbf94a12ca498e72eb8530f7061c8746cf9687b2e with 1 txn prefilled, 871 txn from mempool (incl at least 1 from extra pool) and 0 txn requested
15:51:47.421524 Antpool2 Update Tip 2026-03-23T15:51:47.421524Z [msghand] UpdateTip: new best=00000000000000000000c81cbf94a12ca498e72eb8530f7061c8746cf9687b2e height=941882 version=0x2008c000 log2_work=96.137391 tx=1327001829 date=‘2026-03-23T15:51:19Z’ progress=1.000000 cache=243.8MiB(1625693txo)
15:51:47.421714 Antpool2 UpdateTip 1030531 2026-03-23T15:51:47.421714Z [msghand] [validation] Enqueuing UpdatedBlockTip: new block hash=00000000000000000000c81cbf94a12ca498e72eb8530f7061c8746cf9687b2e fork block hash=00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 (in IBD=false)
15:55:05.887027 Foundry1 GETDATA 33316 2026-03-23T15:55:05.887027Z [msghand] [net] Requesting block 00000000000000000000bd4930a5982911e7749eb491886206e71abdc1ec0cc6 from peer=33316
15:55:05.887054 Foundry2 GETDATA 33316 2026-03-23T15:55:05.887054Z [msghand] [net] Requesting block 00000000000000000000724eac69a18c6699c9f7aaab24bcf18beb2723ccadd2 from peer=33316
15:55:05.887075 Foundry3 GETDATA 33316 2026-03-23T15:55:05.887075Z [msghand] [net] Requesting block 000000000000000000009c9acd0bc3207fa181f79f8573bf27d8a81d1ef3aa8e from peer=33316
15:55:06.374803 Foundry1 BLOCK 33316 2026-03-23T15:55:06.374803Z [msghand] [net] received block 00000000000000000000bd4930a5982911e7749eb491886206e71abdc1ec0cc6 peer=33316
15:55:06.747076 Foundry2 BLOCK 33316 2026-03-23T15:55:06.747076Z [msghand] [net] received block 00000000000000000000724eac69a18c6699c9f7aaab24bcf18beb2723ccadd2 peer=33316
15:55:07.071938 Foundry3 BLOCK 33316 2026-03-23T15:55:07.071938Z [msghand] [net] received block 000000000000000000009c9acd0bc3207fa181f79f8573bf27d8a81d1ef3aa8e peer=33316
15:55:07.497775 Antpool1 Update Tip 2026-03-23T15:55:07.497775Z [msghand] UpdateTip: new best=00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 height=941881 version=0x3092c000 log2_work=96.137382 tx=1327000957 date=‘2026-03-23T15:49:35Z’ progress=0.999999 cache=243.8MiB(1625385txo)
15:55:07.497915 Antpool2 Block Disconnected 2026-03-23T15:55:07.497915Z [msghand] [validation] Enqueuing BlockDisconnected: block hash=00000000000000000000c81cbf94a12ca498e72eb8530f7061c8746cf9687b2e block height=941882
15:55:07.498351 Antpool2 Block Disconnected 2026-03-23T15:55:07.498351Z [scheduler] [validation] BlockDisconnected: block hash=00000000000000000000c81cbf94a12ca498e72eb8530f7061c8746cf9687b2e block height=941882
15:55:08.260316 Antpool1 Block Disconnected 2026-03-23T15:55:08.260316Z [msghand] [validation] Enqueuing BlockDisconnected: block hash=00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 block height=941881
15:55:08.260635 Antpool1 Block Disconnected 2026-03-23T15:55:08.260635Z [scheduler] [validation] BlockDisconnected: block hash=00000000000000000001b3ff8b13e57c3ec1eca3ba7d2937edbd9f219eb2d9f3 block height=941881
15:55:08.715253 Foundry1 Update Tip 2026-03-23T15:55:08.715253Z [msghand] UpdateTip: new best=00000000000000000000bd4930a5982911e7749eb491886206e71abdc1ec0cc6 height=941881 version=0x22cd4000 log2_work=96.137382 tx=1327000861 date=‘2026-03-23T15:49:47Z’ progress=0.999999 cache=243.8MiB(1625382txo)
15:55:09.519207 Foundry2 Update Tip 2026-03-23T15:55:09.519207Z [msghand] UpdateTip: new best=00000000000000000000724eac69a18c6699c9f7aaab24bcf18beb2723ccadd2 height=941882 version=0x3427e000 log2_work=96.137391 tx=1327005400 date=‘2026-03-23T15:51:25Z’ progress=0.999999 cache=243.9MiB(1629023txo)
15:55:10.206706 Foundry3 Update Tip 2026-03-23T15:55:10.206706Z [msghand] UpdateTip: new best=000000000000000000009c9acd0bc3207fa181f79f8573bf27d8a81d1ef3aa8e height=941883 version=0x2fc2e000 log2_work=96.137401 tx=1327009788 date=‘2026-03-23T15:54:49Z’ progress=1.000000 cache=243.9MiB(1632872txo)
15:55:10.273036 Foundry3 UpdateTip 33316 2026-03-23T15:55:10.273036Z [msghand] [validation] Enqueuing UpdatedBlockTip: new block hash=000000000000000000009c9acd0bc3207fa181f79f8573bf27d8a81d1ef3aa8e fork block hash=0000000000000000000199a739b635c5707a2bda57231b8abe846216ca0cc989 (in IBD=false)
16:02:30.912021 Main4 HEADERS 33316 2026-03-23T16:02:30.912021Z [msghand] Saw new cmpctblock header hash=0000000000000000000085ebdcd669c404195dbfeccbee902fc646dadc367a20 peer=33316
16:02:31.006373 Main4 CMPCTBLOCK Reconstructed 33316 2026-03-23T16:02:31.006373Z [msghand] [cmpctblock] Successfully reconstructed block 0000000000000000000085ebdcd669c404195dbfeccbee902fc646dadc367a20 with 1 txn prefilled, 4004 txn from mempool (incl at least 1 from extra pool) and 0 txn requested
16:02:31.941123 Main4 Update Tip 2026-03-23T16:02:31.941123Z [msghand] UpdateTip: new best=0000000000000000000085ebdcd669c404195dbfeccbee902fc646dadc367a20 height=941884 version=0x212b4000 log2_work=96.137410 tx=1327013793 date=‘2026-03-23T16:02:14Z’ progress=1.000000 cache=243.9MiB(1636692txo)
16:02:31.941341 Main4 UpdateTip 33316 2026-03-23T16:02:31.941341Z [msghand] [validation] Enqueuing UpdatedBlockTip: new block hash=0000000000000000000085ebdcd669c404195dbfeccbee902fc646dadc367a20 fork block hash=000000000000000000009c9acd0bc3207fa181f79f8573bf27d8a81d1ef3aa8e (in IBD=false)
16:04:49.519298 Main5 HEADERS 33316 2026-03-23T16:04:49.519298Z [msghand] Saw new cmpctblock header hash=00000000000000000001f8562235e11fc74d5eea589e4c37b671b4213e89f52f peer=33316
16:04:49.548922 Main5 GETBLOCKTXN 33316 2026-03-23T16:04:49.547835Z [msghand] [cmpctblock] Initialized PartiallyDownloadedBlock for block 00000000000000000001f8562235e11fc74d5eea589e4c37b671b4213e89f52f using a cmpctblock of size 25887 \n 2026-03-23T16:04:49.548922Z [msghand] [net] sending getblocktxn (57 bytes) peer=33316
16:04:49.587102 Main5 BLOCKTXN 33316 2026-03-23T16:04:49.587102Z [msghand] [net] received: blocktxn (16112 bytes) peer=33316
16:04:49.645486 Main5 CMPCTBLOCK Reconstructed 33316 2026-03-23T16:04:49.645486Z [msghand] [cmpctblock] Successfully reconstructed block 00000000000000000001f8562235e11fc74d5eea589e4c37b671b4213e89f52f with 1 txn prefilled, 4216 txn from mempool (incl at least 14 from extra pool) and 20 txn requested
16:04:50.698310 Main5 Update Tip 2026-03-23T16:04:50.698310Z [msghand] UpdateTip: new best=00000000000000000001f8562235e11fc74d5eea589e4c37b671b4213e89f52f height=941885 version=0x22abe000 log2_work=96.137420 tx=1327018030 date=‘2026-03-23T16:04:46Z’ progress=1.000000 cache=243.9MiB(1640026txo)
16:04:50.698627 Main5 UpdateTip 33316 2026-03-23T16:04:50.698627Z [msghand] [validation] Enqueuing UpdatedBlockTip: new block hash=00000000000000000001f8562235e11fc74d5eea589e4c37b671b4213e89f52f fork block hash=0000000000000000000085ebdcd669c404195dbfeccbee902fc646dadc367a20 (in IBD=false)
16:21:26.500000 Main5 BLOCKTXN 33316 2026-03-23T16:21:26.500000Z [msghand] [net] received: blocktxn (12652 bytes) peer=33316
1 Like

The peer that stalled some of the nodes is using the UserAgent semikek. The code for this client seems to be bitlens-rs/src/connect/mod.rs at dad40cb2ed98d4ba99792bc0ee621ec2982b4b97 · Charterino/bitlens-rs · GitHub

1 Like

Perhaps charterino, its dev is completely unaware of how the node’s behavior affects other nodes. Maybe someone should open an issue there to make him aware?

1 Like

I’ve opened bitlens-rs connections causing block download stalling on the network? · Issue #2 · Charterino/bitlens-rs · GitHub

Probaly good to also link the IRC discussion here: #bitcoin-core-dev on 2026-03-26 — searchable irc log

Thank you for bringing this up to my attention! murchandamus is correct, I was unaware of this. What do you guys think is the best thing for me to do here? Other than respond to GETDATA requests.

2 Likes

But it all seems to point to intentional.

While I agree that a selfish-minnig attack might look similar on monitoring tools, I don’t think this was one. And if it was one, it was a poorly exectued one:

  • why briefly mine on AntPools and ViaBTC’s blocks?
  • why reveal it to the world during a low fee period? The two reorged blocks made 0.008 + 0.017 BTC in fees.. This is a bad risk (of e.g. miners moving away) reward I would not take.

I guess if I’m wrong, then we are going to see it again soonish.

However, my main reason is the following: The data we’ve seen exactly matches the expected network and relay behavior. I’ve written a Bitcoin Core functional test that shows this.

To summarize some data:

  1. We know Foundry uses preciousblock and we’ve seen them use it multiple times before in e.g. Mining Pool Behavior during Forks - and it’s a very reasonable thing to do, if you want to maximize profits.
  2. We know Foundry switched to their blocks AFTER briefly mining on the AntPool & ViaBTC blocks for a second each: Two block reorg at height 941880 - #20 by b10c
  3. We know the Foundry blocks didn’t propagate well. We had @matthias share a very valuable global network view on this in Two block reorg at height 941880 - #18 by matthias, we have Json Huge from OCEAN who said No DATUM miner on OCEAN ever built work on top of either Foundry block 941881 or 941882 (around a thousand globally diverse nodes) (https://x.com/wk057/status/2036674054971703361), and we have a bunch of monitoring nodes that didn’t see them either before we reorged.

This functional test mimics the event stratum job event order from 2), uses preciousblock 1), and checks that the header does not propagate further than one hop from the miner (and the block does not propagate at all) as seen in 3).

#!/usr/bin/env python3
# Copyright (c) 2022-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
""" Shows what nodes consider the tip during a two block fork where miners use preciousblock.

This is meant to explain the network behavior seen in:
https://bnoc.xyz/t/two-block-reorg-at-height-941880/97

- The two Foundry blocks didn't propagate well as they weren't seen first (here F881 and F882)
- Only a few nodes announced the Foundry blocks F881 and F882, but the headers weren't realyed
- The AntPool and ViaBTC blocks did propagate well as they were seen first
"""

from test_framework.test_framework import BitcoinTestFramework
from test_framework.blocktools import create_block
from test_framework.util import assert_equal

class TwoBlockReorg(BitcoinTestFramework):

    def set_test_params(self):
        self.setup_clean_chain = True
        self.num_nodes = 4

    def setup_network(self):
        self.setup_nodes()
        # Construct a network:
        # minerA -> node1 <-> node2 <- minerF
        # node1 is connected to minerA and node2
        # node2 is connected to minerF and node1
        #
        # minerA to node1
        self.connect_nodes(0, 1)
        # node1 to node2 and node2 to node1
        self.connect_nodes(1, 2)
        self.connect_nodes(2, 1)
        # minerF to node2
        self.connect_nodes(3, 2)

    def run_test(self):
        minerA = self.nodes[0]
        node1 = self.nodes[1]
        node2 = self.nodes[2]
        minerF = self.nodes[3]

        self.log.info("Setup network: minerA -> node1 <-> node2 <- minerF")
        self.log.info("Mining one block on node1 and verify all nodes sync")

        # generate verify's that all nodes are in sync under the hood
        self.generate(node1, 880)
        # We start of at height 880. G is the Genesis block.
        #  G - ... -  880

        assert_equal(node1.getblockcount(), 880)
        assert_equal(node1.getblockcount(), node2.getblockcount())
        assert_equal(node1.getblockcount(), minerA.getblockcount())
        assert_equal(minerF.getblockcount(), minerA.getblockcount())

        self.log.info("minerF starts working on a block block F881")
        blockF881 = create_block(
            hashprev=int(node1.getbestblockhash(), 16),
            tmpl={"height": 881}
        )

        self.log.info("however, minerA beats it and publishes a block A881 and everybody sees it")
        self.generate(minerA, 1)

        #   G ... -- 880 -- A881
        #                     ^: minerA, minerF, node1, node2
        assert_equal(node1.getblockcount(), 881)
        assert_equal(node1.getblockcount(), node2.getblockcount())
        assert_equal(node1.getblockcount(), minerA.getblockcount())
        assert_equal(minerF.getblockcount(), minerA.getblockcount())

        self.log.info("minerF finds the block F881, publishes it, and switches to it")
        blockF881.solve()
        minerF.submitblock(blockF881.serialize().hex())
        # minerF is still on the minerA block, as it heard about this one first
        assert_equal(minerF.getbestblockhash(), minerA.getbestblockhash())
        minerF.preciousblock(blockF881.hash_hex)
        assert_equal(minerF.getbestblockhash(), blockF881.hash_hex)

        # We now have a fork.
        #                   v: minerF
        #             /- F881
        #  G .. -- 880
        #             \- A881
        #                   ^: minerA, node1, node2

        self.log.info("Only node2, who is directly connected to minerF, has seen F881. It did not relay the header/block to node1")
        chaintips_node1 = node1.getchaintips()
        chaintips_node2 = node2.getchaintips()
        assert_equal(len(chaintips_node2), 2)
        for tip in chaintips_node2:
            if tip["status"] == "active":
                assert_equal(tip["hash"], minerA.getbestblockhash())
            elif tip["status"] == "headers-only":
                assert_equal(tip["hash"], blockF881.hash_hex)

        assert_equal(len(chaintips_node1), 1)
        assert_equal(chaintips_node1[0]["hash"], minerA.getbestblockhash())


        self.log.info("Our node1 node is still on the A881 block, as it saw this one first")
        assert_equal(node1.getbestblockhash(), minerA.getbestblockhash())

        self.log.info("Again, minerF starts mining on a block F882")
        blockF882 = create_block(
            hashprev=int(minerF.getbestblockhash(), 16),
            tmpl={"height": 882}
        )

        self.log.info("minerA finds block A882. node1 and minerF switch to it")
        self.generate(minerA, 1)
        #
        #             /- F881
        #  G .. -- 880
        #             \- A881 - A882
        #                          ^: minerA, minerF, node1
        assert_equal(node1.getbestblockhash(), minerA.getbestblockhash())
        assert_equal(minerF.getbestblockhash(), minerA.getbestblockhash())

        self.log.info("minerF finds block F882, publishes it, and switches to it")
        blockF882.solve()
        minerF.submitblock(blockF882.serialize().hex())
        # minerF is still on the minerA block, as it heard about this one first
        assert_equal(minerF.getbestblockhash(), minerA.getbestblockhash())
        minerF.preciousblock(blockF882.hash_hex)
        assert_equal(minerF.getbestblockhash(), blockF882.hash_hex)

        # We now have a two block fork.
        #                          v: minerF
        #             /- F881 - F882
        #  G .. -- 880
        #             \- A881 - A882
        #                          ^: minerA, node1

        self.log.info("Only node2, who is directly connected to minerF, has seen F882. It did not relay the header/block to node1")
        chaintips_node1 = node1.getchaintips()
        chaintips_node2 = node2.getchaintips()
        assert_equal(len(chaintips_node2), 2)
        for tip in chaintips_node2:
            if tip["status"] == "active":
                assert_equal(tip["hash"], minerA.getbestblockhash())
            elif tip["status"] == "headers-only":
                assert_equal(tip["hash"], blockF882.hash_hex)

        assert_equal(len(chaintips_node1), 1)
        assert_equal(chaintips_node1[0]["hash"], minerA.getbestblockhash())

        self.log.info("minerF finds block F883, causing a two block reorg")
        self.generate(minerF, 1)

        #                                 v: minerF, minerA, node1
        #             /- F881 - F882 - F883
        #  G .. -- 880
        #             \- A881 - A882
        #
        assert_equal(node1.getbestblockhash(), minerA.getbestblockhash())
        assert_equal(minerF.getbestblockhash(), minerA.getbestblockhash())

        self.log.info("minerF wins the fork race")


if __name__ == '__main__':
    TwoBlockReorg(__file__).main()

Can also be found here.

The test outputs the following:

TestFramework (INFO): PRNG seed is: 1283534758446118571
TestFramework (INFO): Initializing test directory bitcoin_func_test_k7vv6x4w
TestFramework (INFO): Setup network: minerA -> node1 <-> node2 <- minerF
TestFramework (INFO): Mining one block on node1 and verify all nodes sync
TestFramework (INFO): minerF starts working on a block block F881
TestFramework (INFO): however, minerA beats it and publishes a block A881 and everybody sees it
TestFramework (INFO): minerF finds the block F881, publishes it, and switches to it
TestFramework (INFO): Only node2, who is directly connected to minerF, has seen F881. It did not relay the header/block to node1
TestFramework (INFO): Our node1 node is still on the A881 block, as it saw this one first
TestFramework (INFO): Again, minerF starts mining on a block F882
TestFramework (INFO): minerA finds block A882. node1 and minerF switch to it
TestFramework (INFO): minerF finds block F882, publishes it, and switches to it
TestFramework (INFO): Only node2, who is directly connected to minerF, has seen F882. It did not relay the header/block to node1
TestFramework (INFO): minerF finds block F883, causing a two block reorg
TestFramework (INFO): minerF wins the fork race
TestFramework (INFO): Stopping nodes
TestFramework (INFO): Cleaning up bitcoin_func_test_k7vv6x4w on exit
TestFramework (INFO): Tests successful

In reply on: OCEAN's Jason Hughes shares weird details about the two-block reorg \ stacker news, @murchandamus comes to a similar conclusion: expected network behavior.

1 Like

The question for me is “Why did they stop mining on them?” Maybe they received a “late header” from their miner but had not yet validated the block for the “foreign” header, so they go with the first block they can validate. Maybe the reason Foundry’s blocks 1 and 2 were so late in being seen is because most nodes had validated the other blocks before seeing them.

If this is the case, then the rules aren’t strictly following proof of work which means “the partition with the highest hashrate”. We’re following “proof of fastest block validation”. To strictly follow proof of work, both headers should be relayed and mined equally by each miner who receives them immediately from the same peer, or within half a typical network propagation delay from different peers, until both blocks have had “sufficient” time to be transmitted and validated. If that estimated time has not passed, other miners continue working on the header that came first even if the other header’s block can be validated. In this way, the partition that had the highest hashrate wins.

The timestamp differences are less than 1/2 the standard deviation of their accuracy, so they can’t be used to make a determination.

The link didn’t seem to indicate @murchandamus concludes it was normal network behavior.

With only 33% hashrate and apparently not getting much help from other miners, Foundry can’t expect to profit from it being a selfish mining attack (at 33% it would be break even for “gamma=0”).

In the case where your pool is competing with another pool in a block-race, you want to be mining on your own block. Let’s play the scenario through with an example:

Imagine you’re a pool with 1% of the hashrate and you just found a valid block, but know that the other 99% of hashrate just switched to another block found by a competing pool.

Case A: You mine on the competing block and give up on your own block. The chance of you finding the next block are 1%. You can gain the block reward of that next block, if you find it.

Case B: You mine on your own block (e.g. by switching to it via preciousblock) and ignore the competing one. Your chance of finding the next block is still 1%, however if you do, you get the reward from your previous block and the next one.

Case B is strictly better for a pool. No matter the odds.

2 Likes

I guess @murchandamus might chime in at some point, but to quote him a bit from that link:

However, if the timestamps are accurate, it is possible that Foundry found both of those blocks just after the competing blocks were found, before they had learned about the competing blocks, but after their block would have propagated widely on the network.

It could perhaps even be the case that the block was found by a pool participant even after Foundry saw the Antpool block, but before they had updated all the jobs. It would still make sense and not be malicious for a pool to mine on their own block when they have one.

As I mentioned above, Bitcoin Core nodes will request and store blocks in competing chaintips with the same PoW as their best chaintip, but they will not announce those blocks to their own peers. Even if Foundry had announced it after having found it, the block could have not made it beyond the Foundry nodes’ peers if they all had seen the competing block before it, or it would have possible made it another hope from a few peers before getting blackholed.

Thanks for these details. So, it’s not a huge leap to say that it was mostly a coincidence that some nodes didn’t see Foundry’s blocks until after the reorganization?

I think that it is plausible, yeah. And if we’re honest, Bitcoiners tend to entertain conspiracy theories perhaps a little to enthusiastically.

I completely missed that, but it works the same as a selfish mining attack. It’s not following proof of work, i.e. mining on the partitition with the highest hashrate. The pool knows which block came first and it knows the majority hashrate isn’t working on his “private” block.

Murch doesn’t seem to “conclude it was” an accident, but shows it could have been. But an accidental delay in getting new jobs out to workers works the same as a selfish mining attack. We can’t know if a pool does it on purpose or not. The pool only has to “accidentally” delay getting new jobs out to workers for it to be a purposeful attack. Another tactic is to “accidentally” delay releasing a block, or for the 2 largest pools to make sure they have a faster connection to each other than to the rest of the network. For example if they are 50 ms apart instead of the median of 500 ms, they gain 0.45/600 = 0.075% excess rewards.

To reiterate what I said before, selfish mining is possible only because timestamps aren’t enforced more accurately than MTP and FTL. It’s not a clever attack as much as it’s the result of not following strict proof of work rules, i.e. staying on the chain that has the highest hashrate since the last block, which requires checking to see if the timestamps are reasonable compared to typical network delays in the manner I described.

I’d say it would be helpful to only INV blocks that you can actually provide. Ideally answering GETDATA requests, or if that is an issue for some reason, not sending INV for those blocks in the first place. That being said, the Bitcoin Core behavior should be improved too (e.g. by lowering timeouts or allowing to download from another peer if there is a staller), which is being discussed in Seemingly second (very long) validation at the same height · Issue #33687 · bitcoin/bitcoin · GitHub

1 Like