Skip to content

Commit 1c2fea5

Browse files
authored
simulators/eth2/engine: Fix Optimistic Sync Tests + Refactor Allow Client Restarts + No Viable Head Test Case (#637)
* simulators/eth2/engine: Refactor to allow client shutdown, delayed start * simulators/eth2/engine: No viable head due to opt sync * simulators/eth2/engine: add optional extra options on client start * simulators/eth2/engine: fix shutdown * simulators/eth2/engine: fix optimistic expectation * simulators/eth2/engine: More info print * simulators/eth2/engine: Add debug printing * simulators/eth2/engine: Remove unnecessary print
1 parent f3d0a2d commit 1c2fea5

File tree

8 files changed

+1438
-497
lines changed

8 files changed

+1438
-497
lines changed

simulators/eth2/engine/engineapi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type EngineClient struct {
5050
}
5151

5252
// NewClient creates a engine client that uses the given RPC client.
53-
func NewEngineClient(t *hivesim.T, n *Eth1Node, ttd *big.Int) *EngineClient {
53+
func NewEngineClient(t *hivesim.T, n *ExecutionClient, ttd *big.Int) *EngineClient {
5454
engineRPCAddress, err := n.EngineRPCAddress()
5555
if err != nil {
5656
panic(err)

simulators/eth2/engine/helper.go

Lines changed: 184 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type node struct {
4242
ExecutionClientTTD *big.Int
4343
BeaconNodeTTD *big.Int
4444
TestVerificationNode bool
45+
DisableStartup bool
4546
ChainGenerator ChainGenerator
4647
Chain []*types.Block
4748
}
@@ -562,106 +563,205 @@ func forkchoiceResponseSpoof(method string, status PayloadStatusV1, payloadID *P
562563
}, nil
563564
}
564565

565-
// List of Hashes that can be accessed concurrently
566-
type SyncHashes struct {
567-
Hashes []common.Hash
568-
Lock *sync.Mutex
566+
type EngineResponse struct {
567+
Status PayloadStatus
568+
LatestValidHash *common.Hash
569569
}
570570

571-
func NewSyncHashes(hashes ...common.Hash) *SyncHashes {
572-
newSyncHashes := &SyncHashes{
573-
Hashes: make([]common.Hash, 0),
574-
Lock: &sync.Mutex{},
571+
type EngineResponseHash struct {
572+
Response *EngineResponse
573+
Hash common.Hash
574+
}
575+
576+
//
577+
type EngineResponseMocker struct {
578+
Lock sync.Mutex
579+
DefaultResponse *EngineResponse
580+
HashToResponse map[common.Hash]*EngineResponse
581+
HashPassthrough map[common.Hash]bool
582+
NewPayloadCalled chan EngineResponseHash
583+
ForkchoiceUpdatedCalled chan EngineResponseHash
584+
Mocking bool
585+
}
586+
587+
func NewEngineResponseMocker(defaultResponse *EngineResponse, perHashResponses ...*EngineResponseHash) *EngineResponseMocker {
588+
e := &EngineResponseMocker{
589+
DefaultResponse: defaultResponse,
590+
HashToResponse: make(map[common.Hash]*EngineResponse),
591+
HashPassthrough: make(map[common.Hash]bool),
592+
NewPayloadCalled: make(chan EngineResponseHash),
593+
ForkchoiceUpdatedCalled: make(chan EngineResponseHash),
594+
Mocking: true,
575595
}
576-
for _, h := range hashes {
577-
newSyncHashes.Hashes = append(newSyncHashes.Hashes, h)
596+
for _, r := range perHashResponses {
597+
e.AddResponse(r.Hash, r.Response)
578598
}
579-
return newSyncHashes
599+
return e
580600
}
581601

582-
func (syncHashes *SyncHashes) Contains(hash common.Hash) bool {
583-
syncHashes.Lock.Lock()
584-
defer syncHashes.Lock.Unlock()
585-
if syncHashes.Hashes == nil {
586-
return false
602+
func (e *EngineResponseMocker) AddResponse(h common.Hash, r *EngineResponse) {
603+
e.Lock.Lock()
604+
defer e.Lock.Unlock()
605+
if e.HashToResponse == nil {
606+
e.HashToResponse = make(map[common.Hash]*EngineResponse)
587607
}
588-
for _, h := range syncHashes.Hashes {
589-
if h == hash {
590-
return true
591-
}
608+
e.HashToResponse[h] = r
609+
}
610+
611+
func (e *EngineResponseMocker) AddPassthrough(h common.Hash, pass bool) {
612+
e.Lock.Lock()
613+
defer e.Lock.Unlock()
614+
e.HashPassthrough[h] = pass
615+
}
616+
617+
func (e *EngineResponseMocker) CanPassthrough(h common.Hash) bool {
618+
e.Lock.Lock()
619+
defer e.Lock.Unlock()
620+
if pass, ok := e.HashPassthrough[h]; ok && pass {
621+
return true
592622
}
593623
return false
594624
}
595625

596-
func (syncHashes *SyncHashes) Add(hash common.Hash) {
597-
syncHashes.Lock.Lock()
598-
defer syncHashes.Lock.Unlock()
599-
syncHashes.Hashes = append(syncHashes.Hashes, hash)
626+
func (e *EngineResponseMocker) GetResponse(h common.Hash) *EngineResponse {
627+
e.Lock.Lock()
628+
defer e.Lock.Unlock()
629+
if e.HashToResponse != nil {
630+
if r, ok := e.HashToResponse[h]; ok {
631+
return r
632+
}
633+
}
634+
return e.DefaultResponse
600635
}
601636

602-
// Generate a callback that invalidates either a call to `engine_forkchoiceUpdatedV1` or `engine_newPayloadV1`
603-
// for all hashes with given exceptions, and a given LatestValidHash.
604-
func InvalidateExecutionPayloads(method string, exceptions *SyncHashes, latestValidHash *common.Hash, invalidated chan<- common.Hash) func([]byte, []byte) *proxy.Spoof {
605-
if method == EngineForkchoiceUpdatedV1 {
606-
return func(res []byte, req []byte) *proxy.Spoof {
607-
var (
608-
fcState ForkchoiceStateV1
609-
pAttr PayloadAttributesV1
610-
spoof *proxy.Spoof
611-
err error
612-
)
613-
err = UnmarshalFromJsonRPCRequest(req, &fcState, &pAttr)
637+
func (e *EngineResponseMocker) SetDefaultResponse(r *EngineResponse) {
638+
e.Lock.Lock()
639+
defer e.Lock.Unlock()
640+
e.DefaultResponse = r
641+
}
642+
643+
func (e *EngineResponseMocker) AddGetPayloadPassthroughToProxy(p *Proxy) {
644+
p.AddResponseCallback(EngineGetPayloadV1, func(res []byte, req []byte) *proxy.Spoof {
645+
// Hash of the payload built is being added to the passthrough list
646+
var (
647+
payload ExecutableDataV1
648+
)
649+
err := UnmarshalFromJsonRPCResponse(res, &payload)
650+
if err != nil {
651+
panic(err)
652+
}
653+
e.AddPassthrough(payload.BlockHash, true)
654+
return nil
655+
})
656+
}
657+
658+
func (e *EngineResponseMocker) AddNewPayloadCallbackToProxy(p *Proxy) {
659+
p.AddResponseCallback(EngineNewPayloadV1, func(res []byte, req []byte) *proxy.Spoof {
660+
var (
661+
payload ExecutableDataV1
662+
status PayloadStatusV1
663+
spoof *proxy.Spoof
664+
err error
665+
)
666+
err = UnmarshalFromJsonRPCRequest(req, &payload)
667+
if err != nil {
668+
panic(err)
669+
}
670+
err = UnmarshalFromJsonRPCResponse(res, &status)
671+
if err != nil {
672+
panic(err)
673+
}
674+
if r := e.GetResponse(payload.BlockHash); e.Mocking && !e.CanPassthrough(payload.BlockHash) && r != nil {
675+
// We are mocking this specific response, either with a hash specific response, or the default response
676+
spoof, err = payloadStatusSpoof(EngineNewPayloadV1, &PayloadStatusV1{
677+
Status: r.Status,
678+
LatestValidHash: r.LatestValidHash,
679+
ValidationError: nil,
680+
})
614681
if err != nil {
615682
panic(err)
616683
}
617-
if !exceptions.Contains(fcState.HeadBlockHash) {
618-
spoof, err = forkchoiceResponseSpoof(EngineForkchoiceUpdatedV1, PayloadStatusV1{
619-
Status: Invalid,
620-
LatestValidHash: latestValidHash,
621-
ValidationError: nil,
622-
}, nil)
623-
if err != nil {
624-
panic(err)
625-
}
626-
select {
627-
case invalidated <- fcState.HeadBlockHash:
628-
default:
629-
}
630-
return spoof
684+
select {
685+
case e.NewPayloadCalled <- EngineResponseHash{
686+
Response: r,
687+
Hash: payload.BlockHash,
688+
}:
689+
default:
690+
}
691+
return spoof
692+
} else {
693+
select {
694+
case e.NewPayloadCalled <- EngineResponseHash{
695+
Response: &EngineResponse{
696+
Status: status.Status,
697+
LatestValidHash: status.LatestValidHash,
698+
},
699+
Hash: payload.BlockHash,
700+
}:
701+
default:
631702
}
632-
return nil
633703
}
634-
}
635-
if method == EngineNewPayloadV1 {
636-
return func(res []byte, req []byte) *proxy.Spoof {
637-
var (
638-
payload ExecutableDataV1
639-
spoof *proxy.Spoof
640-
err error
641-
)
642-
err = UnmarshalFromJsonRPCRequest(req, &payload)
704+
return nil
705+
})
706+
}
707+
708+
func (e *EngineResponseMocker) AddForkchoiceUpdatedCallbackToProxy(p *Proxy) {
709+
p.AddResponseCallback(EngineForkchoiceUpdatedV1, func(res []byte, req []byte) *proxy.Spoof {
710+
var (
711+
fcState ForkchoiceStateV1
712+
pAttr PayloadAttributesV1
713+
fResp ForkChoiceResponse
714+
spoof *proxy.Spoof
715+
err error
716+
)
717+
err = UnmarshalFromJsonRPCRequest(req, &fcState, &pAttr)
718+
if err != nil {
719+
panic(err)
720+
}
721+
err = UnmarshalFromJsonRPCResponse(res, &fResp)
722+
if err != nil {
723+
panic(err)
724+
}
725+
726+
if r := e.GetResponse(fcState.HeadBlockHash); e.Mocking && !e.CanPassthrough(fcState.HeadBlockHash) && r != nil {
727+
// We are mocking this specific response, either with a hash specific response, or the default response
728+
spoof, err = forkchoiceResponseSpoof(EngineForkchoiceUpdatedV1, PayloadStatusV1{
729+
Status: r.Status,
730+
LatestValidHash: r.LatestValidHash,
731+
ValidationError: nil,
732+
}, nil)
643733
if err != nil {
644734
panic(err)
645735
}
646-
if !exceptions.Contains(payload.BlockHash) {
647-
spoof, err = payloadStatusSpoof(EngineNewPayloadV1, &PayloadStatusV1{
648-
Status: Invalid,
649-
LatestValidHash: latestValidHash,
650-
ValidationError: nil,
651-
})
652-
if err != nil {
653-
panic(err)
654-
}
655-
select {
656-
case invalidated <- payload.BlockHash:
657-
default:
658-
}
659-
return spoof
736+
737+
select {
738+
case e.ForkchoiceUpdatedCalled <- EngineResponseHash{
739+
Response: r,
740+
Hash: fcState.HeadBlockHash,
741+
}:
742+
default:
743+
}
744+
return spoof
745+
} else {
746+
// Let the original response pass through
747+
select {
748+
case e.ForkchoiceUpdatedCalled <- EngineResponseHash{
749+
Response: &EngineResponse{
750+
Status: fResp.PayloadStatus.Status,
751+
LatestValidHash: fResp.PayloadStatus.LatestValidHash,
752+
},
753+
Hash: fcState.HeadBlockHash,
754+
}:
755+
default:
660756
}
661-
return nil
662757
}
663-
}
664-
panic(fmt.Errorf("ERROR: Invalid method to generate callback: %s", method))
758+
return nil
759+
})
760+
}
761+
762+
func (e *EngineResponseMocker) AddCallbacksToProxy(p *Proxy) {
763+
e.AddForkchoiceUpdatedCallbackToProxy(p)
764+
e.AddNewPayloadCallbackToProxy(p)
665765
}
666766

667767
// Generates a callback that detects when a ForkchoiceUpdated with Payload Attributes fails.
@@ -716,14 +816,19 @@ func combine(a, b *proxy.Spoof) *proxy.Spoof {
716816
return a
717817
}
718818

819+
func ContextWithSlotsTimeout(parent context.Context, t *Testnet, slots beacon.Slot) (context.Context, context.CancelFunc) {
820+
timeout := time.Duration(uint64(slots)*uint64(t.spec.SECONDS_PER_SLOT)) * time.Second
821+
return context.WithTimeout(parent, timeout)
822+
}
823+
719824
// Try to approximate how much time until the merge based on current time, bellatrix fork epoch,
720825
// TTD, execution clients' consensus mechanism, current total difficulty.
721826
// This function is used to calculate timeouts, so it will always return a pessimistic value.
722827
func SlotsUntilMerge(t *Testnet, c *Config) beacon.Slot {
723828
l := make([]beacon.Slot, 0)
724829
l = append(l, SlotsUntilBellatrix(t.genesisTime, t.spec))
725830

726-
for i, e := range t.eth1 {
831+
for i, e := range t.ExecutionClients().Running() {
727832
l = append(l, beacon.Slot(TimeUntilTerminalBlock(e, c.Eth1Consensus, c.TerminalTotalDifficulty, c.Nodes[i])/uint64(t.spec.SECONDS_PER_SLOT)))
728833
}
729834

@@ -755,7 +860,7 @@ func SlotsUntilBellatrix(genesisTime beacon.Timestamp, spec *beacon.Spec) beacon
755860
return s
756861
}
757862

758-
func TimeUntilTerminalBlock(e *Eth1Node, c setup.Eth1Consensus, defaultTTD *big.Int, n node) uint64 {
863+
func TimeUntilTerminalBlock(e *ExecutionClient, c setup.Eth1Consensus, defaultTTD *big.Int, n node) uint64 {
759864
var ttd = defaultTTD
760865
if n.ExecutionClientTTD != nil {
761866
ttd = n.ExecutionClientTTD

simulators/eth2/engine/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var transitionTests = []testSpec{
4242
{Name: "syncing-with-chain-having-invalid-transition-block", Run: SyncingWithChainHavingInvalidTransitionBlock},
4343
{Name: "syncing-with-chain-having-invalid-post-transition-block", Run: SyncingWithChainHavingInvalidPostTransitionBlock},
4444
{Name: "re-org-and-sync-with-chain-having-invalid-terminal-block", Run: ReOrgSyncWithChainHavingInvalidTerminalBlock},
45+
{Name: "no-viable-head-due-to-optimistic-sync", Run: NoViableHeadDueToOptimisticSync},
4546
}
4647

4748
func main() {

0 commit comments

Comments
 (0)