Skip to content

Commit 8c20fe1

Browse files
committed
[release/1.4.10] core, eth: enforce network split post DAO hard-fork
(cherry picked from commit 7f00e8c)
1 parent a0cc73a commit 8c20fe1

File tree

4 files changed

+141
-7
lines changed

4 files changed

+141
-7
lines changed

core/block_validator.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,13 @@ func ValidateHeader(config *ChainConfig, pow pow.PoW, header *types.Header, pare
248248
return &BlockNonceErr{header.Number, header.Hash(), header.Nonce.Uint64()}
249249
}
250250
}
251+
// If all checks passed, validate the extra-data field for hard forks
252+
return ValidateHeaderExtraData(config, header)
253+
}
254+
255+
// ValidateHeaderExtraData validates the extra-data field of a block header to
256+
// ensure it conforms to hard-fork rules.
257+
func ValidateHeaderExtraData(config *ChainConfig, header *types.Header) error {
251258
// DAO hard-fork extension to the header validity: a) if the node is no-fork,
252259
// do not accept blocks in the [fork, fork+10) range with the fork specific
253260
// extra-data set; b) if the node is pro-fork, require blocks in the specific

eth/handler.go

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ const (
4545
estHeaderRlpSize = 500 // Approximate size of an RLP encoded block header
4646
)
4747

48+
var (
49+
daoChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the DAO handshake challenge
50+
)
51+
4852
// errIncompatibleConfig is returned if the requested protocols and configs are
4953
// not compatible (low protocol version restrictions and high requirements).
5054
var errIncompatibleConfig = errors.New("incompatible configuration")
@@ -62,9 +66,10 @@ type ProtocolManager struct {
6266
fastSync uint32 // Flag whether fast sync is enabled (gets disabled if we already have blocks)
6367
synced uint32 // Flag whether we're considered synchronised (enables transaction processing)
6468

65-
txpool txPool
66-
blockchain *core.BlockChain
67-
chaindb ethdb.Database
69+
txpool txPool
70+
blockchain *core.BlockChain
71+
chaindb ethdb.Database
72+
chainconfig *core.ChainConfig
6873

6974
downloader *downloader.Downloader
7075
fetcher *fetcher.Fetcher
@@ -99,6 +104,7 @@ func NewProtocolManager(config *core.ChainConfig, fastSync bool, networkId int,
99104
txpool: txpool,
100105
blockchain: blockchain,
101106
chaindb: chaindb,
107+
chainconfig: config,
102108
peers: newPeerSet(),
103109
newPeerCh: make(chan *peer),
104110
noMorePeers: make(chan struct{}),
@@ -278,6 +284,18 @@ func (pm *ProtocolManager) handle(p *peer) error {
278284
// after this will be sent via broadcasts.
279285
pm.syncTransactions(p)
280286

287+
// If we're DAO hard-fork aware, validate any remote peer with regard to the hard-fork
288+
if daoBlock := pm.chainconfig.DAOForkBlock; daoBlock != nil {
289+
// Request the peer's DAO fork header for extra-data validation
290+
if err := p.RequestHeadersByNumber(daoBlock.Uint64(), 1, 0, false); err != nil {
291+
return err
292+
}
293+
// Start a timer to disconnect if the peer doesn't reply in time
294+
p.forkDrop = time.AfterFunc(daoChallengeTimeout, func() {
295+
glog.V(logger.Warn).Infof("%v: timed out DAO fork-check, dropping", p)
296+
pm.removePeer(p.id)
297+
})
298+
}
281299
// main loop. handle incoming messages.
282300
for {
283301
if err := pm.handleMsg(p); err != nil {
@@ -479,9 +497,43 @@ func (pm *ProtocolManager) handleMsg(p *peer) error {
479497
if err := msg.Decode(&headers); err != nil {
480498
return errResp(ErrDecode, "msg %v: %v", msg, err)
481499
}
500+
// If no headers were received, but we're expending a DAO fork check, maybe it's that
501+
if len(headers) == 0 && p.forkDrop != nil {
502+
// Possibly an empty reply to the fork header checks, sanity check TDs
503+
verifyDAO := true
504+
505+
// If we already have a DAO header, we can check the peer's TD against it. If
506+
// the peer's ahead of this, it too must have a reply to the DAO check
507+
if daoHeader := pm.blockchain.GetHeaderByNumber(pm.chainconfig.DAOForkBlock.Uint64()); daoHeader != nil {
508+
if p.Td().Cmp(pm.blockchain.GetTd(daoHeader.Hash(), daoHeader.Number.Uint64())) >= 0 {
509+
verifyDAO = false
510+
}
511+
}
512+
// If we're seemingly on the same chain, disable the drop timer
513+
if verifyDAO {
514+
glog.V(logger.Info).Infof("%v: seems to be on the same side of the DAO fork", p)
515+
p.forkDrop.Stop()
516+
p.forkDrop = nil
517+
return nil
518+
}
519+
}
482520
// Filter out any explicitly requested headers, deliver the rest to the downloader
483521
filter := len(headers) == 1
484522
if filter {
523+
// If it's a potential DAO fork check, validate against the rules
524+
if p.forkDrop != nil && pm.chainconfig.DAOForkBlock.Cmp(headers[0].Number) == 0 {
525+
// Disable the fork drop timer
526+
p.forkDrop.Stop()
527+
p.forkDrop = nil
528+
529+
// Validate the header and either drop the peer or continue
530+
if err := core.ValidateHeaderExtraData(pm.chainconfig, headers[0]); err != nil {
531+
glog.V(logger.Info).Infof("%v: verified to be on the other side of the DAO fork, dropping", p)
532+
return err
533+
}
534+
glog.V(logger.Info).Infof("%v: verified to be on the same side of the DAO fork", p)
535+
}
536+
// Irrelevant of the fork checks, send the header to the fetcher just in case
485537
headers = pm.fetcher.FilterHeaders(headers, time.Now())
486538
}
487539
if len(headers) > 0 || !filter {

eth/handler_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"math/big"
2121
"math/rand"
2222
"testing"
23+
"time"
2324

2425
"github.com/ethereum/go-ethereum/common"
2526
"github.com/ethereum/go-ethereum/core"
@@ -28,6 +29,7 @@ import (
2829
"github.com/ethereum/go-ethereum/crypto"
2930
"github.com/ethereum/go-ethereum/eth/downloader"
3031
"github.com/ethereum/go-ethereum/ethdb"
32+
"github.com/ethereum/go-ethereum/event"
3133
"github.com/ethereum/go-ethereum/p2p"
3234
"github.com/ethereum/go-ethereum/params"
3335
)
@@ -580,3 +582,74 @@ func testGetReceipt(t *testing.T, protocol int) {
580582
t.Errorf("receipts mismatch: %v", err)
581583
}
582584
}
585+
586+
// Tests that post eth protocol handshake, DAO fork-enabled clients also execute
587+
// a DAO "challenge" verifying each others' DAO fork headers to ensure they're on
588+
// compatible chains.
589+
func TestDAOChallengeNoVsNo(t *testing.T) { testDAOChallenge(t, false, false, false) }
590+
func TestDAOChallengeNoVsPro(t *testing.T) { testDAOChallenge(t, false, true, false) }
591+
func TestDAOChallengeProVsNo(t *testing.T) { testDAOChallenge(t, true, false, false) }
592+
func TestDAOChallengeProVsPro(t *testing.T) { testDAOChallenge(t, true, true, false) }
593+
func TestDAOChallengeNoVsTimeout(t *testing.T) { testDAOChallenge(t, false, false, true) }
594+
func TestDAOChallengeProVsTimeout(t *testing.T) { testDAOChallenge(t, true, true, true) }
595+
596+
func testDAOChallenge(t *testing.T, localForked, remoteForked bool, timeout bool) {
597+
// Reduce the DAO handshake challenge timeout
598+
if timeout {
599+
defer func(old time.Duration) { daoChallengeTimeout = old }(daoChallengeTimeout)
600+
daoChallengeTimeout = 500 * time.Millisecond
601+
}
602+
// Create a DAO aware protocol manager
603+
var (
604+
evmux = new(event.TypeMux)
605+
pow = new(core.FakePow)
606+
db, _ = ethdb.NewMemDatabase()
607+
genesis = core.WriteGenesisBlockForTesting(db)
608+
config = &core.ChainConfig{DAOForkBlock: big.NewInt(1), DAOForkSupport: localForked}
609+
blockchain, _ = core.NewBlockChain(db, config, pow, evmux)
610+
)
611+
pm, err := NewProtocolManager(config, false, NetworkId, evmux, new(testTxPool), pow, blockchain, db)
612+
if err != nil {
613+
t.Fatalf("failed to start test protocol manager: %v", err)
614+
}
615+
pm.Start()
616+
defer pm.Stop()
617+
618+
// Connect a new peer and check that we receive the DAO challenge
619+
peer, _ := newTestPeer("peer", eth63, pm, true)
620+
defer peer.close()
621+
622+
challenge := &getBlockHeadersData{
623+
Origin: hashOrNumber{Number: config.DAOForkBlock.Uint64()},
624+
Amount: 1,
625+
Skip: 0,
626+
Reverse: false,
627+
}
628+
if err := p2p.ExpectMsg(peer.app, GetBlockHeadersMsg, challenge); err != nil {
629+
t.Fatalf("challenge mismatch: %v", err)
630+
}
631+
// Create a block to reply to the challenge if no timeout is simualted
632+
if !timeout {
633+
blocks, _ := core.GenerateChain(genesis, db, 1, func(i int, block *core.BlockGen) {
634+
if remoteForked {
635+
block.SetExtra(params.DAOForkBlockExtra)
636+
}
637+
})
638+
if err := p2p.Send(peer.app, BlockHeadersMsg, []*types.Header{blocks[0].Header()}); err != nil {
639+
t.Fatalf("failed to answer challenge: %v", err)
640+
}
641+
} else {
642+
// Otherwise wait until the test timeout passes
643+
time.Sleep(daoChallengeTimeout + 500*time.Millisecond)
644+
}
645+
// Verify that depending on fork side, the remote peer is maintained or dropped
646+
if localForked == remoteForked && !timeout {
647+
if peers := pm.peers.Len(); peers != 1 {
648+
t.Fatalf("peer count mismatch: have %d, want %d", peers, 1)
649+
}
650+
} else {
651+
if peers := pm.peers.Len(); peers != 0 {
652+
t.Fatalf("peer count mismatch: have %d, want %d", peers, 0)
653+
}
654+
}
655+
}

eth/peer.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@ type peer struct {
5959
*p2p.Peer
6060
rw p2p.MsgReadWriter
6161

62-
version int // Protocol version negotiated
63-
head common.Hash
64-
td *big.Int
65-
lock sync.RWMutex
62+
version int // Protocol version negotiated
63+
forkDrop *time.Timer // Timed connection dropper if forks aren't validated in time
64+
65+
head common.Hash
66+
td *big.Int
67+
lock sync.RWMutex
6668

6769
knownTxs *set.Set // Set of transaction hashes known to be known by this peer
6870
knownBlocks *set.Set // Set of block hashes known to be known by this peer

0 commit comments

Comments
 (0)