From dedfef1f9f66370d5b9da8e571d7488ab9dd2c84 Mon Sep 17 00:00:00 2001 From: Shane Bammel Date: Tue, 18 May 2021 03:37:18 -0500 Subject: [PATCH] feat(eth): implement EIP-2364 - xdc/164 handshake with fork ID #20140 Adapt go-ethereum PR #20140 (EIP-2364) to extend the Status handshake for xdc/164 with ForkID-based compatibility checks. - Keep runtime protocol negotiation as [xdc164, xdc100, eth63]. - Use statusData (with ForkID) for xdc164 and statusData63 (legacy, no ForkID) for xdc100/eth63. - Split status decoding into readStatus/readStatusLegacy and validate ForkID through the ProtocolManager fork filter. - Align status error naming with upstream and include ErrForkIDRejected. Tests: - Move the newly added 64-path coverage to 164-path coverage to match runtime protocol support. - Add status error coverage for protocol 63/100/164. - Validate fork split behavior with protocol 164. - Add 164 variants for header/body/node-data/receipt and tx send/receive tests. Compatibility note: xdc100 keeps the legacy statusData63 layout for peers that still run older xdpos2/100 implementations without ForkID. Refs: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2364.md --- core/forkid/forkid.go | 7 +- eth/handler.go | 7 +- eth/handler_test.go | 18 ++- eth/helper_test.go | 33 +++-- eth/peer.go | 92 +++++++++++--- eth/protocol.go | 47 +++++--- eth/protocol_test.go | 275 +++++++++++++++++++++++++++++++++++++++--- 7 files changed, 408 insertions(+), 71 deletions(-) diff --git a/core/forkid/forkid.go b/core/forkid/forkid.go index a3eb261034fa..fba8fcbe4d33 100644 --- a/core/forkid/forkid.go +++ b/core/forkid/forkid.go @@ -59,6 +59,9 @@ type ID struct { Next uint64 // Block number of the next upcoming fork, or 0 if no forks are known } +// Filter is a fork id filter to validate a remotely advertised ID. +type Filter func(id ID) error + // NewID calculates the XDC fork ID from the chain config, genesis hash, head. func NewID(config *params.ChainConfig, genesis *types.Block, head uint64) ID { // Calculate the starting checksum from the genesis hash @@ -79,7 +82,7 @@ func NewID(config *params.ChainConfig, genesis *types.Block, head uint64) ID { // NewFilter creates a filter that returns if a fork ID should be rejected or not // based on the local chain's status. -func NewFilter(chain Blockchain) func(id ID) error { +func NewFilter(chain Blockchain) Filter { return newFilter( chain.Config(), chain.Genesis().Hash(), @@ -136,7 +139,7 @@ func newFilter(config *params.ChainConfig, genesis common.Hash, headfn func() ui for i, fork := range forks { // If our head is beyond this fork, continue to the next (we have a dummy // fork of maxuint64 as the last item to always fail this check eventually). - if head > fork { + if head >= fork { continue } // Found the first unpassed fork block, check if our current state matches diff --git a/eth/handler.go b/eth/handler.go index 6ce236a7d9b2..69d6401e6567 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -30,6 +30,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" "github.com/XinFinOrg/XDPoSChain/consensus/misc" "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/forkid" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/eth/bft" "github.com/XinFinOrg/XDPoSChain/eth/downloader" @@ -61,7 +62,8 @@ func errResp(code errCode, format string, v ...interface{}) error { } type ProtocolManager struct { - networkId uint64 + networkId uint64 + forkFilter forkid.Filter // Fork ID filter, constant across the lifetime of the node snapSync uint32 // Flag whether snap sync is enabled (gets disabled if we already have blocks) acceptTxs uint32 // Flag whether we're considered synchronised (enables transaction processing) @@ -122,6 +124,7 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne // Create the protocol manager with the base fields manager := &ProtocolManager{ networkId: networkID, + forkFilter: forkid.NewFilter(blockchain), eventMux: mux, txpool: txpool, blockchain: blockchain, @@ -343,7 +346,7 @@ func (pm *ProtocolManager) handle(p *peer) error { number = head.Number.Uint64() td = pm.blockchain.GetTd(hash, number) ) - if err := p.Handshake(pm.networkId, td, hash, genesis.Hash()); err != nil { + if err := p.Handshake(pm.networkId, td, hash, genesis.Hash(), forkid.NewID(pm.blockchain.Config(), genesis, number), pm.forkFilter); err != nil { p.Log().Debug("Ethereum handshake failed", "err", err) return err } diff --git a/eth/handler_test.go b/eth/handler_test.go index f1bf84d7b9bc..210df648d43a 100644 --- a/eth/handler_test.go +++ b/eth/handler_test.go @@ -39,10 +39,8 @@ import ( ) // Tests that block headers can be retrieved from a remote chain based on user queries. -func TestGetBlockHeaders62(t *testing.T) { testGetBlockHeaders(t, 62) } - -// TestGetBlockHeaders63 tests get block headers 63. -func TestGetBlockHeaders63(t *testing.T) { testGetBlockHeaders(t, 63) } +func TestGetBlockHeaders63(t *testing.T) { testGetBlockHeaders(t, 63) } +func TestGetBlockHeaders164(t *testing.T) { testGetBlockHeaders(t, 164) } func testGetBlockHeaders(t *testing.T, protocol int) { pm, _ := newTestProtocolManagerMust(t, downloader.FullSync, downloader.MaxHashFetch+15, nil, nil) @@ -200,10 +198,8 @@ func testGetBlockHeaders(t *testing.T, protocol int) { } // Tests that block contents can be retrieved from a remote chain based on their hashes. -func TestGetBlockBodies62(t *testing.T) { testGetBlockBodies(t, 62) } - -// TestGetBlockBodies63 tests get block bodies 63. -func TestGetBlockBodies63(t *testing.T) { testGetBlockBodies(t, 63) } +func TestGetBlockBodies63(t *testing.T) { testGetBlockBodies(t, 63) } +func TestGetBlockBodies164(t *testing.T) { testGetBlockBodies(t, 164) } func testGetBlockBodies(t *testing.T, protocol int) { pm, _ := newTestProtocolManagerMust(t, downloader.FullSync, downloader.MaxBlockFetch+15, nil, nil) @@ -274,7 +270,8 @@ func testGetBlockBodies(t *testing.T, protocol int) { } // Tests that the node state database can be retrieved based on hashes. -func TestGetNodeData63(t *testing.T) { testGetNodeData(t, 63) } +func TestGetNodeData63(t *testing.T) { testGetNodeData(t, 63) } +func TestGetNodeData164(t *testing.T) { testGetNodeData(t, 164) } func testGetNodeData(t *testing.T, protocol int) { // Define three accounts to simulate transactions with @@ -372,7 +369,8 @@ func testGetNodeData(t *testing.T, protocol int) { } // Tests that the transaction receipts can be retrieved based on hashes. -func TestGetReceipt63(t *testing.T) { testGetReceipt(t, 63) } +func TestGetReceipt63(t *testing.T) { testGetReceipt(t, 63) } +func TestGetReceipt164(t *testing.T) { testGetReceipt(t, 164) } func testGetReceipt(t *testing.T, protocol int) { // Define three accounts to simulate transactions with diff --git a/eth/helper_test.go b/eth/helper_test.go index c586cd9a5828..042af635244a 100644 --- a/eth/helper_test.go +++ b/eth/helper_test.go @@ -22,6 +22,7 @@ package eth import ( "crypto/ecdsa" "crypto/rand" + "fmt" "math/big" "sort" "sync" @@ -30,6 +31,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/consensus/ethash" "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/forkid" "github.com/XinFinOrg/XDPoSChain/core/rawdb" "github.com/XinFinOrg/XDPoSChain/core/txpool" "github.com/XinFinOrg/XDPoSChain/core/types" @@ -229,20 +231,35 @@ func newTestPeer(name string, version int, pm *ProtocolManager, shake bool) (*te head = pm.blockchain.CurrentHeader() td = pm.blockchain.GetTd(head.Hash(), head.Number.Uint64()) ) - tp.handshake(nil, td, head.Hash(), genesis.Hash()) + tp.handshake(nil, td, head.Hash(), genesis.Hash(), forkid.NewID(pm.blockchain.Config(), genesis, head.Number.Uint64()), forkid.NewFilter(pm.blockchain)) } return tp, errc } // handshake simulates a trivial handshake that expects the same state from the // remote side as we are simulating locally. -func (p *testPeer) handshake(t *testing.T, td *big.Int, head common.Hash, genesis common.Hash) { - msg := &statusData{ - ProtocolVersion: uint32(p.version), - NetworkId: ethconfig.Defaults.NetworkId, - TD: td, - CurrentBlock: head, - GenesisBlock: genesis, +func (p *testPeer) handshake(t *testing.T, td *big.Int, head common.Hash, genesis common.Hash, forkID forkid.ID, forkFilter forkid.Filter) { + var msg interface{} + switch p.version { + case xdc164: + msg = &statusData{ + ProtocolVersion: uint32(p.version), + NetworkID: ethconfig.Defaults.NetworkId, + TD: td, + Head: head, + Genesis: genesis, + ForkID: forkID, + } + case xdc100, eth63: + msg = &statusData63{ + ProtocolVersion: uint32(p.version), + NetworkId: ethconfig.Defaults.NetworkId, + TD: td, + CurrentBlock: head, + GenesisBlock: genesis, + } + default: + panic(fmt.Sprintf("unsupported eth protocol version: %d", p.version)) } if err := p2p.ExpectMsg(p.app, StatusMsg, msg); err != nil { t.Fatalf("status recv: %v", err) diff --git a/eth/peer.go b/eth/peer.go index a0be36675725..7194e68677de 100644 --- a/eth/peer.go +++ b/eth/peer.go @@ -24,6 +24,7 @@ import ( "time" "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core/forkid" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/p2p" "github.com/XinFinOrg/XDPoSChain/rlp" @@ -378,26 +379,50 @@ func (p *peer) RequestReceipts(hashes []common.Hash) error { // Handshake executes the eth protocol handshake, negotiating version number, // network IDs, difficulties, head and genesis blocks. -func (p *peer) Handshake(network uint64, td *big.Int, head common.Hash, genesis common.Hash) error { +func (p *peer) Handshake(network uint64, td *big.Int, head common.Hash, genesis common.Hash, forkID forkid.ID, forkFilter forkid.Filter) error { // Send out own handshake in a new thread errc := make(chan error, 2) - var status statusData // safe to read after two values have been received from errc + var ( + status63 statusData63 // safe to read after two values have been received from errc + status statusData // safe to read after two values have been received from errc + ) go func() { - errc <- p2p.Send(p.rw, StatusMsg, &statusData{ - ProtocolVersion: uint32(p.version), - NetworkId: network, - TD: td, - CurrentBlock: head, - GenesisBlock: genesis, - }) + switch p.version { + case xdc164: + errc <- p2p.Send(p.rw, StatusMsg, &statusData{ + ProtocolVersion: uint32(p.version), + NetworkID: network, + TD: td, + Head: head, + Genesis: genesis, + ForkID: forkID, + }) + case xdc100, eth63: + errc <- p2p.Send(p.rw, StatusMsg, &statusData63{ + ProtocolVersion: uint32(p.version), + NetworkId: network, + TD: td, + CurrentBlock: head, + GenesisBlock: genesis, + }) + default: + errc <- errResp(ErrProtocolVersionMismatch, "unsupported eth protocol version: %d", p.version) + } }() go func() { - errc <- p.readStatus(network, &status, genesis) + switch p.version { + case xdc164: + errc <- p.readStatus(network, &status, genesis, forkFilter) + case xdc100, eth63: + errc <- p.readStatusLegacy(network, &status63, genesis) + default: + errc <- errResp(ErrProtocolVersionMismatch, "unsupported eth protocol version: %d", p.version) + } }() timeout := time.NewTimer(handshakeTimeout) defer timeout.Stop() - for i := 0; i < 2; i++ { + for range 2 { select { case err := <-errc: if err != nil { @@ -407,11 +432,18 @@ func (p *peer) Handshake(network uint64, td *big.Int, head common.Hash, genesis return p2p.DiscReadTimeout } } - p.td, p.head = status.TD, status.CurrentBlock + switch p.version { + case xdc164: + p.td, p.head = status.TD, status.Head + case xdc100, eth63: + p.td, p.head = status63.TD, status63.CurrentBlock + default: + return errResp(ErrProtocolVersionMismatch, "unsupported eth protocol version: %d", p.version) + } return nil } -func (p *peer) readStatus(network uint64, status *statusData, genesis common.Hash) (err error) { +func (p *peer) readStatusLegacy(network uint64, status *statusData63, genesis common.Hash) error { msg, err := p.rw.ReadMsg() if err != nil { return err @@ -427,14 +459,44 @@ func (p *peer) readStatus(network uint64, status *statusData, genesis common.Has return errResp(ErrDecode, "msg %v: %v", msg, err) } if status.GenesisBlock != genesis { - return errResp(ErrGenesisBlockMismatch, "%x (!= %x)", status.GenesisBlock[:8], genesis[:8]) + return errResp(ErrGenesisMismatch, "%x (!= %x)", status.GenesisBlock[:8], genesis[:8]) } if status.NetworkId != network { - return errResp(ErrNetworkIdMismatch, "%d (!= %d)", status.NetworkId, network) + return errResp(ErrNetworkIDMismatch, "%d (!= %d)", status.NetworkId, network) + } + if int(status.ProtocolVersion) != p.version { + return errResp(ErrProtocolVersionMismatch, "%d (!= %d)", status.ProtocolVersion, p.version) + } + return nil +} + +func (p *peer) readStatus(network uint64, status *statusData, genesis common.Hash, forkFilter forkid.Filter) error { + msg, err := p.rw.ReadMsg() + if err != nil { + return err + } + if msg.Code != StatusMsg { + return errResp(ErrNoStatusMsg, "first msg has code %x (!= %x)", msg.Code, StatusMsg) + } + if msg.Size > protocolMaxMsgSize { + return errResp(ErrMsgTooLarge, "%v > %v", msg.Size, protocolMaxMsgSize) + } + // Decode the handshake and make sure everything matches + if err := msg.Decode(&status); err != nil { + return errResp(ErrDecode, "msg %v: %v", msg, err) + } + if status.NetworkID != network { + return errResp(ErrNetworkIDMismatch, "%d (!= %d)", status.NetworkID, network) } if int(status.ProtocolVersion) != p.version { return errResp(ErrProtocolVersionMismatch, "%d (!= %d)", status.ProtocolVersion, p.version) } + if status.Genesis != genesis { + return errResp(ErrGenesisMismatch, "%x (!= %x)", status.Genesis, genesis) + } + if err := forkFilter(status.ForkID); err != nil { + return errResp(ErrForkIDRejected, "%v", err) + } return nil } diff --git a/eth/protocol.go b/eth/protocol.go index ca3583647e22..d2307b51f1be 100644 --- a/eth/protocol.go +++ b/eth/protocol.go @@ -23,6 +23,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/forkid" "github.com/XinFinOrg/XDPoSChain/core/txpool" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/event" @@ -31,25 +32,24 @@ import ( // Constants to match up protocol versions and messages const ( - eth62 = 62 eth63 = 63 - xdpos2 = 100 + xdc100 = 100 + xdc164 = 164 // eth64 ) // protocolName is the official short name of the protocol used during capability negotiation. var protocolName = "eth" // ProtocolVersions are the supported versions of the eth protocol (first is primary). -var ProtocolVersions = []uint{xdpos2, eth63} +var ProtocolVersions = []uint{xdc164, xdc100, eth63} // protocolLengths are the number of implemented message corresponding to different protocol versions. -var protocolLengths = map[uint]uint64{xdpos2: 227, eth63: 17, eth62: 8} +var protocolLengths = map[uint]uint64{xdc164: 227, xdc100: 227, eth63: 17} const protocolMaxMsgSize = 10 * 1024 * 1024 // Maximum cap on the size of a protocol message // eth protocol message codes const ( - // Protocol messages belonging to eth/62 StatusMsg = 0x00 NewBlockHashesMsg = 0x01 TxMsg = 0x02 @@ -60,13 +60,12 @@ const ( NewBlockMsg = 0x07 OrderTxMsg = 0x08 LendingTxMsg = 0x09 - // Protocol messages belonging to eth/63 - GetNodeDataMsg = 0x0d - NodeDataMsg = 0x0e - GetReceiptsMsg = 0x0f - ReceiptsMsg = 0x10 + GetNodeDataMsg = 0x0d + NodeDataMsg = 0x0e + GetReceiptsMsg = 0x0f + ReceiptsMsg = 0x10 - // Protocol messages belonging to xdpos2/100 + // Protocol message codes introduced in xdc/100 VoteMsg = 0xe0 TimeoutMsg = 0xe1 SyncInfoMsg = 0xe2 @@ -79,11 +78,11 @@ const ( ErrDecode ErrInvalidMsgCode ErrProtocolVersionMismatch - ErrNetworkIdMismatch - ErrGenesisBlockMismatch + ErrNetworkIDMismatch + ErrGenesisMismatch + ErrForkIDRejected ErrNoStatusMsg ErrExtraStatusMsg - ErrSuspendedPeer ) func (e errCode) String() string { @@ -96,11 +95,11 @@ var errorToString = map[int]string{ ErrDecode: "Invalid message", ErrInvalidMsgCode: "Invalid message code", ErrProtocolVersionMismatch: "Protocol version mismatch", - ErrNetworkIdMismatch: "NetworkId mismatch", - ErrGenesisBlockMismatch: "Genesis block mismatch", + ErrNetworkIDMismatch: "Network ID mismatch", + ErrGenesisMismatch: "Genesis mismatch", + ErrForkIDRejected: "Fork ID rejected", ErrNoStatusMsg: "No status message", ErrExtraStatusMsg: "Extra status message", - ErrSuspendedPeer: "Suspended peer", } type txPool interface { @@ -143,8 +142,8 @@ type lendingPool interface { SubscribeTxPreEvent(chan<- core.LendingTxPreEvent) event.Subscription } -// statusData is the network packet for the status message. -type statusData struct { +// statusData63 is the network packet for the status message for eth/63 and xdc/100. +type statusData63 struct { ProtocolVersion uint32 NetworkId uint64 TD *big.Int @@ -152,6 +151,16 @@ type statusData struct { GenesisBlock common.Hash } +// statusData is the network packet for the status message for eth/64, xdc/164 and later. +type statusData struct { + ProtocolVersion uint32 + NetworkID uint64 + TD *big.Int + Head common.Hash + Genesis common.Hash + ForkID forkid.ID +} + // newBlockHashesData is the network packet for the block announcements. type newBlockHashesData []struct { Hash common.Hash // Hash of one particular block being announced diff --git a/eth/protocol_test.go b/eth/protocol_test.go index c85815150c2e..79162d7366b7 100644 --- a/eth/protocol_test.go +++ b/eth/protocol_test.go @@ -18,16 +18,25 @@ package eth import ( "fmt" + "math/big" "sync" "testing" "time" "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" + "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/forkid" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/core/vm" "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/eth/downloader" "github.com/XinFinOrg/XDPoSChain/eth/ethconfig" + "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/p2p" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/rlp" ) @@ -38,15 +47,64 @@ func init() { var testAccount, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") // Tests that handshake failures are detected and reported correctly. -func TestStatusMsgErrors62(t *testing.T) { testStatusMsgErrors(t, 62) } -func TestStatusMsgErrors63(t *testing.T) { testStatusMsgErrors(t, 63) } +func TestStatusMsgErrors63(t *testing.T) { + pm, _ := newTestProtocolManagerMust(t, downloader.FullSync, 0, nil, nil) + var ( + genesis = pm.blockchain.Genesis() + head = pm.blockchain.CurrentHeader() + td = pm.blockchain.GetTd(head.Hash(), head.Number.Uint64()) + ) + defer pm.Stop() + + tests := []struct { + code uint64 + data interface{} + wantError error + }{ + { + code: TxMsg, data: []interface{}{}, + wantError: errResp(ErrNoStatusMsg, "first msg has code 2 (!= 0)"), + }, + { + code: StatusMsg, data: statusData63{10, ethconfig.Defaults.NetworkId, td, head.Hash(), genesis.Hash()}, + wantError: errResp(ErrProtocolVersionMismatch, "10 (!= %d)", 63), + }, + { + code: StatusMsg, data: statusData63{63, 999, td, head.Hash(), genesis.Hash()}, + wantError: errResp(ErrNetworkIDMismatch, "999 (!= %d)", ethconfig.Defaults.NetworkId), + }, + { + code: StatusMsg, data: statusData63{63, ethconfig.Defaults.NetworkId, td, head.Hash(), common.Hash{3}}, + wantError: errResp(ErrGenesisMismatch, "0300000000000000 (!= %x)", genesis.Hash().Bytes()[:8]), + }, + } + for i, test := range tests { + p, errc := newTestPeer("peer", 63, pm, false) + // The send call might hang until reset because + // the protocol might not read the payload. + go p2p.Send(p.app, test.code, test.data) + + select { + case err := <-errc: + if err == nil { + t.Errorf("test %d: protocol returned nil error, want %q", i, test.wantError) + } else if err.Error() != test.wantError.Error() { + t.Errorf("test %d: wrong error: got %q, want %q", i, err, test.wantError) + } + case <-time.After(2 * time.Second): + t.Errorf("protocol did not shut down within 2 seconds") + } + p.close() + } +} -func testStatusMsgErrors(t *testing.T, protocol int) { +func TestStatusMsgErrors164(t *testing.T) { pm, _ := newTestProtocolManagerMust(t, downloader.FullSync, 0, nil, nil) var ( genesis = pm.blockchain.Genesis() head = pm.blockchain.CurrentHeader() td = pm.blockchain.GetTd(head.Hash(), head.Number.Uint64()) + forkID = forkid.NewID(pm.blockchain.Config(), genesis, head.Number.Uint64()) ) defer pm.Stop() @@ -60,21 +118,75 @@ func testStatusMsgErrors(t *testing.T, protocol int) { wantError: errResp(ErrNoStatusMsg, "first msg has code 2 (!= 0)"), }, { - code: StatusMsg, data: statusData{10, ethconfig.Defaults.NetworkId, td, head.Hash(), genesis.Hash()}, - wantError: errResp(ErrProtocolVersionMismatch, "10 (!= %d)", protocol), + code: StatusMsg, data: statusData{10, ethconfig.Defaults.NetworkId, td, head.Hash(), genesis.Hash(), forkID}, + wantError: errResp(ErrProtocolVersionMismatch, "10 (!= %d)", 164), }, { - code: StatusMsg, data: statusData{uint32(protocol), 999, td, head.Hash(), genesis.Hash()}, - wantError: errResp(ErrNetworkIdMismatch, "999 (!= 0)"), + code: StatusMsg, data: statusData{164, 999, td, head.Hash(), genesis.Hash(), forkID}, + wantError: errResp(ErrNetworkIDMismatch, "999 (!= %d)", ethconfig.Defaults.NetworkId), }, { - code: StatusMsg, data: statusData{uint32(protocol), ethconfig.Defaults.NetworkId, td, head.Hash(), common.Hash{3}}, - wantError: errResp(ErrGenesisBlockMismatch, "0300000000000000 (!= %x)", genesis.Hash().Bytes()[:8]), + code: StatusMsg, data: statusData{164, ethconfig.Defaults.NetworkId, td, head.Hash(), common.Hash{3}, forkID}, + wantError: errResp(ErrGenesisMismatch, "0300000000000000000000000000000000000000000000000000000000000000 (!= %x)", genesis.Hash()), }, + { + code: StatusMsg, data: statusData{164, ethconfig.Defaults.NetworkId, td, head.Hash(), genesis.Hash(), forkid.ID{Hash: [4]byte{0x00, 0x01, 0x02, 0x03}}}, + wantError: errResp(ErrForkIDRejected, "%s", forkid.ErrLocalIncompatibleOrStale.Error()), + }, + } + for i, test := range tests { + p, errc := newTestPeer("peer", 164, pm, false) + // The send call might hang until reset because + // the protocol might not read the payload. + go p2p.Send(p.app, test.code, test.data) + + select { + case err := <-errc: + if err == nil { + t.Errorf("test %d: protocol returned nil error, want %q", i, test.wantError) + } else if err.Error() != test.wantError.Error() { + t.Errorf("test %d: wrong error: got %q, want %q", i, err, test.wantError) + } + case <-time.After(2 * time.Second): + t.Errorf("protocol did not shut down within 2 seconds") + } + p.close() } +} +func TestStatusMsgErrors100(t *testing.T) { + pm, _ := newTestProtocolManagerMust(t, downloader.FullSync, 0, nil, nil) + var ( + genesis = pm.blockchain.Genesis() + head = pm.blockchain.CurrentHeader() + td = pm.blockchain.GetTd(head.Hash(), head.Number.Uint64()) + ) + defer pm.Stop() + + tests := []struct { + code uint64 + data interface{} + wantError error + }{ + { + code: TxMsg, data: []interface{}{}, + wantError: errResp(ErrNoStatusMsg, "first msg has code 2 (!= 0)"), + }, + { + code: StatusMsg, data: statusData63{10, ethconfig.Defaults.NetworkId, td, head.Hash(), genesis.Hash()}, + wantError: errResp(ErrProtocolVersionMismatch, "10 (!= %d)", 100), + }, + { + code: StatusMsg, data: statusData63{100, 999, td, head.Hash(), genesis.Hash()}, + wantError: errResp(ErrNetworkIDMismatch, "999 (!= %d)", ethconfig.Defaults.NetworkId), + }, + { + code: StatusMsg, data: statusData63{100, ethconfig.Defaults.NetworkId, td, head.Hash(), common.Hash{3}}, + wantError: errResp(ErrGenesisMismatch, "0300000000000000 (!= %x)", genesis.Hash().Bytes()[:8]), + }, + } for i, test := range tests { - p, errc := newTestPeer("peer", protocol, pm, false) + p, errc := newTestPeer("peer", 100, pm, false) // The send call might hang until reset because // the protocol might not read the payload. go p2p.Send(p.app, test.code, test.data) @@ -86,16 +198,148 @@ func testStatusMsgErrors(t *testing.T, protocol int) { } else if err.Error() != test.wantError.Error() { t.Errorf("test %d: wrong error: got %q, want %q", i, err, test.wantError) } - case <-time.After(5 * time.Second): + case <-time.After(2 * time.Second): t.Errorf("protocol did not shut down within 2 seconds") } p.close() } } +func TestForkIDSplit(t *testing.T) { + var ( + engine = ethash.NewFaker() + + configNoFork = ¶ms.ChainConfig{ + ChainID: big.NewInt(1234), + HomesteadBlock: big.NewInt(1), + Ethash: new(params.EthashConfig), + } + configProFork = ¶ms.ChainConfig{ + ChainID: big.NewInt(1234), + HomesteadBlock: big.NewInt(1), + EIP150Block: big.NewInt(2), + EIP155Block: big.NewInt(2), + EIP158Block: big.NewInt(2), + ByzantiumBlock: big.NewInt(3), + Ethash: new(params.EthashConfig), + } + dbNoFork = rawdb.NewMemoryDatabase() + dbProFork = rawdb.NewMemoryDatabase() + + gspecNoFork = &core.Genesis{Config: configNoFork} + gspecProFork = &core.Genesis{Config: configProFork} + + genesisNoFork = gspecNoFork.MustCommit(dbNoFork) + genesisProFork = gspecProFork.MustCommit(dbProFork) + + chainNoFork, _ = core.NewBlockChain(dbNoFork, nil, gspecNoFork, engine, vm.Config{}) + chainProFork, _ = core.NewBlockChain(dbProFork, nil, gspecProFork, engine, vm.Config{}) + + blocksNoFork, _ = core.GenerateChain(configNoFork, genesisNoFork, engine, dbNoFork, 2, nil) + blocksProFork, _ = core.GenerateChain(configProFork, genesisProFork, engine, dbProFork, 2, nil) + + ethNoFork, _ = NewProtocolManager(configNoFork, downloader.FullSync, 1, new(event.TypeMux), new(testTxPool), engine, chainNoFork, dbNoFork) + ethProFork, _ = NewProtocolManager(configProFork, downloader.FullSync, 1, new(event.TypeMux), new(testTxPool), engine, chainProFork, dbProFork) + ) + ethNoFork.Start(1000) + ethProFork.Start(1000) + defer ethNoFork.Stop() + defer ethProFork.Stop() + defer chainNoFork.Stop() + defer chainProFork.Stop() + + // Both nodes should allow the other to connect (same genesis, next fork is the same) + p2pNoFork, p2pProFork := p2p.MsgPipe() + peerNoFork := newPeer(164, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork) + peerProFork := newPeer(164, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork) + + errc := make(chan error, 2) + go func() { errc <- ethNoFork.handle(peerProFork) }() + go func() { errc <- ethProFork.handle(peerNoFork) }() + + select { + case err := <-errc: + t.Fatalf("frontier nofork <-> profork failed: %v", err) + case <-time.After(250 * time.Millisecond): + p2pNoFork.Close() + p2pProFork.Close() + } + // Wait for both handle goroutines to fully return so their removePeer defers + // finish before the next phase reuses the same enode IDs. Otherwise a stale + // removePeer could disconnect the peer registered under the same ID next. + <-errc + <-errc + + // Progress into Homestead. Fork's match, so we don't care what the future holds + chainNoFork.InsertChain(blocksNoFork[:1]) + chainProFork.InsertChain(blocksProFork[:1]) + + p2pNoFork, p2pProFork = p2p.MsgPipe() + peerNoFork = newPeer(164, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork) + peerProFork = newPeer(164, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork) + + errc = make(chan error, 2) + go func() { errc <- ethNoFork.handle(peerProFork) }() + go func() { errc <- ethProFork.handle(peerNoFork) }() + + select { + case err := <-errc: + t.Fatalf("homestead nofork <-> profork failed: %v", err) + case <-time.After(250 * time.Millisecond): + p2pNoFork.Close() + p2pProFork.Close() + } + // Wait for both handle goroutines to fully return so their removePeer defers + // finish before the next phase reuses the same enode IDs. + <-errc + <-errc + + // Progress into Spurious. Forks mismatch, signalling differing chains, reject + chainNoFork.InsertChain(blocksNoFork[1:2]) + chainProFork.InsertChain(blocksProFork[1:2]) + + p2pNoFork, p2pProFork = p2p.MsgPipe() + peerNoFork = newPeer(164, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork) + peerProFork = newPeer(164, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork) + + errc = make(chan error, 2) + go func() { errc <- ethNoFork.handle(peerProFork) }() + go func() { errc <- ethProFork.handle(peerNoFork) }() + + // On a fork split the fork filter is asymmetric: at least one side rejects + // the peer during the handshake, but the other side may pass the handshake + // and block in the message loop. A rejecting peer returns before its + // removePeer defer is installed, so it never tears down the pipe itself. + // Wait for the first (rejecting) result, then close the pipes to unblock the + // peer that may still be waiting, and drain the second result. + var rejected bool + timeout := time.NewTimer(2 * time.Second) + defer timeout.Stop() + + for received := 0; received < 2; received++ { + select { + case err := <-errc: + if err != nil { + rejected = true + } + // Tear down the pipes so the peer still blocked in the message + // loop unblocks and its handle goroutine returns. + p2pNoFork.Close() + p2pProFork.Close() + case <-timeout.C: + t.Fatalf("split peers not rejected") + } + } + + if !rejected { + t.Fatalf("fork ID rejection didn't happen") + } +} + // This test checks that received transactions are added to the local pool. -func TestRecvTransactions62(t *testing.T) { testRecvTransactions(t, 62) } -func TestRecvTransactions63(t *testing.T) { testRecvTransactions(t, 63) } +func TestRecvTransactions63(t *testing.T) { testRecvTransactions(t, 63) } +func TestRecvTransactions100(t *testing.T) { testRecvTransactions(t, 100) } +func TestRecvTransactions164(t *testing.T) { testRecvTransactions(t, 164) } func testRecvTransactions(t *testing.T, protocol int) { txAdded := make(chan []*types.Transaction) @@ -122,8 +366,9 @@ func testRecvTransactions(t *testing.T, protocol int) { } // This test checks that pending transactions are sent. -func TestSendTransactions62(t *testing.T) { testSendTransactions(t, 62) } -func TestSendTransactions63(t *testing.T) { testSendTransactions(t, 63) } +func TestSendTransactions63(t *testing.T) { testSendTransactions(t, 63) } +func TestSendTransactions100(t *testing.T) { testSendTransactions(t, 100) } +func TestSendTransactions164(t *testing.T) { testSendTransactions(t, 164) } func testSendTransactions(t *testing.T, protocol int) { pm, _ := newTestProtocolManagerMust(t, downloader.FullSync, 0, nil, nil)