Skip to content

Commit 308188b

Browse files
authored
simulators/ethereum/engine: Invalid Terminal Block due to Invalid Execution Tests (#626)
* simulators/ethereum/engine: Multiple bootnodes on client start * simulator/ethereum/engine: Refactor helper, add block modifiers * simulator/ethereum/engine: Invalid terminal block gossip tests * simulators/ethereum/engine: block distant future test * simulators/ethereum/engine: update readme
1 parent 4500c1e commit 308188b

File tree

10 files changed

+770
-491
lines changed

10 files changed

+770
-491
lines changed

simulators/ethereum/engine/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,13 +372,50 @@ Test cases using multiple Proof of Work chains to test the client's behavior whe
372372
- Blocks `PoW4` through `PoW8` are all siblings and all reach TTD
373373
- Verify that `PoW Receiver` receives exactly 7 gossiped new blocks (`PoW2` through `PoW8`)
374374

375+
- Terminal blocks are gossiped (Common Ancestor Depth 5)
376+
- Clients `C1`, `PoW Producer` and `PoW Receiver` start with `G <- PoW1`
377+
- `PoW Producer` and `PoW Receiver` are connected only to `C1`, not between each other
378+
- `PoW Producer` continues mining chains `PoW1 <- PoW2 <- PoW3 <- PoW4 <- PoW5 <- PoW6` and `PoW1 <- PoW2' <- PoW3' <- PoW4' <- PoW5' <- PoW6'`
379+
- Blocks `PoW2` and `PoW2'` have same parent `PoW1`, but have different hashes (e.g. different `ethash` seal)
380+
- Blocks `PoW6` and `PoW6'` reach TTD
381+
- Verify that `PoW Receiver` receives exactly 10 gossiped new blocks (`PoW2` through `PoW6` and `PoW2'` through `PoW6'`)
382+
375383
- Build Payload After Multiple Terminal blocks via gossip
376384
- Clients `C1` and `PoW Producer` start with `G <- PoW1`
377385
- `PoW Producer` continues mining chains `PoW1 <- PoW2 <- PoW3`, `PoW1 <- PoW2 <- PoW4`, ..., `PoW1 <- PoW2 <- PoW7`
378386
- Blocks `PoW3` through `PoW7` are all siblings and all reach TTD
379387
- Send `newPayload(P1)` where `P1.parentHash == PoW7` to `C1`
380388
- Verify that `C1` immediately validates `P1` and returns `VALID`
381389

390+
- Build Payload After Multiple Terminal blocks via gossip (Common Ancestor Depth 5)
391+
- Clients `C1` and `PoW Producer` start with `G <- PoW1`
392+
- `PoW Producer` continues mining chains `PoW1 <- PoW2 <- PoW3 <- PoW4 <- PoW5 <- PoW6` and `PoW1 <- PoW2' <- PoW3' <- PoW4' <- PoW5' <- PoW6'`
393+
- Blocks `PoW2` and `PoW2'` have same parent `PoW1`, but have different hashes (e.g. different `ethash` seal)
394+
- Blocks `PoW6` and `PoW6'` reach TTD
395+
- Send `newPayload(P1)` where `P1.parentHash == PoW6'` to `C1`
396+
- Verify that `C1` immediately validates `P1` and returns `VALID`
397+
398+
- Transition on an Invalid Terminal Execution - Difficulty
399+
- Clients `C1` and `PoW Producer` start with `G <- PoW1`
400+
- `PoW Producer` mines and gossips `PoW2`
401+
- `PoW2` has a `difficulty` value which reaches `TTD`, but it's not the correct expected value for `PoW2` according to `ethash` consensus rules
402+
- Send `newPayload(P1)` where `P1.parentHash == PoW2` to `C1`
403+
- Verify that `C1` does not follow `PoS` chain built on top of `P1` and its head still points to `PoW1`
404+
405+
- Transition on an Invalid Terminal Execution - Distant Future
406+
- Clients `C1` and `PoW Producer` start with `G <- PoW1`
407+
- `PoW Producer` mines and gossips `PoW2`
408+
- `PoW2` has a correct `difficulty` value and reaches `TTD`, but its `timestamp` value is 60 seconds into the future
409+
- Send `newPayload(P1)` where `P1.parentHash == PoW2` to `C1`
410+
- Verify that `C1` does not follow `PoS` chain built on top of `P1` and its head still points to `PoW1`
411+
412+
- Transition on an Invalid Terminal Execution - Sealed MixHash/Nonce
413+
- Clients `C1` and `PoW Producer` start with `G <- PoW1`
414+
- `PoW Producer` mines and gossips `PoW2`
415+
- `PoW2` has a correct `difficulty` value and reaches `TTD`, but its `mixHash`/`nonce` values are incorrect according to `ethash` consensus rules
416+
- Send `newPayload(P1)` where `P1.parentHash == PoW2` to `C1`
417+
- Verify that `C1` does not follow `PoS` chain built on top of `P1` and its head still points to `PoW1`
418+
382419

383420
## Engine API Authentication (JWT) Tests:
384421
- No time drift, correct secret:

simulators/ethereum/engine/client/engine.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ type EngineClient interface {
5757
}
5858

5959
type EngineStarter interface {
60-
StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClient EngineClient) (EngineClient, error)
60+
StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...EngineClient) (EngineClient, error)
6161
}
6262

