End To End Tests for Demo and Minimal config (#3932)

* Begin working on end to end tests using geth dev chain

* Start on beacon node set up

* More progress on bnode setup

* Complete flow until chainstart, begin work on evaluators

* More progress on evaluators

* Start changing bazel run to direct binary

* Move endtoend to inside beacon-chain

* use bazel provided geth, use bazel test

* tempdir

* use fork rules_go

* Change to use UUID dir and bazel binaries

* Truncate UUID a bit

* Get full run from chainstart to evaluating

* Rewrite to react to logs rather than arbitrarily wait

* Fix export

* Move evaluators to evaluators.go

* Add peer check test

* Add more comments

* Remove unneeded exports

* Check all nodes have the correct amount of peers

* Change name to onGenesisEpoch

* Remove extra wait times where not needed

* Cleanup

* Add log for beacon start

* Fix deposit amount

* Make room for eth1follow distnce

* Cleanup and fix minimal test

* Goimports

* Fix imports

* gazelle and minimal

* manual

* Fix for comments

* Make timing rely on reading logs, and cleanup

* Fix for comments

* Fix workspace

* Cleanup

* Fix visibility

* Cleanup and some comments

* Address comments

* Fix for v0.9

* Modify for v0.9

* Move to own package outside of beacon-chain

* Gazelle

* Polishing, logging

* Fix filenames

* Add more logs

* Add flag logging

* Cover for page not having libp2p info

* Improve multiAddr detection

* Add more logs

* Add missing flags

* Add log printing to defer

* Get multiAddr from logs

* Fix logging and detection

* Change evaluators to rely on EpochTimer

* Add evaluator for ValidatorParticipation

* Fix validator participation evaluator

* Cleanup, comments and fix participation calculation

* Cleanup

* Let the file searcher search for longer

* Change participation to check for full

* Log out file contents if text isnt found

* Split into different files

* Disable IPC and use RPC instead, change tmp dir to bazel dir

* Change visibility

* Gazelle

* Add e2e tag

* new line
This commit is contained in:
Ivan Martinez 2019-11-15 13:56:26 -05:00 committed by Preston Van Loon
parent bb2f329562
commit 68edad13bc
16 changed files with 955 additions and 13 deletions

View File

@ -8,15 +8,6 @@ http_archive(
url = "https://github.com/bazelbuild/bazel-skylib/archive/0.8.0.tar.gz",
)
http_archive(
name = "io_bazel_rules_go",
sha256 = "513c12397db1bc9aa46dd62f02dd94b49a9b5d17444d49b5a04c5a89f3053c1c",
urls = [
"https://storage.googleapis.com/bazel-mirror/github.com/bazelbuild/rules_go/releases/download/v0.19.5/rules_go-v0.19.5.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.19.5/rules_go-v0.19.5.tar.gz",
],
)
http_archive(
name = "bazel_gazelle",
sha256 = "7fc87f4170011201b1690326e8c16c5d802836e3a0d617d8f75c3af2b23180c4",
@ -40,6 +31,13 @@ http_archive(
url = "https://github.com/bazelbuild/rules_docker/archive/v0.10.1.tar.gz",
)
http_archive(
name = "io_bazel_rules_go",
sha256 = "886db2f8d620fcb5791c8e2a402a575bc70728e17ec116841d78f3837a09f69e",
strip_prefix = "rules_go-9bb1562710f7077cd109b66cd4b45900e6d7ae73",
urls = ["https://github.com/bazelbuild/rules_go/archive/9bb1562710f7077cd109b66cd4b45900e6d7ae73.tar.gz"],
)
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "0942d188f4d0de6ddb743b9f6642a26ce1ad89f09c0035a9a5ca5ba9615c96aa",

View File

@ -79,7 +79,10 @@ docker_push(
go_binary(
name = "beacon-chain",
embed = [":go_default_library"],
visibility = ["//beacon-chain:__subpackages__"],
visibility = [
"//beacon-chain:__subpackages__",
"//endtoend:__pkg__",
],
)
go_test(

View File

@ -80,7 +80,7 @@ func NewBeaconNode(ctx *cli.Context) (*BeaconNode, error) {
stop: make(chan struct{}),
}
// Use custom config values if the --no-custom-config flag is set.
// Use custom config values if the --no-custom-config flag is not set.
if !ctx.GlobalBool(flags.NoCustomConfigFlag.Name) {
if featureconfig.Get().MinimalConfig {
log.WithField(

61
endtoend/BUILD.bazel Normal file
View File

@ -0,0 +1,61 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_test(
name = "go_default_test",
size = "enormous",
srcs = [
"demo_e2e_test.go",
"endtoend_test.go",
"minimal_e2e_test.go",
],
data = [
"//beacon-chain",
"//validator",
"@com_github_ethereum_go_ethereum//cmd/geth",
],
embed = [":go_default_library"],
shard_count = 2,
tags = [
"block-network",
"exclusive",
"manual",
"minimal",
"e2e",
],
deps = [
"//endtoend/evaluators:go_default_library",
"//proto/eth/v1alpha1:go_default_library",
"//shared/params:go_default_library",
"//shared/testutil:go_default_library",
"@com_github_gogo_protobuf//types:go_default_library",
"@io_bazel_rules_go//go/tools/bazel:go_default_library",
"@org_golang_google_grpc//:go_default_library",
],
)
go_library(
name = "go_default_library",
testonly = True,
srcs = [
"beacon_node.go",
"epochTimer.go",
"eth1.go",
"validator.go",
],
importpath = "github.com/prysmaticlabs/prysm/endtoend",
visibility = ["//endtoend:__subpackages__"],
deps = [
"//contracts/deposit-contract:go_default_library",
"//endtoend/evaluators:go_default_library",
"//shared/params:go_default_library",
"//shared/roughtime:go_default_library",
"//shared/testutil:go_default_library",
"@com_github_ethereum_go_ethereum//accounts/abi/bind:go_default_library",
"@com_github_ethereum_go_ethereum//accounts/keystore:go_default_library",
"@com_github_ethereum_go_ethereum//common:go_default_library",
"@com_github_ethereum_go_ethereum//core/types:go_default_library",
"@com_github_ethereum_go_ethereum//ethclient:go_default_library",
"@com_github_ethereum_go_ethereum//rpc:go_default_library",
"@io_bazel_rules_go//go/tools/bazel:go_default_library",
],
)

163
endtoend/beacon_node.go Normal file
View File

@ -0,0 +1,163 @@
package endtoend
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"testing"
"time"
"github.com/bazelbuild/rules_go/go/tools/bazel"
"github.com/ethereum/go-ethereum/common"
ev "github.com/prysmaticlabs/prysm/endtoend/evaluators"
)
type beaconNodeInfo struct {
processID int
datadir string
rpcPort uint64
monitorPort uint64
grpcPort uint64
multiAddr string
}
type end2EndConfig struct {
minimalConfig bool
tmpPath string
epochsToRun uint64
numValidators uint64
numBeaconNodes uint64
contractAddr common.Address
evaluators []ev.Evaluator
}
// startBeaconNodes starts the requested amount of beacon nodes, passing in the deposit contract given.
func startBeaconNodes(t *testing.T, config *end2EndConfig) []*beaconNodeInfo {
numNodes := config.numBeaconNodes
nodeInfo := []*beaconNodeInfo{}
for i := uint64(0); i < numNodes; i++ {
newNode := startNewBeaconNode(t, config, nodeInfo)
nodeInfo = append(nodeInfo, newNode)
}
return nodeInfo
}
func startNewBeaconNode(t *testing.T, config *end2EndConfig, beaconNodes []*beaconNodeInfo) *beaconNodeInfo {
tmpPath := config.tmpPath
index := len(beaconNodes)
binaryPath, found := bazel.FindBinary("beacon-chain", "beacon-chain")
if !found {
t.Log(binaryPath)
t.Fatal("beacon chain binary not found")
}
file, err := os.Create(path.Join(tmpPath, fmt.Sprintf("beacon-%d.log", index)))
if err != nil {
t.Fatal(err)
}
args := []string{
"--no-discovery",
"--http-web3provider=http://127.0.0.1:8545",
"--web3provider=ws://127.0.0.1:8546",
fmt.Sprintf("--datadir=%s/eth2-beacon-node-%d", tmpPath, index),
fmt.Sprintf("--deposit-contract=%s", config.contractAddr.Hex()),
fmt.Sprintf("--rpc-port=%d", 4000+index),
fmt.Sprintf("--p2p-udp-port=%d", 12000+index),
fmt.Sprintf("--p2p-tcp-port=%d", 13000+index),
fmt.Sprintf("--monitoring-port=%d", 8080+index),
fmt.Sprintf("--grpc-gateway-port=%d", 3200+index),
}
if config.minimalConfig {
args = append(args, "--minimal-config")
}
// After the first node is made, have all following nodes connect to all previously made nodes.
if index >= 1 {
for p := 0; p < index; p++ {
args = append(args, fmt.Sprintf("--peer=%s", beaconNodes[p].multiAddr))
}
}
t.Logf("Starting beacon chain with flags %s", strings.Join(args, " "))
cmd := exec.Command(binaryPath, args...)
cmd.Stderr = file
cmd.Stdout = file
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start beacon node: %v", err)
}
if err = waitForTextInFile(file, "Node started p2p server"); err != nil {
t.Fatal(err)
}
multiAddr, err := getMultiAddrFromLogFile(file.Name())
if err != nil {
t.Fatal(err)
}
return &beaconNodeInfo{
processID: cmd.Process.Pid,
datadir: fmt.Sprintf("%s/eth2-beacon-node-%d", tmpPath, index),
rpcPort: 4000 + uint64(index),
monitorPort: 8080 + uint64(index),
grpcPort: 3200 + uint64(index),
multiAddr: multiAddr,
}
}
func getMultiAddrFromLogFile(name string) (string, error) {
byteContent, err := ioutil.ReadFile(name)
if err != nil {
return "", err
}
contents := string(byteContent)
searchText := "\"Node started p2p server\" multiAddr=\""
startIdx := strings.Index(contents, searchText)
if startIdx == -1 {
return "", fmt.Errorf("did not find peer text in %s", contents)
}
startIdx += len(searchText)
endIdx := strings.Index(contents[startIdx:], "\"")
if endIdx == -1 {
return "", fmt.Errorf("did not find peer text in %s", contents)
}
return contents[startIdx : startIdx+endIdx], nil
}
func waitForTextInFile(file *os.File, text string) error {
wait := 0
// Putting the wait cap at 24 seconds.
totalWait := 24
for wait < totalWait {
time.Sleep(2 * time.Second)
// Rewind the file pointer to the start of the file so we can read it again.
_, err := file.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("could not rewind file to start: %v", err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if strings.Contains(scanner.Text(), text) {
return nil
}
}
if err := scanner.Err(); err != nil {
return err
}
wait += 2
}
contents, err := ioutil.ReadFile(file.Name())
if err != nil {
return err
}
return fmt.Errorf("could not find requested text %s in logs:\n%s", text, string(contents))
}

27
endtoend/demo_e2e_test.go Normal file
View File

@ -0,0 +1,27 @@
package endtoend
import (
"testing"
ev "github.com/prysmaticlabs/prysm/endtoend/evaluators"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/testutil"
)
func TestEndToEnd_DemoConfig(t *testing.T) {
testutil.ResetCache()
params.UseDemoBeaconConfig()
demoConfig := &end2EndConfig{
minimalConfig: false,
epochsToRun: 5,
numBeaconNodes: 4,
numValidators: params.BeaconConfig().MinGenesisActiveValidatorCount,
evaluators: []ev.Evaluator{
ev.ValidatorsAreActive,
ev.ValidatorsParticipating,
ev.FinalizationOccurs,
},
}
runEndToEndTest(t, demoConfig)
}

153
endtoend/endtoend_test.go Normal file
View File

@ -0,0 +1,153 @@
package endtoend
import (
"bufio"
"context"
"fmt"
"github.com/bazelbuild/rules_go/go/tools/bazel"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"strings"
"testing"
"time"
ptypes "github.com/gogo/protobuf/types"
"github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
"github.com/prysmaticlabs/prysm/shared/params"
"google.golang.org/grpc"
)
func runEndToEndTest(t *testing.T, config *end2EndConfig) {
tmpPath := bazel.TestTmpDir()
config.tmpPath = tmpPath
t.Logf("Test Path: %s\n", tmpPath)
contractAddr, keystorePath, eth1PID := startEth1(t, tmpPath)
config.contractAddr = contractAddr
beaconNodes := startBeaconNodes(t, config)
valClients := initializeValidators(t, config, keystorePath, beaconNodes)
processIDs := []int{eth1PID}
for _, vv := range valClients {
processIDs = append(processIDs, vv.processID)
}
for _, bb := range beaconNodes {
processIDs = append(processIDs, bb.processID)
}
defer logOutput(t, tmpPath)
defer killProcesses(t, processIDs)
if config.numBeaconNodes > 1 {
t.Run("all_peers_connect", func(t *testing.T) {
for _, bNode := range beaconNodes {
if err := peersConnect(bNode.monitorPort, config.numBeaconNodes-1); err != nil {
t.Fatalf("failed to connect to peers: %v", err)
}
}
})
}
beaconLogFile, err := os.Open(path.Join(tmpPath, "beacon-0.log"))
if err != nil {
t.Fatal(err)
}
if err := waitForTextInFile(beaconLogFile, "Sending genesis time notification"); err != nil {
t.Fatal(err)
}
conn, err := grpc.Dial("127.0.0.1:4000", grpc.WithInsecure())
if err != nil {
t.Fatalf("fail to dial: %v", err)
}
beaconClient := eth.NewBeaconChainClient(conn)
nodeClient := eth.NewNodeClient(conn)
genesis, err := nodeClient.GetGenesis(context.Background(), &ptypes.Empty{})
if err != nil {
t.Fatal(err)
}
// Small offset so evaluators perform in the middle of an epoch.
epochSeconds := params.BeaconConfig().SecondsPerSlot * params.BeaconConfig().SlotsPerEpoch
genesisTime := time.Unix(genesis.GenesisTime.Seconds+int64(epochSeconds/2), 0)
currentEpoch := uint64(0)
ticker := GetEpochTicker(genesisTime, epochSeconds)
for c := range ticker.C() {
if c >= config.epochsToRun {
ticker.Done()
break
}
for _, evaluator := range config.evaluators {
// Only run if the policy says so.
if !evaluator.Policy(currentEpoch) {
continue
}
t.Run(fmt.Sprintf(evaluator.Name, currentEpoch), func(t *testing.T) {
if err := evaluator.Evaluation(beaconClient); err != nil {
t.Fatal(err)
}
})
}
currentEpoch++
}
if currentEpoch < config.epochsToRun {
t.Fatalf("test ended prematurely, only reached epoch %d", currentEpoch)
}
}
func peersConnect(port uint64, expectedPeers uint64) error {
response, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/p2p", port))
if err != nil {
return err
}
dataInBytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
pageContent := string(dataInBytes)
if err := response.Body.Close(); err != nil {
return err
}
// Subtracting by 2 here since the libp2p page has "3 peers" as text.
// With a starting index before the "p", going two characters back should give us
// the number we need.
startIdx := strings.Index(pageContent, "peers") - 2
if startIdx == -3 {
return fmt.Errorf("could not find needed text in %s", pageContent)
}
peerCount, err := strconv.Atoi(pageContent[startIdx : startIdx+1])
if err != nil {
return err
}
if expectedPeers != uint64(peerCount) {
return fmt.Errorf("unexpected amount of peers, expected %d, received %d", expectedPeers, peerCount)
}
return nil
}
func killProcesses(t *testing.T, pIDs []int) {
for _, id := range pIDs {
process, err := os.FindProcess(id)
if err != nil {
t.Fatalf("could not find process %d: %v", id, err)
}
if err := process.Kill(); err != nil {
t.Fatal(err)
}
}
}
func logOutput(t *testing.T, tmpPath string) {
if t.Failed() {
beacon1LogFile, err := os.Open(path.Join(tmpPath, "beacon-1.log"))
if err != nil {
t.Fatal(err)
}
scanner := bufio.NewScanner(beacon1LogFile)
for scanner.Scan() {
currentLine := scanner.Text()
t.Log(currentLine)
}
}
}

79
endtoend/epochTimer.go Normal file
View File

@ -0,0 +1,79 @@
package endtoend
import (
"time"
"github.com/prysmaticlabs/prysm/shared/roughtime"
)
// EpochTicker is a special ticker for timing epoch changes.
// The channel emits over the epoch interval, and ensures that
// the ticks are in line with the genesis time. This means that
// the duration between the ticks and the genesis time are always a
// multiple of the epoch duration.
// In addition, the channel returns the new epoch number.
type EpochTicker struct {
c chan uint64
done chan struct{}
}
// C returns the ticker channel. Call Cancel afterwards to ensure
// that the goroutine exits cleanly.
func (s *EpochTicker) C() <-chan uint64 {
return s.c
}
// Done should be called to clean up the ticker.
func (s *EpochTicker) Done() {
go func() {
s.done <- struct{}{}
}()
}
// GetEpochTicker is the constructor for EpochTicker.
func GetEpochTicker(genesisTime time.Time, secondsPerEpoch uint64) *EpochTicker {
ticker := &EpochTicker{
c: make(chan uint64),
done: make(chan struct{}),
}
ticker.start(genesisTime, secondsPerEpoch, roughtime.Since, roughtime.Until, time.After)
return ticker
}
func (s *EpochTicker) start(
genesisTime time.Time,
secondsPerEpoch uint64,
since func(time.Time) time.Duration,
until func(time.Time) time.Duration,
after func(time.Duration) <-chan time.Time) {
d := time.Duration(secondsPerEpoch) * time.Second
go func() {
sinceGenesis := since(genesisTime)
var nextTickTime time.Time
var epoch uint64
if sinceGenesis < 0 {
// Handle when the current time is before the genesis time.
nextTickTime = genesisTime
epoch = 0
} else {
nextTick := sinceGenesis.Truncate(d) + d
nextTickTime = genesisTime.Add(nextTick)
epoch = uint64(nextTick / d)
}
for {
waitTime := until(nextTickTime)
select {
case <-after(waitTime):
s.c <- epoch
epoch++
nextTickTime = nextTickTime.Add(d)
case <-s.done:
return
}
}
}()
}

146
endtoend/eth1.go Normal file
View File

@ -0,0 +1,146 @@
package endtoend
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"math/big"
"os"
"os/exec"
"path"
"strings"
"testing"
"time"
"github.com/bazelbuild/rules_go/go/tools/bazel"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
contracts "github.com/prysmaticlabs/prysm/contracts/deposit-contract"
"github.com/prysmaticlabs/prysm/shared/params"
)
// startEth1 starts an eth1 local dev chain and deploys a deposit contract.
func startEth1(t *testing.T, tmpPath string) (common.Address, string, int) {
binaryPath, found := bazel.FindBinary("cmd/geth", "geth")
if !found {
t.Fatal("go-ethereum binary not found")
}
args := []string{
fmt.Sprintf("--datadir=%s", path.Join(tmpPath, "eth1data/")),
"--rpc",
"--rpcaddr=0.0.0.0",
"--rpccorsdomain=\"*\"",
"--rpcvhosts=\"*\"",
"--ws",
"--wsaddr=0.0.0.0",
"--wsorigins=\"*\"",
"--dev",
"--dev.period=0",
"--ipcdisable",
}
cmd := exec.Command(binaryPath, args...)
file, err := os.Create(path.Join(tmpPath, "eth1.log"))
if err != nil {
t.Fatal(err)
}
cmd.Stdout = file
cmd.Stderr = file
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start eth1 chain: %v", err)
}
if err = waitForTextInFile(file, "Commit new mining work"); err != nil {
t.Fatal(err)
}
// Connect to the started geth dev chain.
client, err := rpc.DialHTTP("http://127.0.0.1:8545")
if err != nil {
t.Fatalf("failed to connect to ipc: %v", err)
}
web3 := ethclient.NewClient(client)
// Access the dev account keystore to deploy the contract.
fileName, err := exec.Command("ls", path.Join(tmpPath, "eth1data/keystore")).Output()
if err != nil {
t.Fatal(err)
}
keystorePath := path.Join(tmpPath, fmt.Sprintf("eth1data/keystore/%s", strings.TrimSpace(string(fileName))))
jsonBytes, err := ioutil.ReadFile(keystorePath)
if err != nil {
t.Fatal(err)
}
keystore, err := keystore.DecryptKey(jsonBytes, "" /*password*/)
if err != nil {
t.Fatal(err)
}
// Advancing the blocks eth1follow distance to prevent issues reading the chain.
if err := mineBlocks(web3, keystore, params.BeaconConfig().Eth1FollowDistance); err != nil {
t.Fatalf("unable to advance chain: %v", err)
}
txOpts, err := bind.NewTransactor(bytes.NewReader(jsonBytes), "" /*password*/)
if err != nil {
t.Fatal(err)
}
nonce, err := web3.PendingNonceAt(context.Background(), keystore.Address)
if err != nil {
t.Fatal(err)
}
txOpts.Nonce = big.NewInt(int64(nonce))
contractAddr, tx, _, err := contracts.DeployDepositContract(txOpts, web3, txOpts.From)
if err != nil {
t.Fatalf("failed to deploy deposit contract: %v", err)
}
// Wait for contract to mine.
for pending := true; pending; _, pending, err = web3.TransactionByHash(context.Background(), tx.Hash()) {
if err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
}
return contractAddr, keystorePath, cmd.Process.Pid
}
func mineBlocks(web3 *ethclient.Client, keystore *keystore.Key, blocksToMake uint64) error {
nonce, err := web3.PendingNonceAt(context.Background(), keystore.Address)
if err != nil {
return err
}
chainID, err := web3.NetworkID(context.Background())
if err != nil {
return err
}
block, err := web3.BlockByNumber(context.Background(), nil)
if err != nil {
return err
}
finishBlock := block.NumberU64() + blocksToMake
for block.NumberU64() <= finishBlock {
spamTX := types.NewTransaction(nonce, keystore.Address, big.NewInt(0), 21000, big.NewInt(1e6), []byte{})
signed, err := types.SignTx(spamTX, types.NewEIP155Signer(chainID), keystore.PrivateKey)
if err != nil {
return err
}
if err := web3.SendTransaction(context.Background(), signed); err != nil {
return err
}
nonce++
time.Sleep(250 * time.Microsecond)
block, err = web3.BlockByNumber(context.Background(), nil)
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,18 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
testonly = True,
srcs = [
"finality.go",
"validator.go",
],
importpath = "github.com/prysmaticlabs/prysm/endtoend/evaluators",
visibility = ["//endtoend:__subpackages__"],
deps = [
"//proto/eth/v1alpha1:go_default_library",
"//shared/params:go_default_library",
"@com_github_gogo_protobuf//types:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
)

View File

@ -0,0 +1,54 @@
package evaluators
import (
"context"
"fmt"
ptypes "github.com/gogo/protobuf/types"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
"github.com/prysmaticlabs/prysm/shared/params"
)
// FinalizationOccurs is an evaluator to make sure finalization is performing as it should.
// Requires to be run after at least 4 epochs have passed.
var FinalizationOccurs = Evaluator{
Name: "finalizes_at_epoch_%d",
Policy: afterNthEpoch(3),
Evaluation: finalizationOccurs,
}
func finalizationOccurs(client eth.BeaconChainClient) error {
chainHead, err := client.GetChainHead(context.Background(), &ptypes.Empty{})
if err != nil {
return errors.Wrap(err, "failed to get chain head")
}
currentEpoch := chainHead.BlockSlot / params.BeaconConfig().SlotsPerEpoch
finalizedEpoch := chainHead.FinalizedSlot / params.BeaconConfig().SlotsPerEpoch
expectedFinalizedEpoch := currentEpoch - 2
if expectedFinalizedEpoch != finalizedEpoch {
return fmt.Errorf(
"expected finalized epoch to be %d, received: %d",
expectedFinalizedEpoch,
finalizedEpoch,
)
}
previousJustifiedEpoch := chainHead.PreviousJustifiedSlot / params.BeaconConfig().SlotsPerEpoch
currentJustifiedEpoch := chainHead.JustifiedSlot / params.BeaconConfig().SlotsPerEpoch
if previousJustifiedEpoch+1 != currentJustifiedEpoch {
return fmt.Errorf(
"there should be no gaps between current and previous justified epochs, received current %d and previous %d",
currentJustifiedEpoch,
previousJustifiedEpoch,
)
}
if currentJustifiedEpoch+1 != currentEpoch {
return fmt.Errorf(
"there should be no gaps between current epoch and current justified epoch, received current %d and justified %d",
currentEpoch,
currentJustifiedEpoch,
)
}
return nil
}

View File

@ -0,0 +1,94 @@
package evaluators
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/proto/eth/v1alpha1"
"github.com/prysmaticlabs/prysm/shared/params"
)
// Evaluator defines the structure of the evaluators used to
// conduct the current beacon state during the E2E.
type Evaluator struct {
Name string
Policy func(currentEpoch uint64) bool
Evaluation func(client eth.BeaconChainClient) error
}
// ValidatorsAreActive ensures the expected amount of validators are active.
var ValidatorsAreActive = Evaluator{
Name: "validators_active_epoch_%d",
Policy: onGenesisEpoch,
Evaluation: validatorsAreActive,
}
// ValidatorsParticipating ensures the expected amount of validators are active.
var ValidatorsParticipating = Evaluator{
Name: "validators_participating_epoch_%d",
Policy: afterNthEpoch(1),
Evaluation: validatorsParticipating,
}
func onGenesisEpoch(currentEpoch uint64) bool {
return currentEpoch < 2
}
// Not including first epoch because of issues with genesis.
func afterNthEpoch(afterEpoch uint64) func(uint64) bool {
return func(currentEpoch uint64) bool {
return currentEpoch > afterEpoch
}
}
func validatorsAreActive(client eth.BeaconChainClient) error {
// Balances actually fluctuate but we just want to check initial balance.
validatorRequest := &eth.GetValidatorsRequest{}
validators, err := client.GetValidators(context.Background(), validatorRequest)
if err != nil {
return errors.Wrap(err, "failed to get validators")
}
expectedCount := params.BeaconConfig().MinGenesisActiveValidatorCount
receivedCount := uint64(len(validators.Validators))
if expectedCount != receivedCount {
return fmt.Errorf("expected validator count to be %d, recevied %d", expectedCount, receivedCount)
}
for _, val := range validators.Validators {
if val.ActivationEpoch != 0 {
return fmt.Errorf("expected genesis validator epoch to be 0, received %d", val.ActivationEpoch)
}
if val.ExitEpoch != params.BeaconConfig().FarFutureEpoch {
return fmt.Errorf("expected genesis validator exit epoch to be far future, received %d", val.ExitEpoch)
}
if val.WithdrawableEpoch != params.BeaconConfig().FarFutureEpoch {
return fmt.Errorf("expected genesis validator withdrawable epoch to be far future, received %d", val.WithdrawableEpoch)
}
if val.EffectiveBalance != params.BeaconConfig().MaxEffectiveBalance {
return fmt.Errorf(
"expected genesis validator effective balance to be %d, received %d",
params.BeaconConfig().MaxEffectiveBalance,
val.EffectiveBalance,
)
}
}
return nil
}
// validatorsParticipating ensures the validators have an acceptable participation rate.
func validatorsParticipating(client eth.BeaconChainClient) error {
validatorRequest := &eth.GetValidatorParticipationRequest{}
participation, err := client.GetValidatorParticipation(context.Background(), validatorRequest)
if err != nil {
return errors.Wrap(err, "failed to get validator participation")
}
partRate := participation.Participation.GlobalParticipationRate
expected := float32(1)
if partRate < expected {
return fmt.Errorf("validator participation was below expected %f, received: %f", expected, partRate)
}
return nil
}

View File

@ -0,0 +1,27 @@
package endtoend
import (
"testing"
ev "github.com/prysmaticlabs/prysm/endtoend/evaluators"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/testutil"
)
func TestEndToEnd_MinimalConfig(t *testing.T) {
testutil.ResetCache()
params.UseMinimalConfig()
minimalConfig := &end2EndConfig{
minimalConfig: true,
epochsToRun: 5,
numBeaconNodes: 4,
numValidators: params.BeaconConfig().MinGenesisActiveValidatorCount,
evaluators: []ev.Evaluator{
ev.ValidatorsAreActive,
ev.ValidatorsParticipating,
ev.FinalizationOccurs,
},
}
runEndToEndTest(t, minimalConfig)
}

116
endtoend/validator.go Normal file
View File

@ -0,0 +1,116 @@
package endtoend
import (
"bytes"
"fmt"
"io/ioutil"
"math/big"
"os"
"os/exec"
"path"
"strings"
"testing"
"github.com/bazelbuild/rules_go/go/tools/bazel"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
contracts "github.com/prysmaticlabs/prysm/contracts/deposit-contract"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/testutil"
)
type validatorClientInfo struct {
processID int
monitorPort uint64
}
// initializeValidators sends the deposits to the eth1 chain and starts the validator clients.
func initializeValidators(
t *testing.T,
config *end2EndConfig,
keystorePath string,
beaconNodes []*beaconNodeInfo,
) []*validatorClientInfo {
binaryPath, found := bazel.FindBinary("validator", "validator")
if !found {
t.Fatal("validator binary not found")
}
tmpPath := config.tmpPath
validatorNum := config.numValidators
beaconNodeNum := config.numBeaconNodes
if validatorNum%beaconNodeNum != 0 {
t.Fatal("Validator count is not easily divisible by beacon node count.")
}
valClients := make([]*validatorClientInfo, beaconNodeNum)
validatorsPerNode := validatorNum / beaconNodeNum
for n := uint64(0); n < beaconNodeNum; n++ {
file, err := os.Create(path.Join(tmpPath, fmt.Sprintf("vals-%d.log", n)))
if err != nil {
t.Fatal(err)
}
args := []string{
fmt.Sprintf("--interop-num-validators=%d", validatorsPerNode),
fmt.Sprintf("--interop-start-index=%d", validatorsPerNode*n),
fmt.Sprintf("--monitoring-port=%d", 9080+n),
fmt.Sprintf("--beacon-rpc-provider=localhost:%d", 4000+n),
}
cmd := exec.Command(binaryPath, args...)
cmd.Stdout = file
cmd.Stderr = file
t.Logf("Starting validator client with flags %s", strings.Join(args, " "))
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
valClients[n] = &validatorClientInfo{
processID: cmd.Process.Pid,
monitorPort: 9080 + n,
}
}
client, err := rpc.DialHTTP("http://127.0.0.1:8545")
if err != nil {
t.Fatal(err)
}
web3 := ethclient.NewClient(client)
jsonBytes, err := ioutil.ReadFile(keystorePath)
if err != nil {
t.Fatal(err)
}
txOps, err := bind.NewTransactor(bytes.NewReader(jsonBytes), "" /*password*/)
if err != nil {
t.Fatal(err)
}
depositInGwei := big.NewInt(int64(params.BeaconConfig().MaxEffectiveBalance))
txOps.Value = depositInGwei.Mul(depositInGwei, big.NewInt(int64(params.BeaconConfig().GweiPerEth)))
txOps.GasLimit = 4000000
contract, err := contracts.NewDepositContract(config.contractAddr, web3)
if err != nil {
t.Fatal(err)
}
deposits, roots, _ := testutil.SetupInitialDeposits(t, validatorNum)
for index, dd := range deposits {
_, err = contract.Deposit(txOps, dd.Data.PublicKey, dd.Data.WithdrawalCredentials, dd.Data.Signature, roots[index])
if err != nil {
t.Error("unable to send transaction to contract")
}
}
keystore, err := keystore.DecryptKey(jsonBytes, "" /*password*/)
if err != nil {
t.Fatal(err)
}
// Picked 20 for this as a "safe" number of blocks to mine so the deposits
// are detected.
if err := mineBlocks(web3, keystore, 20); err != nil {
t.Fatal(err)
}
return valClients
}

View File

@ -29,7 +29,7 @@ type BeaconChainConfig struct {
// Gwei value constants.
MinDepositAmount uint64 `yaml:"MIN_DEPOSIT_AMOUNT"` // MinDepositAmount is the maximal amount of Gwei a validator can send to the deposit contract at once.
MaxEffectiveBalance uint64 `yaml:"MAX_EFFECTIVE_BALANCE"` // MaxEffectiveBalance is the maximal amount of Gwie that is effective for staking.
MaxEffectiveBalance uint64 `yaml:"MAX_EFFECTIVE_BALANCE"` // MaxEffectiveBalance is the maximal amount of Gwei that is effective for staking.
EjectionBalance uint64 `yaml:"EJECTION_BALANCE"` // EjectionBalance is the minimal GWei a validator needs to have before ejected.
EffectiveBalanceIncrement uint64 `yaml:"EFFECTIVE_BALANCE_INCREMENT"` // EffectiveBalanceIncrement is used for converting the high balance into the low balance for validators.

View File

@ -79,7 +79,10 @@ go_binary(
name = "validator",
embed = [":go_default_library"],
pure = "off", # Enabled unless there is a valid reason to include cgo dep.
visibility = ["//validator:__subpackages__"],
visibility = [
"//validator:__subpackages__",
"//endtoend:__pkg__",
],
)
[go_binary(