mirror of https://github.com/ethereum/go-ethereum
cmd/devp2p: add eth protocol test suite (#21598)
This change adds a test framework for the "eth" protocol and some basic tests. The tests can be run using the './devp2p rlpx eth-test' command.pull/21603/head
parent
c1544423d6
commit
a25899f3dc
@ -0,0 +1,113 @@ |
||||
package ethtest |
||||
|
||||
import ( |
||||
"compress/gzip" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"math/big" |
||||
"os" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/core" |
||||
"github.com/ethereum/go-ethereum/core/forkid" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/params" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
type Chain struct { |
||||
blocks []*types.Block |
||||
chainConfig *params.ChainConfig |
||||
} |
||||
|
||||
func (c *Chain) WriteTo(writer io.Writer) error { |
||||
for _, block := range c.blocks { |
||||
if err := rlp.Encode(writer, block); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Len returns the length of the chain.
|
||||
func (c *Chain) Len() int { |
||||
return len(c.blocks) |
||||
} |
||||
|
||||
// TD calculates the total difficulty of the chain.
|
||||
func (c *Chain) TD(height int) *big.Int { // TODO later on channge scheme so that the height is included in range
|
||||
sum := big.NewInt(0) |
||||
for _, block := range c.blocks[:height] { |
||||
sum.Add(sum, block.Difficulty()) |
||||
} |
||||
return sum |
||||
} |
||||
|
||||
// ForkID gets the fork id of the chain.
|
||||
func (c *Chain) ForkID() forkid.ID { |
||||
return forkid.NewID(c.chainConfig, c.blocks[0].Hash(), uint64(c.Len())) |
||||
} |
||||
|
||||
// Shorten returns a copy chain of a desired height from the imported
|
||||
func (c *Chain) Shorten(height int) *Chain { |
||||
blocks := make([]*types.Block, height) |
||||
copy(blocks, c.blocks[:height]) |
||||
|
||||
config := *c.chainConfig |
||||
return &Chain{ |
||||
blocks: blocks, |
||||
chainConfig: &config, |
||||
} |
||||
} |
||||
|
||||
// Head returns the chain head.
|
||||
func (c *Chain) Head() *types.Block { |
||||
return c.blocks[c.Len()-1] |
||||
} |
||||
|
||||
// loadChain takes the given chain.rlp file, and decodes and returns
|
||||
// the blocks from the file.
|
||||
func loadChain(chainfile string, genesis string) (*Chain, error) { |
||||
// Open the file handle and potentially unwrap the gzip stream
|
||||
fh, err := os.Open(chainfile) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer fh.Close() |
||||
|
||||
var reader io.Reader = fh |
||||
if strings.HasSuffix(chainfile, ".gz") { |
||||
if reader, err = gzip.NewReader(reader); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
stream := rlp.NewStream(reader, 0) |
||||
var blocks []*types.Block |
||||
for i := 0; ; i++ { |
||||
var b types.Block |
||||
if err := stream.Decode(&b); err == io.EOF { |
||||
break |
||||
} else if err != nil { |
||||
return nil, fmt.Errorf("at block %d: %v", i, err) |
||||
} |
||||
blocks = append(blocks, &b) |
||||
} |
||||
|
||||
// Open the file handle and potentially unwrap the gzip stream
|
||||
chainConfig, err := ioutil.ReadFile(genesis) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var gen core.Genesis |
||||
if err := json.Unmarshal(chainConfig, &gen); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &Chain{ |
||||
blocks: blocks, |
||||
chainConfig: gen.Config, |
||||
}, nil |
||||
} |
@ -0,0 +1,337 @@ |
||||
package ethtest |
||||
|
||||
import ( |
||||
"crypto/ecdsa" |
||||
"fmt" |
||||
"net" |
||||
"reflect" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/crypto" |
||||
"github.com/ethereum/go-ethereum/internal/utesting" |
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/p2p/enode" |
||||
"github.com/ethereum/go-ethereum/p2p/rlpx" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
// Suite represents a structure used to test the eth
|
||||
// protocol of a node(s).
|
||||
type Suite struct { |
||||
Dest *enode.Node |
||||
|
||||
chain *Chain |
||||
fullChain *Chain |
||||
} |
||||
|
||||
type Conn struct { |
||||
*rlpx.Conn |
||||
ourKey *ecdsa.PrivateKey |
||||
} |
||||
|
||||
func (c *Conn) Read() Message { |
||||
code, rawData, _, err := c.Conn.Read() |
||||
if err != nil { |
||||
return &Error{fmt.Errorf("could not read from connection: %v", err)} |
||||
} |
||||
|
||||
var msg Message |
||||
switch int(code) { |
||||
case (Hello{}).Code(): |
||||
msg = new(Hello) |
||||
case (Disconnect{}).Code(): |
||||
msg = new(Disconnect) |
||||
case (Status{}).Code(): |
||||
msg = new(Status) |
||||
case (GetBlockHeaders{}).Code(): |
||||
msg = new(GetBlockHeaders) |
||||
case (BlockHeaders{}).Code(): |
||||
msg = new(BlockHeaders) |
||||
case (GetBlockBodies{}).Code(): |
||||
msg = new(GetBlockBodies) |
||||
case (BlockBodies{}).Code(): |
||||
msg = new(BlockBodies) |
||||
case (NewBlock{}).Code(): |
||||
msg = new(NewBlock) |
||||
case (NewBlockHashes{}).Code(): |
||||
msg = new(NewBlockHashes) |
||||
default: |
||||
return &Error{fmt.Errorf("invalid message code: %d", code)} |
||||
} |
||||
|
||||
if err := rlp.DecodeBytes(rawData, msg); err != nil { |
||||
return &Error{fmt.Errorf("could not rlp decode message: %v", err)} |
||||
} |
||||
|
||||
return msg |
||||
} |
||||
|
||||
func (c *Conn) Write(msg Message) error { |
||||
payload, err := rlp.EncodeToBytes(msg) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
_, err = c.Conn.Write(uint64(msg.Code()), payload) |
||||
return err |
||||
|
||||
} |
||||
|
||||
// handshake checks to make sure a `HELLO` is received.
|
||||
func (c *Conn) handshake(t *utesting.T) Message { |
||||
// write protoHandshake to client
|
||||
pub0 := crypto.FromECDSAPub(&c.ourKey.PublicKey)[1:] |
||||
ourHandshake := &Hello{ |
||||
Version: 3, |
||||
Caps: []p2p.Cap{{Name: "eth", Version: 64}, {Name: "eth", Version: 65}}, |
||||
ID: pub0, |
||||
} |
||||
if err := c.Write(ourHandshake); err != nil { |
||||
t.Fatalf("could not write to connection: %v", err) |
||||
} |
||||
// read protoHandshake from client
|
||||
switch msg := c.Read().(type) { |
||||
case *Hello: |
||||
return msg |
||||
default: |
||||
t.Fatalf("bad handshake: %v", msg) |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
// statusExchange performs a `Status` message exchange with the given
|
||||
// node.
|
||||
func (c *Conn) statusExchange(t *utesting.T, chain *Chain) Message { |
||||
// read status message from client
|
||||
var message Message |
||||
switch msg := c.Read().(type) { |
||||
case *Status: |
||||
if msg.Head != chain.blocks[chain.Len()-1].Hash() { |
||||
t.Fatalf("wrong head in status: %v", msg.Head) |
||||
} |
||||
if msg.TD.Cmp(chain.TD(chain.Len())) != 0 { |
||||
t.Fatalf("wrong TD in status: %v", msg.TD) |
||||
} |
||||
if !reflect.DeepEqual(msg.ForkID, chain.ForkID()) { |
||||
t.Fatalf("wrong fork ID in status: %v", msg.ForkID) |
||||
} |
||||
message = msg |
||||
default: |
||||
t.Fatalf("bad status message: %v", msg) |
||||
} |
||||
// write status message to client
|
||||
status := Status{ |
||||
ProtocolVersion: 65, |
||||
NetworkID: 1, |
||||
TD: chain.TD(chain.Len()), |
||||
Head: chain.blocks[chain.Len()-1].Hash(), |
||||
Genesis: chain.blocks[0].Hash(), |
||||
ForkID: chain.ForkID(), |
||||
} |
||||
if err := c.Write(status); err != nil { |
||||
t.Fatalf("could not write to connection: %v", err) |
||||
} |
||||
|
||||
return message |
||||
} |
||||
|
||||
// waitForBlock waits for confirmation from the client that it has
|
||||
// imported the given block.
|
||||
func (c *Conn) waitForBlock(block *types.Block) error { |
||||
for { |
||||
req := &GetBlockHeaders{Origin: hashOrNumber{Hash: block.Hash()}, Amount: 1} |
||||
if err := c.Write(req); err != nil { |
||||
return err |
||||
} |
||||
|
||||
switch msg := c.Read().(type) { |
||||
case *BlockHeaders: |
||||
if len(*msg) > 0 { |
||||
return nil |
||||
} |
||||
time.Sleep(100 * time.Millisecond) |
||||
default: |
||||
return fmt.Errorf("invalid message: %v", msg) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// NewSuite creates and returns a new eth-test suite that can
|
||||
// be used to test the given node against the given blockchain
|
||||
// data.
|
||||
func NewSuite(dest *enode.Node, chainfile string, genesisfile string) *Suite { |
||||
chain, err := loadChain(chainfile, genesisfile) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return &Suite{ |
||||
Dest: dest, |
||||
chain: chain.Shorten(1000), |
||||
fullChain: chain, |
||||
} |
||||
} |
||||
|
||||
func (s *Suite) AllTests() []utesting.Test { |
||||
return []utesting.Test{ |
||||
{Name: "Status", Fn: s.TestStatus}, |
||||
{Name: "GetBlockHeaders", Fn: s.TestGetBlockHeaders}, |
||||
{Name: "Broadcast", Fn: s.TestBroadcast}, |
||||
{Name: "GetBlockBodies", Fn: s.TestGetBlockBodies}, |
||||
} |
||||
} |
||||
|
||||
// TestStatus attempts to connect to the given node and exchange
|
||||
// a status message with it, and then check to make sure
|
||||
// the chain head is correct.
|
||||
func (s *Suite) TestStatus(t *utesting.T) { |
||||
conn, err := s.dial() |
||||
if err != nil { |
||||
t.Fatalf("could not dial: %v", err) |
||||
} |
||||
// get protoHandshake
|
||||
conn.handshake(t) |
||||
// get status
|
||||
switch msg := conn.statusExchange(t, s.chain).(type) { |
||||
case *Status: |
||||
t.Logf("%+v\n", msg) |
||||
default: |
||||
t.Fatalf("error: %v", msg) |
||||
} |
||||
} |
||||
|
||||
// TestGetBlockHeaders tests whether the given node can respond to
|
||||
// a `GetBlockHeaders` request and that the response is accurate.
|
||||
func (s *Suite) TestGetBlockHeaders(t *utesting.T) { |
||||
conn, err := s.dial() |
||||
if err != nil { |
||||
t.Fatalf("could not dial: %v", err) |
||||
} |
||||
|
||||
conn.handshake(t) |
||||
conn.statusExchange(t, s.chain) |
||||
|
||||
// get block headers
|
||||
req := &GetBlockHeaders{ |
||||
Origin: hashOrNumber{ |
||||
Hash: s.chain.blocks[1].Hash(), |
||||
}, |
||||
Amount: 2, |
||||
Skip: 1, |
||||
Reverse: false, |
||||
} |
||||
|
||||
if err := conn.Write(req); err != nil { |
||||
t.Fatalf("could not write to connection: %v", err) |
||||
} |
||||
|
||||
switch msg := conn.Read().(type) { |
||||
case *BlockHeaders: |
||||
headers := msg |
||||
for _, header := range *headers { |
||||
num := header.Number.Uint64() |
||||
assert.Equal(t, s.chain.blocks[int(num)].Header(), header) |
||||
t.Logf("\nHEADER FOR BLOCK NUMBER %d: %+v\n", header.Number, header) |
||||
} |
||||
default: |
||||
t.Fatalf("error: %v", msg) |
||||
} |
||||
} |
||||
|
||||
// TestGetBlockBodies tests whether the given node can respond to
|
||||
// a `GetBlockBodies` request and that the response is accurate.
|
||||
func (s *Suite) TestGetBlockBodies(t *utesting.T) { |
||||
conn, err := s.dial() |
||||
if err != nil { |
||||
t.Fatalf("could not dial: %v", err) |
||||
} |
||||
|
||||
conn.handshake(t) |
||||
conn.statusExchange(t, s.chain) |
||||
// create block bodies request
|
||||
req := &GetBlockBodies{s.chain.blocks[54].Hash(), s.chain.blocks[75].Hash()} |
||||
if err := conn.Write(req); err != nil { |
||||
t.Fatalf("could not write to connection: %v", err) |
||||
} |
||||
|
||||
switch msg := conn.Read().(type) { |
||||
case *BlockBodies: |
||||
bodies := msg |
||||
for _, body := range *bodies { |
||||
t.Logf("\nBODY: %+v\n", body) |
||||
} |
||||
default: |
||||
t.Fatalf("error: %v", msg) |
||||
} |
||||
} |
||||
|
||||
// TestBroadcast tests whether a block announcement is correctly
|
||||
// propagated to the given node's peer(s).
|
||||
func (s *Suite) TestBroadcast(t *utesting.T) { |
||||
// create conn to send block announcement
|
||||
sendConn, err := s.dial() |
||||
if err != nil { |
||||
t.Fatalf("could not dial: %v", err) |
||||
} |
||||
// create conn to receive block announcement
|
||||
receiveConn, err := s.dial() |
||||
if err != nil { |
||||
t.Fatalf("could not dial: %v", err) |
||||
} |
||||
|
||||
sendConn.handshake(t) |
||||
receiveConn.handshake(t) |
||||
|
||||
sendConn.statusExchange(t, s.chain) |
||||
receiveConn.statusExchange(t, s.chain) |
||||
|
||||
// sendConn sends the block announcement
|
||||
blockAnnouncement := &NewBlock{ |
||||
Block: s.fullChain.blocks[1000], |
||||
TD: s.fullChain.TD(1001), |
||||
} |
||||
if err := sendConn.Write(blockAnnouncement); err != nil { |
||||
t.Fatalf("could not write to connection: %v", err) |
||||
} |
||||
|
||||
switch msg := receiveConn.Read().(type) { |
||||
case *NewBlock: |
||||
assert.Equal(t, blockAnnouncement.Block.Header(), msg.Block.Header(), |
||||
"wrong block header in announcement") |
||||
assert.Equal(t, blockAnnouncement.TD, msg.TD, |
||||
"wrong TD in announcement") |
||||
case *NewBlockHashes: |
||||
hashes := *msg |
||||
assert.Equal(t, blockAnnouncement.Block.Hash(), hashes[0].Hash, |
||||
"wrong block hash in announcement") |
||||
default: |
||||
t.Fatal(msg) |
||||
} |
||||
// update test suite chain
|
||||
s.chain.blocks = append(s.chain.blocks, s.fullChain.blocks[1000]) |
||||
// wait for client to update its chain
|
||||
if err := receiveConn.waitForBlock(s.chain.Head()); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
// dial attempts to dial the given node and perform a handshake,
|
||||
// returning the created Conn if successful.
|
||||
func (s *Suite) dial() (*Conn, error) { |
||||
var conn Conn |
||||
|
||||
fd, err := net.Dial("tcp", fmt.Sprintf("%v:%d", s.Dest.IP(), s.Dest.TCP())) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
conn.Conn = rlpx.NewConn(fd, s.Dest.Pubkey()) |
||||
|
||||
// do encHandshake
|
||||
conn.ourKey, _ = crypto.GenerateKey() |
||||
_, err = conn.Handshake(conn.ourKey) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &conn, nil |
||||
} |
@ -0,0 +1,134 @@ |
||||
package ethtest |
||||
|
||||
import ( |
||||
"fmt" |
||||
"io" |
||||
"math/big" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/core/forkid" |
||||
"github.com/ethereum/go-ethereum/core/types" |
||||
"github.com/ethereum/go-ethereum/p2p" |
||||
"github.com/ethereum/go-ethereum/rlp" |
||||
) |
||||
|
||||
type Message interface { |
||||
Code() int |
||||
} |
||||
|
||||
type Error struct { |
||||
err error |
||||
} |
||||
|
||||
func (e *Error) Unwrap() error { return e.err } |
||||
func (e *Error) Error() string { return e.err.Error() } |
||||
func (e *Error) Code() int { return -1 } |
||||
|
||||
// Hello is the RLP structure of the protocol handshake.
|
||||
type Hello struct { |
||||
Version uint64 |
||||
Name string |
||||
Caps []p2p.Cap |
||||
ListenPort uint64 |
||||
ID []byte // secp256k1 public key
|
||||
|
||||
// Ignore additional fields (for forward compatibility).
|
||||
Rest []rlp.RawValue `rlp:"tail"` |
||||
} |
||||
|
||||
func (h Hello) Code() int { return 0x00 } |
||||
|
||||
// Disconnect is the RLP structure for a disconnect message.
|
||||
type Disconnect struct { |
||||
Reason p2p.DiscReason |
||||
} |
||||
|
||||
func (d Disconnect) Code() int { return 0x01 } |
||||
|
||||
// Status is the network packet for the status message for eth/64 and later.
|
||||
type Status struct { |
||||
ProtocolVersion uint32 |
||||
NetworkID uint64 |
||||
TD *big.Int |
||||
Head common.Hash |
||||
Genesis common.Hash |
||||
ForkID forkid.ID |
||||
} |
||||
|
||||
func (s Status) Code() int { return 16 } |
||||
|
||||
// NewBlockHashes is the network packet for the block announcements.
|
||||
type NewBlockHashes []struct { |
||||
Hash common.Hash // Hash of one particular block being announced
|
||||
Number uint64 // Number of one particular block being announced
|
||||
} |
||||
|
||||
func (nbh NewBlockHashes) Code() int { return 17 } |
||||
|
||||
// NewBlock is the network packet for the block propagation message.
|
||||
type NewBlock struct { |
||||
Block *types.Block |
||||
TD *big.Int |
||||
} |
||||
|
||||
func (nb NewBlock) Code() int { return 23 } |
||||
|
||||
// GetBlockHeaders represents a block header query.
|
||||
type GetBlockHeaders struct { |
||||
Origin hashOrNumber // Block from which to retrieve headers
|
||||
Amount uint64 // Maximum number of headers to retrieve
|
||||
Skip uint64 // Blocks to skip between consecutive headers
|
||||
Reverse bool // Query direction (false = rising towards latest, true = falling towards genesis)
|
||||
} |
||||
|
||||
func (g GetBlockHeaders) Code() int { return 19 } |
||||
|
||||
type BlockHeaders []*types.Header |
||||
|
||||
func (bh BlockHeaders) Code() int { return 20 } |
||||
|
||||
// HashOrNumber is a combined field for specifying an origin block.
|
||||
type hashOrNumber struct { |
||||
Hash common.Hash // Block hash from which to retrieve headers (excludes Number)
|
||||
Number uint64 // Block hash from which to retrieve headers (excludes Hash)
|
||||
} |
||||
|
||||
// EncodeRLP is a specialized encoder for hashOrNumber to encode only one of the
|
||||
// two contained union fields.
|
||||
func (hn *hashOrNumber) EncodeRLP(w io.Writer) error { |
||||
if hn.Hash == (common.Hash{}) { |
||||
return rlp.Encode(w, hn.Number) |
||||
} |
||||
if hn.Number != 0 { |
||||
return fmt.Errorf("both origin hash (%x) and number (%d) provided", hn.Hash, hn.Number) |
||||
} |
||||
return rlp.Encode(w, hn.Hash) |
||||
} |
||||
|
||||
// DecodeRLP is a specialized decoder for hashOrNumber to decode the contents
|
||||
// into either a block hash or a block number.
|
||||
func (hn *hashOrNumber) DecodeRLP(s *rlp.Stream) error { |
||||
_, size, _ := s.Kind() |
||||
origin, err := s.Raw() |
||||
if err == nil { |
||||
switch { |
||||
case size == 32: |
||||
err = rlp.DecodeBytes(origin, &hn.Hash) |
||||
case size <= 8: |
||||
err = rlp.DecodeBytes(origin, &hn.Number) |
||||
default: |
||||
err = fmt.Errorf("invalid input size %d for origin", size) |
||||
} |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// GetBlockBodies represents a GetBlockBodies request
|
||||
type GetBlockBodies []common.Hash |
||||
|
||||
func (gbb GetBlockBodies) Code() int { return 21 } |
||||
|
||||
// BlockBodies is the network packet for block content distribution.
|
||||
type BlockBodies []*types.Body |
||||
|
||||
func (bb BlockBodies) Code() int { return 22 } |
Loading…
Reference in new issue