6363
var (

simulators/ethereum/engine/client/hive_rpc/hive_rpc.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math/big"
88
"net"
99
"net/http"
10+
"strings"
1011
"time"
1112

1213
"github.com/ethereum/go-ethereum"
@@ -33,7 +34,7 @@ type HiveRPCEngineStarter struct {
3334
JWTSecret []byte
3435
}
3536

36-
func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClient client.EngineClient) (client.EngineClient, error) {
37+
func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (client.EngineClient, error) {
3738
var (
3839
clientType = s.ClientType
3940
enginePort = s.EnginePort
@@ -78,12 +79,19 @@ func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Cont
7879
ttdInt := helper.CalculateRealTTD(ClientFiles["/genesis.json"], ttd.Int64())
7980
ClientParams = ClientParams.Set("HIVE_TERMINAL_TOTAL_DIFFICULTY", fmt.Sprintf("%d", ttdInt))
8081
}
81-
if bootClient != nil {
82-
enode, err := bootClient.EnodeURL()
83-
if err != nil {
84-
return nil, fmt.Errorf("Unable to obtain bootnode: %v", err)
82+
if bootClients != nil && len(bootClients) > 0 {
83+
var (
84+
enodes = make([]string, len(bootClients))
85+
err error
86+
)
87+
for i, bootClient := range bootClients {
88+
enodes[i], err = bootClient.EnodeURL()
89+
if err != nil {
90+
return nil, fmt.Errorf("Unable to obtain bootnode: %v", err)
91+
}
8592
}
86-
ClientParams = ClientParams.Set("HIVE_BOOTNODE", enode)
93+
enodeString := strings.Join(enodes, ",")
94+
ClientParams = ClientParams.Set("HIVE_BOOTNODE", enodeString)
8795
}
8896

8997
// Start the client and create the engine client object

simulators/ethereum/engine/client/node/node.go

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ type GethNodeTestConfiguration struct {
4545
// The node mines proof of work blocks
4646
PoWMiner bool
4747
PoWMinerEtherBase common.Address
48+
DisableGossiping bool
49+
50+
// Block Modifier
51+
BlockModifier helper.BlockModifier
4852

4953
// In PoW production mode, produce many terminal blocks which shall be gossiped
5054
TerminalBlockSiblingCount *big.Int
@@ -81,15 +85,14 @@ var (
8185
DefaultTerminalBlockSiblingDepth = big.NewInt(1)
8286
)
8387

84-
func (s GethNodeEngineStarter) StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClient client.EngineClient) (client.EngineClient, error) {
85-
return s.StartGethNode(T, testContext, ClientParams, ClientFiles, bootClient)
88+
func (s GethNodeEngineStarter) StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (client.EngineClient, error) {
89+
return s.StartGethNode(T, testContext, ClientParams, ClientFiles, bootClients...)
8690
}
8791

88-
func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClient client.EngineClient) (*GethNode, error) {
92+
func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (*GethNode, error) {
8993
var (
90-
ttd = s.TerminalTotalDifficulty
91-
bootnode string
92-
err error
94+
ttd = s.TerminalTotalDifficulty
95+
err error
9396
)
9497
genesisPath, ok := ClientFiles["/genesis.json"]
9598
if !ok {
@@ -111,10 +114,14 @@ func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.C
111114
}
112115
genesis.Config.TerminalTotalDifficulty = ttd
113116

114-
if bootClient != nil {
115-
bootnode, err = bootClient.EnodeURL()
116-
if err != nil {
117-
return nil, fmt.Errorf("Unable to obtain bootnode: %v", err)
117+
var enodes []string
118+
if bootClients != nil && len(bootClients) > 0 {
119+
enodes = make([]string, len(bootClients))
120+
for i, bootClient := range bootClients {
121+
enodes[i], err = bootClient.EnodeURL()
122+
if err != nil {
123+
return nil, fmt.Errorf("Unable to obtain bootnode: %v", err)
124+
}
118125
}
119126
}
120127

@@ -138,7 +145,7 @@ func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.C
138145
s.Config.TerminalBlockSiblingDepth = DefaultTerminalBlockSiblingDepth
139146
}
140147

141-
g, err := newNode(s.Config, bootnode, &genesis)
148+
g, err := newNode(s.Config, enodes, &genesis)
142149
if err != nil {
143150
return nil, err
144151
}
@@ -166,7 +173,6 @@ type GethNode struct {
166173
mustHeadBlock *types.Block
167174

168175
datadir string
169-
bootnode string
170176
genesis *core.Genesis
171177
ttd *big.Int
172178
api *ethcatalyst.ConsensusAPI
@@ -190,14 +196,14 @@ type GethNode struct {
190196
config GethNodeTestConfiguration
191197
}
192198

193-
func newNode(config GethNodeTestConfiguration, bootnode string, genesis *core.Genesis) (*GethNode, error) {
199+
func newNode(config GethNodeTestConfiguration, bootnodes []string, genesis *core.Genesis) (*GethNode, error) {
194200
// Define the basic configurations for the Ethereum node
195201
datadir, _ := os.MkdirTemp("", "")
196202

197-
return restart(config, bootnode, datadir, genesis)
203+
return restart(config, bootnodes, datadir, genesis)
198204
}
199205

200-
func restart(startConfig GethNodeTestConfiguration, bootnode, datadir string, genesis *core.Genesis) (*GethNode, error) {
206+
func restart(startConfig GethNodeTestConfiguration, bootnodes []string, datadir string, genesis *core.Genesis) (*GethNode, error) {
201207
if startConfig.Name == "" {
202208
startConfig.Name = "Modified Geth Module"
203209
}
@@ -247,16 +253,20 @@ func restart(startConfig GethNodeTestConfiguration, bootnode, datadir string, ge
247253
time.Sleep(250 * time.Millisecond)
248254
}
249255
// Connect the node to the bootnode
250-
node := enode.MustParse(bootnode)
251-
stack.Server().AddPeer(node)
256+
if bootnodes != nil && len(bootnodes) > 0 {
257+
for _, bootnode := range bootnodes {
258+
node := enode.MustParse(bootnode)
259+
stack.Server().AddTrustedPeer(node)
260+
stack.Server().AddPeer(node)
261+
}
262+
}
252263

253264
stack.Server().EnableMsgEvents = true
254265

255266
g := &GethNode{
256267
node: stack,
257268
eth: ethBackend,
258269
datadir: datadir,
259-
bootnode: bootnode,
260270
genesis: genesis,
261271
ttd: genesis.Config.TerminalTotalDifficulty,
262272
api: ethcatalyst.NewConsensusAPI(ethBackend),
@@ -392,6 +402,14 @@ func (n *GethNode) PoWMiningLoop() {
392402
// Wait until the previous block is succesfully propagated
393403
<-time.After(time.Millisecond * 200)
394404

405+
// Modify the block before sealing
406+
if n.config.BlockModifier != nil {
407+
b, err = n.config.BlockModifier.ModifyUnsealedBlock(b)
408+
if err != nil {
409+
panic(err)
410+
}
411+
}
412+
395413
// Seal the next block to broadcast
396414
rChan := make(chan *types.Block, 0)
397415
stopChan := make(chan struct{})
@@ -401,8 +419,21 @@ func (n *GethNode) PoWMiningLoop() {
401419
if b == nil {
402420
panic(fmt.Errorf("no block got sealed"))
403421
}
422+
// Modify the sealed block if necessary
423+
if n.config.BlockModifier != nil {
424+
sealVerifier := func(h *types.Header) bool {
425+
return n.powEngine.VerifyHeader(n.eth.BlockChain(), h, true) == nil
426+
}
427+
b, err = n.config.BlockModifier.ModifySealedBlock(sealVerifier, b)
428+
if err != nil {
429+
panic(err)
430+
}
431+
}
432+
404433
// Broadcast
405-
n.eth.EventMux().Post(core.NewMinedBlockEvent{Block: b})
434+
if !n.config.DisableGossiping {
435+
n.eth.EventMux().Post(core.NewMinedBlockEvent{Block: b})
436+
}
406437

407438
// Check whether the block was a terminal block
408439
if t, td, err := n.isBlockTerminal(b); err == nil {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package helper
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"math/big"
7+
"math/rand"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
)
12+
13+
type BlockModifier interface {
14+
ModifyUnsealedBlock(*types.Block) (*types.Block, error)
15+
ModifySealedBlock(func(*types.Header) bool, *types.Block) (*types.Block, error)
16+
}
17+
18+
type PoWBlockModifier struct {
19+
Difficulty *big.Int
20+
RandomStateRoot bool
21+
InvalidSealedMixHash bool
22+
InvalidSealedNonce bool
23+
TimeSecondsInFuture uint64
24+
}
25+
26+
func (m PoWBlockModifier) ModifyUnsealedBlock(baseBlock *types.Block) (*types.Block, error) {
27+
modifiedHeader := types.CopyHeader(baseBlock.Header())
28+
29+
if m.Difficulty != nil {
30+
modifiedHeader.Difficulty = m.Difficulty
31+
}
32+
if m.RandomStateRoot {
33+
rand.Read(modifiedHeader.Root[:])
34+
}
35+
if m.TimeSecondsInFuture > 0 {
36+
modifiedHeader.Time += m.TimeSecondsInFuture
37+
}
38+
39+
modifiedBlock := types.NewBlockWithHeader(modifiedHeader)
40+
modifiedBlock = modifiedBlock.WithBody(baseBlock.Transactions(), baseBlock.Uncles())
41+
42+
js, _ := json.MarshalIndent(modifiedBlock.Header(), "", " ")
43+
fmt.Printf("DEBUG: Modified unsealed block with hash %v:\n%s\n", modifiedBlock.Hash(), js)
44+
45+
return modifiedBlock, nil
46+
}
47+
48+
func (m PoWBlockModifier) ModifySealedBlock(f func(*types.Header) bool, baseBlock *types.Block) (*types.Block, error) {
49+
modifiedHeader := types.CopyHeader(baseBlock.Header())
50+
51+
if m.InvalidSealedMixHash {
52+
modifiedHeader.MixDigest = common.Hash{}
53+
for f(modifiedHeader) {
54+
// Increase the hash until it's not valid
55+
modifiedHeader.MixDigest[len(modifiedHeader.MixDigest[:])]++
56+
}
57+
}
58+
if m.InvalidSealedNonce {
59+
modifiedHeader.Nonce = types.BlockNonce{}
60+
for f(modifiedHeader) {
61+
// Increase the nonce until it's not valid
62+
modifiedHeader.Nonce[len(modifiedHeader.Nonce[:])]++
63+
}
64+
}
65+
66+
modifiedBlock := types.NewBlockWithHeader(modifiedHeader)
67+
modifiedBlock = modifiedBlock.WithBody(baseBlock.Transactions(), baseBlock.Uncles())
68+
69+
js, _ := json.MarshalIndent(modifiedBlock.Header(), "", " ")
70+
fmt.Printf("DEBUG: Modified sealed block with hash %v:\n%s\n", modifiedBlock.Hash(), js)
71+
return modifiedBlock, nil
72+
}

0 commit comments

Comments
 (0)