diff --git a/p2p/rlpx/bufsema.go b/p2p/rlpx/bufsema.go
new file mode 100644
index 0000000000..65edbc7459
--- /dev/null
+++ b/p2p/rlpx/bufsema.go
@@ -0,0 +1,95 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "errors"
+ "fmt"
+ "sync/atomic"
+ "time"
+)
+
+var errAcquireTimeout = errors.New("acquisition timeout")
+
+// bufSema is a counting semaphore.
+type bufSema struct {
+ val, cap, waiting uint32
+ wakeup chan struct{}
+}
+
+func newBufSema(cap uint32) *bufSema {
+ return &bufSema{cap: cap, val: cap, wakeup: make(chan struct{})}
+}
+
+func (sem *bufSema) get() uint32 {
+ return atomic.LoadUint32(&sem.val)
+}
+
+// release increments sem, potentially unblocking a call to
+// waitAcquire if there is one. release never blocks.
+func (sem *bufSema) release(n uint32) {
+ new := atomic.AddUint32(&sem.val, n)
+ if new > sem.cap {
+ panic(fmt.Sprintf("semaphore count %d exceeds cap after release(%d)", new, n))
+ }
+ // Wake up a pending waitAcquire call if there is one.
+ if atomic.CompareAndSwapUint32(&sem.waiting, 1, 0) {
+ sem.wakeup <- struct{}{}
+ }
+}
+
+// waitAcquire decrements the semaphore by n. If less than
+// n units are available, waitAcquire blocks until release is called.
+// It may only be called from one goroutine at a time.
+func (sem *bufSema) waitAcquire(n uint32, timeout time.Duration) error {
+ if n > sem.cap {
+ return fmt.Errorf("requested amount %d exceeds semaphore cap of %d", n, sem.cap)
+ }
+ var timer *time.Timer
+ for {
+ // Set the waiting flag so release will try to wake us after
+ // incrementing sem.val.
+ if !atomic.CompareAndSwapUint32(&sem.waiting, 0, 1) {
+ panic("concurrent call to waitAcquire")
+ }
+ // Decrement if sem.val if possible.
+ if atomic.LoadUint32(&sem.val) >= n {
+ atomic.AddUint32(&sem.val, ^(n - 1))
+ // Gobble up wakeup signal in case release decremented sem.waiting.
+ if !atomic.CompareAndSwapUint32(&sem.waiting, 1, 0) {
+ <-sem.wakeup
+ }
+ return nil
+ }
+ // Start the timeout on the first iteration.
+ if timer == nil {
+ timer = time.NewTimer(timeout)
+ defer timer.Stop()
+ }
+ select {
+ case <-sem.wakeup:
+ // Woken by release. It has decremented sem.waiting back to zero.
+ case <-timer.C:
+ // Gobble up wakeup signal in case release decremented sem.waiting.
+ if !atomic.CompareAndSwapUint32(&sem.waiting, 1, 0) {
+ <-sem.wakeup
+ }
+ return errAcquireTimeout
+ }
+ }
+ return nil
+}
diff --git a/p2p/rlpx/bufsema_test.go b/p2p/rlpx/bufsema_test.go
new file mode 100644
index 0000000000..9c42e2b0c4
--- /dev/null
+++ b/p2p/rlpx/bufsema_test.go
@@ -0,0 +1,95 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "errors"
+ "math/rand"
+ "reflect"
+ "testing"
+ "time"
+)
+
+func TestBufSemaCountSimple(t *testing.T) {
+ sem := newBufSema(2000)
+
+ checkacquire := func(count, wantCount uint32, wantErr error) {
+ err := sem.waitAcquire(count, 10*time.Millisecond)
+ if !reflect.DeepEqual(err, wantErr) {
+ t.Fatalf("wrong error after acquire(%d): got %q, want %q", count, err, wantErr)
+ }
+ if val := sem.get(); val != wantCount {
+ t.Fatalf("wrong count after acquire(%d): got %d, want %d", count, val, wantCount)
+ }
+ }
+ checkrelease := func(count, wantCount uint32) {
+ sem.release(count)
+ if val := sem.get(); val != wantCount {
+ t.Fatalf("wrong count after release(%d): got %d, want %d", count, val, wantCount)
+ }
+ }
+
+ // Check that the counter is maintained correctly.
+ checkacquire(1000, 1000, nil)
+ checkacquire(1000, 0, nil)
+ checkacquire(1000, 0, errAcquireTimeout)
+ checkrelease(900, 900)
+ checkrelease(900, 1800)
+ checkrelease(199, 1999)
+ checkrelease(1, 2000)
+
+ // Check that requesting more than sem.cap fails.
+ checkacquire(2001, 2000, errors.New("requested amount 2001 exceeds semaphore cap of 2000"))
+
+ // Check that a failed waitAcquire leaves sem.val as is when it is < sem.cap.
+ checkacquire(500, 1500, nil)
+ checkrelease(200, 1700)
+ checkacquire(2000, 1700, errAcquireTimeout)
+}
+
+// This test checks that release wakes up waitAcquire.
+func TestBufSemaRace(t *testing.T) {
+ const (
+ waitCount = 10000
+ iterations = 5000
+ )
+ sem := newBufSema(waitCount)
+ pleaserelease := make(chan uint32, 500)
+ releaser := func() {
+ for rv := range pleaserelease {
+ sem.release(rv)
+ }
+ }
+ defer close(pleaserelease)
+ go releaser()
+ go releaser()
+ go releaser()
+
+ for i := 0; i < iterations; i++ {
+ if err := sem.waitAcquire(waitCount, 1*time.Second); err != nil {
+ t.Fatalf("iteration %d: %v", i, err)
+ }
+ for i := uint32(0); i < waitCount; {
+ rv := rand.Uint32() % waitCount
+ if i+rv > waitCount {
+ rv = waitCount - i
+ }
+ i += rv
+ pleaserelease <- rv
+ }
+ }
+}
diff --git a/p2p/rlpx/framing.go b/p2p/rlpx/framing.go
new file mode 100644
index 0000000000..7abd7e75bb
--- /dev/null
+++ b/p2p/rlpx/framing.go
@@ -0,0 +1,483 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+const (
+ staticFrameSize uint32 = 8 * 1024
+ frameHeaderSize = 16 // encoded header
+ frameHeaderFullSize = 32 // encoded header + MAC
+)
+
+var (
+ errProtocolClaimTimeout = errors.New("protocol for pending message was not claimed in time")
+ errUnexpectedChunkStart = errors.New("received chunk start header for existing transfer")
+ errChunkTooLarge = errors.New("chunk size larger than remaining message size")
+)
+
+// readLoop runs in its own goroutine for each connection,
+// dispatching frames to protocols.
+func readLoop(c *Conn) (err error) {
+ defer func() {
+ // When the loop ends, forward the error to all protocols so
+ // their next ReadPacket fails. Active chunked transfers also
+ // need to cancel immediately so shutdown is not delayed.
+ c.mu.Lock()
+ for _, p := range c.proto {
+ p.readClose(err)
+ for _, pr := range p.xfers {
+ pr.close(err)
+ }
+ }
+ c.readErr = err
+ c.mu.Unlock()
+ }()
+
+ // Local cache of claimed protocols.
+ protos := make(map[uint16]*Protocol)
+
+ for {
+ // Read the next frame header.
+ c.fd.SetReadDeadline(time.Now().Add(c.cfg.readIdleTimeout()))
+ fsize, hdr, err := c.rw.readFrameHeader()
+ if err != nil {
+ return err
+ }
+ // Grab the protocol, checking the local cache before
+ // interacting with the claims machinery in Conn.
+ proto := protos[hdr.protocol]
+ if proto == nil {
+ if proto = c.waitForProtocol(hdr.protocol); proto == nil {
+ return errProtocolClaimTimeout
+ }
+ protos[proto.id] = proto
+ }
+ // Wait until there is enough buffer space for the body
+ // before reading it.
+ err = proto.readBufSema.waitAcquire(fsize, c.cfg.readBufferWaitTimeout())
+ if err != nil {
+ return err
+ }
+ // Read the body of the frame.
+ c.fd.SetReadDeadline(time.Now().Add(c.cfg.readTimeout()))
+ body, err := c.rw.readFrameBody(fsize)
+ if err != nil {
+ return err
+ }
+ // Dispatch the frame to the protocol.
+ // This shouldn't block.
+ if pr := proto.xfers[hdr.contextID]; pr != nil {
+ if hdr.chunkStart {
+ return errUnexpectedChunkStart
+ }
+ end, err := pr.feed(body)
+ if end {
+ delete(proto.xfers, hdr.contextID)
+ }
+ if err != nil {
+ return err
+ }
+ } else {
+ pr, err := frameToPacket(proto, hdr, body)
+ if err != nil {
+ return err
+ }
+ if pr.bufN > 0 {
+ // Track as ongoing transfer if there is still something
+ // to buffer after the initial frame.
+ proto.xfers[hdr.contextID] = pr
+ }
+ proto.feedPacket(pr)
+ }
+ }
+}
+
+// frameToPacket handles the initial frame for a new packet.
+func frameToPacket(proto *Protocol, hdr frameHeader, frame frameBuffer) (pr *packetReader, err error) {
+ if hdr.chunkStart {
+ if uint32(len(frame)) > hdr.totalSize {
+ return nil, fmt.Errorf("initial chunk size %d larger than total size %d", len(frame), hdr.totalSize)
+ }
+ if uint32(len(frame)) < hdr.totalSize {
+ return newPacketReader(proto.readBufSema, hdr.totalSize, frame), nil
+ }
+ }
+ return newPacketReader(proto.readBufSema, uint32(len(frame)), frame), nil
+}
+
+// packetReader is the payload of a packet.
+// frames are appended to it as they are read from the connection.
+type packetReader struct {
+ // all of these can be accessed without locking
+ // because Read is not safe for concurrent use.
+ readBufs []frameBuffer
+ origBufs []frameBuffer
+ bufSema *bufSema
+ readN uint32 // how much can still be read
+
+ // these fields are protected by cond.L
+ cond *sync.Cond // wakes waitFrame
+ newBufs []frameBuffer // buffer inbox
+ err error // error inbox
+ bufN uint32 // how much still needs to be buffered
+}
+
+func newPacketReader(bsem *bufSema, psize uint32, initialFrame frameBuffer) *packetReader {
+ pr := &packetReader{
+ bufSema: bsem,
+ cond: sync.NewCond(new(sync.Mutex)),
+ readN: psize,
+ bufN: psize,
+ }
+ if len(initialFrame) > 0 {
+ pr.bufN -= uint32(len(initialFrame))
+ pr.readBufs = []frameBuffer{initialFrame}
+ pr.origBufs = []frameBuffer{initialFrame}
+ }
+ return pr
+}
+
+func (pr *packetReader) Read(rslice []byte) (int, error) {
+ if err := pr.waitFrame(); err != nil {
+ return 0, err
+ }
+ n := 0
+ for i := 0; i < len(pr.readBufs) && n < len(rslice); i++ {
+ nn, _ := pr.readBufs[i].Read(rslice[n:])
+ n += nn
+ }
+ pr.afterRead(n)
+ return n, nil
+}
+
+func (pr *packetReader) ReadByte() (byte, error) {
+ if err := pr.waitFrame(); err != nil {
+ return 0, err
+ }
+ b, _ := pr.readBufs[0].ReadByte()
+ pr.afterRead(1)
+ return b, nil
+}
+
+// blocks until at least one frame is available,
+// then transfers any new frame buffers that have appeared
+// to readBufs/origBufs.
+func (pr *packetReader) waitFrame() error {
+ if len(pr.readBufs) > 0 {
+ return nil
+ }
+ if pr.readN == 0 {
+ return io.EOF
+ }
+ pr.cond.L.Lock()
+ defer pr.cond.L.Unlock()
+ for len(pr.newBufs) == 0 && pr.err == nil {
+ pr.cond.Wait()
+ }
+ pr.readBufs = append(pr.readBufs, pr.newBufs...)
+ pr.origBufs = append(pr.origBufs, pr.newBufs...)
+ pr.newBufs = pr.newBufs[:0]
+ return pr.err
+}
+
+// removes drained buffers and decrements the read buffer semaphore.
+func (pr *packetReader) afterRead(n int) {
+ pr.readN -= uint32(n)
+ drained := 0
+ drainedLen := uint32(0)
+ for i, buf := range pr.readBufs {
+ if len(buf) != 0 {
+ break
+ }
+ drained++
+ drainedLen += uint32(len(pr.origBufs[i]))
+ }
+ if drained > 0 {
+ pr.readBufs = pr.readBufs[:copy(pr.readBufs, pr.readBufs[drained:])]
+ pr.origBufs = pr.origBufs[:copy(pr.origBufs, pr.origBufs[drained:])]
+ pr.bufSema.release(drainedLen)
+ }
+}
+
+func (pr *packetReader) close(err error) {
+ pr.cond.L.Lock()
+ pr.err = err
+ pr.cond.Signal() // wake up waitFrame
+ pr.cond.L.Unlock()
+}
+
+func (pr *packetReader) feed(frame frameBuffer) (end bool, err error) {
+ pr.cond.L.Lock()
+ defer pr.cond.L.Unlock()
+ if uint32(len(frame)) > pr.bufN {
+ pr.err = errChunkTooLarge
+ end = true
+ } else {
+ pr.bufN -= uint32(len(frame))
+ pr.newBufs = append(pr.newBufs, frame)
+ end = pr.bufN == 0
+ }
+ pr.cond.Signal() // wake up waitFrame
+ return end, pr.err
+}
+
+// represents a frame header that has been read.
+type frameHeader struct {
+ protocol, contextID uint16
+ chunkStart bool // initial frame of chunked message
+ totalSize uint32 // total number of bytes of chunked message
+}
+
+// header types for sending
+type chunkStartHeader struct {
+ Protocol, ContextID uint16
+ TotalSize uint32
+}
+type regularHeader struct {
+ Protocol, ContextID uint16
+}
+
+func decodeHeader(b []byte) (fsize uint32, h frameHeader, err error) {
+ fsize = readInt24(b)
+ if fsize == 0 {
+ return 0, h, errors.New("zero-sized frame")
+ }
+ b = b[3:]
+ lc, rest, err := rlp.SplitList(b)
+ if err != nil {
+ return fsize, h, err
+ }
+ // This is silly. rlp.DecodeBytes errors for data
+ // after the value, so we need to pass a slice
+ // containing just the value.
+ hlist := b[:len(b)-len(rest)]
+
+ switch cnt, _ := rlp.CountValues(lc); cnt {
+ case 1:
+ var in struct{ Protocol uint16 }
+ err = rlp.DecodeBytes(hlist, &in)
+ h.protocol = in.Protocol
+ case 2:
+ var in regularHeader
+ err = rlp.DecodeBytes(hlist, &in)
+ h.protocol = in.Protocol
+ h.contextID = in.ContextID
+ case 3:
+ var in chunkStartHeader
+ err = rlp.DecodeBytes(hlist, &in)
+ h.protocol = in.Protocol
+ h.contextID = in.ContextID
+ h.totalSize = in.TotalSize
+ h.chunkStart = true
+ default:
+ err = fmt.Errorf("too many list elements")
+ }
+ return fsize, h, err
+}
+
+// frameRW implements the framed wire protocol.
+type frameRW struct {
+ conn io.ReadWriter
+ // for reading
+ headbuf []byte
+ dec cipher.Stream
+ ingressMacCipher cipher.Block
+ ingressMac hash.Hash
+ // for writing
+ enc cipher.Stream
+ egressMacCipher cipher.Block
+ egressMac hash.Hash
+}
+
+func newFrameRW(conn io.ReadWriter, ingress, egress secrets) *frameRW {
+ return &frameRW{
+ conn: conn,
+ headbuf: make([]byte, 32),
+ enc: cipher.NewCTR(mustBlockCipher("egress.encKey", egress.encKey), egress.encIV),
+ egressMacCipher: mustBlockCipher("egress.macKey", egress.macKey),
+ egressMac: egress.mac,
+ dec: cipher.NewCTR(mustBlockCipher("ingress.encKey", ingress.encKey), ingress.encIV),
+ ingressMacCipher: mustBlockCipher("ingress.macKey", ingress.macKey),
+ ingressMac: ingress.mac,
+ }
+}
+
+func mustBlockCipher(what string, key []byte) cipher.Block {
+ c, err := aes.NewCipher(key)
+ if err != nil {
+ panic(fmt.Sprintf("invalid %s: %v", what, err))
+ }
+ return c
+}
+
+// sends a frame on the connection. the body buffer must placeholder bytes
+// for the encoded frame header and its MAC.
+func (rw *frameRW) sendFrame(hdr interface{}, body *frameBuffer) error {
+ wbuf := *body
+ usize := uint32(len(wbuf))
+ if usize < frameHeaderFullSize {
+ panic(fmt.Sprintf("invalid body buffer, size < %d", frameHeaderFullSize))
+ }
+ if usize-frameHeaderFullSize > maxUint24 {
+ return errors.New("frame size overflows uint24")
+ }
+
+ // Write and encrypt the frame header to the buffer.
+ headbuf := wbuf[:frameHeaderSize]
+ putInt24(headbuf, usize-frameHeaderFullSize)
+ headbufAfterSize := headbuf[3:3]
+ rlp.Encode(&headbufAfterSize, hdr)
+ rw.enc.XORKeyStream(headbuf, headbuf)
+ copy(wbuf[frameHeaderSize:], updateMAC(rw.egressMac, rw.egressMacCipher, headbuf))
+
+ // Write and encrypt frame data to the buffer.
+ wbuf.pad16()
+ rw.enc.XORKeyStream(wbuf[frameHeaderFullSize:], wbuf[frameHeaderFullSize:])
+ rw.egressMac.Write(wbuf[frameHeaderFullSize:])
+ fmacseed := rw.egressMac.Sum(nil)
+ wbuf = append(wbuf, zero[:frameHeaderSize]...)
+ copy(wbuf[len(wbuf)-16:], updateMAC(rw.egressMac, rw.egressMacCipher, fmacseed))
+
+ // Send the whole buffered frame on the socket.
+ _, err := rw.conn.Write(wbuf)
+ *body = wbuf
+ return err
+}
+
+func (rw *frameRW) readFrameHeader() (fsize uint32, hdr frameHeader, err error) {
+ // Read the header and verify its MAC.
+ if _, err := io.ReadFull(rw.conn, rw.headbuf); err != nil {
+ return 0, hdr, err
+ }
+ shouldMAC := updateMAC(rw.ingressMac, rw.ingressMacCipher, rw.headbuf[:16])
+ if !hmac.Equal(shouldMAC, rw.headbuf[16:]) {
+ return 0, hdr, errors.New("bad header MAC")
+ }
+ rw.dec.XORKeyStream(rw.headbuf[:16], rw.headbuf[:16])
+
+ // Parse the header.
+ fsize, hdr, err = decodeHeader(rw.headbuf)
+ if err != nil {
+ err = fmt.Errorf("can't decode frame header: %v", err)
+ }
+ return fsize, hdr, err
+}
+
+func (rw *frameRW) readFrameBody(fsize uint32) (frameBuffer, error) {
+ // Grab a buffer for the content.
+ var rsize = fsize
+ if padding := fsize % 16; padding > 0 {
+ rsize += 16 - padding // frame size rounded up to 16 byte boundary
+ }
+ fb := makeFrameReadBuffer(rsize + 16)
+ if _, err := io.ReadFull(rw.conn, fb); err != nil {
+ return nil, err
+ }
+
+ // Verify the body MAC and decrypt the content.
+ mac, bb := fb[len(fb)-16:], fb[:len(fb)-16]
+ rw.ingressMac.Write(bb)
+ fmacseed := rw.ingressMac.Sum(nil)
+ shouldMAC := updateMAC(rw.ingressMac, rw.ingressMacCipher, fmacseed)
+ if !hmac.Equal(shouldMAC, mac) {
+ return nil, errors.New("bad frame body MAC")
+ }
+ rw.dec.XORKeyStream(bb, bb)
+ return bb[:fsize], nil
+}
+
+// updateMAC reseeds the given hash with encrypted seed.
+// it returns the first 16 bytes of the hash sum after seeding.
+func updateMAC(mac hash.Hash, block cipher.Block, seed []byte) []byte {
+ aesbuf := make([]byte, aes.BlockSize)
+ block.Encrypt(aesbuf, mac.Sum(aesbuf[:0]))
+ for i := range aesbuf {
+ aesbuf[i] ^= seed[i]
+ }
+ mac.Write(aesbuf)
+ return mac.Sum(nil)[:16]
+}
+
+type frameBuffer []byte
+
+func makeFrameWriteBuffer() *frameBuffer {
+ buf := make(frameBuffer, frameHeaderFullSize, frameHeaderFullSize+staticFrameSize)
+ return &buf
+}
+
+func makeFrameReadBuffer(size uint32) frameBuffer {
+ return make(frameBuffer, size)
+}
+
+// resetForWrite truncates the buffer so it contains just enough space
+// for an encoded frame header. it must be called before writing
+// payload content for a new frame.
+func (buf *frameBuffer) resetForWrite() {
+ *buf = append((*buf)[:0], zero[:frameHeaderFullSize]...)
+}
+
+func (buf *frameBuffer) Write(s []byte) (n int, err error) {
+ *buf = append(*buf, s...)
+ return len(s), nil
+}
+
+func (buf *frameBuffer) Read(s []byte) (int, error) {
+ if buf == nil || len(*buf) == 0 {
+ return 0, io.EOF
+ }
+ n := copy(s, *buf)
+ *buf = (*buf)[n:]
+ return n, nil
+}
+
+func (buf *frameBuffer) ReadByte() (byte, error) {
+ if buf == nil || len(*buf) == 0 {
+ return 0, io.EOF
+ }
+ b := (*buf)[0]
+ *buf = (*buf)[1:]
+ return b, nil
+}
+
+func (buf *frameBuffer) pad16() {
+ if padding := len(*buf) % 16; padding > 0 {
+ *buf = append(*buf, zero[:16-padding]...)
+ }
+}
+
+func readInt24(b []byte) uint32 {
+ return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16
+}
+
+func putInt24(s []byte, v uint32) {
+ s[0] = byte(v >> 16)
+ s[1] = byte(v >> 8)
+ s[2] = byte(v)
+}
diff --git a/p2p/rlpx/framing_test.go b/p2p/rlpx/framing_test.go
new file mode 100644
index 0000000000..bfbcf02627
--- /dev/null
+++ b/p2p/rlpx/framing_test.go
@@ -0,0 +1,156 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "crypto/ecdsa"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "math/rand"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+func TestPacketReader(t *testing.T) {
+ feed := func(pr *packetReader, n uint32) {
+ for sent := uint32(0); sent < n; {
+ chunk := randomChunk(sent, n-sent)
+ sent += uint32(len(chunk))
+ if err := pr.bufSema.waitAcquire(uint32(len(chunk)), 200*time.Millisecond); err != nil {
+ panic(err.Error())
+ }
+ end, err := pr.feed(chunk)
+ if err != nil {
+ panic(fmt.Errorf("pr.feed returned error: %v", err))
+ }
+ if end && sent != n {
+ panic(fmt.Errorf("pr.feed returned end=true with %d/%d bytes of input", sent, n))
+ }
+ }
+ }
+ for size := uint32(1); size < 2<<17; size *= 2 {
+ sem := newBufSema(staticFrameSize * 2)
+ pr := newPacketReader(sem, size, nil)
+ go feed(pr, size)
+ if err := checkSeq(pr, size); err != nil {
+ t.Fatalf("size %d: read error: %v", size, err)
+ }
+ if val := sem.get(); val != staticFrameSize*2 {
+ t.Fatalf("size %d: wrong semaphore value after reading all data. got %d, want %d", size, val, staticFrameSize*2)
+ }
+ }
+}
+
+func checkSeq(r io.Reader, size uint32) error {
+ content := make([]byte, size)
+ if _, err := io.ReadFull(r, content); err != nil {
+ return err
+ }
+ for i, b := range content {
+ if b != byte(i) {
+ return fmt.Errorf("mismatch at index %d: have %d, want %d", i, b, byte(i))
+ }
+ }
+ return nil
+}
+
+func randomChunk(seed uint32, maxSize uint32) frameBuffer {
+ size := rand.Uint32()%staticFrameSize + 1
+ if size > maxSize {
+ size = maxSize
+ }
+ chunk := make(frameBuffer, size)
+ for i := range chunk {
+ chunk[i] = byte(uint32(i) + seed)
+ }
+ return chunk
+}
+
+/*
+func TestFrameFakeGolden(t *testing.T) {
+ buf := new(bytes.Buffer)
+ hash := fakeHash{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
+ rw := newFrameRW(buf, secrets{
+ AES: crypto.Sha3(),
+ MAC: crypto.Sha3(),
+ IngressMAC: hash,
+ EgressMAC: hash,
+ })
+
+ golden := hexb(`
+00828ddae471818bb0bfa6b551d1cb42
+01010101010101010101010101010101
+ba628a4ba590cb43f7848f41c4382885
+01010101010101010101010101010101
+`)
+ body := hexb(`08C401020304`)
+
+ // Check sendFrame. This encodes the frame to buf.
+ fwbuf := makeFrameWriteBuffer()
+ fwbuf.Write(body)
+ if err := rw.sendFrame(regularHeader{0, 0}, fwbuf); err != nil {
+ t.Fatalf("sendFrame error: %v", err)
+ }
+ written := buf.Bytes()
+ if !bytes.Equal(written, golden) {
+ t.Fatalf("output mismatch:\n got: %x\n want: %x", written, golden)
+ }
+
+ // Check readFrame. It reads the message encoded by sendFrame, which
+ // must be equivalent to the golden message above.
+ fsize, hdr, err := rw.readFrameHeader()
+ if err != nil {
+ t.Fatalf("readFrameHeader error: %v", err)
+ }
+ if (hdr != frameHeader{}) {
+ t.Errorf("read header mismatch: got %v, want zero header", hdr)
+ }
+ if int(fsize) != len(body) {
+ t.Errorf("read size mismatch: got %d, want %d", fsize, len(body))
+ }
+ // if !bytes.Equal(bodybuf.Bytes(), body) {
+ // t.Errorf("read body mismatch:\ngot %x\nwant %x", bodybuf.Bytes(), body)
+ // }
+}
+
+type fakeHash []byte
+
+func (fakeHash) Write(p []byte) (int, error) { return len(p), nil }
+func (fakeHash) Reset() {}
+func (fakeHash) BlockSize() int { return 0 }
+
+func (h fakeHash) Size() int { return len(h) }
+func (h fakeHash) Sum(b []byte) []byte { return append(b, h...) }
+
+*/
+
+func hexb(str string) []byte {
+ unspace := strings.NewReplacer("\n", "", "\t", "", " ", "")
+ b, err := hex.DecodeString(unspace.Replace(str))
+ if err != nil {
+ panic(fmt.Sprintf("invalid hex string: %q", str))
+ }
+ return b
+}
+
+func hexkey(str string) *ecdsa.PrivateKey {
+ return crypto.ToECDSA(hexb(str))
+}
diff --git a/p2p/rlpx/handshake.go b/p2p/rlpx/handshake.go
new file mode 100644
index 0000000000..7b5328a947
--- /dev/null
+++ b/p2p/rlpx/handshake.go
@@ -0,0 +1,339 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "hash"
+ "io"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/crypto/ecies"
+ "github.com/ethereum/go-ethereum/crypto/secp256k1"
+ "github.com/ethereum/go-ethereum/crypto/sha3"
+)
+
+const (
+ maxUint24 = ^uint32(0) >> 8
+ kdfSharedDataPrefix = "rlpx handshake\x05"
+
+ // Handshake Sizes
+ sskLen = 16 // ecies.MaxSharedKeyLength(pubKey) / 2
+ sigLen = 65 // elliptic S256
+ pubLen = 64 // 512 bit pubkey in uncompressed representation without format byte
+ shaLen = 32 // hash length (for nonce etc)
+ nonceLen = 24
+ authMsgLen = sigLen + shaLen + pubLen + shaLen + 1
+ authRespLen = pubLen + shaLen + 1
+ eciesBytes = 65 + 16 + 32
+ encAuthMsgLen = authMsgLen + eciesBytes // size of the final ECIES payload sent as initiator's handshake
+ encAuthRespLen = authRespLen + eciesBytes // size of the final ECIES payload sent as receiver's handshake
+)
+
+var zero [32]byte
+
+// encHandshake contains the state of the encryption handshake.
+type handshake struct {
+ conn io.ReadWriter
+ initiator bool
+ localPrivKey *ecdsa.PrivateKey
+ remotePub *ecies.PublicKey // remote-pubk
+ initNonce, respNonce []byte // nonce
+ randomPrivKey *ecies.PrivateKey // ecdhe-random
+ remoteRandomPub *ecies.PublicKey // ecdhe-random-pubk
+}
+
+// secrets represents the derived secrets for authenticated encryption.
+type secrets struct {
+ encKey, encIV, macKey []byte
+ mac hash.Hash
+}
+
+type handshakeRandSource interface {
+ generateNonce(b []byte) error
+ generateKey() (*ecies.PrivateKey, error)
+}
+
+type realRandSource struct{}
+
+func (realRandSource) generateNonce(b []byte) error {
+ _, err := io.ReadFull(rand.Reader, b)
+ return err
+}
+
+func (realRandSource) generateKey() (*ecies.PrivateKey, error) {
+ return ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil)
+}
+
+func (h *handshake) deriveSecrets(forceV4 bool, auth, authResp []byte) (vsn uint, ingress, egress secrets, err error) {
+ remoteNonce := h.initNonce
+ if h.initiator {
+ remoteNonce = h.respNonce
+ }
+ remoteVersion := binary.BigEndian.Uint64(remoteNonce[nonceLen:])
+ if forceV4 || remoteVersion > 255 {
+ return h.deriveSecretsV4(auth, authResp)
+ }
+ return h.deriveSecretsV5()
+}
+
+func (h *handshake) deriveSecretsV4(auth, authResp []byte) (vsn uint, ingress, egress secrets, err error) {
+ vsn = 4
+ ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
+ if err != nil {
+ return vsn, ingress, egress, err
+ }
+ sharedSecret := crypto.Sha3(ecdheSecret, crypto.Sha3(h.respNonce, h.initNonce))
+ aesSecret := crypto.Sha3(ecdheSecret, sharedSecret)
+ macSecret := crypto.Sha3(ecdheSecret, aesSecret)
+
+ egress = secrets{encKey: aesSecret, encIV: zero[:16], macKey: macSecret}
+ egress.mac = sha3.NewKeccak256()
+ egress.mac.Write(xor(h.initNonce, macSecret))
+ egress.mac.Write(authResp)
+ ingress = secrets{encKey: aesSecret, encIV: zero[:16], macKey: macSecret}
+ ingress.mac = sha3.NewKeccak256()
+ ingress.mac.Write(xor(h.respNonce, macSecret))
+ ingress.mac.Write(auth)
+ if h.initiator {
+ ingress, egress = egress, ingress
+ }
+ return vsn, ingress, egress, nil
+}
+
+func (h *handshake) deriveSecretsV5() (vsn uint, ingress, egress secrets, err error) {
+ vsn = 5
+ ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
+ if err != nil {
+ return vsn, ingress, egress, err
+ }
+ initPub := exportPubkey(h.remotePub)
+ respPub := elliptic.Marshal(h.localPrivKey.Curve, h.localPrivKey.X, h.localPrivKey.Y)[1:]
+ if h.initiator {
+ initPub, respPub = respPub, initPub
+ }
+ sharedData := make([]byte, len(kdfSharedDataPrefix)+nonceLen*2+pubLen*2)
+ n := copy(sharedData, kdfSharedDataPrefix)
+ n += copy(sharedData[n:], h.initNonce[:nonceLen])
+ n += copy(sharedData[n:], h.respNonce[:nonceLen])
+ n += copy(sharedData[n:], initPub)
+ n += copy(sharedData[n:], respPub)
+ derived, err := ecies.ConcatKDF(sha3.NewKeccak256(), ecdheSecret, sharedData, 160)
+ if err != nil {
+ return vsn, ingress, egress, err
+ }
+
+ ingress = secrets{encKey: derived[0:32], encIV: derived[64:80], macKey: derived[96:128]}
+ ingress.mac = sha3.NewKeccak256()
+ ingress.mac.Write(ingress.macKey)
+ egress = secrets{encKey: derived[32:64], encIV: derived[80:96], macKey: derived[128:160]}
+ egress.mac = sha3.NewKeccak256()
+ egress.mac.Write(egress.macKey)
+ if h.initiator {
+ ingress, egress = egress, ingress
+ }
+ return vsn, ingress, egress, nil
+}
+
+func (h *handshake) ecdhShared(prv *ecdsa.PrivateKey) ([]byte, error) {
+ return ecies.ImportECDSA(prv).GenerateShared(h.remotePub, sskLen, sskLen)
+}
+
+func (c *Conn) fillHandshake(nonce *[]byte, key **ecies.PrivateKey) (err error) {
+ *nonce = make([]byte, shaLen)
+ if c.cfg.ForceV4 {
+ err = c.handshakeRand.generateNonce(*nonce)
+ } else {
+ binary.BigEndian.PutUint64((*nonce)[nonceLen:], 5)
+ err = c.handshakeRand.generateNonce((*nonce)[:nonceLen])
+ }
+ if err != nil {
+ return err
+ }
+ *key, err = c.handshakeRand.generateKey()
+ return err
+}
+
+// initiatorHandshake negotiates connection secrets on conn.
+// it should be called on the dialing end of the connection.
+// prv is the local client's private key.
+func (c *Conn) initiatorHandshake() (vsn uint, ingress, egress secrets, err error) {
+ h := &handshake{initiator: true, localPrivKey: c.cfg.Key, remotePub: ecies.ImportECDSAPublic(c.remoteID)}
+ if err := c.fillHandshake(&h.initNonce, &h.randomPrivKey); err != nil {
+ return 0, ingress, egress, err
+ }
+ auth, err := h.authMsg()
+ if err != nil {
+ return 0, ingress, egress, err
+ }
+ if _, err := c.fd.Write(auth); err != nil {
+ return 0, ingress, egress, err
+ }
+
+ response := make([]byte, encAuthRespLen)
+ if _, err := io.ReadFull(c.fd, response); err != nil {
+ return 0, ingress, egress, err
+ }
+ if err := h.decodeAuthResp(response); err != nil {
+ return 0, ingress, egress, err
+ }
+ return h.deriveSecrets(c.cfg.ForceV4, auth, response)
+}
+
+// authMsg creates an encrypted initiator handshake message.
+func (h *handshake) authMsg() ([]byte, error) {
+ staticSharedSecret, err := h.ecdhShared(h.localPrivKey)
+ if err != nil {
+ return nil, err
+ }
+ // sign static-shared-secret^nonce
+ signed := xor(staticSharedSecret, h.initNonce)
+ signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
+ if err != nil {
+ return nil, err
+ }
+ // encode auth message: sig || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag
+ msg := make([]byte, authMsgLen)
+ n := copy(msg, signature)
+ n += copy(msg[n:], crypto.Sha3(exportPubkey(&h.randomPrivKey.PublicKey)))
+ n += copy(msg[n:], crypto.FromECDSAPub(&h.localPrivKey.PublicKey)[1:])
+ n += copy(msg[n:], h.initNonce)
+ msg[n] = 0
+ // encrypt auth message using remote-pubk
+ return ecies.Encrypt(rand.Reader, h.remotePub, msg, nil, nil)
+}
+
+// decodeAuthResp decode an encrypted authentication response message.
+func (h *handshake) decodeAuthResp(auth []byte) error {
+ msg, err := crypto.Decrypt(h.localPrivKey, auth)
+ if err != nil {
+ return fmt.Errorf("could not decrypt auth response (%v)", err)
+ }
+ h.respNonce = msg[pubLen : pubLen+shaLen]
+ h.remoteRandomPub, err = importPublicKey(msg[:pubLen])
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// recipientHandshake negotiates connection secrets on conn.
+// it should be called on the listening side of the connection.
+// prv is the local client's private key.
+func (c *Conn) recipientHandshake() (vsn uint, remoteID *ecdsa.PublicKey, ingress, egress secrets, err error) {
+ auth := make([]byte, encAuthMsgLen)
+ if _, err := io.ReadFull(c.fd, auth); err != nil {
+ return 0, nil, ingress, egress, err
+ }
+ h := &handshake{localPrivKey: c.cfg.Key}
+ if err := h.decodeAuthMsg(auth); err != nil {
+ return 0, nil, ingress, egress, fmt.Errorf("invalid auth: %v", err)
+ }
+ if err := c.fillHandshake(&h.respNonce, &h.randomPrivKey); err != nil {
+ return 0, nil, ingress, egress, err
+ }
+
+ resp, err := h.authResp()
+ if err != nil {
+ return 0, nil, ingress, egress, fmt.Errorf("can't create auth resp: %v", err)
+ }
+ if _, err := c.fd.Write(resp); err != nil {
+ return 0, nil, ingress, egress, err
+ }
+ vsn, ingress, egress, err = h.deriveSecrets(c.cfg.ForceV4, auth, resp)
+ if h.remotePub != nil {
+ remoteID = h.remotePub.ExportECDSA()
+ }
+ return vsn, remoteID, ingress, egress, err
+}
+
+func (h *handshake) decodeAuthMsg(auth []byte) error {
+ msg, err := crypto.Decrypt(h.localPrivKey, auth)
+ if err != nil {
+ return err
+ }
+ // signature || sha3(ecdhe-random-pubk) || pubk || nonce || token-flag
+ h.initNonce = msg[authMsgLen-shaLen-1 : authMsgLen-1]
+ h.remotePub, err = importPublicKey(msg[sigLen+shaLen : sigLen+shaLen+pubLen])
+ if err != nil {
+ return fmt.Errorf("invalid remote identity: %v", err)
+ }
+ // recover remote random pubkey from signed message.
+ staticSharedSecret, err := h.ecdhShared(h.localPrivKey)
+ if err != nil {
+ return err
+ }
+ signed := xor(staticSharedSecret, h.initNonce)
+ remoteRandomPub, err := secp256k1.RecoverPubkey(signed, msg[:sigLen])
+ if err != nil {
+ return err
+ }
+ // validate the sha3 of recovered pubkey
+ remoteRandomPubMAC := msg[sigLen : sigLen+shaLen]
+ shaRemoteRandomPub := crypto.Sha3(remoteRandomPub[1:])
+ if !bytes.Equal(remoteRandomPubMAC, shaRemoteRandomPub) {
+ return fmt.Errorf("recovered pubkey hash mismatch")
+ }
+ h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)
+ return nil
+}
+
+// authResp generates the encrypted authentication response message.
+func (h *handshake) authResp() ([]byte, error) {
+ // E(remote-pubk, ecdhe-random-pubk || nonce || token-flag)
+ resp := make([]byte, authRespLen)
+ n := copy(resp, exportPubkey(&h.randomPrivKey.PublicKey))
+ n += copy(resp[n:], h.respNonce)
+ resp[n] = 0
+ return ecies.Encrypt(rand.Reader, h.remotePub, resp, nil, nil)
+}
+
+// importPublicKey unmarshals 512 bit public keys.
+func importPublicKey(pubKey []byte) (*ecies.PublicKey, error) {
+ var pubKey65 []byte
+ switch len(pubKey) {
+ case 64:
+ // add 'uncompressed key' flag
+ pubKey65 = append([]byte{0x04}, pubKey...)
+ case 65:
+ pubKey65 = pubKey
+ default:
+ return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey))
+ }
+ // TODO: fewer pointless conversions
+ return ecies.ImportECDSAPublic(crypto.ToECDSAPub(pubKey65)), nil
+}
+
+func exportPubkey(pub *ecies.PublicKey) []byte {
+ if pub == nil {
+ panic("nil pubkey")
+ }
+ return elliptic.Marshal(pub.Curve, pub.X, pub.Y)[1:]
+}
+
+func xor(one, other []byte) (xor []byte) {
+ xor = make([]byte, len(one))
+ for i := 0; i < len(one); i++ {
+ xor[i] = one[i] ^ other[i]
+ }
+ return xor
+}
diff --git a/p2p/rlpx/handshake_test.go b/p2p/rlpx/handshake_test.go
new file mode 100644
index 0000000000..560f4c0500
--- /dev/null
+++ b/p2p/rlpx/handshake_test.go
@@ -0,0 +1,255 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/crypto/ecies"
+)
+
+func init() {
+ spew.Config.Indent = "\t"
+}
+
+func TestSharedSecret(t *testing.T) {
+ prv0, _ := crypto.GenerateKey()
+ pub0 := &prv0.PublicKey
+ prv1, _ := crypto.GenerateKey()
+ pub1 := &prv1.PublicKey
+ ss0, err := ecies.ImportECDSA(prv0).GenerateShared(ecies.ImportECDSAPublic(pub1), sskLen, sskLen)
+ if err != nil {
+ return
+ }
+ ss1, err := ecies.ImportECDSA(prv1).GenerateShared(ecies.ImportECDSAPublic(pub0), sskLen, sskLen)
+ if err != nil {
+ return
+ }
+ if !bytes.Equal(ss0, ss1) {
+ t.Errorf("secret mismatch")
+ }
+}
+
+// This test does random V5 handshakes and compares the secrets.
+func TestHandshake(t *testing.T) {
+ for i := 0; i < 10 && !t.Failed(); i++ {
+ start := time.Now()
+ doTestHandshake(t)
+ t.Logf("%d %v\n", i+1, time.Since(start))
+ }
+}
+
+func doTestHandshake(t *testing.T) {
+ var (
+ prv0, _ = crypto.GenerateKey()
+ prv1, _ = crypto.GenerateKey()
+ p1, p2 = net.Pipe()
+ c1 = Server(p1, &Config{Key: prv0})
+ c2 = Client(p2, &prv0.PublicKey, &Config{Key: prv1})
+ )
+ shake := func(conn *Conn, rkey *ecdsa.PublicKey) error {
+ defer conn.Close()
+ if err := conn.Handshake(); err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(conn.RemoteID(), rkey) {
+ return fmt.Errorf("remote ID mismatch: got %v, want: %v", conn.RemoteID(), rkey)
+ }
+ return nil
+ }
+ run(t, rig{
+ "initiator": func() error { return shake(c1, &prv1.PublicKey) },
+ "recipient": func() error { return shake(c2, &prv0.PublicKey) },
+ })
+
+ // compare derived secrets
+ if !reflect.DeepEqual(c1.rw.egressMac, c2.rw.ingressMac) {
+ t.Errorf("egress mac mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.egressMac, c2.rw.ingressMac)
+ }
+ if !reflect.DeepEqual(c1.rw.ingressMac, c2.rw.egressMac) {
+ t.Errorf("ingress mac mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.ingressMac, c2.rw.egressMac)
+ }
+ if !reflect.DeepEqual(c1.rw.enc, c2.rw.dec) {
+ t.Errorf("enc cipher mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.enc, c2.rw.dec)
+ }
+ if !reflect.DeepEqual(c1.rw.dec, c2.rw.enc) {
+ t.Errorf("dec cipher mismatch:\n c1.rw: %#v\n c2.rw: %#v", c1.rw.dec, c2.rw.enc)
+ }
+}
+
+// This test runs initator/recipient against each other for each test vector.
+func TestHandshakeTV(t *testing.T) {
+ for i, ht := range handshakeTV {
+ p1, p2 := net.Pipe()
+ run(t, rig{
+ "initiator": func() error { return checkInitiator(p1, ht) },
+ "recipient": func() error { return checkRecipient(p2, ht) },
+ })
+ if t.Failed() {
+ t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht))
+ }
+ }
+}
+
+// This test runs the encrypted auth packets from the test vectors against
+// the recipient code.
+func TestHandshakePacketsRecipientTV(t *testing.T) {
+ for i, ht := range handshakeTV {
+ p1, p2 := net.Pipe()
+ run(t, rig{
+ "recipient": func() error {
+ defer p1.Close()
+ return checkRecipient(p1, ht)
+ },
+ "auth packet send": func() error {
+ _, err := p2.Write(ht.encAuth)
+ return err
+ },
+ "authResp packet recv": func() error {
+ ioutil.ReadAll(p2)
+ return nil
+ },
+ })
+ if t.Failed() {
+ t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht))
+ }
+ }
+}
+
+// This test runs the encrypted authResp packets from the test vectors against
+// the initiator code.
+func TestHandshakePacketsInitiatorTV(t *testing.T) {
+ for i, ht := range handshakeTV {
+ p1, p2 := net.Pipe()
+ run(t, rig{
+ "initiator": func() error {
+ defer p1.Close()
+ return checkInitiator(p1, ht)
+ },
+ "authResp packet send": func() error {
+ _, err := p2.Write(ht.encAuthResp)
+ return err
+ },
+ "auth packet recv": func() error {
+ ioutil.ReadAll(p2)
+ return nil
+ },
+ })
+ if t.Failed() {
+ t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht))
+ }
+ }
+}
+
+// This test checks that secrets.mac is initialized correctly.
+func TestHandshakeDeriveMacTV(t *testing.T) {
+ for i, ht := range handshakeTV {
+ h := handshake{
+ initiator: true,
+ localPrivKey: ht.initiator.Key,
+ remotePub: ecies.ImportECDSAPublic(&ht.recipient.Key.PublicKey),
+ initNonce: ht.initiatorNonce,
+ respNonce: ht.recipientNonce,
+ randomPrivKey: ecies.ImportECDSA(ht.initiatorEphemeralKey),
+ remoteRandomPub: ecies.ImportECDSAPublic(&ht.recipientEphemeralKey.PublicKey),
+ }
+ vsn, ingress, egress, err := h.deriveSecrets(ht.initiator.ForceV4, ht.encAuth, ht.encAuthResp)
+ if err != nil {
+ t.Error("deriveSecrets error: %v", err)
+ }
+ if sum := ingress.mac.Sum(nil); !bytes.Equal(sum, ht.initiatorIngressMacDigest) {
+ t.Errorf("ingress mac mismatch: got %x, want %x", sum, ht.initiatorIngressMacDigest)
+ }
+ if sum := egress.mac.Sum(nil); !bytes.Equal(sum, ht.initiatorEgressMacDigest) {
+ t.Errorf("egress mac mismatch: got %x, want %x", sum, ht.initiatorEgressMacDigest)
+ }
+ if err := ht.checkSecrets(vsn, nil, ingress, egress); err != nil {
+ t.Error(err)
+ }
+ if t.Failed() {
+ t.Fatalf("failed test case %d:\n%s", i, spew.Sdump(ht))
+ }
+ }
+}
+
+func checkInitiator(pipe net.Conn, ht handshakeTest) error {
+ remotePub := &ht.recipient.Key.PublicKey
+ conn := Client(pipe, remotePub, ht.initiator)
+ conn.handshakeRand = fakeRandSource{key: ht.initiatorEphemeralKey, nonce: ht.initiatorNonce}
+ vsn, ingress, egress, err := conn.initiatorHandshake()
+ if err != nil {
+ return err
+ }
+ return ht.checkSecrets(vsn, nil, ingress, egress)
+}
+
+func checkRecipient(pipe net.Conn, ht handshakeTest) error {
+ conn := Server(pipe, ht.recipient)
+ conn.handshakeRand = fakeRandSource{key: ht.recipientEphemeralKey, nonce: ht.recipientNonce}
+ vsn, remoteID, ingress, egress, err := conn.recipientHandshake()
+ if err != nil {
+ return err
+ }
+ return ht.checkSecrets(vsn, remoteID, egress, ingress)
+}
+
+func (ht handshakeTest) checkSecrets(vsn uint, remoteID *ecdsa.PublicKey, ingress, egress secrets) error {
+ if remoteID != nil && !reflect.DeepEqual(remoteID, &ht.initiator.Key.PublicKey) {
+ return fmt.Errorf("remoteID mismatch:\ngot %x\nwant %x",
+ crypto.FromECDSAPub(remoteID), crypto.FromECDSAPub(&ht.initiator.Key.PublicKey))
+ }
+ if vsn != ht.negotiatedVersion {
+ return fmt.Errorf("version mismatch: got %d, want %d", vsn, ht.negotiatedVersion)
+ }
+ // Remove the MACs so secrets can be compared with DeepEqual.
+ ingress.mac, egress.mac = nil, nil
+ if !reflect.DeepEqual(ingress, ht.initiatorIngressSecrets) {
+ return fmt.Errorf("initiatorIngressSecrets mismatch:\ngot %swant %s",
+ spew.Sdump(ingress), spew.Sdump(ht.initiatorEgressSecrets))
+ }
+ if !reflect.DeepEqual(egress, ht.initiatorEgressSecrets) {
+ return fmt.Errorf("initiatorEgressSecrets mismatch:\ngot %swant %s",
+ spew.Sdump(egress), spew.Sdump(ht.initiatorIngressSecrets))
+ }
+ return nil
+}
+
+type fakeRandSource struct {
+ key *ecdsa.PrivateKey
+ nonce []byte
+}
+
+func (ht fakeRandSource) generateNonce(b []byte) error {
+ if len(b) > len(ht.nonce) {
+ panic(fmt.Sprintf("requested %d bytes of nonce data, have %d", len(b), len(ht.nonce)))
+ }
+ copy(b, ht.nonce)
+ return nil
+}
+
+func (ht fakeRandSource) generateKey() (*ecies.PrivateKey, error) {
+ return ecies.ImportECDSA(ht.key), nil
+}
diff --git a/p2p/rlpx/handshake_tv_test.go b/p2p/rlpx/handshake_tv_test.go
new file mode 100644
index 0000000000..8e8834660d
--- /dev/null
+++ b/p2p/rlpx/handshake_tv_test.go
@@ -0,0 +1,254 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import "crypto/ecdsa"
+
+type handshakeTest struct {
+ // Inputs
+ initiator, recipient *Config
+ initiatorEphemeralKey, recipientEphemeralKey *ecdsa.PrivateKey
+ initiatorNonce, recipientNonce []byte
+
+ // Derived Values: These must match exactly in all Test*TV
+ // functions and do not depend on any random values apart from the
+ // ones above.
+ negotiatedVersion uint
+ initiatorEgressSecrets, initiatorIngressSecrets secrets
+
+ // Encrypted Packets: We can't check them directly because both
+ // encryption and signing introduce random values.
+ // TestHandshakePacketsRecipientTV and
+ // TestHandshakePacketsInitiatorTV check that each 'side' accepts
+ // the other packet and computes the right secrets.
+ encAuth, encAuthResp []byte
+
+ // Digests of the empty string created with each MAC hash. These
+ // are checked TestHandshakeDeriveMacTV with the packets above
+ // because RLPx V4 includes the ciphertext in the hash.
+ initiatorIngressMacDigest, initiatorEgressMacDigest []byte
+}
+
+var handshakeTV = []handshakeTest{
+ // initiator V5, recipient V5
+ {
+ initiator: &Config{
+ Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"),
+ ForceV4: false,
+ },
+ recipient: &Config{
+ Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"),
+ ForceV4: false,
+ },
+ initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"),
+ recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"),
+ initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e10000000000000005"),
+ recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d0000000000000005"),
+
+ encAuth: hexb(`
+ 04f0849817e9483c39b1eead6f5a0dfb09cbc4c43151172c2549c5b07c9364f7
+ 853cbae79e7d2a3b9a79d042ddbdf1d95db1f8c7428989123afb02fcad93bfac
+ 413ad9a9f2bf9b2a621a09fe804229f31f9a71ae6776f10bc8d13bb372fa4af5
+ 9a39fd0ae546cb5fce9fd55d59b13e6cbcc234421ab089f1f08932d4622e460c
+ b0f63b3375b8388e25f84db55a415c764386e00bc19da675baf8e643f48d14c9
+ 89432062ed3495943bb6b3f46e8a5011edc3648a0396bccbaa0fe164bd2b8919
+ df542da5fd34f24e2d5b84082a9be5fce2e17625d90078eafa8d3125314553e7
+ 008dedd9fd5d9d082811a08581d596f7605eba7500aafd2f3c5c8b8cfdce2ac3
+ 417559c3d52d6d326a832f0c43077d0697c06db24a5a28c1d67033ecda5d4ff8
+ 74831427bcda3ef422b8cc9f34b1eabf39607a
+ `),
+ encAuthResp: hexb(`
+ 0496117783823744f0f58cd952ed34a1866e4ade1a2d66cdfd041f877c4e4216
+ d45645ad1dedee1d54d5a767d87231fbadbc6dcb2b48c75b3ab46cb18b224a7f
+ cd3d9619e03d24813aaa0cc37adb32aa2fa7fbc13aa1fbc01d24d402715fe213
+ 62a457a986ec649983f0ff81f5f207799849bce8061dab17b491ac7f0090c426
+ f7e63c31f11917e8e33c65d74bd2094435e73ffab1dfbaff368de079244d4ebd
+ 7f8b542f7081756a1e94b4ed26fb1c3bddabbd642064a15ad597a4f63894ea31
+ 13cab7533eec3b8ae163f8ebd61d7bac71e4
+ `),
+
+ negotiatedVersion: 5,
+ initiatorIngressSecrets: secrets{
+ encKey: hexb("5d268dbeede1c3ce4e7cd1f900543f671467284d53c6f6fd6b284789652bd1f6"),
+ encIV: hexb("e5703e8952a6eafcdb2c1940c7615843"),
+ macKey: hexb("09726cd8b6414cb1f5858b0339badeeed377a48cbe5f3d28a4f74ae41e610c4d"),
+ },
+ initiatorEgressSecrets: secrets{
+ encKey: hexb("40bfcb0da6d30512f57187f61b4816fcdc9aaaf107184e29467fe6f6ccefe4a7"),
+ encIV: hexb("0e906055c0ca86940626d0fde4f3a9c2"),
+ macKey: hexb("c676534122bd3a555ca8f2d924b63222b1b5b5efccb7b37a52795c1c49450fdc"),
+ },
+ initiatorIngressMacDigest: hexb("de72d7161bc7a9ddda4a70a48d08eda55d6fc4d90ef80a4b6645f81d6373e66b"),
+ initiatorEgressMacDigest: hexb("47bb77bff168de73c7ae34473f4d085abdf97cce7e01cab6ee3a4f69a021645f"),
+ },
+
+ // initiator V5, recipient V4
+ {
+ initiator: &Config{
+ Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"),
+ ForceV4: false,
+ },
+ recipient: &Config{
+ Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"),
+ ForceV4: true,
+ },
+ initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"),
+ recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"),
+ initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e10000000000000005"),
+ recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d8c7453c0089687a7"),
+
+ encAuth: hexb(`
+ 04e96a95f95ca7188aedd8abcf58f5f99efc7efed406923084655ce9b197cfe1
+ 0921159e105d1cf335e2f4755813598b868784130f4f0ae0bf1776b70d9e3061
+ 9205869af0963e0503133dabd7e5ed8ddf531ac3ac4d243f131252b8fd5a1638
+ 7296236b9b6b23432dcf02064284522690b8a07cf7e8cd9935409693203f31b9
+ 4fe5b2fba7e49a90711597e13ab318230a5fa3c89965569c9da21aa20686df25
+ a39bd3393c87ae0f9d11bd6b270480f3544be771fdca8fd2cebb161cc508ee38
+ f1eedb793d75f7e081fdf837be699fee2af1f00e2e8924d2ec5c64dabe445d0f
+ 14caae3c20583252fa8adace052a5f5832ebb957d3324cf12d27232934193806
+ a4bfdf9adc3a0921e0bdc7c7457cf35b5d99e729d7e0fd9aa09ab1217ff5ceca
+ 768fb1fc636499eecab58bb01f46eea652ccbd
+ `),
+ encAuthResp: hexb(`
+ 04d0f8e56113ee9402ab4fed101fe03842f265e13e9bb76af2ac1ffba11d8892
+ 524e59f1906eb2e6e35f774ccb3449d2f5084b96063e668fb73d90a94b0114dc
+ c14364a087f270adc65421741d87c492eafe1ca3b86a76313d026564e1abbb48
+ 6d03727c9baf8d0314af54296ea829aa086174a7836113f1dd420750af98f0b8
+ 802940ab16af05421d6b812054b285fdf1ae82ae0c08f1dbee3d60691979e8bd
+ 0b31599ac47138ba24d404699ae4558fec8bb94f120e63362e4b94a50894021e
+ 70e69101820018472823a48bc0d61c617c21
+ `),
+
+ negotiatedVersion: 4,
+ initiatorIngressSecrets: secrets{
+ encKey: hexb("3ca5db8d7d13af7bb3763fee9cef628925a4abda5961d7392fae731c02278377"),
+ encIV: hexb("00000000000000000000000000000000"),
+ macKey: hexb("cb2cd684639c1b64b80687b977c4140bea8c953a1f3975aca6f1589a850879ba"),
+ },
+ initiatorEgressSecrets: secrets{
+ encKey: hexb("3ca5db8d7d13af7bb3763fee9cef628925a4abda5961d7392fae731c02278377"),
+ encIV: hexb("00000000000000000000000000000000"),
+ macKey: hexb("cb2cd684639c1b64b80687b977c4140bea8c953a1f3975aca6f1589a850879ba"),
+ },
+ initiatorIngressMacDigest: hexb("d835ba6c6ac42d4c686a2e3cee6b2ee0190a7da79d6275f2b0b4bdc71fb66709"),
+ initiatorEgressMacDigest: hexb("1901e950288f010d005ccafede47d1dc177c442605a9702fcd6c5a0e717dc130"),
+ },
+
+ // initiator V4, recipient V5
+ {
+ initiator: &Config{
+ Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"),
+ ForceV4: true,
+ },
+ recipient: &Config{
+ Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"),
+ ForceV4: false,
+ },
+ initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"),
+ recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"),
+ initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e1c6b7ec66f077bb11"),
+ recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d0000000000000005"),
+
+ encAuth: hexb(`
+ 0461109a208261e0bedca9b3d474e991409fe0f5be7fd757d33c3a2934be7819
+ 269509b86229f36677f23aebe239c21e0c2b244811f9ee0d5370bc4cef1510a0
+ 77e1d4d295a6219a9393d7493a52b9e98ccca17a196b43efb30cdfaf2ea99fff
+ bd29c44b5b417924ad3afad6c436e85a0024e24f55f27bb8f86735a76586093f
+ 4d496348363cb2fb9905e62786c18030a8b4e3fe4a621439ae1c10598a09f9f5
+ e61315cea0cd09fb0438d9c76b1d516e183a8df2bdc2d7e3a51f4011a990999c
+ b5fb737b9dd16181f44253b313811286004efe9298d4ff49eefd28dc095ed362
+ 8b56551f052c4d94c0c0108d08656a0201eb29d66702acb06e2894fab08a684a
+ 394258171fc3f099c4f58a075ecde74c8084731639e19b194ff9eef6824a1330
+ 59bd884d81ef14541fd9475b9f3d8bcb613eb6
+ `),
+ encAuthResp: hexb(`
+ 04c77decb1d4abe500fb924a5972d495b6ca6782122e1e795d8282575302ed32
+ 85b0d4bae57ac07c2f455e67cf73d2c77b9dcd295252ca146a65ec3a7e9d6336
+ a1ed212843cafac42831c5a785fe7fc6e18a1ce7ae3d9603c439cdd991ec2f7a
+ 5197838efbd8c0ad68e31559c8a711ca3368bb6f4ed6de53db86df7a56eb2897
+ bbb251a2c2e86af3198e87bd98af9f3d96ae7f0777656a3a0c9dec4718f49377
+ 0f78b6fecc51f398c0e36e696da3b482584c00581b6b7e596b71580777972bf4
+ e158ce46acf49c893a509c00415996948df2
+ `),
+
+ negotiatedVersion: 4,
+ initiatorIngressSecrets: secrets{
+ encKey: hexb("fc8e46d37d756d53af5f6cebb35d94118bf305fe1c73fc9e672350cbc1dedd75"),
+ encIV: hexb("00000000000000000000000000000000"),
+ macKey: hexb("274693e239751ff505004ed5ab680fd80823d49a12139554bffce549b32d048c"),
+ },
+ initiatorEgressSecrets: secrets{
+ encKey: hexb("fc8e46d37d756d53af5f6cebb35d94118bf305fe1c73fc9e672350cbc1dedd75"),
+ encIV: hexb("00000000000000000000000000000000"),
+ macKey: hexb("274693e239751ff505004ed5ab680fd80823d49a12139554bffce549b32d048c"),
+ },
+ initiatorIngressMacDigest: hexb("395908f0f3da7e588aad3e6ec04fab504f0a65664bf8fe135b67ebaf4cc41daf"),
+ initiatorEgressMacDigest: hexb("09cc2e837d5cf048f41ea39dccc4b07814a125dc5d47e416ae13dac8d4523239"),
+ },
+
+ // old V4 test vector from https://gist.github.com/fjl/3a78780d17c755d22df2
+ {
+ initiator: &Config{
+ Key: hexkey("5e173f6ac3c669587538e7727cf19b782a4f2fda07c1eaa662c593e5e85e3051"),
+ ForceV4: true,
+ },
+ recipient: &Config{
+ Key: hexkey("c45f950382d542169ea207959ee0220ec1491755abe405cd7498d6b16adb6df8"),
+ ForceV4: true,
+ },
+ initiatorEphemeralKey: hexkey("19c2185f4f40634926ebed3af09070ca9e029f2edd5fae6253074896205f5f6c"),
+ recipientEphemeralKey: hexkey("d25688cf0ab10afa1a0e2dba7853ed5f1e5bf1c631757ed4e103b593ff3f5620"),
+ initiatorNonce: hexb("cd26fecb93657d1cd9e9eaf4f8be720b56dd1d39f190c4e1c6b7ec66f077bb11"),
+ recipientNonce: hexb("f37ec61d84cea03dcc5e8385db93248584e8af4b4d1c832d8c7453c0089687a7"),
+
+ encAuth: hexb(`
+ 04a0274c5951e32132e7f088c9bdfdc76c9d91f0dc6078e848f8e3361193dbdc
+ 43b94351ea3d89e4ff33ddcefbc80070498824857f499656c4f79bbd97b6c51a
+ 514251d69fd1785ef8764bd1d262a883f780964cce6a14ff206daf1206aa073a
+ 2d35ce2697ebf3514225bef186631b2fd2316a4b7bcdefec8d75a1025ba2c540
+ 4a34e7795e1dd4bc01c6113ece07b0df13b69d3ba654a36e35e69ff9d482d88d
+ 2f0228e7d96fe11dccbb465a1831c7d4ad3a026924b182fc2bdfe016a6944312
+ 021da5cc459713b13b86a686cf34d6fe6615020e4acf26bf0d5b7579ba813e77
+ 23eb95b3cef9942f01a58bd61baee7c9bdd438956b426a4ffe238e61746a8c93
+ d5e10680617c82e48d706ac4953f5e1c4c4f7d013c87d34a06626f498f34576d
+ c017fdd3d581e83cfd26cf125b6d2bda1f1d56
+ `),
+ encAuthResp: hexb(`
+ 049934a7b2d7f9af8fd9db941d9da281ac9381b5740e1f64f7092f3588d4f87f
+ 5ce55191a6653e5e80c1c5dd538169aa123e70dc6ffc5af1827e546c0e958e42
+ dad355bcc1fcb9cdf2cf47ff524d2ad98cbf275e661bf4cf00960e74b5956b79
+ 9771334f426df007350b46049adb21a6e78ab1408d5e6ccde6fb5e69f0f4c92b
+ b9c725c02f99fa72b9cdc8dd53cff089e0e73317f61cc5abf6152513cb7d833f
+ 09d2851603919bf0fbe44d79a09245c6e8338eb502083dc84b846f2fee1cc310
+ d2cc8b1b9334728f97220bb799376233e113
+ `),
+
+ negotiatedVersion: 4,
+ initiatorIngressSecrets: secrets{
+ encKey: hexb("c0458fa97a5230830e05f4f20b7c755c1d4e54b1ce5cf43260bb191eef4e418d"),
+ encIV: hexb("00000000000000000000000000000000"),
+ macKey: hexb("48c938884d5067a1598272fcddaa4b833cd5e7d92e8228c0ecdfabbe68aef7f1"),
+ },
+ initiatorEgressSecrets: secrets{
+ encKey: hexb("c0458fa97a5230830e05f4f20b7c755c1d4e54b1ce5cf43260bb191eef4e418d"),
+ encIV: hexb("00000000000000000000000000000000"),
+ macKey: hexb("48c938884d5067a1598272fcddaa4b833cd5e7d92e8228c0ecdfabbe68aef7f1"),
+ },
+ initiatorIngressMacDigest: hexb("75823d96e23136c89666ee025fb21a432be906512b3dd4a3049e898adb433847"),
+ initiatorEgressMacDigest: hexb("09771e93b1a6109e97074cbe2d2b0cf3d3878efafe68f53c41bb60c0ec49097e"),
+ },
+}
diff --git a/p2p/rlpx/rlpx.go b/p2p/rlpx/rlpx.go
new file mode 100644
index 0000000000..49d78aeffa
--- /dev/null
+++ b/p2p/rlpx/rlpx.go
@@ -0,0 +1,408 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Package rlpx implements the RLPx secure transport protocol.
+//
+// RLPx multiplexes packet streams over an authenticated and encrypted
+// network connection.
+//
+// The wire protocol specification lives at https://github.com/ethereum/devp2p.
+//
+// Protocols
+//
+// RLPx transports packet streams for multiple protocols on the same
+// connection, ensuring that available bandwidth is fairly distributed
+// among them. Negotiation of protocol identifiers is not part of the
+// transport layer and is typically done by sending messages with
+// protocol identifier 0.
+package rlpx
+
+import (
+ "crypto/ecdsa"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "sync"
+ "time"
+)
+
+const (
+ defaultHandshakeTimeout = 5 * time.Second
+ defaultReadTimeout = 10 * time.Second
+ defaultReadIdleTimeout = 25 * time.Second
+ defaultWriteTimeout = 10 * time.Second
+ defaultReadBufferSize = 2 * 1024 * 1024
+ defaultReadBufferWaitTimeout = 5 * time.Second
+)
+
+// A Config structure is used to configure an RLPx client or server
+// connection. After one has been passed to any function in package
+// rlpx, it must not be modified. A Config may be reused; the rlpx
+// package will also not modify it.
+type Config struct {
+ // Key is the private key of the server. The key must use the
+ // secp256k1 curve, other curves are not supported.
+ // This field is required for both client and server connections.
+ Key *ecdsa.PrivateKey
+
+ HandshakeTimeout time.Duration // for the key negotiation handshake (default 5s)
+ ReadIdleTimeout time.Duration // applies while waiting for a new frame (default 25s)
+ ReadTimeout time.Duration // for reading the payload data of a single frame (default 10s)
+ WriteTimeout time.Duration // for writing one frame of data (default 10s)
+
+ // ReadBufferSize controls how much data can be buffered for each
+ // protocol. The default is 2MB for compatibility with legacy
+ // peers.
+ //
+ // If the read buffer is full, the implementation waits for
+ // buffer space to become available. The connection is closed if
+ // no space becomes available within the timeout (default 5s).
+ ReadBufferSize uint32
+ ReadBufferWaitTimeout time.Duration
+
+ // Forces use of the version 4 handshake.
+ ForceV4 bool
+}
+
+func (cfg *Config) handshakeTimeout() time.Duration {
+ if cfg.HandshakeTimeout != 0 {
+ return cfg.HandshakeTimeout
+ }
+ return defaultHandshakeTimeout
+}
+
+func (cfg *Config) readTimeout() time.Duration {
+ if cfg.ReadTimeout != 0 {
+ return cfg.ReadTimeout
+ }
+ return defaultReadTimeout
+}
+
+func (cfg *Config) readIdleTimeout() time.Duration {
+ if cfg.ReadIdleTimeout != 0 {
+ return cfg.ReadIdleTimeout
+ }
+ return defaultReadIdleTimeout
+}
+
+func (cfg *Config) writeTimeout() time.Duration {
+ if cfg.WriteTimeout != 0 {
+ return cfg.WriteTimeout
+ }
+ return defaultWriteTimeout
+}
+
+func (cfg *Config) readBufferWaitTimeout() time.Duration {
+ if cfg.ReadBufferWaitTimeout != 0 {
+ return cfg.ReadBufferWaitTimeout
+ }
+ return defaultReadBufferWaitTimeout
+}
+
+func (cfg *Config) readBufferSize() uint32 {
+ if cfg.ReadBufferSize != 0 {
+ return cfg.ReadBufferSize
+ }
+ return defaultReadBufferSize
+}
+
+// Conn represents an RLPx connection.
+type Conn struct {
+ // readonly fields
+ cfg *Config
+ isServer bool
+ fd net.Conn
+ handshake sync.Once
+ handshakeRand handshakeRandSource // for testing
+
+ wmu sync.Mutex // excludes writes on rw
+ rw *frameRW // set after handshake
+ remoteID *ecdsa.PublicKey
+ vsn uint // negotiated version
+
+ mu sync.Mutex
+ proto map[uint16]*Protocol
+ readErr error
+}
+
+// Client returns a new client side RLPx connection using fd as the
+// underlying transport. The public key of the remote end must be
+// known in advance.
+//
+// config must not be nil and must contain a
+// valid private key.
+func Client(fd net.Conn, remotePubkey *ecdsa.PublicKey, config *Config) *Conn {
+ c := newConn(fd, config)
+ c.remoteID = remotePubkey
+ return c
+}
+
+// Server returns a new server side RLPx connection using fd as the
+// underlying transport. The configuration config must be non-nil and
+// must contain a valid private key
+func Server(fd net.Conn, config *Config) *Conn {
+ c := newConn(fd, config)
+ c.isServer = true
+ return c
+}
+
+func newConn(fd net.Conn, config *Config) *Conn {
+ return &Conn{
+ fd: fd,
+ cfg: config,
+ proto: make(map[uint16]*Protocol),
+ }
+}
+
+// Handshake runs the client or server handshake protocol if it has
+// not yet been run. Most uses of this package need not call Handshake
+// explicitly: the first Read or Write will call it automatically.
+func (c *Conn) Handshake() (err error) {
+ // TODO: check cfg.Key curve, maybe panic earlier
+ c.handshake.Do(func() {
+ if c.handshakeRand == nil {
+ c.handshakeRand = realRandSource{}
+ }
+ var (
+ ingress, egress secrets
+ rid *ecdsa.PublicKey
+ vsn uint
+ )
+ c.fd.SetDeadline(time.Now().Add(c.cfg.handshakeTimeout()))
+ if c.isServer {
+ vsn, rid, ingress, egress, err = c.recipientHandshake()
+ } else {
+ vsn, ingress, egress, err = c.initiatorHandshake()
+ }
+ if err != nil {
+ return
+ }
+
+ c.mu.Lock()
+ c.vsn = vsn
+ if rid != nil {
+ c.remoteID = rid
+ }
+ c.mu.Unlock()
+ c.rw = newFrameRW(c.fd, ingress, egress)
+ go readLoop(c)
+ })
+ if err == nil && c.rw == nil {
+ return errors.New("handshake failed")
+ }
+ return err
+}
+
+// LocalAddr returns the local network address of the underlying net.Conn.
+func (c *Conn) LocalAddr() net.Addr {
+ return c.fd.LocalAddr()
+}
+
+// RemoteAddr returns the remote network address of the underlying net.Conn.
+func (c *Conn) RemoteAddr() net.Addr {
+ return c.fd.RemoteAddr()
+}
+
+// RemoteID returns the public key of the remote end.
+// If the remote identity is not yet known, it returns nil.
+func (c *Conn) RemoteID() *ecdsa.PublicKey {
+ c.mu.Lock()
+ id := c.remoteID
+ c.mu.Unlock()
+ return id
+}
+
+// Version returns the negotiated RLPx version of the connection.
+// The return value is zero before the handshake has executed and
+// can be 4 or 5 afterwards.
+func (c *Conn) Version() uint {
+ c.mu.Lock()
+ vsn := c.vsn
+ c.mu.Unlock()
+ return vsn
+}
+
+// Close closes the connection.
+func (c *Conn) Close() error {
+ // TODO: shut down reader/wr
+ return c.fd.Close()
+}
+
+// Protocol returns a handle for the given protocol id.
+// It can be called at most once for any given id,
+// subsequent call with the same id will panic.
+func (c *Conn) Protocol(id uint16) *Protocol {
+ p := c.getProtocol(id)
+ close(p.claimSignal) // panics when claimed twice
+ return p
+}
+
+// waits until the given protocol is claimed by a call to Protocol.
+func (c *Conn) waitForProtocol(id uint16) *Protocol {
+ p := c.getProtocol(id)
+ timeout := time.NewTimer(5 * time.Second)
+ defer timeout.Stop()
+ select {
+ case <-timeout.C:
+ return nil
+ case <-p.claimSignal:
+ return p
+ }
+}
+
+func (c *Conn) getProtocol(id uint16) *Protocol {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.proto[id] == nil {
+ c.proto[id] = newProtocol(c, id)
+ }
+ return c.proto[id]
+}
+
+// Protocol is a handle for the given protocol.
+type Protocol struct {
+ c *Conn
+ claimed bool
+ id uint16
+ claimSignal chan struct{}
+
+ // for readLoop
+ xfers map[uint16]*packetReader
+ readBufSema *bufSema
+
+ // for ReadPacket
+ readCond *sync.Cond // unblocks ReadPacket
+ newPackets []*packetReader
+ readErr error
+
+ // for writing
+ contextidSeq uint16
+}
+
+func newProtocol(c *Conn, id uint16) *Protocol {
+ return &Protocol{
+ c: c,
+ id: id,
+ claimSignal: make(chan struct{}),
+ xfers: make(map[uint16]*packetReader),
+ readBufSema: newBufSema(c.cfg.readBufferSize()),
+ readCond: sync.NewCond(new(sync.Mutex)),
+ }
+}
+
+func (p *Protocol) feedPacket(pr *packetReader) {
+ p.readCond.L.Lock()
+ p.newPackets = append(p.newPackets, pr)
+ p.readCond.Signal()
+ p.readCond.L.Unlock()
+}
+
+func (p *Protocol) readClose(err error) {
+ p.readCond.L.Lock()
+ p.readErr = err
+ p.readCond.Broadcast()
+ p.readCond.L.Unlock()
+}
+
+// ReadHeader waits for a packet to appear. The content of the packet
+// can be read from r as it is received. More packets can be read
+// immediately, r does not need to be consumed before the next call.
+func (p *Protocol) ReadPacket() (totalSize uint32, r io.Reader, err error) {
+ // Lazy handshake.
+ if err := p.c.Handshake(); err != nil {
+ return 0, nil, err
+ }
+ // Wait for a packet or error.
+ p.readCond.L.Lock()
+ defer p.readCond.L.Unlock()
+ for len(p.newPackets) == 0 && p.readErr == nil {
+ p.readCond.Wait()
+ }
+ if p.readErr != nil {
+ return 0, nil, p.readErr
+ }
+ pr := p.newPackets[0]
+ p.newPackets = p.newPackets[:copy(p.newPackets, p.newPackets[1:])]
+ return pr.readN, pr, nil
+}
+
+// SendPacket sends len bytes from the payload reader on the connection.
+func (p *Protocol) SendPacket(len uint32, payload io.Reader) error {
+ if err := p.c.Handshake(); err != nil {
+ return err
+ }
+ if len <= staticFrameSize {
+ // The message is small enough and can be sent in a single frame.
+ buf := makeFrameWriteBuffer()
+ if n, err := io.CopyN(buf, payload, int64(len)); err != nil {
+ return fmt.Errorf("read from packet payload failed at pos %d: %v", n, err)
+ }
+ return p.c.sendFrame(regularHeader{p.id, 0}, buf)
+ }
+ return p.sendChunked(len, payload)
+}
+
+func (p *Protocol) sendChunked(size uint32, payload io.Reader) error {
+ contextid := p.nextContextID()
+ initial := true
+ buf := makeFrameWriteBuffer()
+ var rpos int64
+ for seq := uint16(0); size > 0; seq++ {
+ var header interface{}
+ if initial {
+ header = chunkStartHeader{p.id, contextid, size}
+ initial = false
+ } else {
+ header = regularHeader{p.id, contextid}
+ }
+
+ fsize := staticFrameSize
+ if size < fsize {
+ fsize = size
+ }
+ if !initial {
+ buf.resetForWrite()
+ }
+ if n, err := io.CopyN(buf, payload, int64(fsize)); err != nil {
+ // The remote end is waiting for the rest of the packet
+ // but we can't provide it. Since there is no way to cancel
+ // partial transfers, our only option is closing the connection.
+ // TODO: close the connection
+ return fmt.Errorf("read from packet payload failed at pos %d: %v", rpos+n, err)
+ }
+ rpos += int64(fsize)
+ if err := p.c.sendFrame(header, buf); err != nil {
+ return err
+ }
+ size -= fsize
+ }
+ return nil
+}
+
+// returns the next context ID for a chunked transfer.
+// never returns 0, which is reserved for single-frame transfers.
+func (p *Protocol) nextContextID() uint16 {
+ p.contextidSeq++
+ return p.contextidSeq
+}
+
+func (c *Conn) sendFrame(header interface{}, body *frameBuffer) error {
+ c.wmu.Lock()
+ defer c.wmu.Unlock()
+ c.fd.SetWriteDeadline(time.Now().Add(c.cfg.writeTimeout()))
+ return c.rw.sendFrame(header, body)
+}
diff --git a/p2p/rlpx/rlpx_test.go b/p2p/rlpx/rlpx_test.go
new file mode 100644
index 0000000000..3c6aad97af
--- /dev/null
+++ b/p2p/rlpx/rlpx_test.go
@@ -0,0 +1,186 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rlpx
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "sync"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/crypto"
+)
+
+func TestSequentialTransfer(t *testing.T) {
+ var (
+ p1, p2 = net.Pipe()
+ k1, k2 = newkey(), newkey()
+ sc = Server(p1, &Config{Key: k1})
+ cc = Client(p2, &k1.PublicKey, &Config{Key: k2})
+ )
+ run(t, rig{
+ "server": func() error { return testProtoReaders(t, sc, 1) },
+ "client": func() error { return testProtoWriters(t, cc, 1) },
+ })
+}
+
+func TestConcurrentTransfer(t *testing.T) {
+ var (
+ p1, p2 = net.Pipe()
+ k1, k2 = newkey(), newkey()
+ sc = Server(p1, &Config{Key: k1})
+ cc = Client(p2, &k1.PublicKey, &Config{Key: k2})
+ )
+ run(t, rig{
+ "server": func() error { return testProtoReaders(t, cc, 10) },
+ "client": func() error { return testProtoWriters(t, sc, 10) },
+ })
+}
+
+func TestConcurrentTransferReadError(t *testing.T) {
+ var (
+ p1, p2 = net.Pipe()
+ k1, k2 = newkey(), newkey()
+ sc = Server(p1, &Config{Key: k1})
+ cc = Client(p2, &k1.PublicKey, &Config{Key: k2})
+ badPacketSize = uint32(16 * 8 * 1024)
+ )
+ run(t, rig{
+ "client": func() error { return testProtoWriters(t, sc, 10) },
+ "server": func() error { return testProtoReaders(t, cc, 10) },
+
+ // This sends a bad frame after a two sane ones.
+ "badFrameWrite": func() error {
+ if err := cc.Handshake(); err != nil {
+ return fmt.Errorf("handshake error: %v", err)
+ }
+ heads := []interface{}{
+ chunkStartHeader{Protocol: 11, ContextID: 1, TotalSize: badPacketSize},
+ regularHeader{Protocol: 11, ContextID: 1},
+ // The bad frame is a chunk start header with a context id
+ // that is already in use.
+ chunkStartHeader{Protocol: 11, ContextID: 1, TotalSize: 22},
+ }
+ for i, h := range heads {
+ buf := makeFrameWriteBuffer()
+ buf.Write(make([]byte, 1024))
+ if err := cc.sendFrame(h, buf); err != nil {
+ return fmt.Errorf("error sending frame %d: %v", i, err)
+ }
+ }
+ return nil
+ },
+
+ // The other end should receive an error from Read.
+ "badFrameRead": func() error {
+ proto := sc.Protocol(11)
+ _, r, err := proto.ReadPacket()
+ if err != nil {
+ return fmt.Errorf("unexpected ReadPacket error: %v", err)
+ }
+ _, err = io.CopyN(ioutil.Discard, r, int64(badPacketSize))
+ if err == nil {
+ return fmt.Errorf("no error received")
+ }
+ if err != errUnexpectedChunkStart {
+ return fmt.Errorf("wrong error: got %q want %q", err, errUnexpectedChunkStart)
+ }
+ // TODO: shouldn't all transfers fail?
+ return nil
+ },
+ })
+}
+
+func testProtoWriters(t *testing.T, conn *Conn, nprotos uint16) error {
+ defer conn.Close()
+ writers := rig{}
+ for i := uint16(0); i < nprotos; i++ {
+ i := i
+ writers[fmt.Sprint("protocol ", i)] = func() error { return testWriter(t, conn.Protocol(i)) }
+ }
+ run(t, writers)
+ return nil
+}
+
+func testProtoReaders(t *testing.T, conn *Conn, nprotos uint16) error {
+ defer conn.Close()
+ readers := rig{}
+ for i := uint16(0); i < nprotos; i++ {
+ i := i
+ readers[fmt.Sprint("protocol ", i)] = func() error { return testReader(t, conn.Protocol(i)) }
+ }
+ run(t, readers)
+ return nil
+}
+
+func testWriter(t *testing.T, p *Protocol) error {
+ for size := 1; size < 8*1024*1024; size *= 2 {
+ if err := sendBytes(p, make([]byte, size)); err != nil {
+ return fmt.Errorf("error sending %d bytes: %v", size, err)
+ }
+ }
+ return nil
+}
+
+func testReader(t *testing.T, p *Protocol) error {
+ for size := 1; size < 8*1024*1024; size *= 2 {
+ len, r, err := p.ReadPacket()
+ if err != nil {
+ return fmt.Errorf("ReadPacket error with size %d: %v", size, err)
+ }
+ if len != uint32(size) {
+ return fmt.Errorf("len mismatch, got %d want %d", len, size)
+ }
+ if n, err := io.CopyN(ioutil.Discard, r, int64(size)); err != nil {
+ return fmt.Errorf("body read error at %d of %d bytes: %v", n, size, err)
+ }
+ }
+ return nil
+}
+
+type rig map[string]func() error
+
+func run(t *testing.T, rig rig) {
+ var wg sync.WaitGroup
+ wg.Add(len(rig))
+ for name, fn := range rig {
+ name, fn := name, fn
+ go func() {
+ if err := fn(); err != nil {
+ t.Error(name, err)
+ }
+ wg.Done()
+ }()
+ }
+ wg.Wait()
+}
+
+func sendBytes(p *Protocol, data []byte) error {
+ return p.SendPacket(uint32(len(data)), bytes.NewReader(data))
+}
+
+func newkey() *ecdsa.PrivateKey {
+ key, err := crypto.GenerateKey()
+ if err != nil {
+ panic("couldn't generate key: " + err.Error())
+ }
+ return key
+}