2020-04-29 21:32:39 +00:00
// Package endtoend performs full a end-to-end test for Prysm,
// including spinning up an ETH1 dev chain, sending deposits to the deposit
// contract, and making sure the beacon node and validators are running and
// performing properly for a few epochs.
2019-11-15 18:56:26 +00:00
package endtoend
import (
"context"
2021-03-26 03:15:58 +00:00
"errors"
2019-11-15 18:56:26 +00:00
"fmt"
"os"
"path"
2021-03-26 03:15:58 +00:00
"strings"
2021-09-10 19:59:43 +00:00
"sync"
2019-11-15 18:56:26 +00:00
"testing"
"time"
2021-02-22 23:20:57 +00:00
types "github.com/prysmaticlabs/eth2-types"
2021-09-08 10:41:47 +00:00
"github.com/prysmaticlabs/prysm/beacon-chain/core/transition"
2020-03-22 23:04:23 +00:00
"github.com/prysmaticlabs/prysm/endtoend/components"
2020-01-28 19:16:00 +00:00
ev "github.com/prysmaticlabs/prysm/endtoend/evaluators"
2020-03-22 23:04:23 +00:00
"github.com/prysmaticlabs/prysm/endtoend/helpers"
e2e "github.com/prysmaticlabs/prysm/endtoend/params"
2021-02-09 10:05:22 +00:00
e2etypes "github.com/prysmaticlabs/prysm/endtoend/types"
2021-07-21 21:34:07 +00:00
eth "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1"
2019-11-15 18:56:26 +00:00
"github.com/prysmaticlabs/prysm/shared/params"
2020-08-25 15:23:06 +00:00
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
2021-03-26 03:15:58 +00:00
log "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
2019-11-15 18:56:26 +00:00
"google.golang.org/grpc"
2021-05-17 18:32:04 +00:00
"google.golang.org/protobuf/types/known/emptypb"
2019-11-15 18:56:26 +00:00
)
2021-03-26 03:15:58 +00:00
const (
// allNodesStartTimeout defines period after which nodes are considered
// stalled (safety measure for nodes stuck at startup, shouldn't normally happen).
allNodesStartTimeout = 5 * time . Minute
2021-07-22 04:00:12 +00:00
// errGeneralCode is used to represent the string value for all general process errors.
errGeneralCode = "exit status 1"
2021-03-26 03:15:58 +00:00
)
2020-04-02 02:09:54 +00:00
func init ( ) {
2021-09-08 10:41:47 +00:00
transition . SkipSlotCache . Disable ( )
2020-04-02 02:09:54 +00:00
}
2021-03-26 03:15:58 +00:00
// testRunner abstracts E2E test configuration and running.
type testRunner struct {
t * testing . T
config * e2etypes . E2EConfig
}
// newTestRunner creates E2E test runner.
func newTestRunner ( t * testing . T , config * e2etypes . E2EConfig ) * testRunner {
return & testRunner {
t : t ,
config : config ,
}
}
// run executes configured E2E test.
func ( r * testRunner ) run ( ) {
t , config := r . t , r . config
2020-03-31 15:15:33 +00:00
t . Logf ( "Shard index: %d\n" , e2e . TestParams . TestShardIndex )
2020-01-07 17:00:51 +00:00
t . Logf ( "Starting time: %s\n" , time . Now ( ) . String ( ) )
2020-08-02 21:30:59 +00:00
t . Logf ( "Log Path: %s\n" , e2e . TestParams . LogPath )
minGenesisActiveCount := int ( params . BeaconConfig ( ) . MinGenesisActiveValidatorCount )
2019-11-15 18:56:26 +00:00
2021-03-26 03:15:58 +00:00
ctx , done := context . WithCancel ( context . Background ( ) )
g , ctx := errgroup . WithContext ( ctx )
2019-11-15 18:56:26 +00:00
2021-08-11 15:38:00 +00:00
tracingSink := components . NewTracingSink ( config . TracingSinkEndpoint )
g . Go ( func ( ) error {
return tracingSink . Start ( ctx )
} )
2021-03-26 03:15:58 +00:00
// ETH1 node.
eth1Node := components . NewEth1Node ( )
g . Go ( func ( ) error {
return eth1Node . Start ( ctx )
} )
g . Go ( func ( ) error {
if err := helpers . ComponentsStarted ( ctx , [ ] e2etypes . ComponentRunner { eth1Node } ) ; err != nil {
return fmt . Errorf ( "sending and mining deposits require ETH1 node to run: %w" , err )
}
return components . SendAndMineDeposits ( eth1Node . KeystorePath ( ) , minGenesisActiveCount , 0 , true /* partial */ )
2020-06-03 18:44:13 +00:00
} )
2020-01-06 20:50:36 +00:00
2021-03-26 03:15:58 +00:00
// Boot node.
bootNode := components . NewBootNode ( )
g . Go ( func ( ) error {
return bootNode . Start ( ctx )
} )
// Beacon nodes.
beaconNodes := components . NewBeaconNodes ( config )
g . Go ( func ( ) error {
if err := helpers . ComponentsStarted ( ctx , [ ] e2etypes . ComponentRunner { eth1Node , bootNode } ) ; err != nil {
return fmt . Errorf ( "beacon nodes require ETH1 and boot node to run: %w" , err )
}
beaconNodes . SetENR ( bootNode . ENR ( ) )
return beaconNodes . Start ( ctx )
} )
2020-01-06 20:50:36 +00:00
2021-03-26 03:15:58 +00:00
// Validator nodes.
validatorNodes := components . NewValidatorNodeSet ( config )
g . Go ( func ( ) error {
if err := helpers . ComponentsStarted ( ctx , [ ] e2etypes . ComponentRunner { beaconNodes } ) ; err != nil {
return fmt . Errorf ( "validator nodes require beacon nodes to run: %w" , err )
}
return validatorNodes . Start ( ctx )
} )
// Slasher nodes.
var slasherNodes e2etypes . ComponentRunner
2020-07-31 02:42:18 +00:00
if config . TestSlasher {
2021-03-26 03:15:58 +00:00
slasherNodes := components . NewSlasherNodeSet ( config )
g . Go ( func ( ) error {
if err := helpers . ComponentsStarted ( ctx , [ ] e2etypes . ComponentRunner { beaconNodes } ) ; err != nil {
return fmt . Errorf ( "slasher nodes require beacon nodes to run: %w" , err )
}
return slasherNodes . Start ( ctx )
} )
2020-04-29 13:42:12 +00:00
}
2021-03-26 03:15:58 +00:00
// Run E2E evaluators and tests.
g . Go ( func ( ) error {
// When everything is done, cancel parent context (will stop all spawned nodes).
2020-04-14 16:41:09 +00:00
defer func ( ) {
2021-03-26 03:15:58 +00:00
log . Info ( "All E2E evaluations are finished, cleaning up" )
done ( )
2020-04-14 16:41:09 +00:00
} ( )
2021-03-26 03:15:58 +00:00
// Wait for all required nodes to start.
requiredComponents := [ ] e2etypes . ComponentRunner {
2021-08-11 15:38:00 +00:00
tracingSink , eth1Node , bootNode , beaconNodes , validatorNodes ,
2021-03-26 03:15:58 +00:00
}
if config . TestSlasher && slasherNodes != nil {
requiredComponents = append ( requiredComponents , slasherNodes )
}
ctxAllNodesReady , cancel := context . WithTimeout ( ctx , allNodesStartTimeout )
defer cancel ( )
if err := helpers . ComponentsStarted ( ctxAllNodesReady , requiredComponents ) ; err != nil {
return fmt . Errorf ( "components take too long to start: %w" , err )
}
// Since defer unwraps in LIFO order, parent context will be closed only after logs are written.
defer helpers . LogOutput ( t , config )
if config . UsePprof {
defer func ( ) {
log . Info ( "Writing output pprof files" )
for i := 0 ; i < e2e . TestParams . BeaconNodeCount ; i ++ {
assert . NoError ( t , helpers . WritePprofFiles ( e2e . TestParams . LogPath , i ) )
}
} ( )
}
// Blocking, wait period varies depending on number of validators.
r . waitForChainStart ( )
// Failing early in case chain doesn't start.
if t . Failed ( ) {
return errors . New ( "chain cannot start" )
}
if config . TestDeposits {
log . Info ( "Running deposit tests" )
r . testDeposits ( ctx , g , eth1Node , [ ] e2etypes . ComponentRunner { beaconNodes } )
}
// Create GRPC connection to beacon nodes.
conns , closeConns , err := helpers . NewLocalConnections ( ctx , e2e . TestParams . BeaconNodeCount )
require . NoError ( t , err , "Cannot create local connections" )
defer closeConns ( )
// Calculate genesis time.
nodeClient := eth . NewNodeClient ( conns [ 0 ] )
2021-05-17 18:32:04 +00:00
genesis , err := nodeClient . GetGenesis ( context . Background ( ) , & emptypb . Empty { } )
2021-03-26 03:15:58 +00:00
require . NoError ( t , err )
tickingStartTime := helpers . EpochTickerStartTime ( genesis )
// Run assigned evaluators.
if err := r . runEvaluators ( conns , tickingStartTime ) ; err != nil {
return err
}
// If requested, run sync test.
if ! config . TestSync {
return nil
}
2021-07-22 04:00:12 +00:00
if err := r . testBeaconChainSync ( ctx , g , conns , tickingStartTime , bootNode . ENR ( ) ) ; err != nil {
return err
}
return r . testDoppelGangerProtection ( ctx )
2021-03-26 03:15:58 +00:00
} )
if err := g . Wait ( ) ; err != nil && ! errors . Is ( err , context . Canceled ) {
// At the end of the main evaluator goroutine all nodes are killed, no need to fail the test.
if strings . Contains ( err . Error ( ) , "signal: killed" ) {
return
}
t . Fatalf ( "E2E test ended in error: %v" , err )
2020-03-15 05:09:23 +00:00
}
2021-03-26 03:15:58 +00:00
}
2020-01-28 19:16:00 +00:00
2021-03-26 03:15:58 +00:00
// waitForChainStart allows to wait up until beacon nodes are started.
func ( r * testRunner ) waitForChainStart ( ) {
// Sleep depending on the count of validators, as generating the genesis state could take some time.
time . Sleep ( time . Duration ( params . BeaconConfig ( ) . GenesisDelay ) * time . Second )
beaconLogFile , err := os . Open ( path . Join ( e2e . TestParams . LogPath , fmt . Sprintf ( e2e . BeaconNodeLogFileName , 0 ) ) )
require . NoError ( r . t , err )
2020-04-14 20:27:03 +00:00
2021-03-26 03:15:58 +00:00
r . t . Run ( "chain started" , func ( t * testing . T ) {
require . NoError ( t , helpers . WaitForTextInFile ( beaconLogFile , "Chain started in sync service" ) , "Chain did not start" )
} )
}
// runEvaluators executes assigned evaluators.
func ( r * testRunner ) runEvaluators ( conns [ ] * grpc . ClientConn , tickingStartTime time . Time ) error {
t , config := r . t , r . config
secondsPerEpoch := uint64 ( params . BeaconConfig ( ) . SlotsPerEpoch . Mul ( params . BeaconConfig ( ) . SecondsPerSlot ) )
ticker := helpers . NewEpochTicker ( tickingStartTime , secondsPerEpoch )
2020-01-25 07:39:56 +00:00
for currentEpoch := range ticker . C ( ) {
2021-09-10 19:59:43 +00:00
wg := new ( sync . WaitGroup )
for _ , ev := range config . Evaluators {
// Fix reference to evaluator as it will be running
// in a separate goroutine.
evaluator := ev
2019-11-15 18:56:26 +00:00
// Only run if the policy says so.
2021-02-09 10:05:22 +00:00
if ! evaluator . Policy ( types . Epoch ( currentEpoch ) ) {
2019-11-15 18:56:26 +00:00
continue
}
2021-09-10 19:59:43 +00:00
// Add evaluator to our waitgroup.
wg . Add ( 1 )
go t . Run ( fmt . Sprintf ( evaluator . Name , currentEpoch ) , func ( t * testing . T ) {
2021-03-26 03:15:58 +00:00
err := evaluator . Evaluation ( conns ... )
assert . NoError ( t , err , "Evaluation failed for epoch %d: %v" , currentEpoch , err )
2021-09-10 19:59:43 +00:00
wg . Done ( )
2019-11-15 18:56:26 +00:00
} )
}
2021-09-10 19:59:43 +00:00
// Wait for all evaluators to finish their evaluation for the epoch.
wg . Wait ( )
2020-03-22 23:04:23 +00:00
if t . Failed ( ) || currentEpoch >= config . EpochsToRun - 1 {
2020-01-25 07:39:56 +00:00
ticker . Done ( )
2020-01-30 00:14:10 +00:00
if t . Failed ( ) {
2021-03-26 03:15:58 +00:00
return errors . New ( "test failed" )
2020-01-30 00:14:10 +00:00
}
2020-01-25 07:39:56 +00:00
break
}
2019-11-15 18:56:26 +00:00
}
2021-03-26 03:15:58 +00:00
return nil
}
2019-11-15 18:56:26 +00:00
2021-03-26 03:15:58 +00:00
// testDeposits runs tests when config.TestDeposits is enabled.
func ( r * testRunner ) testDeposits ( ctx context . Context , g * errgroup . Group ,
eth1Node * components . Eth1Node , requiredNodes [ ] e2etypes . ComponentRunner ) {
minGenesisActiveCount := int ( params . BeaconConfig ( ) . MinGenesisActiveValidatorCount )
2020-03-15 05:09:23 +00:00
2021-03-26 03:15:58 +00:00
depositCheckValidator := components . NewValidatorNode ( r . config , int ( e2e . DepositCount ) , e2e . TestParams . BeaconNodeCount , minGenesisActiveCount )
g . Go ( func ( ) error {
if err := helpers . ComponentsStarted ( ctx , requiredNodes ) ; err != nil {
return fmt . Errorf ( "deposit check validator node requires beacon nodes to run: %w" , err )
}
go func ( ) {
err := components . SendAndMineDeposits ( eth1Node . KeystorePath ( ) , int ( e2e . DepositCount ) , minGenesisActiveCount , false /* partial */ )
if err != nil {
r . t . Fatal ( err )
}
} ( )
return depositCheckValidator . Start ( ctx )
} )
}
// testBeaconChainSync creates another beacon node, and tests whether it can sync to head using previous nodes.
func ( r * testRunner ) testBeaconChainSync ( ctx context . Context , g * errgroup . Group ,
conns [ ] * grpc . ClientConn , tickingStartTime time . Time , enr string ) error {
t , config := r . t , r . config
2020-03-22 23:04:23 +00:00
index := e2e . TestParams . BeaconNodeCount
2021-03-26 03:15:58 +00:00
syncBeaconNode := components . NewBeaconNode ( config , index , enr )
g . Go ( func ( ) error {
return syncBeaconNode . Start ( ctx )
} )
if err := helpers . ComponentsStarted ( ctx , [ ] e2etypes . ComponentRunner { syncBeaconNode } ) ; err != nil {
return fmt . Errorf ( "sync beacon node not ready: %w" , err )
}
2020-03-22 23:04:23 +00:00
syncConn , err := grpc . Dial ( fmt . Sprintf ( "127.0.0.1:%d" , e2e . TestParams . BeaconNodeRPCPort + index ) , grpc . WithInsecure ( ) )
2020-08-25 15:23:06 +00:00
require . NoError ( t , err , "Failed to dial" )
2020-03-15 05:09:23 +00:00
conns = append ( conns , syncConn )
2020-01-28 19:16:00 +00:00
2020-08-05 14:58:50 +00:00
// Sleep a second for every 4 blocks that need to be synced for the newly started node.
2021-03-26 03:15:58 +00:00
secondsPerEpoch := uint64 ( params . BeaconConfig ( ) . SlotsPerEpoch . Mul ( params . BeaconConfig ( ) . SecondsPerSlot ) )
extraSecondsToSync := ( config . EpochsToRun ) * secondsPerEpoch + uint64 ( params . BeaconConfig ( ) . SlotsPerEpoch . Div ( 4 ) . Mul ( config . EpochsToRun ) )
2020-04-29 13:42:12 +00:00
waitForSync := tickingStartTime . Add ( time . Duration ( extraSecondsToSync ) * time . Second )
time . Sleep ( time . Until ( waitForSync ) )
2020-01-28 19:16:00 +00:00
2020-03-22 23:04:23 +00:00
syncLogFile , err := os . Open ( path . Join ( e2e . TestParams . LogPath , fmt . Sprintf ( e2e . BeaconNodeLogFileName , index ) ) )
2020-08-25 15:23:06 +00:00
require . NoError ( t , err )
2020-03-22 23:04:23 +00:00
defer helpers . LogErrorOutput ( t , syncLogFile , "beacon chain node" , index )
2020-06-03 18:44:13 +00:00
t . Run ( "sync completed" , func ( t * testing . T ) {
2020-08-25 15:23:06 +00:00
assert . NoError ( t , helpers . WaitForTextInFile ( syncLogFile , "Synced up to" ) , "Failed to sync" )
2020-06-03 18:44:13 +00:00
} )
if t . Failed ( ) {
2021-03-26 03:15:58 +00:00
return errors . New ( "cannot sync beacon node" )
2019-11-15 18:56:26 +00:00
}
2020-07-14 18:08:17 +00:00
// Sleep a slot to make sure the synced state is made.
time . Sleep ( time . Duration ( params . BeaconConfig ( ) . SecondsPerSlot ) * time . Second )
2021-02-09 10:05:22 +00:00
syncEvaluators := [ ] e2etypes . Evaluator { ev . FinishedSyncing , ev . AllNodesHaveSameHead }
2020-03-15 05:09:23 +00:00
for _ , evaluator := range syncEvaluators {
t . Run ( evaluator . Name , func ( t * testing . T ) {
2020-08-25 15:23:06 +00:00
assert . NoError ( t , evaluator . Evaluation ( conns ... ) , "Evaluation failed for sync node" )
2020-03-15 05:09:23 +00:00
} )
}
2021-03-26 03:15:58 +00:00
return nil
2019-11-15 18:56:26 +00:00
}
2021-07-22 04:00:12 +00:00
func ( r * testRunner ) testDoppelGangerProtection ( ctx context . Context ) error {
// Exit if we are running from the previous release.
if r . config . UsePrysmShValidator {
return nil
}
g , ctx := errgroup . WithContext ( ctx )
// Follow same parameters as older validators.
validatorNum := int ( params . BeaconConfig ( ) . MinGenesisActiveValidatorCount )
beaconNodeNum := e2e . TestParams . BeaconNodeCount
if validatorNum % beaconNodeNum != 0 {
return errors . New ( "validator count is not easily divisible by beacon node count" )
}
validatorsPerNode := validatorNum / beaconNodeNum
valIndex := beaconNodeNum + 1
// Replicate starting up validator client 0 to test doppleganger protection.
valNode := components . NewValidatorNode ( r . config , validatorsPerNode , valIndex , validatorsPerNode * 0 )
g . Go ( func ( ) error {
return valNode . Start ( ctx )
} )
if err := helpers . ComponentsStarted ( ctx , [ ] e2etypes . ComponentRunner { valNode } ) ; err != nil {
return fmt . Errorf ( "validator not ready: %w" , err )
}
logFile , err := os . Create ( path . Join ( e2e . TestParams . LogPath , fmt . Sprintf ( e2e . ValidatorLogFileName , valIndex ) ) )
if err != nil {
return fmt . Errorf ( "unable to open log file: %v" , err )
}
r . t . Run ( "doppelganger found" , func ( t * testing . T ) {
assert . NoError ( t , helpers . WaitForTextInFile ( logFile , "Duplicate instances exists in the network for validator keys" ) , "Failed to carry out doppelganger check correctly" )
} )
if r . t . Failed ( ) {
return errors . New ( "doppelganger was unable to be found" )
}
// Expect an abrupt exit for the validator client.
if err := g . Wait ( ) ; err == nil || ! strings . Contains ( err . Error ( ) , errGeneralCode ) {
return fmt . Errorf ( "wanted an error of %s but received %v" , errGeneralCode , err )
}
return nil
}