diff --git a/simulators/ethereum/pyspec/main.go b/simulators/ethereum/pyspec/main.go index e725efb987..72924f4e9c 100644 --- a/simulators/ethereum/pyspec/main.go +++ b/simulators/ethereum/pyspec/main.go @@ -68,21 +68,21 @@ func fixtureRunner(t *hivesim.T) { // spawn `parallelism` workers to run fixtures against clients var wg sync.WaitGroup - var testCh = make(chan *testcase) + var testCh = make(chan *TestCase) wg.Add(parallelism) for i := 0; i < parallelism; i++ { go func() { defer wg.Done() for test := range testCh { t.Run(hivesim.TestSpec{ - Name: test.name, + Name: test.Name, Description: ("Test Link: " + - repoLink(test.filepath)), + repoLink(test.FilePath)), Run: test.run, AlwaysRun: false, }) - if test.failedErr != nil { - failedTests[test.clientType+"/"+test.name] = test.failedErr + if test.FailedErr != nil { + failedTests[test.ClientType+"/"+test.Name] = test.FailedErr } } }() @@ -92,13 +92,13 @@ func fixtureRunner(t *hivesim.T) { re := regexp.MustCompile(testPattern) // deliver and run test cases against each client - loadFixtureTests(t, fileRoot, re, func(tc testcase) { + loadFixtureTests(t, fileRoot, re, func(tc TestCase) { for _, client := range clientTypes { if !client.HasRole("eth1") { continue } tc := tc // shallow copy - tc.clientType = client.Name + tc.ClientType = client.Name testCh <- &tc } }) diff --git a/simulators/ethereum/pyspec/runner.go b/simulators/ethereum/pyspec/runner.go index 6781b968ca..41e2946e63 100644 --- a/simulators/ethereum/pyspec/runner.go +++ b/simulators/ethereum/pyspec/runner.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "io/fs" "math/big" @@ -11,21 +10,19 @@ import ( "strings" "time" - api "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/tests" "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/simulators/ethereum/engine/client/hive_rpc" "github.com/ethereum/hive/simulators/ethereum/engine/globals" +) - typ "github.com/ethereum/hive/simulators/ethereum/engine/types" +var ( + SyncTimeout = 10 * time.Second ) // loadFixtureTests extracts tests from fixture.json files in a given directory, // creates a testcase for each test, and passes the testcase struct to fn. -func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(testcase)) { +func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(TestCase)) { filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { // check file is actually a fixture if err != nil { @@ -40,8 +37,8 @@ func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(test return nil } - // extract fixture.json tests (multiple forks) into fixtureTest structs - var fixtureTests map[string]fixtureTest + // extract fixture.json tests (multiple forks) into fixture structs + var fixtureTests map[string]*Fixture if err := common.LoadJSON(path, &fixtureTests); err != nil { t.Logf("invalid test file: %v, unable to load json", err) return nil @@ -50,25 +47,20 @@ func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(test // create testcase structure from fixtureTests for name, fixture := range fixtureTests { // skip networks post merge or not supported - network := fixture.json.Fork + network := fixture.Fork if _, exist := envForks[network]; !exist { continue } // define testcase (tc) struct with initial fields - tc := testcase{ - fixture: fixture, - name: path[10:len(path)-5] + "/" + name, - filepath: path, + tc := TestCase{ + Name: path[10:len(path)-5] + "/" + name, + FilePath: path, + Fixture: fixture, } // match test case name against regex if provided - if !re.MatchString(tc.name) { + if !re.MatchString(tc.Name) { continue } - // extract genesis, payloads & post allocation field to tc - if err := tc.extractFixtureFields(fixture.json); err != nil { - t.Logf("test %v / %v: unable to extract fixture fields: %v", d.Name(), name, err) - tc.failedErr = fmt.Errorf("unable to extract fixture fields: %v", err) - } // feed tc to single worker within fixtureRunner() fn(tc) } @@ -80,12 +72,13 @@ func loadFixtureTests(t *hivesim.T, root string, re *regexp.Regexp, fn func(test // fixtureRunner, all testcase payloads are sent and executed using the EngineAPI. for // verification all fixture nonce, balance and storage values are checked against the // response received from the lastest block. -func (tc *testcase) run(t *hivesim.T) { +func (tc *TestCase) run(t *hivesim.T) { start := time.Now() + tc.FailCallback = t t.Log("setting variables required for starting client.") engineStarter := hive_rpc.HiveRPCEngineStarter{ - ClientType: tc.clientType, + ClientType: tc.ClientType, EnginePort: globals.EnginePortHTTP, EthPort: globals.EthPortHTTP, JWTSecret: globals.DefaultJwtTokenSecretBytes, @@ -99,123 +92,110 @@ func (tc *testcase) run(t *hivesim.T) { tc.updateEnv(env) t0 := time.Now() // If test is already failed, don't bother spinning up a client - if tc.failedErr != nil { - t.Errorf("test failed early: %v", tc.failedErr) - return + if tc.FailedErr != nil { + t.Fatalf("test failed early: %v", tc.FailedErr) } // start client (also creates an engine RPC client internally) t.Log("starting client with Engine API.") - engineClient, err := engineStarter.StartClient(t, ctx, tc.genesis, env, nil) + engineClient, err := engineStarter.StartClient(t, ctx, tc.Genesis(), env, nil) if err != nil { - tc.failedErr = err - t.Fatalf("can't start client with Engine API: %v", err) + tc.Fatalf("can't start client with Engine API: %v", err) } // verify genesis hash matches that of the fixture genesisBlock, err := engineClient.BlockByNumber(ctx, big.NewInt(0)) if err != nil { - tc.failedErr = err - t.Fatalf("unable to get genesis block: %v", err) + tc.Fatalf("unable to get genesis block: %v", err) } - if genesisBlock.Hash() != tc.fixture.json.Genesis.Hash { - tc.failedErr = errors.New("genesis hash mismatch") - t.Fatalf("genesis hash mismatch") + if genesisBlock.Hash() != tc.GenesisBlock.Hash { + tc.Fatalf("genesis hash mismatch") } t1 := time.Now() // send payloads and check response - latestValidHash := common.Hash{} - for _, engineNewPayload := range tc.engineNewPayloads { - plStatus, plErr := engineClient.NewPayload( - context.Background(), - int(engineNewPayload.Version), - engineNewPayload.HiveExecutionPayload, - ) - // check for rpc errors and compare error codes - errCode := int(engineNewPayload.ErrorCode) - if errCode != 0 { - checkRPCErrors(plErr, errCode, t, tc) - continue - } - // set expected payload return status - expectedStatus := "VALID" - if engineNewPayload.ValidationError != nil { - expectedStatus = "INVALID" - } - // check payload status matches expected - if plStatus.Status != expectedStatus { - tc.failedErr = fmt.Errorf("payload status mismatch: client returned %v and fixture expected %v", plStatus.Status, expectedStatus) - t.Fatalf("payload status mismatch: client returned %v fixture expected %v", plStatus.Status, expectedStatus) + var latestValidPayload *EngineNewPayload + for _, engineNewPayload := range tc.EngineNewPayloads { + engineNewPayload := engineNewPayload + if syncing, err := engineNewPayload.ExecuteValidate( + ctx, + engineClient, + ); err != nil { + tc.Fatalf("Payload validation error: %v", err) + } else if syncing { + tc.Fatalf("Payload validation failed (not synced)") } // update latest valid block hash if payload status is VALID - if plStatus.Status == "VALID" { - latestValidHash = *plStatus.LatestValidHash + if engineNewPayload.Valid() { + latestValidPayload = engineNewPayload } } t2 := time.Now() // only update head of beacon chain if valid response occurred - if latestValidHash != (common.Hash{}) { - // update with latest valid response - fcState := &api.ForkchoiceStateV1{HeadBlockHash: latestValidHash} - if _, fcErr := engineClient.ForkchoiceUpdated(ctx, int(tc.fixture.json.EngineFcuVersion), fcState, nil); fcErr != nil { - tc.failedErr = fcErr - t.Fatalf("unable to update head of beacon chain in test %s: %v ", tc.name, fcErr) + if latestValidPayload != nil { + if syncing, err := latestValidPayload.ForkchoiceValidate(ctx, engineClient, tc.EngineFcuVersion); err != nil { + tc.Fatalf("unable to update head of chain: %v", err) + } else if syncing { + tc.Fatalf("forkchoice update failed (not synced)") } } t3 := time.Now() - - // check nonce, balance & storage of accounts in final block against fixture values - for account, genesisAccount := range *tc.postAlloc { - // get nonce & balance from last block (end of test execution) - gotNonce, errN := engineClient.NonceAt(ctx, account, nil) - gotBalance, errB := engineClient.BalanceAt(ctx, account, nil) - if errN != nil { - tc.failedErr = errN - t.Errorf("unable to call nonce from account: %v, in test %s: %v", account, tc.name, errN) - } else if errB != nil { - tc.failedErr = errB - t.Errorf("unable to call balance from account: %v, in test %s: %v", account, tc.name, errB) - } - // check final nonce & balance matches expected in fixture - if genesisAccount.Nonce != gotNonce { - tc.failedErr = errors.New("nonce received doesn't match expected from fixture") - t.Errorf(`nonce received from account %v doesn't match expected from fixture in test %s: - received from block: %v - expected in fixture: %v`, account, tc.name, gotNonce, genesisAccount.Nonce) - } - if genesisAccount.Balance.Cmp(gotBalance) != 0 { - tc.failedErr = errors.New("balance received doesn't match expected from fixture") - t.Errorf(`balance received from account %v doesn't match expected from fixture in test %s: - received from block: %v - expected in fixture: %v`, account, tc.name, gotBalance, genesisAccount.Balance) - } - // check final storage - if len(genesisAccount.Storage) > 0 { - // extract fixture storage keys - keys := make([]common.Hash, 0, len(genesisAccount.Storage)) - for key := range genesisAccount.Storage { - keys = append(keys, key) - } - // get storage values for account with keys: keys - gotStorage, errS := engineClient.StorageAtKeys(ctx, account, keys, nil) - if errS != nil { - tc.failedErr = errS - t.Errorf("unable to get storage values from account: %v, in test %s: %v", account, tc.name, errS) + if err := tc.ValidatePost(ctx, engineClient); err != nil { + tc.Fatalf("unable to verify post allocation in test %s: %v", tc.Name, err) + } + + if tc.SyncPayload != nil { + // First send a new payload to the already running client + if syncing, err := tc.SyncPayload.ExecuteValidate( + ctx, + engineClient, + ); err != nil { + tc.Fatalf("unable to send sync payload: %v", err) + } else if syncing { + tc.Fatalf("sync payload failed (not synced)") + } + // Send a forkchoice update to the already running client to head to the sync payload + if syncing, err := tc.SyncPayload.ForkchoiceValidate(ctx, engineClient, tc.EngineFcuVersion); err != nil { + tc.Fatalf("unable to update head of chain: %v", err) + } else if syncing { + tc.Fatalf("forkchoice update failed (not synced)") + } + + // Spawn a second client connected to the already running client, + // send the forkchoice updated with the head hash and wait for sync. + // Then verify the post allocation. + // Add a timeout too. + secondEngineClient, err := engineStarter.StartClient(t, ctx, tc.Genesis(), env, nil, engineClient) + if err != nil { + tc.Fatalf("can't start client with Engine API: %v", err) + } + + if _, err := tc.SyncPayload.ExecuteValidate( + ctx, + secondEngineClient, + ); err != nil { + tc.Fatalf("unable to send sync payload: %v", err) + } // Don't check syncing here because some clients do sync immediately + + timeoutCtx, cancel := context.WithTimeout(ctx, SyncTimeout) + defer cancel() + for { + if syncing, err := tc.SyncPayload.ForkchoiceValidate(ctx, secondEngineClient, tc.EngineFcuVersion); err != nil { + tc.Fatalf("unable to update head of chain: %v", err) + } else if !syncing { + break } - // check values in storage match with fixture - for _, key := range keys { - if genesisAccount.Storage[key] != *gotStorage[key] { - tc.failedErr = errors.New("storage received doesn't match expected from fixture") - t.Errorf(`storage received from account %v doesn't match expected from fixture in test %s: from storage address: %v - received from block: %v - expected in fixture: %v`, account, tc.name, key, gotStorage[key], genesisAccount.Storage[key]) - } + select { + case <-timeoutCtx.Done(): + tc.Fatalf("timeout waiting for sync of secondary client") + default: } + time.Sleep(time.Second) } } + end := time.Now() - if tc.failedErr == nil { + if false { // TODO: Activate only on --sim.loglevel > 3 t.Logf(`test timing: setupClientEnv %v startClient %v @@ -229,74 +209,9 @@ func (tc *testcase) run(t *hivesim.T) { // updateEnv updates the environment variables against the fork rules // defined in envForks, for the network specified in the testcase fixture. -func (tc *testcase) updateEnv(env hivesim.Params) { - forkRules := envForks[tc.fixture.json.Fork] +func (tc *TestCase) updateEnv(env hivesim.Params) { + forkRules := envForks[tc.Fork] for k, v := range forkRules { env[k] = fmt.Sprintf("%d", v) } } - -// extractFixtureFields extracts the genesis, post allocation and payload -// fields from the given fixture test and stores them in the testcase struct. -func (tc *testcase) extractFixtureFields(fixture fixtureJSON) (err error) { - if tc.genesis, err = extractGenesis(fixture); err != nil { - return fmt.Errorf("failed to extract genesis: %w", err) - } - if tc.engineNewPayloads, err = extractEngineNewPayloads(fixture); err != nil { - return fmt.Errorf("failed to extract engineNewPayloads: %w", err) - } - tc.postAlloc = &fixture.Post - return nil -} - -// extracts the genesis block information from the given fixture. -func extractGenesis(fixture fixtureJSON) (*core.Genesis, error) { - genesis := &core.Genesis{ - Config: tests.Forks[fixture.Fork], - Coinbase: fixture.Genesis.Coinbase, - Difficulty: fixture.Genesis.Difficulty, - GasLimit: fixture.Genesis.GasLimit, - Timestamp: fixture.Genesis.Timestamp.Uint64(), - ExtraData: fixture.Genesis.ExtraData, - Mixhash: fixture.Genesis.MixHash, - Nonce: fixture.Genesis.Nonce.Uint64(), - BaseFee: fixture.Genesis.BaseFee, - BlobGasUsed: fixture.Genesis.BlobGasUsed, - ExcessBlobGas: fixture.Genesis.ExcessBlobGas, - Alloc: fixture.Pre, - } - return genesis, nil -} - -// extracts all the engineNewPayload information from the given fixture. -func extractEngineNewPayloads(fixture fixtureJSON) ([]engineNewPayload, error) { - var engineNewPayloads []engineNewPayload - for _, engineNewPayload := range fixture.EngineNewPayloads { - engineNewPayload := engineNewPayload - hiveExecutionPayload, err := typ.FromBeaconExecutableData(engineNewPayload.ExecutionPayload) - if err != nil { - return nil, errors.New("executionPayload param within engineNewPayload is invalid") - } - hiveExecutionPayload.VersionedHashes = &engineNewPayload.BlobVersionedHashes - hiveExecutionPayload.ParentBeaconBlockRoot = engineNewPayload.ParentBeaconBlockRoot - engineNewPayload.HiveExecutionPayload = &hiveExecutionPayload - engineNewPayloads = append(engineNewPayloads, engineNewPayload) - } - return engineNewPayloads, nil -} - -// checks for RPC errors and compares error codes if expected. -func checkRPCErrors(plErr error, fxErrCode int, t *hivesim.T, tc *testcase) { - rpcErr, isRpcErr := plErr.(rpc.Error) - if isRpcErr { - plErrCode := rpcErr.ErrorCode() - if plErrCode != fxErrCode { - tc.failedErr = fmt.Errorf("error code mismatch: client returned %v and fixture expected %v", plErrCode, fxErrCode) - t.Fatalf("error code mismatch\n client returned: %v\n fixture expected: %v\n in test %s", plErrCode, fxErrCode, tc.name) - } - t.Logf("expected error code caught by client: %v", plErrCode) - } else { - tc.failedErr = fmt.Errorf("fixture expected rpc error code: %v but none was returned from client", fxErrCode) - t.Fatalf("fixture expected rpc error code: %v but none was returned from client in test %s", fxErrCode, tc.name) - } -} diff --git a/simulators/ethereum/pyspec/types.go b/simulators/ethereum/pyspec/types.go index 7300df297d..1d763499c9 100644 --- a/simulators/ethereum/pyspec/types.go +++ b/simulators/ethereum/pyspec/types.go @@ -1,7 +1,9 @@ package main import ( - "encoding/json" + "context" + "errors" + "fmt" "math/big" api "github.com/ethereum/go-ethereum/beacon/engine" @@ -10,41 +12,105 @@ import ( "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/tests" + "github.com/ethereum/hive/simulators/ethereum/engine/client" typ "github.com/ethereum/hive/simulators/ethereum/engine/types" ) -type testcase struct { +type Fail interface { + Fatalf(format string, args ...interface{}) +} + +type TestCase struct { // test meta data - name string - filepath string - clientType string - failedErr error + Name string + FilePath string + ClientType string + FailedErr error // test fixture data - fixture fixtureTest - genesis *core.Genesis - postAlloc *core.GenesisAlloc - engineNewPayloads []engineNewPayload + *Fixture + FailCallback Fail +} + +func (tc *TestCase) Fatalf(format string, args ...interface{}) { + tc.FailedErr = fmt.Errorf(format, args...) + tc.FailCallback.Fatalf(format, args...) } -type fixtureTest struct { - json fixtureJSON +type Fixture struct { + Fork string `json:"network"` + GenesisBlock genesisBlock `json:"genesisBlockHeader"` + EngineNewPayloads []*EngineNewPayload `json:"engineNewPayloads"` + EngineFcuVersion int `json:"engineFcuVersion,string"` + Pre core.GenesisAlloc `json:"pre"` + PostAlloc core.GenesisAlloc `json:"postState"` + SyncPayload *EngineNewPayload `json:"syncPayload"` } -func (t *fixtureTest) UnmarshalJSON(in []byte) error { - if err := json.Unmarshal(in, &t.json); err != nil { - return err +func (f *Fixture) Genesis() *core.Genesis { + return &core.Genesis{ + Config: tests.Forks[f.Fork], + Coinbase: f.GenesisBlock.Coinbase, + Difficulty: f.GenesisBlock.Difficulty, + GasLimit: f.GenesisBlock.GasLimit, + Timestamp: f.GenesisBlock.Timestamp.Uint64(), + ExtraData: f.GenesisBlock.ExtraData, + Mixhash: f.GenesisBlock.MixHash, + Nonce: f.GenesisBlock.Nonce.Uint64(), + BaseFee: f.GenesisBlock.BaseFee, + BlobGasUsed: f.GenesisBlock.BlobGasUsed, + ExcessBlobGas: f.GenesisBlock.ExcessBlobGas, + Alloc: f.Pre, } - return nil } -type fixtureJSON struct { - Fork string `json:"network"` - Genesis genesisBlock `json:"genesisBlockHeader"` - EngineNewPayloads []engineNewPayload `json:"engineNewPayloads"` - EngineFcuVersion math.HexOrDecimal64 `json:"engineFcuVersion"` - Pre core.GenesisAlloc `json:"pre"` - Post core.GenesisAlloc `json:"postState"` +func (f *Fixture) ValidatePost(ctx context.Context, engineClient client.EngineClient) error { + // check nonce, balance & storage of accounts in final block against fixture values + for address, account := range f.PostAlloc { + // get nonce & balance from last block (end of test execution) + gotNonce, errN := engineClient.NonceAt(ctx, address, nil) + gotBalance, errB := engineClient.BalanceAt(ctx, address, nil) + if errN != nil { + return fmt.Errorf("unable to call nonce from account: %v: %v", address, errN) + } else if errB != nil { + return fmt.Errorf("unable to call balance from account: %v: %v", address, errB) + } + // check final nonce & balance matches expected in fixture + if account.Nonce != gotNonce { + return fmt.Errorf(`nonce received from account %v doesn't match expected from fixture: + received from block: %v + expected in fixture: %v`, address, gotNonce, account.Nonce) + } + if account.Balance.Cmp(gotBalance) != 0 { + return fmt.Errorf(`balance received from account %v doesn't match expected from fixture: + received from block: %v + expected in fixture: %v`, address, gotBalance, account.Balance) + } + // check final storage + if len(account.Storage) > 0 { + // extract fixture storage keys + keys := make([]common.Hash, 0, len(account.Storage)) + for key := range account.Storage { + keys = append(keys, key) + } + // get storage values for account with keys: keys + gotStorage, errS := engineClient.StorageAtKeys(ctx, address, keys, nil) + if errS != nil { + return fmt.Errorf("unable to get storage values from account: %v: %v", address, errS) + } + // check values in storage match with fixture + for _, key := range keys { + if account.Storage[key] != *gotStorage[key] { + return fmt.Errorf(`storage received from account %v doesn't match expected from fixture: + received from block: %v + expected in fixture: %v`, address, gotStorage[key], account.Storage[key]) + } + } + } + } + return nil } //go:generate go run github.com/fjl/gencodec -type genesisBlock -field-override genesisBlockUnmarshaling -out gen_gb.go @@ -73,13 +139,120 @@ type genesisBlockUnmarshaling struct { ExcessBlobGas *math.HexOrDecimal64 `json:"excessDataGas"` } -type engineNewPayload struct { +type EngineNewPayload struct { ExecutionPayload *api.ExecutableData `json:"executionPayload"` BlobVersionedHashes []common.Hash `json:"expectedBlobVersionedHashes"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot"` Version math.HexOrDecimal64 `json:"version"` ValidationError *string `json:"validationError"` - ErrorCode int64 `json:"errorCode,string"` + ErrorCode int `json:"errorCode,string"` +} - HiveExecutionPayload *typ.ExecutableData +func (p *EngineNewPayload) ExecutableData() (*typ.ExecutableData, error) { + executableData, err := typ.FromBeaconExecutableData(p.ExecutionPayload) + if err != nil { + return nil, errors.New("executionPayload param within engineNewPayload is invalid") + } + executableData.VersionedHashes = &p.BlobVersionedHashes + executableData.ParentBeaconBlockRoot = p.ParentBeaconBlockRoot + return &executableData, nil +} + +func (p *EngineNewPayload) Valid() bool { + return p.ErrorCode == 0 && p.ValidationError == nil +} + +func (p *EngineNewPayload) ExpectedStatus() string { + if p.ValidationError != nil { + return "INVALID" + } + return "VALID" +} + +func (p *EngineNewPayload) Execute(ctx context.Context, engineClient client.EngineClient) (api.PayloadStatusV1, rpc.Error) { + executableData, err := p.ExecutableData() + if err != nil { + panic(err) + } + status, err := engineClient.NewPayload( + ctx, + int(p.Version), + executableData, + ) + return status, parseError(err) +} + +func (p *EngineNewPayload) ExecuteValidate(ctx context.Context, engineClient client.EngineClient) (bool, error) { + plStatus, plErr := p.Execute(ctx, engineClient) + if err := p.ValidateRPCError(plErr); err != nil { + return false, err + } else if plErr != nil { + // Got an expected error and is already validated in ValidateRPCError + return false, nil + } + if plStatus.Status == "SYNCING" { + return true, nil + } + // Check payload status matches expected + if plStatus.Status != p.ExpectedStatus() { + return false, fmt.Errorf("payload status mismatch: got %s, want %s", plStatus.Status, p.ExpectedStatus()) + } + return false, nil +} + +func (p *EngineNewPayload) ForkchoiceValidate(ctx context.Context, engineClient client.EngineClient, fcuVersion int) (bool, error) { + response, err := engineClient.ForkchoiceUpdated(ctx, fcuVersion, &api.ForkchoiceStateV1{HeadBlockHash: p.ExecutionPayload.BlockHash}, nil) + if err != nil { + return false, err + } + if response.PayloadStatus.Status == "SYNCING" { + return true, nil + } + if response.PayloadStatus.Status != p.ExpectedStatus() { + return false, fmt.Errorf("forkchoice update status mismatch: got %s, want %s", response.PayloadStatus.Status, p.ExpectedStatus()) + } + return false, nil +} + +type HTTPErrorWithCode struct { + rpc.HTTPError +} + +func (e HTTPErrorWithCode) ErrorCode() int { + return e.StatusCode +} + +func parseError(plErr interface{}) rpc.Error { + if plErr == nil { + return nil + } + rpcErr, isRpcErr := plErr.(rpc.Error) + if isRpcErr { + return rpcErr + } + httpErr, isHttpErr := plErr.(rpc.HTTPError) + if isHttpErr { + return HTTPErrorWithCode{httpErr} + } + panic("unable to parse") +} + +// checks for RPC errors and compares error codes if expected. +func (p *EngineNewPayload) ValidateRPCError(rpcErr rpc.Error) error { + if rpcErr == nil && p.ErrorCode == 0 { + return nil + } + if rpcErr == nil && p.ErrorCode != 0 { + return fmt.Errorf("expected error code %d but received no error", p.ErrorCode) + } + if rpcErr != nil && p.ErrorCode == 0 { + return fmt.Errorf("expected no error code but received %d", rpcErr.ErrorCode()) + } + if rpcErr != nil && p.ErrorCode != 0 { + plErrCode := rpcErr.ErrorCode() + if plErrCode != p.ErrorCode { + return fmt.Errorf("error code mismatch: got: %d, want: %d", plErrCode, p.ErrorCode) + } + } + return nil }