Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions pkg/execution/mimicry/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package mimicry

import (
"bytes"
"crypto/ecdsa"
"time"
)

// StatusProvider is a callback function that returns a Status response
// for an incoming connection. It receives the remote Hello for context.
type StatusProvider func(remoteHello *Hello) (Status, error)

// Network defines filter criteria for allowed networks.
// All non-nil/non-zero fields must match for a peer to be accepted.
// If all fields are nil/zero, matches any network.
type Network struct {
NetworkID *uint64 // nil = don't filter on network ID
ForkIDHash []byte // nil = don't filter on fork ID hash
ForkIDNext *uint64 // nil = don't filter on fork ID next
Genesis []byte // nil = don't filter on genesis hash
}

// Matches returns true if the given Status matches this Network filter.
func (n *Network) Matches(status Status) bool {
if n.NetworkID != nil && *n.NetworkID != status.GetNetworkID() {
return false
}

if n.ForkIDHash != nil && !bytes.Equal(n.ForkIDHash, status.GetForkIDHash()) {
return false
}

if n.ForkIDNext != nil && *n.ForkIDNext != status.GetForkIDNext() {
return false
}

if n.Genesis != nil && !bytes.Equal(n.Genesis, status.GetGenesis()) {
return false
}

return true
}

// ServerConfig configures the Server for accepting incoming connections.
type ServerConfig struct {
// Name is the name to advertise in Hello messages.
Name string

// PrivateKey is the ECDSA private key for the server.
// If nil, a new key will be generated.
PrivateKey *ecdsa.PrivateKey

// ListenAddr is the address to listen on (e.g., ":30303").
ListenAddr string

// MaxPeers is the maximum number of concurrent connections.
// 0 means unlimited.
MaxPeers int

// HandshakeTimeout is the timeout for RLPx handshake.
// Default: 5s
HandshakeTimeout time.Duration

// ReadTimeout is the timeout for reading messages.
// Default: 30s
ReadTimeout time.Duration

// StatusProvider is a required callback that returns the Status
// to send in response to incoming peers.
StatusProvider StatusProvider

// AllowedNetworks filters incoming peers by network.
// If empty/nil, all networks are allowed.
// If set, peer must match at least ONE of the networks.
AllowedNetworks []Network
}

// DefaultServerConfig returns a ServerConfig with sensible defaults.
func DefaultServerConfig() *ServerConfig {
return &ServerConfig{
HandshakeTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
}
}
193 changes: 193 additions & 0 deletions pkg/execution/mimicry/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package mimicry

import (
"testing"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// mockStatus implements the Status interface for testing.
type mockStatus struct {
networkID uint64
forkIDHash []byte
forkIDNext uint64
genesis []byte
head []byte
}

func (m *mockStatus) Code() int { return StatusCode }
func (m *mockStatus) ReqID() uint64 { return 0 }
func (m *mockStatus) GetNetworkID() uint64 { return m.networkID }
func (m *mockStatus) GetForkIDHash() []byte { return m.forkIDHash }
func (m *mockStatus) GetForkIDNext() uint64 { return m.forkIDNext }
func (m *mockStatus) GetGenesis() []byte { return m.genesis }
func (m *mockStatus) GetHead() []byte { return m.head }

func ptr[T any](v T) *T {
return &v
}

func TestNetworkMatches(t *testing.T) {
mainnetGenesis := common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
sepoliaGenesis := common.HexToHash("0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9")
dencunForkHash := []byte{0x9f, 0x3d, 0x22, 0x54}

tests := []struct {
name string
network Network
status Status
want bool
}{
{
name: "empty filter matches any network",
network: Network{},
status: &mockStatus{
networkID: 1,
genesis: mainnetGenesis[:],
forkIDHash: dencunForkHash,
forkIDNext: 0,
},
want: true,
},
{
name: "network ID filter - match",
network: Network{
NetworkID: ptr(uint64(1)),
},
status: &mockStatus{
networkID: 1,
genesis: mainnetGenesis[:],
},
want: true,
},
{
name: "network ID filter - no match",
network: Network{
NetworkID: ptr(uint64(1)),
},
status: &mockStatus{
networkID: 11155111, // sepolia
genesis: sepoliaGenesis[:],
},
want: false,
},
{
name: "genesis filter - match",
network: Network{
Genesis: mainnetGenesis[:],
},
status: &mockStatus{
networkID: 1,
genesis: mainnetGenesis[:],
},
want: true,
},
{
name: "genesis filter - no match",
network: Network{
Genesis: mainnetGenesis[:],
},
status: &mockStatus{
networkID: 11155111,
genesis: sepoliaGenesis[:],
},
want: false,
},
{
name: "fork ID hash filter - match",
network: Network{
ForkIDHash: dencunForkHash,
},
status: &mockStatus{
networkID: 1,
forkIDHash: dencunForkHash,
},
want: true,
},
{
name: "fork ID hash filter - no match",
network: Network{
ForkIDHash: dencunForkHash,
},
status: &mockStatus{
networkID: 1,
forkIDHash: []byte{0x00, 0x00, 0x00, 0x00},
},
want: false,
},
{
name: "fork ID next filter - match",
network: Network{
ForkIDNext: ptr(uint64(1000)),
},
status: &mockStatus{
networkID: 1,
forkIDNext: 1000,
},
want: true,
},
{
name: "fork ID next filter - no match",
network: Network{
ForkIDNext: ptr(uint64(1000)),
},
status: &mockStatus{
networkID: 1,
forkIDNext: 2000,
},
want: false,
},
{
name: "multiple filters - all match",
network: Network{
NetworkID: ptr(uint64(1)),
Genesis: mainnetGenesis[:],
ForkIDHash: dencunForkHash,
},
status: &mockStatus{
networkID: 1,
genesis: mainnetGenesis[:],
forkIDHash: dencunForkHash,
},
want: true,
},
{
name: "multiple filters - one doesn't match",
network: Network{
NetworkID: ptr(uint64(1)),
Genesis: mainnetGenesis[:],
ForkIDHash: dencunForkHash,
},
status: &mockStatus{
networkID: 1,
genesis: sepoliaGenesis[:], // doesn't match
forkIDHash: dencunForkHash,
},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.network.Matches(tt.status)
assert.Equal(t, tt.want, got)
})
}
}

func TestDefaultServerConfig(t *testing.T) {
config := DefaultServerConfig()

require.NotNil(t, config)
assert.Equal(t, 5*time.Second, config.HandshakeTimeout)
assert.Equal(t, 30*time.Second, config.ReadTimeout)
assert.Empty(t, config.ListenAddr)
assert.Empty(t, config.Name)
assert.Nil(t, config.PrivateKey)
assert.Nil(t, config.StatusProvider)
assert.Empty(t, config.AllowedNetworks)
assert.Equal(t, 0, config.MaxPeers)
}
19 changes: 19 additions & 0 deletions pkg/execution/mimicry/message_disconnect.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ func (h *Disconnect) Code() int { return DisconnectCode }

func (h *Disconnect) ReqID() uint64 { return 0 }

// decodeDisconnect decodes a Disconnect message from RLP-encoded data.
func decodeDisconnect(data []byte) (*Disconnect, error) {
d := new(p2p.DiscReason)

if len(data) > 0 {
reason := data[0:1]
// besu sends 2 byte disconnect message
if len(data) > 1 {
reason = data[1:2]
}

if err := rlp.DecodeBytes(reason, &d); err != nil {
return nil, err
}
}

return &Disconnect{Reason: *d}, nil
}

func (c *Client) receiveDisconnect(ctx context.Context, data []byte) *Disconnect {
d := new(p2p.DiscReason)

Expand Down
36 changes: 26 additions & 10 deletions pkg/execution/mimicry/message_hello.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mimicry

import (
"context"
"crypto/ecdsa"
"fmt"

"github.com/ethereum/go-ethereum/crypto"
Expand Down Expand Up @@ -92,7 +93,7 @@ func (h *Hello) ETHProtocolVersion() uint {
}

func SupportedEthCaps() []p2p.Cap {
caps := []p2p.Cap{}
caps := make([]p2p.Cap, 0, maxETHProtocolVersion-minETHProtocolVersion+1)
for i := minETHProtocolVersion; i <= maxETHProtocolVersion; i++ {
caps = append(caps, p2p.Cap{
Name: ETHCapName,
Expand All @@ -103,12 +104,34 @@ func SupportedEthCaps() []p2p.Cap {
return caps
}

func (c *Client) receiveHello(ctx context.Context, data []byte) (*Hello, error) {
// decodeHello decodes a Hello message from RLP-encoded data.
func decodeHello(data []byte) (*Hello, error) {
h := new(Hello)
if err := rlp.DecodeBytes(data, &h); err != nil {
return nil, fmt.Errorf("error decoding hello: %w", err)
}

return h, nil
}

// encodeHello encodes a Hello message to RLP bytes.
func encodeHello(privateKey *ecdsa.PrivateKey, caps []p2p.Cap) ([]byte, error) {
pub0 := crypto.FromECDSAPub(&privateKey.PublicKey)[1:]
hello := &Hello{
Version: P2PProtocolVersion,
Caps: caps,
ID: pub0,
}

return rlp.EncodeToBytes(hello)
}

func (c *Client) receiveHello(ctx context.Context, data []byte) (*Hello, error) {
h, err := decodeHello(data)
if err != nil {
return nil, err
}

c.log.WithFields(logrus.Fields{
"version": h.Version,
"caps": h.Caps,
Expand All @@ -127,14 +150,7 @@ func (c *Client) sendHello(ctx context.Context) error {
"code": HelloCode,
}).Debug("sending Hello")

pub0 := crypto.FromECDSAPub(&c.privateKey.PublicKey)[1:]
hello := &Hello{
Version: P2PProtocolVersion,
Caps: SupportedEthCaps(),
ID: pub0,
}

encodedData, err := rlp.EncodeToBytes(hello)
encodedData, err := encodeHello(c.privateKey, SupportedEthCaps())
if err != nil {
return fmt.Errorf("error encoding hello: %w", err)
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/execution/mimicry/message_new_pooled_transaction_hashes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ func (msg *NewPooledTransactionHashes) Code() int { return NewPooledTransactionH

func (msg *NewPooledTransactionHashes) ReqID() uint64 { return 0 }

// decodeNewPooledTransactionHashes decodes a NewPooledTransactionHashes message from RLP-encoded data.
func decodeNewPooledTransactionHashes(data []byte) (*NewPooledTransactionHashes, error) {
s := new(NewPooledTransactionHashes)
if err := rlp.DecodeBytes(data, &s); err != nil {
return nil, fmt.Errorf("error decoding new pooled transaction hashes: %w", err)
}

return s, nil
}

func (c *Client) receiveNewPooledTransactionHashes(ctx context.Context, data []byte) (*NewPooledTransactionHashes, error) {
s := new(NewPooledTransactionHashes)
if err := rlp.DecodeBytes(data, &s); err != nil {
Expand Down
Loading
Loading