diff --git a/simulators/eth2/engine/Dockerfile b/simulators/eth2/engine/Dockerfile index c7cdb3e13f..c8b1a4a40d 100644 --- a/simulators/eth2/engine/Dockerfile +++ b/simulators/eth2/engine/Dockerfile @@ -1,3 +1,11 @@ +# Generate the ethash verification caches. +# Use a static version because this will never need to be updated. +FROM ethereum/client-go:v1.10.20 AS geth +RUN \ + /usr/local/bin/geth makecache 1 /ethash && \ + /usr/local/bin/geth makedag 1 /ethash + +# Build the simulator binary FROM golang:1-alpine AS builder RUN apk --no-cache add gcc musl-dev linux-headers cmake make clang build-base clang-static clang-dev ADD . /source @@ -8,4 +16,6 @@ RUN go build -o ./sim . FROM alpine:latest ADD . / COPY --from=builder /source/sim / +COPY --from=geth /ethash /ethash + ENTRYPOINT ["./sim"] diff --git a/simulators/eth2/engine/chain_generators.go b/simulators/eth2/engine/chain_generators.go new file mode 100644 index 0000000000..d449912542 --- /dev/null +++ b/simulators/eth2/engine/chain_generators.go @@ -0,0 +1,62 @@ +package main + +import ( + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/hive/simulators/eth2/engine/setup" +) + +type ChainGenerator interface { + Generate(*setup.Eth1Genesis) ([]*types.Block, error) +} + +var PoWChainGeneratorDefaults = ethash.Config{ + PowMode: ethash.ModeNormal, + CachesInMem: 2, + DatasetsOnDisk: 2, + DatasetDir: "/ethash", +} + +type PoWChainGenerator struct { + BlockCount int + ethash.Config + GenFunction func(int, *core.BlockGen) + blocks []*types.Block +} + +// instaSeal wraps a consensus engine with instant block sealing. When a block is produced +// using FinalizeAndAssemble, it also applies Seal. +type instaSeal struct{ consensus.Engine } + +// FinalizeAndAssemble implements consensus.Engine, accumulating the block and uncle rewards, +// setting the final state and assembling the block. +func (e instaSeal) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) { + block, err := e.Engine.FinalizeAndAssemble(chain, header, state, txs, uncles, receipts) + if err != nil { + return nil, err + } + sealedBlock := make(chan *types.Block, 1) + if err = e.Engine.Seal(nil, block, sealedBlock, nil); err != nil { + return nil, err + } + return <-sealedBlock, nil +} + +func (p *PoWChainGenerator) Generate(genesis *setup.Eth1Genesis) ([]*types.Block, error) { + // We generate a new chain only if the generator had not generated one already. + // This is done because the chain generators can be reused on different clients to ensure + // they start with the same chain. + if p.blocks != nil { + return p.blocks, nil + } + db := rawdb.NewMemoryDatabase() + engine := ethash.New(p.Config, nil, false) + insta := instaSeal{engine} + genesisBlock := genesis.Genesis.ToBlock(db) + p.blocks, _ = core.GenerateChain(genesis.Genesis.Config, genesisBlock, insta, db, p.BlockCount, p.GenFunction) + return p.blocks, nil +} diff --git a/simulators/eth2/engine/engineapi.go b/simulators/eth2/engine/engineapi.go index 0a0fbb7e6e..0c7e32ca8b 100644 --- a/simulators/eth2/engine/engineapi.go +++ b/simulators/eth2/engine/engineapi.go @@ -28,6 +28,8 @@ const ( EngineForkchoiceUpdatedV1 string = "engine_forkchoiceUpdatedV1" EngineGetPayloadV1 = "engine_getPayloadV1" EngineNewPayloadV1 = "engine_newPayloadV1" + EthGetBlockByHash = "eth_getBlockByHash" + EthGetBlockByNumber = "eth_getBlockByNumber" ) // EngineClient wrapper for Ethereum Engine RPC for testing purposes. @@ -124,6 +126,20 @@ func (ec *EngineClient) checkTTD() (*types.Header, bool) { return nil, false } +func (ec *EngineClient) waitForTTDWithTimeout(cliqueSeconds uint64, timeout <-chan time.Time) bool { + for { + select { + case <-time.After(time.Duration(cliqueSeconds) * time.Second): + _, ok := ec.checkTTD() + if ok { + return true + } + case <-timeout: + return false + } + } +} + // Engine API Types type PayloadStatusV1 struct { Status PayloadStatus `json:"status"` diff --git a/simulators/eth2/engine/go.mod b/simulators/eth2/engine/go.mod index ff58f160f9..fb206ebb71 100644 --- a/simulators/eth2/engine/go.mod +++ b/simulators/eth2/engine/go.mod @@ -3,7 +3,7 @@ module github.com/ethereum/hive/simulators/eth2/engine go 1.17 require ( - github.com/ethereum/go-ethereum v1.10.17 + github.com/ethereum/go-ethereum v1.10.20 github.com/ethereum/hive v0.0.0-20220707162108-7ef78bf4723f github.com/google/uuid v1.3.0 github.com/herumi/bls-eth-go-binary v0.0.0-20210902234237-7763804ee078 @@ -31,30 +31,31 @@ require ( github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/onsi/gomega v1.15.0 // indirect + github.com/onsi/gomega v1.19.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect github.com/protolambda/bls12-381-util v0.0.0-20210812140640-b03868185758 github.com/shirou/gopsutil v3.21.8+incompatible // indirect - github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.9 // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Microsoft/hcsshim v0.9.2 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.1.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/containerd/cgroups v1.0.3 // indirect github.com/containerd/containerd v1.6.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/docker/docker v20.10.12+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c // indirect github.com/fsouza/go-dockerclient v1.7.11 // indirect github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect @@ -82,14 +83,14 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/tklauser/numcpus v0.3.0 // indirect - github.com/urfave/cli/v2 v2.5.1 // indirect + github.com/urfave/cli/v2 v2.10.2 // indirect github.com/wealdtech/go-bytesutil v1.1.1 // indirect github.com/wealdtech/go-eth2-types/v2 v2.5.6 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/tools v0.1.5 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect + golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 // indirect + golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect gopkg.in/inconshreveable/log15.v2 v2.0.0-20200109203555-b30bc20e4fd1 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect ) diff --git a/simulators/eth2/engine/go.sum b/simulators/eth2/engine/go.sum index 537042b984..8f24130101 100644 --- a/simulators/eth2/engine/go.sum +++ b/simulators/eth2/engine/go.sum @@ -152,6 +152,8 @@ github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR github.com/btcsuite/btcd v0.22.0-beta h1:LTDpDKUM5EeOFBPM8IXpinEcmZ6FWfNZbE3lfrfdnWo= github.com/btcsuite/btcd/btcec/v2 v2.1.2 h1:YoYoC9J0jwfukodSBMzZYUVQ8PTiYg4BnOWiJVzTmLs= github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= @@ -316,6 +318,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -388,6 +391,8 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8= github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= +github.com/ethereum/go-ethereum v1.10.20 h1:75IW830ClSS40yrQC1ZCMZCt5I+zU16oqId2SiQwdQ4= +github.com/ethereum/go-ethereum v1.10.20/go.mod h1:LWUN82TCHGpxB3En5HVmLLzPD7YSrEUFmFfN1nKkVN0= github.com/ethereum/hive v0.0.0-20220202142700-64e211a795ba h1:J/zALdNL6F+ymZqpZdrUxQ8DUVAQScscQG8lwWxXv3E= github.com/ethereum/hive v0.0.0-20220202142700-64e211a795ba/go.mod h1:xptEyWnRW33B208ZrXXvkDpnP/BCvsigi/vir2TxruM= github.com/ethereum/hive v0.0.0-20220707162108-7ef78bf4723f h1:ZgwaHF9LR8qbV2Y3xMt0LQHGA/sYQKxrgq+fxzbIp2M= @@ -410,6 +415,8 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsouza/go-dockerclient v1.7.11 h1:pRmGMANAl+tmr+IYNYq8IWWcSbiKQMSRumYLv8H5sfk= github.com/fsouza/go-dockerclient v1.7.11/go.mod h1:zvYxutUNOK853i1s7VywZxQgxSHbm7A6en/q9MHBN6k= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= @@ -564,6 +571,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -829,6 +837,8 @@ github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1ls github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -839,6 +849,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1027,12 +1039,16 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= @@ -1056,6 +1072,7 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.5.1 h1:YKwdkyA0xTBzOaP2G0DVxBnCheHGP+Y9VbKAs4K1Ess= github.com/urfave/cli/v2 v2.5.1/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs= +github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= @@ -1204,6 +1221,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1266,6 +1284,8 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1398,12 +1418,16 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1492,11 +1516,14 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= @@ -1668,6 +1695,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/simulators/eth2/engine/helper.go b/simulators/eth2/engine/helper.go index c38190ffbb..855590c534 100644 --- a/simulators/eth2/engine/helper.go +++ b/simulators/eth2/engine/helper.go @@ -1,20 +1,25 @@ package main import ( + "context" "crypto/ecdsa" "crypto/rand" "fmt" "math/big" + "net/http" "strings" "sync" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/simulators/eth2/engine/setup" blsu "github.com/protolambda/bls12-381-util" + beacon "github.com/protolambda/zrnt/eth2/beacon/common" "github.com/rauljordan/engine-proxy/proxy" ) @@ -37,6 +42,8 @@ type node struct { ExecutionClientTTD *big.Int BeaconNodeTTD *big.Int TestVerificationNode bool + ChainGenerator ChainGenerator + Chain []*types.Block } type Nodes []node @@ -54,13 +61,15 @@ func (nodes Nodes) Shares() []uint64 { } type Config struct { - AltairForkEpoch *big.Int - MergeForkEpoch *big.Int - CapellaForkEpoch *big.Int - ValidatorCount *big.Int - KeyTranches *big.Int - SlotTime *big.Int - TerminalTotalDifficulty *big.Int + AltairForkEpoch *big.Int + MergeForkEpoch *big.Int + CapellaForkEpoch *big.Int + ValidatorCount *big.Int + KeyTranches *big.Int + SlotTime *big.Int + TerminalTotalDifficulty *big.Int + SafeSlotsToImportOptimistically *big.Int + ExtraShares *big.Int // Node configurations to launch. Each node as a proportional share of // validators. @@ -95,6 +104,8 @@ func (a *Config) join(b *Config) *Config { c.KeyTranches = choose(a.KeyTranches, b.KeyTranches) c.SlotTime = choose(a.SlotTime, b.SlotTime) c.TerminalTotalDifficulty = choose(a.TerminalTotalDifficulty, b.TerminalTotalDifficulty) + c.SafeSlotsToImportOptimistically = choose(a.SafeSlotsToImportOptimistically, b.SafeSlotsToImportOptimistically) + c.ExtraShares = choose(a.ExtraShares, b.ExtraShares) // EL config c.InitialBaseFeePerGas = choose(a.InitialBaseFeePerGas, b.InitialBaseFeePerGas) @@ -405,10 +416,10 @@ func generateInvalidPayloadSpoof(method string, basePayload *ExecutableDataV1, p var customPayloadMod *CustomPayloadData switch payloadField { case InvalidParentHash: - modParentHash := basePayload.ParentHash - modParentHash[common.HashLength-1] = byte(255 - modParentHash[common.HashLength-1]) + var randomParentHash common.Hash + rand.Read(randomParentHash[:]) customPayloadMod = &CustomPayloadData{ - ParentHash: &modParentHash, + ParentHash: &randomParentHash, } case InvalidStateRoot: modStateRoot := basePayload.StateRoot @@ -551,6 +562,108 @@ func forkchoiceResponseSpoof(method string, status PayloadStatusV1, payloadID *P }, nil } +// List of Hashes that can be accessed concurrently +type SyncHashes struct { + Hashes []common.Hash + Lock *sync.Mutex +} + +func NewSyncHashes(hashes ...common.Hash) *SyncHashes { + newSyncHashes := &SyncHashes{ + Hashes: make([]common.Hash, 0), + Lock: &sync.Mutex{}, + } + for _, h := range hashes { + newSyncHashes.Hashes = append(newSyncHashes.Hashes, h) + } + return newSyncHashes +} + +func (syncHashes *SyncHashes) Contains(hash common.Hash) bool { + syncHashes.Lock.Lock() + defer syncHashes.Lock.Unlock() + if syncHashes.Hashes == nil { + return false + } + for _, h := range syncHashes.Hashes { + if h == hash { + return true + } + } + return false +} + +func (syncHashes *SyncHashes) Add(hash common.Hash) { + syncHashes.Lock.Lock() + defer syncHashes.Lock.Unlock() + syncHashes.Hashes = append(syncHashes.Hashes, hash) +} + +// Generate a callback that invalidates either a call to `engine_forkchoiceUpdatedV1` or `engine_newPayloadV1` +// for all hashes with given exceptions, and a given LatestValidHash. +func InvalidateExecutionPayloads(method string, exceptions *SyncHashes, latestValidHash *common.Hash, invalidated chan<- common.Hash) func([]byte, []byte) *proxy.Spoof { + if method == EngineForkchoiceUpdatedV1 { + return func(res []byte, req []byte) *proxy.Spoof { + var ( + fcState ForkchoiceStateV1 + pAttr PayloadAttributesV1 + spoof *proxy.Spoof + err error + ) + err = UnmarshalFromJsonRPCRequest(req, &fcState, &pAttr) + if err != nil { + panic(err) + } + if !exceptions.Contains(fcState.HeadBlockHash) { + spoof, err = forkchoiceResponseSpoof(EngineForkchoiceUpdatedV1, PayloadStatusV1{ + Status: Invalid, + LatestValidHash: latestValidHash, + ValidationError: nil, + }, nil) + if err != nil { + panic(err) + } + select { + case invalidated <- fcState.HeadBlockHash: + default: + } + return spoof + } + return nil + } + } + if method == EngineNewPayloadV1 { + return func(res []byte, req []byte) *proxy.Spoof { + var ( + payload ExecutableDataV1 + spoof *proxy.Spoof + err error + ) + err = UnmarshalFromJsonRPCRequest(req, &payload) + if err != nil { + panic(err) + } + if !exceptions.Contains(payload.BlockHash) { + spoof, err = payloadStatusSpoof(EngineNewPayloadV1, &PayloadStatusV1{ + Status: Invalid, + LatestValidHash: latestValidHash, + ValidationError: nil, + }) + if err != nil { + panic(err) + } + select { + case invalidated <- payload.BlockHash: + default: + } + return spoof + } + return nil + } + } + panic(fmt.Errorf("ERROR: Invalid method to generate callback: %s", method)) +} + // Generates a callback that detects when a ForkchoiceUpdated with Payload Attributes fails. // Requires a lock in case two clients receive the fcU with payload attributes at the same time. // Requires chan(error) to return the final outcome of the callbacks. @@ -602,3 +715,68 @@ func combine(a, b *proxy.Spoof) *proxy.Spoof { } return a } + +// Try to approximate how much time until the merge based on current time, bellatrix fork epoch, +// TTD, execution clients' consensus mechanism, current total difficulty. +// This function is used to calculate timeouts, so it will always return a pessimistic value. +func SlotsUntilMerge(t *Testnet, c *Config) beacon.Slot { + l := make([]beacon.Slot, 0) + l = append(l, SlotsUntilBellatrix(t.genesisTime, t.spec)) + + for i, e := range t.eth1 { + l = append(l, beacon.Slot(TimeUntilTerminalBlock(e, c.Eth1Consensus, c.TerminalTotalDifficulty, c.Nodes[i])/uint64(t.spec.SECONDS_PER_SLOT))) + } + + // Return the worst case + var max = beacon.Slot(0) + for _, s := range l { + if s > max { + max = s + } + } + + fmt.Printf("INFO: Estimated slots until merge %d\n", max) + + // Add more slots give it some wiggle room + return max + 5 +} + +func SlotsUntilBellatrix(genesisTime beacon.Timestamp, spec *beacon.Spec) beacon.Slot { + currentTimestamp := beacon.Timestamp(time.Now().Unix()) + bellatrixTime, err := spec.TimeAtSlot(beacon.Slot(spec.BELLATRIX_FORK_EPOCH*beacon.Epoch(spec.SLOTS_PER_EPOCH)), genesisTime) + if err != nil { + panic(err) + } + if currentTimestamp >= bellatrixTime { + return beacon.Slot(0) + } + s := beacon.Slot((bellatrixTime-currentTimestamp)/spec.SECONDS_PER_SLOT) + 1 + fmt.Printf("INFO: bellatrixTime:%d, currentTimestamp:%d, slots=%d\n", bellatrixTime, currentTimestamp, s) + return s +} + +func TimeUntilTerminalBlock(e *Eth1Node, c setup.Eth1Consensus, defaultTTD *big.Int, n node) uint64 { + var ttd = defaultTTD + if n.ExecutionClientTTD != nil { + ttd = n.ExecutionClientTTD + } + // Get the current total difficulty for node + userRPCAddress, err := e.UserRPCAddress() + if err != nil { + panic(err) + } + client := &http.Client{} + rpcClient, _ := rpc.DialHTTPWithClient(userRPCAddress, client) + var tdh *TotalDifficultyHeader + if err := rpcClient.CallContext(context.Background(), &tdh, "eth_getBlockByNumber", "latest", false); err != nil { + panic(err) + } + td := tdh.TotalDifficulty.ToInt() + if td.Cmp(ttd) >= 0 { + // TTD already reached + return 0 + } + fmt.Printf("INFO: ttd:%d, td:%d, diffPerBlock:%d, secondsPerBlock:%d\n", ttd, td, c.DifficultyPerBlock(), c.SecondsPerBlock()) + td.Sub(ttd, td).Div(td, c.DifficultyPerBlock()).Mul(td, big.NewInt(int64(c.SecondsPerBlock()))) + return td.Uint64() +} diff --git a/simulators/eth2/engine/main.go b/simulators/eth2/engine/main.go index ac66046d23..08c21a1641 100644 --- a/simulators/eth2/engine/main.go +++ b/simulators/eth2/engine/main.go @@ -17,9 +17,10 @@ var ( VAULT_KEY, _ = crypto.HexToECDSA("63b508a03c3b5937ceb903af8b1b0c191012ef6eb7e9c3fb7afa94e5d214d376") ) -var tests = []testSpec{ +var engineTests = []testSpec{ //{Name: "transition-testnet", Run: TransitionTestnet}, {Name: "test-rpc-error", Run: TestRPCError}, + {Name: "block-latest-safe-finalized", Run: BlockLatestSafeFinalized}, {Name: "invalid-canonical-payload", Run: InvalidPayloadGen(2, Invalid)}, {Name: "invalid-payload-block-hash", Run: InvalidPayloadGen(2, InvalidBlockHash)}, {Name: "invalid-header-prevrandao", Run: IncorrectHeaderPrevRandaoPayload}, @@ -27,21 +28,24 @@ var tests = []testSpec{ {Name: "syncing-with-invalid-chain", Run: SyncingWithInvalidChain}, {Name: "basefee-encoding-check", Run: BaseFeeEncodingCheck}, {Name: "invalid-quantity-fields", Run: InvalidQuantityPayloadFields}, - +} +var transitionTests = []testSpec{ // Transition (TERMINAL_TOTAL_DIFFICULTY) tests {Name: "invalid-transition-payload", Run: InvalidPayloadGen(1, Invalid)}, + {Name: "unknown-pow-parent-transition-payload", Run: UnknownPoWParent}, {Name: "ttd-before-bellatrix", Run: TTDBeforeBellatrix}, {Name: "equal-timestamp-terminal-transition-block", Run: EqualTimestampTerminalTransitionBlock}, {Name: "invalid-terminal-block-payload-lower-ttd", Run: IncorrectTerminalBlockGen(-2)}, {Name: "invalid-terminal-block-payload-higher-ttd", Run: IncorrectTerminalBlockGen(1)}, + {Name: "build-atop-invalid-terminal-block", Run: IncorrectTTDConfigEL}, + {Name: "syncing-with-chain-having-valid-transition-block", Run: SyncingWithChainHavingValidTransitionBlock}, + {Name: "syncing-with-chain-having-invalid-transition-block", Run: SyncingWithChainHavingInvalidTransitionBlock}, + {Name: "syncing-with-chain-having-invalid-post-transition-block", Run: SyncingWithChainHavingInvalidPostTransitionBlock}, + {Name: "re-org-and-sync-with-chain-having-invalid-terminal-block", Run: ReOrgSyncWithChainHavingInvalidTerminalBlock}, } func main() { - // Create the test suite that will include all tests - var suite = hivesim.Suite{ - Name: "eth2-engine", - Description: `Collection of test vectors that use a ExecutionClient+BeaconNode+ValidatorClient testnet.`, - } + // Create simulator that runs all tests sim := hivesim.New() // From the simulator we can get all client types provided @@ -59,12 +63,27 @@ func main() { if len(c.Validator) != 1 { panic("choose 1 validator client type") } - // Add all tests to the suite and then run it - addAllTests(&suite, c) - hivesim.MustRunSuite(sim, suite) + // Create the test suites + var ( + engineSuite = hivesim.Suite{ + Name: "eth2-engine", + Description: `Collection of test vectors that use a ExecutionClient+BeaconNode+ValidatorClient testnet.`, + } + transitionSuite = hivesim.Suite{ + Name: "eth2-engine-transition", + Description: `Collection of test vectors that use a ExecutionClient+BeaconNode+ValidatorClient transition testnet.`, + } + ) + // Add all tests to the suites + addAllTests(&engineSuite, c, engineTests) + addAllTests(&transitionSuite, c, transitionTests) + + // Mark suites for execution + hivesim.MustRunSuite(sim, engineSuite) + hivesim.MustRunSuite(sim, transitionSuite) } -func addAllTests(suite *hivesim.Suite, c *ClientDefinitionsByRole) { +func addAllTests(suite *hivesim.Suite, c *ClientDefinitionsByRole, tests []testSpec) { mnemonic := "couple kiwi radio river setup fortune hunt grief buddy forward perfect empty slim wear bounce drift execute nation tobacco dutch chapter festival ice fog" // Generate validator keys to use for all tests. diff --git a/simulators/eth2/engine/nodes.go b/simulators/eth2/engine/nodes.go index f543748058..7fcf615a1d 100644 --- a/simulators/eth2/engine/nodes.go +++ b/simulators/eth2/engine/nodes.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "errors" "fmt" @@ -8,10 +9,14 @@ import ( "strings" "time" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/simulators/eth2/engine/setup" "github.com/protolambda/eth2api" + "github.com/protolambda/eth2api/client/beaconapi" "github.com/protolambda/eth2api/client/nodeapi" + "github.com/protolambda/zrnt/eth2/beacon/bellatrix" + "github.com/protolambda/zrnt/eth2/beacon/common" ) const ( @@ -51,12 +56,15 @@ func (en *Eth1Node) MustGetEnode() string { type BeaconNode struct { *hivesim.Client - API *eth2api.Eth2HttpClient + API *eth2api.Eth2HttpClient + genesisTime common.Timestamp + spec *common.Spec + index int } type BeaconNodes []*BeaconNode -func NewBeaconNode(cl *hivesim.Client) *BeaconNode { +func NewBeaconNode(cl *hivesim.Client, genesisTime common.Timestamp, spec *common.Spec, index int) *BeaconNode { return &BeaconNode{ Client: cl, API: ð2api.Eth2HttpClient{ @@ -64,6 +72,9 @@ func NewBeaconNode(cl *hivesim.Client) *BeaconNode { Cli: &http.Client{}, Codec: eth2api.JSONCodec{}, }, + genesisTime: genesisTime, + spec: spec, + index: index, } } @@ -98,6 +109,138 @@ func (beacons BeaconNodes) ENRs() (string, error) { return strings.Join(enrs, ","), nil } +func (b *BeaconNode) WaitForExecutionPayload(ctx context.Context, timeoutSlots common.Slot) (ethcommon.Hash, error) { + fmt.Printf("Waiting for execution payload on beacon %d\n", b.index) + slotDuration := time.Duration(b.spec.SECONDS_PER_SLOT) * time.Second + timer := time.NewTicker(slotDuration) + var timeout <-chan time.Time + if timeoutSlots > 0 { + timeout = time.After(time.Second * time.Duration(uint64(timeoutSlots)*uint64(b.spec.SECONDS_PER_SLOT))) + } else { + timeout = make(<-chan time.Time) + } + + for { + select { + case <-ctx.Done(): + return ethcommon.Hash{}, fmt.Errorf("context called") + case <-timeout: + return ethcommon.Hash{}, fmt.Errorf("Timeout") + case <-timer.C: + realTimeSlot := b.spec.TimeToSlot(common.Timestamp(time.Now().Unix()), b.genesisTime) + var headInfo eth2api.BeaconBlockHeaderAndInfo + if exists, err := beaconapi.BlockHeader(ctx, b.API, eth2api.BlockHead, &headInfo); err != nil { + return ethcommon.Hash{}, fmt.Errorf("WaitForExecutionPayload: failed to poll head: %v", err) + } else if !exists { + return ethcommon.Hash{}, fmt.Errorf("WaitForExecutionPayload: failed to poll head: !exists") + } + + var versionedBlock eth2api.VersionedSignedBeaconBlock + if exists, err := beaconapi.BlockV2(ctx, b.API, eth2api.BlockIdRoot(headInfo.Root), &versionedBlock); err != nil { + return ethcommon.Hash{}, fmt.Errorf("WaitForExecutionPayload: failed to retrieve block: %v", err) + } else if !exists { + return ethcommon.Hash{}, fmt.Errorf("WaitForExecutionPayload: block not found") + } + var execution ethcommon.Hash + if versionedBlock.Version == "bellatrix" { + block := versionedBlock.Data.(*bellatrix.SignedBeaconBlock) + copy(execution[:], block.Message.Body.ExecutionPayload.BlockHash[:]) + } + zero := ethcommon.Hash{} + fmt.Printf("beacon %d: slot=%d, realTimeSlot=%d, head=%s, exec=%s\n", b.index, headInfo.Header.Message.Slot, realTimeSlot, shorten(headInfo.Root.String()), shorten(execution.Hex())) + if bytes.Compare(execution[:], zero[:]) != 0 { + return execution, nil + } + } + } +} + +// +func (bn *BeaconNode) GetLatestExecutionBeaconBlock(ctx context.Context) (*bellatrix.SignedBeaconBlock, error) { + var headInfo eth2api.BeaconBlockHeaderAndInfo + if exists, err := beaconapi.BlockHeader(ctx, bn.API, eth2api.BlockHead, &headInfo); err != nil { + return nil, fmt.Errorf("failed to poll head: %v", err) + } else if !exists { + return nil, fmt.Errorf("no head block") + } + for slot := headInfo.Header.Message.Slot; slot > 0; slot-- { + var versionedBlock eth2api.VersionedSignedBeaconBlock + if exists, err := beaconapi.BlockV2(ctx, bn.API, eth2api.BlockIdSlot(slot), &versionedBlock); err != nil { + return nil, fmt.Errorf("failed to retrieve block: %v", err) + } else if !exists { + return nil, fmt.Errorf("block not found") + } + if versionedBlock.Version != "bellatrix" { + return nil, nil + } + block := versionedBlock.Data.(*bellatrix.SignedBeaconBlock) + payload := block.Message.Body.ExecutionPayload + if ethcommon.BytesToHash(payload.BlockHash[:]) != (ethcommon.Hash{}) { + return block, nil + } + } + return nil, nil +} + +func (bn *BeaconNode) GetFirstExecutionBeaconBlock(ctx context.Context) (*bellatrix.SignedBeaconBlock, error) { + lastSlot := bn.spec.TimeToSlot(common.Timestamp(time.Now().Unix()), bn.genesisTime) + for slot := common.Slot(0); slot <= lastSlot; slot++ { + var versionedBlock eth2api.VersionedSignedBeaconBlock + if exists, err := beaconapi.BlockV2(ctx, bn.API, eth2api.BlockIdSlot(slot), &versionedBlock); err != nil { + continue + } else if !exists { + continue + } + if versionedBlock.Version != "bellatrix" { + continue + } + block := versionedBlock.Data.(*bellatrix.SignedBeaconBlock) + payload := block.Message.Body.ExecutionPayload + if ethcommon.BytesToHash(payload.BlockHash[:]) != (ethcommon.Hash{}) { + return block, nil + } + } + return nil, nil +} + +func (bn *BeaconNode) GetBeaconBlockByExecutionHash(ctx context.Context, hash ethcommon.Hash) (*bellatrix.SignedBeaconBlock, error) { + var headInfo eth2api.BeaconBlockHeaderAndInfo + if exists, err := beaconapi.BlockHeader(ctx, bn.API, eth2api.BlockHead, &headInfo); err != nil { + return nil, fmt.Errorf("failed to poll head: %v", err) + } else if !exists { + return nil, fmt.Errorf("no head block") + } + + for slot := int(headInfo.Header.Message.Slot); slot > 0; slot -= 1 { + var versionedBlock eth2api.VersionedSignedBeaconBlock + if exists, err := beaconapi.BlockV2(ctx, bn.API, eth2api.BlockIdSlot(slot), &versionedBlock); err != nil { + continue + } else if !exists { + continue + } + if versionedBlock.Version != "bellatrix" { + // Block can't contain an executable payload, and we are not going to find it going backwards, so return. + return nil, nil + } + block := versionedBlock.Data.(*bellatrix.SignedBeaconBlock) + payload := block.Message.Body.ExecutionPayload + if bytes.Compare(payload.BlockHash[:], hash[:]) == 0 { + return block, nil + } + } + return nil, nil +} + +func (b BeaconNodes) GetBeaconBlockByExecutionHash(ctx context.Context, hash ethcommon.Hash) (*bellatrix.SignedBeaconBlock, error) { + for _, bn := range b { + block, err := bn.GetBeaconBlockByExecutionHash(ctx, hash) + if err != nil || block != nil { + return block, err + } + } + return nil, nil +} + type ValidatorClient struct { *hivesim.Client keys []*setup.KeyDetails diff --git a/simulators/eth2/engine/prepared_testnet.go b/simulators/eth2/engine/prepared_testnet.go index 18ba9a55b0..29240a524d 100644 --- a/simulators/eth2/engine/prepared_testnet.go +++ b/simulators/eth2/engine/prepared_testnet.go @@ -2,6 +2,7 @@ package main import ( "encoding/hex" + "encoding/json" "fmt" "math/big" "net" @@ -13,13 +14,17 @@ import ( blsu "github.com/protolambda/bls12-381-util" "github.com/protolambda/ztyp/view" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/simulators/eth2/engine/setup" "github.com/protolambda/zrnt/eth2/beacon/common" "github.com/protolambda/zrnt/eth2/configs" ) -var depositAddress common.Eth1Address +var ( + depositAddress common.Eth1Address + DEFAULT_SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY = big.NewInt(128) +) func init() { _ = depositAddress.UnmarshalText([]byte("0x4242424242424242424242424242424242424242")) @@ -66,6 +71,21 @@ func prepareTestnet(t *hivesim.T, env *testEnv, config *Config) *PreparedTestnet jwtSecret := hivesim.Params{"HIVE_JWTSECRET": "true"} executionOpts := hivesim.Bundle(eth1ConfigOpt, eth1Bundle, execNodeOpts, jwtSecret) + // Pre-generate PoW chains for clients that require it + for i := 0; i < len(config.Nodes); i++ { + if config.Nodes[i].ChainGenerator != nil { + config.Nodes[i].Chain, err = config.Nodes[i].ChainGenerator.Generate(eth1Genesis) + if err != nil { + t.Fatal(err) + } + fmt.Printf("Generated chain for node %d:\n", i+1) + for j, b := range config.Nodes[i].Chain { + js, _ := json.MarshalIndent(b.Header(), "", " ") + fmt.Printf("Block %d: %s\n", j, js) + } + } + } + // Generate beacon spec // // TODO: specify build-target based on preset, to run clients in mainnet or minimal mode. @@ -106,13 +126,25 @@ func prepareTestnet(t *hivesim.T, env *testEnv, config *Config) *PreparedTestnet spec.Config.TERMINAL_TOTAL_DIFFICULTY = view.Uint256View(*tdd) // Generate keys opts for validators - keyTranches := setup.KeyTranches(env.Keys, config.Nodes.Shares()) + shares := config.Nodes.Shares() + // ExtraShares defines an extra set of keys that none of the nodes will have. + // E.g. to produce an environment where none of the nodes has 50%+ of the keys. + if config.ExtraShares != nil { + shares = append(shares, config.ExtraShares.Uint64()) + } + keyTranches := setup.KeyTranches(env.Keys, shares) consensusConfigOpts, err := setup.ConsensusConfigsBundle(spec, eth1Genesis.Genesis, config.ValidatorCount.Uint64()) if err != nil { t.Fatal(err) } + var optimisticSync hivesim.Params + if config.SafeSlotsToImportOptimistically == nil { + config.SafeSlotsToImportOptimistically = DEFAULT_SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY + } + optimisticSync = optimisticSync.Set("HIVE_ETH2_SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY", fmt.Sprintf("%d", config.SafeSlotsToImportOptimistically)) + // prepare genesis beacon state, with all the validators in it. state, err := setup.BuildBeaconState(spec, eth1Genesis.Genesis, eth2GenesisTime, env.Keys) if err != nil { @@ -142,6 +174,7 @@ func prepareTestnet(t *hivesim.T, env *testEnv, config *Config) *PreparedTestnet }, stateOpt, consensusConfigOpts, + optimisticSync, ) validatorOpts := hivesim.Bundle( @@ -176,14 +209,12 @@ func (p *PreparedTestnet) createTestnet(t *hivesim.T) *Testnet { } } -func (p *PreparedTestnet) startEth1Node(testnet *Testnet, eth1Def *hivesim.ClientDefinition, consensus setup.Eth1Consensus, ttd *big.Int) { +func (p *PreparedTestnet) startEth1Node(testnet *Testnet, eth1Def *hivesim.ClientDefinition, consensus setup.Eth1Consensus, ttd *big.Int, chain []*types.Block) { testnet.t.Logf("Starting eth1 node: %s (%s)", eth1Def.Name, eth1Def.Version) opts := []hivesim.StartOption{p.executionOpts} - if len(testnet.eth1) == 0 { - // we only make the first eth1 node a miner - opts = append(opts, consensus.HiveParams()) - } else { + opts = append(opts, consensus.HiveParams(len(testnet.eth1))) + if len(testnet.eth1) > 0 { bootnode, err := testnet.eth1[0].EnodeURL() if err != nil { testnet.t.Fatalf("failed to get eth1 bootnode URL: %v", err) @@ -195,6 +226,14 @@ func (p *PreparedTestnet) startEth1Node(testnet *Testnet, eth1Def *hivesim.Clien if ttd != nil { opts = append(opts, hivesim.Params{"HIVE_TERMINAL_TOTAL_DIFFICULTY": fmt.Sprintf("%d", ttd.Int64())}) } + if chain != nil && len(chain) > 0 { + // Bundle the chain into the container + chainParam, err := setup.ChainBundle(chain) + if err != nil { + panic(err) + } + opts = append(opts, chainParam) + } en := &Eth1Node{testnet.t.StartClient(eth1Def.Name, opts...)} dest, _ := en.EngineRPCAddress() testnet.eth1 = append(testnet.eth1, en) @@ -256,7 +295,7 @@ func (p *PreparedTestnet) startBeaconNode(testnet *Testnet, beaconDef *hivesim.C //if p.configName != "mainnet" && hasBuildTarget(beaconDef, p.configName) { // opts = append(opts, hivesim.WithBuildTarget(p.configName)) //} - bn := NewBeaconNode(testnet.t.StartClient(beaconDef.Name, opts...)) + bn := NewBeaconNode(testnet.t.StartClient(beaconDef.Name, opts...), testnet.genesisTime, testnet.spec, len(testnet.beacons)) testnet.beacons = append(testnet.beacons, bn) } diff --git a/simulators/eth2/engine/running_testnet.go b/simulators/eth2/engine/running_testnet.go index 9370ff558e..dde00dcb0b 100644 --- a/simulators/eth2/engine/running_testnet.go +++ b/simulators/eth2/engine/running_testnet.go @@ -114,7 +114,7 @@ func startTestnet(t *hivesim.T, env *testEnv, config *Config) *Testnet { // for each key partition, we start a validator client with its own beacon node and eth1 node for i, node := range config.Nodes { - prep.startEth1Node(testnet, env.Clients.ClientByNameAndRole(node.ExecutionClient, "eth1"), config.Eth1Consensus, node.ExecutionClientTTD) + prep.startEth1Node(testnet, env.Clients.ClientByNameAndRole(node.ExecutionClient, "eth1"), config.Eth1Consensus, node.ExecutionClientTTD, node.Chain) if node.ConsensusClient != "" { prep.startBeaconNode(testnet, env.Clients.ClientByNameAndRole(fmt.Sprintf("%s-bn", node.ConsensusClient), "beacon"), node.BeaconNodeTTD, []int{i}) prep.startValidatorClient(testnet, env.Clients.ClientByNameAndRole(fmt.Sprintf("%s-vc", node.ConsensusClient), "validator"), i, i) @@ -135,6 +135,10 @@ func (t *Testnet) GenesisTime() time.Time { return time.Unix(int64(t.genesisTime), 0) } +func (t *Testnet) SlotsTimeout(slots common.Slot) <-chan time.Time { + return time.After(time.Duration(uint64(slots)*uint64(t.spec.SECONDS_PER_SLOT)) * time.Second) +} + func (t *Testnet) ValidatorClientIndex(pk [48]byte) (int, error) { for i, v := range t.validators { if v.ContainsKey(pk) { @@ -144,17 +148,25 @@ func (t *Testnet) ValidatorClientIndex(pk [48]byte) (int, error) { return 0, fmt.Errorf("key not found in any validator client") } -// WaitForFinality blocks until a beacon client reaches finality. -func (t *Testnet) WaitForFinality(ctx context.Context) (common.Checkpoint, error) { +// WaitForFinality blocks until a beacon client reaches finality, +// or timeoutSlots have passed, whichever happens first. +func (t *Testnet) WaitForFinality(ctx context.Context, timeoutSlots common.Slot) (common.Checkpoint, error) { genesis := t.GenesisTime() slotDuration := time.Duration(t.spec.SECONDS_PER_SLOT) * time.Second timer := time.NewTicker(slotDuration) done := make(chan common.Checkpoint, len(t.verificationBeacons())) - + var timeout <-chan time.Time + if timeoutSlots > 0 { + timeout = t.SlotsTimeout(timeoutSlots) + } else { + timeout = make(<-chan time.Time) + } for { select { case <-ctx.Done(): return common.Checkpoint{}, fmt.Errorf("context called") + case <-timeout: + return common.Checkpoint{}, fmt.Errorf("Timeout") case finalized := <-done: return finalized, nil case tim := <-timer.C: @@ -269,8 +281,9 @@ func (t *Testnet) WaitForFinality(ctx context.Context) (common.Checkpoint, error } } -// Waits for any execution payload to be available included in a beacon block (merge) -func (t *Testnet) WaitForExecutionPayload(ctx context.Context) (ethcommon.Hash, error) { +// Waits for any execution payload to be available included in a beacon block (merge), +// or timeoutSlots have passed, whichever happens first. +func (t *Testnet) WaitForExecutionPayload(ctx context.Context, timeoutSlots common.Slot) (ethcommon.Hash, error) { genesis := t.GenesisTime() slotDuration := time.Duration(t.spec.SECONDS_PER_SLOT) * time.Second timer := time.NewTicker(slotDuration) @@ -282,13 +295,20 @@ func (t *Testnet) WaitForExecutionPayload(ctx context.Context) (ethcommon.Hash, client := &http.Client{} rpcClient, _ := rpc.DialHTTPWithClient(userRPCAddress, client) ttdReached := false - + var timeout <-chan time.Time + if timeoutSlots > 0 { + timeout = t.SlotsTimeout(timeoutSlots) + } else { + timeout = make(<-chan time.Time) + } for { select { case <-ctx.Done(): return ethcommon.Hash{}, fmt.Errorf("context called") - case execHash := <-done: - return execHash, nil + case result := <-done: + return result, nil + case <-timeout: + return ethcommon.Hash{}, fmt.Errorf("Timeout") case tim := <-timer.C: // start polling after first slot of genesis if tim.Before(genesis.Add(slotDuration)) { @@ -399,32 +419,6 @@ func (t *Testnet) WaitForExecutionPayload(ctx context.Context) (ethcommon.Hash, } } -func (t *Testnet) GetBeaconBlockByExecutionHash(ctx context.Context, hash ethcommon.Hash) *bellatrix.SignedBeaconBlock { - lastSlot := t.spec.TimeToSlot(common.Timestamp(time.Now().Unix()), t.genesisTime) - for slot := int(lastSlot); slot > 0; slot -= 1 { - for _, bn := range t.verificationBeacons() { - var versionedBlock eth2api.VersionedSignedBeaconBlock - if exists, err := beaconapi.BlockV2(ctx, bn.API, eth2api.BlockIdSlot(slot), &versionedBlock); err != nil { - continue - } else if !exists { - continue - } - if versionedBlock.Version != "bellatrix" { - // Block can't contain an executable payload, and we are not going to find it going backwards, so return. - return nil - } - block := versionedBlock.Data.(*bellatrix.SignedBeaconBlock) - payload := block.Message.Body.ExecutionPayload - if bytes.Compare(payload.BlockHash[:], hash[:]) == 0 { - t.t.Logf("INFO: Execution block %v found in %d: %v", hash, block.Message.Slot, ethcommon.BytesToHash(payload.BlockHash[:])) - return block - } - } - - } - return nil -} - func getHealth(ctx context.Context, api *eth2api.Eth2HttpClient, spec *common.Spec, slot common.Slot) (float64, error) { var ( health float64 diff --git a/simulators/eth2/engine/scenarios.go b/simulators/eth2/engine/scenarios.go index c6014ac6e3..190fa9fe2a 100644 --- a/simulators/eth2/engine/scenarios.go +++ b/simulators/eth2/engine/scenarios.go @@ -11,11 +11,13 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + ethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/simulators/eth2/engine/setup" "github.com/protolambda/eth2api" "github.com/protolambda/eth2api/client/beaconapi" "github.com/protolambda/zrnt/eth2/beacon/bellatrix" + beacon "github.com/protolambda/zrnt/eth2/beacon/common" "github.com/rauljordan/engine-proxy/proxy" ) @@ -24,6 +26,8 @@ var ( DEFAULT_SLOT_TIME uint64 = 6 DEFAULT_TERMINAL_TOTAL_DIFFICULTY uint64 = 100 + EPOCHS_TO_FINALITY uint64 = 4 + // Default config used for all tests unless a client specific config exists DEFAULT_CONFIG = &Config{ ValidatorCount: big.NewInt(int64(DEFAULT_VALIDATOR_COUNT)), @@ -45,6 +49,11 @@ var ( "nimbus": true, "prysm": true, } + + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY_CLIENT_OVERRIDE = map[string]*big.Int{ + "teku": big.NewInt(128), + "lighthouse": big.NewInt(128), + } ) func getClientConfig(n node) *Config { @@ -66,8 +75,9 @@ func TransitionTestnet(t *hivesim.T, env *testEnv, n node) { testnet := startTestnet(t, env, config) defer testnet.stopTestnet() - ctx := context.Background() - finalized, err := testnet.WaitForFinality(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + finalized, err := testnet.WaitForFinality(ctx, testnet.spec.SLOTS_PER_EPOCH*beacon.Slot(EPOCHS_TO_FINALITY+1)) if err != nil { t.Fatalf("FAIL: Waiting for finality: %v", err) } @@ -93,8 +103,9 @@ func TestRPCError(t *hivesim.T, env *testEnv, n node) { testnet := startTestnet(t, env, config) defer testnet.stopTestnet() - ctx := context.Background() - finalized, err := testnet.WaitForFinality(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + finalized, err := testnet.WaitForFinality(ctx, testnet.spec.SLOTS_PER_EPOCH*beacon.Slot(EPOCHS_TO_FINALITY+1)) if err != nil { t.Fatalf("FAIL: Waiting for finality: %v", err) } @@ -126,6 +137,171 @@ func TestRPCError(t *hivesim.T, env *testEnv, n node) { } } +// Test `latest`, `safe`, `finalized` block labels on the post-merge testnet. +func BlockLatestSafeFinalized(t *hivesim.T, env *testEnv, n node) { + config := getClientConfig(n).join(&Config{ + Nodes: []node{ + n, + n, + }, + }) + + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + _, err := testnet.WaitForFinality(ctx, testnet.spec.SLOTS_PER_EPOCH*beacon.Slot(EPOCHS_TO_FINALITY+1)) + if err != nil { + t.Fatalf("FAIL: Waiting for finality: %v", err) + } + if err := VerifyELBlockLabels(testnet, ctx); err != nil { + t.Fatalf("FAIL: Verifying EL block labels: %v", err) + } +} + +// Generate a testnet where the transition payload contains an unknown PoW parent. +// Verify that the testnet can finalize after this. +func UnknownPoWParent(t *hivesim.T, env *testEnv, n node) { + config := getClientConfig(n).join(&Config{ + Nodes: Nodes{ + n, + n, + n, + }, + }) + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + var ( + getPayloadLock sync.Mutex + getPayloadCount int + invalidPayloadHash common.Hash + /* + invalidPayloadNewParent common.Hash + invalidPayloadOldParent common.Hash + */ + invalidPayloadNodeID int + ) + + // The EL mock will intercept an engine_getPayloadV1 call and set a random parent block in the response + getPayloadCallbackGen := func(node int) func([]byte, []byte) *proxy.Spoof { + return func(res []byte, req []byte) *proxy.Spoof { + getPayloadLock.Lock() + defer getPayloadLock.Unlock() + getPayloadCount++ + // Invalidate the transition payload + if getPayloadCount == 1 { + var ( + payload ExecutableDataV1 + spoof *proxy.Spoof + err error + ) + err = UnmarshalFromJsonRPCResponse(res, &payload) + if err != nil { + panic(err) + } + t.Logf("INFO (%v): Generating payload with unknown PoW parent: %s", t.TestID, res) + invalidPayloadHash, spoof, err = generateInvalidPayloadSpoof(EngineGetPayloadV1, &payload, InvalidParentHash) + if err != nil { + panic(err) + } + t.Logf("INFO (%v): Invalidated payload hash: %v", t.TestID, invalidPayloadHash) + invalidPayloadNodeID = node + return spoof + } + return nil + } + } + // The EL mock will intercept an engine_newPayloadV1 on the node that generated the invalid hash in order to validate it and broadcast it. + newPayloadCallbackGen := func(node int) func([]byte, []byte) *proxy.Spoof { + return func(res []byte, req []byte) *proxy.Spoof { + var ( + payload ExecutableDataV1 + spoof *proxy.Spoof + err error + ) + err = UnmarshalFromJsonRPCRequest(req, &payload) + if err != nil { + panic(err) + } + + // Validate the new payload in the node that produced it + if invalidPayloadNodeID == node && payload.BlockHash == invalidPayloadHash { + t.Logf("INFO (%v): Validating new payload: %s", t.TestID, payload.BlockHash) + + spoof, err = payloadStatusSpoof(EngineNewPayloadV1, &PayloadStatusV1{ + Status: Valid, + LatestValidHash: &payload.BlockHash, + ValidationError: nil, + }) + if err != nil { + panic(err) + } + return spoof + } + return nil + } + } + // The EL mock will intercept an engine_forkchoiceUpdatedV1 on the node that generated the invalid hash in order to validate it and broadcast it. + fcUCallbackGen := func(node int) func([]byte, []byte) *proxy.Spoof { + return func(res []byte, req []byte) *proxy.Spoof { + var ( + fcState ForkchoiceStateV1 + pAttr PayloadAttributesV1 + spoof *proxy.Spoof + err error + ) + err = UnmarshalFromJsonRPCRequest(req, &fcState, &pAttr) + if err != nil { + panic(err) + } + + // Validate the new payload in the node that produced it + if invalidPayloadNodeID == node && fcState.HeadBlockHash == invalidPayloadHash { + t.Logf("INFO (%v): Validating forkchoiceUpdated: %s", t.TestID, fcState.HeadBlockHash) + + spoof, err = forkchoiceResponseSpoof(EngineForkchoiceUpdatedV1, PayloadStatusV1{ + Status: Valid, + LatestValidHash: &fcState.HeadBlockHash, + ValidationError: nil, + }, nil) + if err != nil { + panic(err) + } + return spoof + } + return nil + } + } + for n, p := range testnet.proxies { + p.AddResponseCallback(EngineGetPayloadV1, getPayloadCallbackGen(n)) + p.AddResponseCallback(EngineNewPayloadV1, newPayloadCallbackGen(n)) + p.AddResponseCallback(EngineForkchoiceUpdatedV1, fcUCallbackGen(n)) + } + + // Network should recover from this + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + finalized, err := testnet.WaitForFinality(ctx, testnet.spec.SLOTS_PER_EPOCH*beacon.Slot(EPOCHS_TO_FINALITY+1)) + if err != nil { + t.Fatalf("FAIL: Waiting for finality: %v", err) + } + if err := VerifyParticipation(testnet, ctx, FirstSlotAfterCheckpoint{&finalized}, 0.95); err != nil { + t.Fatalf("FAIL: Verifying participation: %v", err) + } + if err := VerifyExecutionPayloadIsCanonical(testnet, ctx, LastSlotAtCheckpoint{&finalized}); err != nil { + t.Fatalf("FAIL: Verifying execution payload is canonical: %v", err) + } + if err := VerifyProposers(testnet, ctx, LastSlotAtCheckpoint{&finalized}, true); err != nil { + t.Fatalf("FAIL: Verifying proposers: %v", err) + } + if err := VerifyELHeads(testnet, ctx); err != nil { + t.Fatalf("FAIL: Verifying EL Heads: %v", err) + } + +} + // Generates a testnet case where one payload is invalidated in the recipient nodes. // invalidPayloadNumber: The number of the payload to invalidate -- 1 is transition payload, 2+ is any canonical chain payload. // invalidStatusResponse: The validation error response to inject in the recipient nodes. @@ -219,8 +395,9 @@ func InvalidPayloadGen(invalidPayloadNumber int, invalidStatusResponse PayloadSt p.AddResponseCallback(EngineNewPayloadV1, newPayloadCallbackGen(i)) } - ctx := context.Background() - _, err := testnet.WaitForExecutionPayload(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) if err != nil { t.Fatalf("FAIL: Waiting for execution payload: %v", err) } @@ -289,8 +466,9 @@ func IncorrectHeaderPrevRandaoPayload(t *hivesim.T, env *testEnv, n node) { p.AddResponseCallback(EngineGetPayloadV1, c) } - ctx := context.Background() - _, err := testnet.WaitForExecutionPayload(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) if err != nil { t.Fatalf("FAIL: Waiting for execution payload: %v", err) } @@ -396,7 +574,8 @@ func InvalidTimestampPayload(t *hivesim.T, env *testEnv, n node) { } // Verify beacon block with invalid payload is not accepted - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() b, err := VerifyExecutionPayloadHashInclusion(testnet, ctx, LastestSlotByHead{}, invalidPayloadHash) if err != nil { t.Fatalf("FAIL: Error during payload verification: %v", err) @@ -405,6 +584,47 @@ func InvalidTimestampPayload(t *hivesim.T, env *testEnv, n node) { } } +func IncorrectTTDConfigEL(t *hivesim.T, env *testEnv, n node) { + config := getClientConfig(n) + elTTD := config.TerminalTotalDifficulty.Int64() - 2 + config = config.join(&Config{ + Nodes: Nodes{ + node{ + // Add a node with an incorrect TTD to reject the invalid payload + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ExecutionClientTTD: big.NewInt(elTTD), + }, + }, + }) + + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + var ( + builder = testnet.beacons[0] + eth = testnet.eth1[0] + ec = NewEngineClient(t, eth, big.NewInt(elTTD)) + ) + + if !ec.waitForTTDWithTimeout(setup.CLIQUE_PERIOD_DEFAULT, time.After(time.Duration(setup.CLIQUE_PERIOD_DEFAULT*uint64(elTTD)*2)*time.Second)) { + t.Fatalf("FAIL: Bad TTD was never reached by the Execution Client") + } + // Wait a couple of slots + time.Sleep(time.Duration(config.SlotTime.Uint64()*5) * time.Second) + + // Try to get the latest execution payload, must be nil + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + b, err := builder.GetLatestExecutionBeaconBlock(ctx) + if err != nil { + t.Fatalf("FAIL: Unable to query for the latest execution payload: %v", err) + } + if b != nil { + t.Fatalf("FAIL: Execution payload was included in the beacon chain with a misconfigured TTD on the EL: %v", b.Message.StateRoot) + } +} + // The produced and broadcasted transition payload has parent with an invalid total difficulty. func IncorrectTerminalBlockGen(ttdDelta int64) func(t *hivesim.T, env *testEnv, n node) { return func(t *hivesim.T, env *testEnv, n node) { @@ -444,9 +664,14 @@ func IncorrectTerminalBlockGen(ttdDelta int64) func(t *hivesim.T, env *testEnv, testnet := startTestnet(t, env, config) defer testnet.stopTestnet() + var ( + badTTDImporter = testnet.beacons[3] + ) + // Wait for all execution clients with the correct TTD reach the merge - ctx := context.Background() - transitionPayloadHash, err := testnet.WaitForExecutionPayload(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + transitionPayloadHash, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) if err != nil { t.Fatalf("FAIL: Waiting for execution payload: %v", err) } @@ -468,8 +693,7 @@ func IncorrectTerminalBlockGen(ttdDelta int64) func(t *hivesim.T, env *testEnv, time.Sleep(time.Duration(5*config.SlotTime.Uint64()) * time.Second) // Transition payload should not be part of the beacon node with bad TTD - bn := testnet.beacons[len(testnet.beacons)-1] - b, err := VerifyExecutionPayloadHashInclusionNode(testnet, ctx, LastestSlotByHead{}, bn, transitionPayloadHash) + b, err := VerifyExecutionPayloadHashInclusionNode(testnet, ctx, LastestSlotByHead{}, badTTDImporter, transitionPayloadHash) if err != nil { t.Fatalf("FAIL: Error during payload verification: %v", err) } else if b != nil { @@ -481,20 +705,21 @@ func IncorrectTerminalBlockGen(ttdDelta int64) func(t *hivesim.T, env *testEnv, func SyncingWithInvalidChain(t *hivesim.T, env *testEnv, n node) { config := getClientConfig(n).join(&Config{ Nodes: Nodes{ - // First two nodes will do all the proposals + // Builder 1 node{ ExecutionClient: n.ExecutionClient, ConsensusClient: n.ConsensusClient, ValidatorShares: 1, TestVerificationNode: false, }, + // Builder 2 node{ ExecutionClient: n.ExecutionClient, ConsensusClient: n.ConsensusClient, ValidatorShares: 1, TestVerificationNode: false, }, - // Last node will receive invalidated payloads and verify + // Importer node{ ExecutionClient: n.ExecutionClient, ConsensusClient: n.ConsensusClient, @@ -619,12 +844,17 @@ func SyncingWithInvalidChain(t *hivesim.T, env *testEnv, n node) { return spoof } + var ( + importerProxy = testnet.proxies[2] + ) + // Add the callback to the last proxy which will not produce blocks - testnet.proxies[len(testnet.proxies)-1].AddResponseCallback(EngineNewPayloadV1, newPayloadCallback) - testnet.proxies[len(testnet.proxies)-1].AddResponseCallback(EngineForkchoiceUpdatedV1, forkchoiceUpdatedCallback) + importerProxy.AddResponseCallback(EngineNewPayloadV1, newPayloadCallback) + importerProxy.AddResponseCallback(EngineForkchoiceUpdatedV1, forkchoiceUpdatedCallback) <-done - ctx := context.Background() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // Wait a few slots for re-org to happen time.Sleep(time.Duration(testnet.spec.SECONDS_PER_SLOT) * time.Second * 5) @@ -678,8 +908,9 @@ func BaseFeeEncodingCheck(t *hivesim.T, env *testEnv, n node) { testnet := startTestnet(t, env, config) defer testnet.stopTestnet() - ctx := context.Background() - transitionPayloadHash, err := testnet.WaitForExecutionPayload(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + transitionPayloadHash, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) if err != nil { t.Fatalf("FAIL: Waiting for execution payload: %v", err) } @@ -755,11 +986,16 @@ func TTDBeforeBellatrix(t *hivesim.T, env *testEnv, n node) { testnet := startTestnet(t, env, config) defer testnet.stopTestnet() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration((config.MergeForkEpoch.Uint64()+1)*config.SlotTime.Uint64()*32)) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() - - _, err := testnet.WaitForExecutionPayload(ctx) + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) if err != nil { + for i, e := range testnet.eth1 { + ec := NewEngineClient(t, e, config.TerminalTotalDifficulty) + if b, err := ec.Eth.BlockByNumber(ec.Ctx(), nil); err == nil { + t.Logf("INFO: Last block on execution client %d: number=%d, hash=%s", i, b.NumberU64(), b.Hash()) + } + } t.Fatalf("FAIL: Waiting for execution payload: %v", err) } if err := VerifyELHeads(testnet, ctx); err != nil { @@ -780,8 +1016,9 @@ func InvalidQuantityPayloadFields(t *hivesim.T, env *testEnv, n node) { defer testnet.stopTestnet() // First we are going to wait for the transition to happen - ctx := context.Background() - _, err := testnet.WaitForExecutionPayload(ctx) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) if err != nil { t.Fatalf("FAIL: Waiting for execution payload: %v", err) } @@ -916,7 +1153,7 @@ func InvalidQuantityPayloadFields(t *hivesim.T, env *testEnv, n node) { var testFailed bool select { case <-done: - case <-time.After(time.Second * time.Duration(int(config.SlotTime.Int64())*len(allQuantityFields)*int(InvalidationTypeCount)*2)): + case <-testnet.SlotsTimeout(beacon.Slot(len(allQuantityFields) * int(InvalidationTypeCount) * 2)): t.Logf("FAIL: Timeout while waiting for CL requesting all payloads, test is invalid.") testFailed = true } @@ -938,3 +1175,490 @@ func InvalidQuantityPayloadFields(t *hivesim.T, env *testEnv, n node) { t.Logf("INFO: Success, none of the hashes were included") } } + +func SyncingWithChainHavingValidTransitionBlock(t *hivesim.T, env *testEnv, n node) { + var ( + safeSlotsToImportOptimistically = big.NewInt(16) + safeSlotsImportThreshold = uint64(4) + ) + if clientSafeSlots, ok := SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY_CLIENT_OVERRIDE[n.ConsensusClient]; ok { + safeSlotsToImportOptimistically = clientSafeSlots + } + + config := getClientConfig(n).join(&Config{ + Nodes: Nodes{ + // Builder + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 1, + ChainGenerator: &PoWChainGenerator{ + BlockCount: 1, + Config: PoWChainGeneratorDefaults, + }, + }, + // Importer + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 0, + ChainGenerator: &PoWChainGenerator{ + BlockCount: 1, + Config: PoWChainGeneratorDefaults, + }, + }, + }, + Eth1Consensus: setup.Eth1EthashConsensus{ + MiningNodes: 2, + }, + SafeSlotsToImportOptimistically: safeSlotsToImportOptimistically, + }) + + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + var ( + builder = testnet.beacons[0] + importer = testnet.beacons[1] + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Wait until the builder creates the first block with an execution payload + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on builder: %v", err) + } + builderExecutionBlock, err := builder.GetFirstExecutionBeaconBlock(ctx) + if err != nil || builderExecutionBlock == nil { + t.Fatalf("FAIL: Could not find first execution block") + } + t.Logf("Builder Execution block found on slot %d", builderExecutionBlock.Message.Slot) + + // Wait for the importer to get an execution payload + _, err = importer.WaitForExecutionPayload(ctx, beacon.Slot(safeSlotsToImportOptimistically.Uint64()+safeSlotsImportThreshold)) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on importer: %v", err) + } + + // Check the time at which the importer finally imported the block + importerSlot := testnet.spec.TimeToSlot(beacon.Timestamp(time.Now().Unix()), testnet.genesisTime) + + // Delta bewteen the first built execution block and the time when the importer + // finally imports the block must be at least SafeSlotsToImportOptimistically + diff := importerSlot - builderExecutionBlock.Message.Slot + if diff < beacon.Slot(safeSlotsToImportOptimistically.Uint64()) || diff > beacon.Slot(safeSlotsToImportOptimistically.Uint64()+safeSlotsImportThreshold) { + t.Fatalf("FAIL: Execution block imported outside of slot range: SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY=%d, ImporterSlot=%d, BuilderSlot=%d, Diff=%d", safeSlotsToImportOptimistically.Uint64(), importerSlot, builderExecutionBlock.Message.Slot, diff) + } + + // Wait for the importer to fully sync and then verify heads + maxTimeout := testnet.SlotsTimeout(5) +forloop: + for { + select { + case <-testnet.SlotsTimeout(1): + if err := VerifyELHeads(testnet, ctx); err == nil { + t.Logf("INFO: EL heads are in sync") + break forloop + } + case <-maxTimeout: + t.Fatalf("FAIL: Timeout waiting for EL Heads to sync up") + case <-ctx.Done(): + t.Fatalf("FAIL: Context done waiting for EL Heads to sync up") + } + } + +} + +func SyncingWithChainHavingInvalidTransitionBlock(t *hivesim.T, env *testEnv, n node) { + var ( + safeSlotsToImportOptimistically = big.NewInt(16) + safeSlotsImportThreshold = uint64(4) + ) + if clientSafeSlots, ok := SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY_CLIENT_OVERRIDE[n.ConsensusClient]; ok { + safeSlotsToImportOptimistically = clientSafeSlots + } + + config := getClientConfig(n).join(&Config{ + Nodes: Nodes{ + // Builder + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 1, + ChainGenerator: &PoWChainGenerator{ + BlockCount: 1, + Config: PoWChainGeneratorDefaults, + }, + }, + // Importer + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 0, + ChainGenerator: &PoWChainGenerator{ + BlockCount: 1, + Config: PoWChainGeneratorDefaults, + }, + }, + }, + Eth1Consensus: setup.Eth1EthashConsensus{ + MiningNodes: 2, + }, + SafeSlotsToImportOptimistically: safeSlotsToImportOptimistically, + }) + + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + var ( + builder = testnet.beacons[0] + importer = testnet.beacons[1] + importerProxy = testnet.proxies[1] + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Wait until the builder creates the first block with an execution payload + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on builder: %v", err) + } + builderExecutionBlock, err := builder.GetFirstExecutionBeaconBlock(ctx) + if err != nil || builderExecutionBlock == nil { + t.Fatalf("FAIL: Could not find first execution block") + } + transitionPayloadHash := common.BytesToHash(builderExecutionBlock.Message.Body.ExecutionPayload.BlockHash[:]) + t.Logf("Builder Execution block found on slot %d, hash=%s", builderExecutionBlock.Message.Slot, transitionPayloadHash) + + // The importer's execution client will invalidate all payloads including the transition payload + callbackCalled := make(chan common.Hash) + zeroHash := common.Hash{} + importerProxy.AddResponseCallback(EngineForkchoiceUpdatedV1, InvalidateExecutionPayloads(EngineForkchoiceUpdatedV1, NewSyncHashes(), &zeroHash, callbackCalled)) + + // Wait here until `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` slots have passed + safeSlotsTimeout := testnet.SlotsTimeout(beacon.Slot(safeSlotsToImportOptimistically.Uint64() + safeSlotsImportThreshold)) +forloop: + for { + select { + case invalidatedHash := <-callbackCalled: + t.Logf("INFO: Callback invalidated payload %v", invalidatedHash) + break forloop + case <-safeSlotsTimeout: + t.Fatalf("FAIL: Test timeout waiting for importer to optimistically sync the invalid payload") + case <-testnet.SlotsTimeout(1): + t.Logf("INFO: Waiting for importer to try to optimistically sync the invalid payload, realTimeSlot=%d", importer.spec.TimeToSlot(beacon.Timestamp(time.Now().Unix()), importer.genesisTime)) + case <-ctx.Done(): + t.Fatalf("FAIL: Context done while waiting for importer") + } + } + + // Wait a couple of slots here to make sure syncing does not produce a false positive + time.Sleep(time.Duration(config.SlotTime.Uint64()+5) * time.Second) + + // Query the beacon chain head of the importer node, it should still + // point to a pre-merge block. + var headInfo eth2api.BeaconBlockHeaderAndInfo + if exists, err := beaconapi.BlockHeader(ctx, importer.API, eth2api.BlockHead, &headInfo); err != nil { + t.Fatalf("FAIL: Failed to poll head importer head: %v", err) + } else if !exists { + t.Fatalf("FAIL: Failed to poll head importer head: !exists") + } + + if headInfo.Header.Message.Slot != (builderExecutionBlock.Message.Slot - 1) { + t.Fatalf("FAIL: Importer head is beyond the invalid execution payload block: importer=%v:%d, builder=%v:%d", headInfo.Root, headInfo.Header.Message.Slot, builderExecutionBlock.Message.StateRoot, builderExecutionBlock.Message.Slot) + } +} + +func SyncingWithChainHavingInvalidPostTransitionBlock(t *hivesim.T, env *testEnv, n node) { + var ( + safeSlotsToImportOptimistically = big.NewInt(16) + safeSlotsImportThreshold = uint64(4) + ) + if clientSafeSlots, ok := SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY_CLIENT_OVERRIDE[n.ConsensusClient]; ok { + safeSlotsToImportOptimistically = clientSafeSlots + } + + config := getClientConfig(n).join(&Config{ + Nodes: Nodes{ + // Builder + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 1, + ChainGenerator: &PoWChainGenerator{ + BlockCount: 1, + Config: PoWChainGeneratorDefaults, + }, + }, + // Importer + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 0, + ChainGenerator: &PoWChainGenerator{ + BlockCount: 1, + Config: PoWChainGeneratorDefaults, + }, + }, + }, + Eth1Consensus: setup.Eth1EthashConsensus{ + MiningNodes: 2, + }, + SafeSlotsToImportOptimistically: safeSlotsToImportOptimistically, + }) + + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + var ( + builder = testnet.beacons[0] + importer = testnet.beacons[1] + importerProxy = testnet.proxies[1] + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Wait until the builder creates the first block with an execution payload + _, err := testnet.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on builder: %v", err) + } + builderExecutionBlock, err := builder.GetFirstExecutionBeaconBlock(ctx) + if err != nil || builderExecutionBlock == nil { + t.Fatalf("FAIL: Could not find first execution block") + } + transitionPayloadHash := common.BytesToHash(builderExecutionBlock.Message.Body.ExecutionPayload.BlockHash[:]) + t.Logf("Builder Execution block found on slot %d, hash=%s", builderExecutionBlock.Message.Slot, transitionPayloadHash) + + // The importer's execution client will invalidate all payloads excluding the transition payload + callbackCalled := make(chan common.Hash) + exceptions := NewSyncHashes(transitionPayloadHash) + importerProxy.AddResponseCallback(EngineForkchoiceUpdatedV1, InvalidateExecutionPayloads(EngineForkchoiceUpdatedV1, exceptions, &transitionPayloadHash, callbackCalled)) + + // Wait here until `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` slots have passed + safeSlotsTimeout := testnet.SlotsTimeout(beacon.Slot(safeSlotsToImportOptimistically.Uint64() + safeSlotsImportThreshold)) +forloop: + for { + select { + case invalidatedHash := <-callbackCalled: + t.Logf("INFO: Callback invalidated payload %v", invalidatedHash) + break forloop + case <-safeSlotsTimeout: + t.Fatalf("FAIL: Test timeout waiting for importer to optimistically sync the invalid payload") + case <-testnet.SlotsTimeout(1): + t.Logf("INFO: Waiting for importer to try to optimistically sync the invalid payload, realTimeSlot=%d", importer.spec.TimeToSlot(beacon.Timestamp(time.Now().Unix()), importer.genesisTime)) + case <-ctx.Done(): + t.Fatalf("FAIL: Context done while waiting for importer") + } + } + + // Wait a couple of slots here to make sure syncing does not produce a false positive + time.Sleep(time.Duration(config.SlotTime.Uint64()+5) * time.Second) + + // Query the beacon chain head of the importer node, it should point to transition payload block. + block, err := importer.GetFirstExecutionBeaconBlock(ctx) + if err != nil || block == nil { + t.Fatalf("FAIL: Block not found: %v", err) + } + payload := block.Message.Body.ExecutionPayload + if ethcommon.BytesToHash(payload.BlockHash[:]) != transitionPayloadHash { + t.Fatalf("FAIL: Latest payload in the importer is not the transition payload: %v", ethcommon.BytesToHash(payload.BlockHash[:])) + } +} + +func ReOrgSyncWithChainHavingInvalidTerminalBlock(t *hivesim.T, env *testEnv, n node) { + var ( + safeSlotsToImportOptimistically = big.NewInt(16) + safeSlotsImportThreshold = uint64(2) + ) + if clientSafeSlots, ok := SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY_CLIENT_OVERRIDE[n.ConsensusClient]; ok { + safeSlotsToImportOptimistically = clientSafeSlots + } + + // We are going to produce two PoW chains for three different clients + // EL_A: + EL_A := &PoWChainGenerator{ // TD = 0x40000 + BlockCount: 2, + Config: PoWChainGeneratorDefaults, + } + // EL_B: + EL_B := &PoWChainGenerator{ // TD = 0x40000 + BlockCount: 2, + Config: PoWChainGeneratorDefaults, + } + + config := getClientConfig(n).join(&Config{ + Nodes: Nodes{ + // Valid Builder + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 10, + ChainGenerator: EL_A, + }, + // Invalid Builder + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 10, + ChainGenerator: EL_B, + }, + // Importer + node{ + ExecutionClient: n.ExecutionClient, + ConsensusClient: n.ConsensusClient, + ValidatorShares: 0, + ChainGenerator: EL_A, + }, + }, + Eth1Consensus: setup.Eth1EthashConsensus{ + MiningNodes: 2, + }, + TerminalTotalDifficulty: big.NewInt(0x40000), + SafeSlotsToImportOptimistically: safeSlotsToImportOptimistically, + // To ensure none of the nodes reaches 50% of the keys and make the test case more deterministic + ExtraShares: big.NewInt(1), + }) + + testnet := startTestnet(t, env, config) + defer testnet.stopTestnet() + + var ( + validBuilder = testnet.beacons[0] + validBuilderProxy = testnet.proxies[0] + invalidBuilder = testnet.beacons[1] + importer = testnet.beacons[2] + importerProxy = testnet.proxies[2] + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // Wait until the builders create their first blocks with an execution payload + _, err := invalidBuilder.WaitForExecutionPayload(ctx, SlotsUntilMerge(testnet, config)) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on invalid builder: %v", err) + } + b, err := invalidBuilder.GetFirstExecutionBeaconBlock(ctx) + if err != nil { + t.Fatalf("FAIL: Getting the first execution block invalid builder: %v", err) + } + if b == nil { + t.Fatalf("FAIL: Getting the first execution block invalid builder: %v", b) + } + invalidBuilderPayloadHash := ethcommon.BytesToHash(b.Message.Body.ExecutionPayload.BlockHash[:]) + fmt.Printf("INFO: First execution block on invalid builder: slot=%d, head=%s, exec=%s\n", b.Message.Slot, shorten(b.Message.StateRoot.String()), shorten(invalidBuilderPayloadHash.Hex())) + + _, err = validBuilder.WaitForExecutionPayload(ctx, 10) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on valid builder: %v", err) + } + b, err = validBuilder.GetFirstExecutionBeaconBlock(ctx) + if err != nil { + t.Fatalf("FAIL: Getting the first execution block on valid builder: %v", err) + } + if b == nil { + t.Fatalf("FAIL: Getting the first execution block on valid builder: %v", b) + } + validBuilderPayloadHash := ethcommon.BytesToHash(b.Message.Body.ExecutionPayload.BlockHash[:]) + fmt.Printf("INFO: First execution block on valid builder: slot=%d, head=%s, exec=%s\n", b.Message.Slot, shorten(b.Message.StateRoot.String()), shorten(invalidBuilderPayloadHash.Hex())) + + if invalidBuilderPayloadHash == validBuilderPayloadHash { + t.Fatalf("FAIL: Valid builder and invalid builder execution blocks are equal: %v == %v", validBuilderPayloadHash, invalidBuilderPayloadHash) + } + + _, err = importer.WaitForExecutionPayload(ctx, 10) + if err != nil { + t.Fatalf("FAIL: Waiting for execution payload on importer: %v", err) + } + b, err = importer.GetFirstExecutionBeaconBlock(ctx) + if err != nil { + t.Fatalf("FAIL: Getting the first execution block on importer: %v", err) + } + if b == nil { + t.Fatalf("FAIL: Getting the first execution block on importer: %v", b) + } + importerPayloadHash := ethcommon.BytesToHash(b.Message.Body.ExecutionPayload.BlockHash[:]) + if importerPayloadHash != validBuilderPayloadHash { + t.Fatalf("FAIL: Valid builder and importer execution blocks are the unequal: %v == %v", validBuilderPayloadHash, importerPayloadHash) + } + + // Payloads from the Invalid Builder need to be invalidated by the EL Mock + var ( + validPayloads = NewSyncHashes() + callbackCalled = make(chan common.Hash) + ) + + // From the valid builder we will get all generated payloads, and all of them + // will be exceptions to the list of payloads to invalidate. + getPayloadCallback := func(res []byte, req []byte) *proxy.Spoof { + // Invalidate the transition payload + var ( + payload ExecutableDataV1 + err error + ) + err = UnmarshalFromJsonRPCResponse(res, &payload) + if err != nil { + panic(err) + } + + // Payloads generated by the valid builder are whitelisted. + validPayloads.Add(payload.BlockHash) + t.Logf("INFO: Added hash to the list of exceptions: %s", payload.BlockHash) + return nil + } + validBuilderProxy.AddResponseCallback(EngineGetPayloadV1, getPayloadCallback) + + // Then we invalidate all the payloads not found in this list on the validBuilderProxy + // and the importer + validBuilderProxy.AddResponseCallback(EngineForkchoiceUpdatedV1, InvalidateExecutionPayloads(EngineForkchoiceUpdatedV1, validPayloads, &common.Hash{}, callbackCalled)) + validBuilderProxy.AddResponseCallback(EngineNewPayloadV1, InvalidateExecutionPayloads(EngineNewPayloadV1, validPayloads, &common.Hash{}, callbackCalled)) + + importerProxy.AddResponseCallback(EngineForkchoiceUpdatedV1, InvalidateExecutionPayloads(EngineForkchoiceUpdatedV1, validPayloads, &common.Hash{}, callbackCalled)) + importerProxy.AddResponseCallback(EngineNewPayloadV1, InvalidateExecutionPayloads(EngineNewPayloadV1, validPayloads, &common.Hash{}, callbackCalled)) + + // Keep log of the payloads received/invalidated + go func(ctx context.Context, c <-chan common.Hash) { + for { + select { + case h := <-c: + t.Logf("INFO: Invalidated payload: %s", h) + case <-ctx.Done(): + return + } + } + }(ctx, callbackCalled) + + // We need to wait until `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` pass, plus a couple more slots + safeSlotsTimeout := testnet.SlotsTimeout(beacon.Slot(safeSlotsToImportOptimistically.Uint64() + safeSlotsImportThreshold)) +forloop: + for { + select { + case <-safeSlotsTimeout: + break forloop + case <-testnet.SlotsTimeout(1): + // Keep checking that the valid builder does not re-org before time + b, err = validBuilder.GetBeaconBlockByExecutionHash(ctx, invalidBuilderPayloadHash) + if err != nil { + t.Fatalf("FAIL: Error checking re-org: %v", err) + } + if b != nil { + t.Fatalf("FAIL: Client re-org'd before `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY`", err) + } + case <-ctx.Done(): + t.Fatalf("FAIL: Context done while waiting for importer") + } + } + + // Check that the invalid payload hash was not incorporated into the valid builder or the importer. + b, err = BeaconNodes{ + importer, + validBuilder, + }.GetBeaconBlockByExecutionHash(ctx, invalidBuilderPayloadHash) + if err != nil { + t.Fatalf("FAIL: Error while searching for invalid beacon block: %v", err) + } + if b != nil { + t.Fatalf("FAIL: Invalid beacon block (incorrect TTD) was incorporated after optimistic sync: %v", b) + } +} diff --git a/simulators/eth2/engine/setup/eth1config.go b/simulators/eth2/engine/setup/eth1config.go index 89ae916e43..7472ce4cf4 100644 --- a/simulators/eth2/engine/setup/eth1config.go +++ b/simulators/eth2/engine/setup/eth1config.go @@ -63,11 +63,14 @@ var ( type Eth1Consensus interface { Configure(*Eth1Genesis) error - HiveParams() hivesim.Params + HiveParams(int) hivesim.Params + DifficultyPerBlock() *big.Int + SecondsPerBlock() uint64 } type Eth1EthashConsensus struct { MinerAddress string + MiningNodes int } func (c Eth1EthashConsensus) Configure(*Eth1Genesis) error { @@ -75,11 +78,28 @@ func (c Eth1EthashConsensus) Configure(*Eth1Genesis) error { return nil } -func (c Eth1EthashConsensus) HiveParams() hivesim.Params { +func (c Eth1EthashConsensus) HiveParams(node int) hivesim.Params { if c.MinerAddress == "" { c.MinerAddress = DEFAULT_ETHASH_MINER_ADDRESS } - return hivesim.Params{"HIVE_MINER": c.MinerAddress} + if c.MiningNodes == 0 { + // Default is that only one node is a miner + c.MiningNodes = 1 + } + if node < c.MiningNodes { + return hivesim.Params{"HIVE_MINER": c.MinerAddress} + } + return hivesim.Params{} +} + +func (c Eth1EthashConsensus) DifficultyPerBlock() *big.Int { + // Approximately 0x20000 + return big.NewInt(131072) +} + +func (c Eth1EthashConsensus) SecondsPerBlock() uint64 { + // It is really hard to approxmate this value + return 10 } type Eth1CliqueConsensus struct { @@ -100,7 +120,10 @@ func (c Eth1CliqueConsensus) Configure(genesis *Eth1Genesis) error { return nil } -func (c Eth1CliqueConsensus) HiveParams() hivesim.Params { +func (c Eth1CliqueConsensus) HiveParams(node int) hivesim.Params { + if node > 0 { + return hivesim.Params{} + } if c.PrivateKey == "" { c.PrivateKey = DEFAULT_CLIQUE_PRIVATE_KEY } @@ -113,6 +136,17 @@ func (c Eth1CliqueConsensus) HiveParams() hivesim.Params { } } +func (c Eth1CliqueConsensus) DifficultyPerBlock() *big.Int { + return big.NewInt(2) +} + +func (c Eth1CliqueConsensus) SecondsPerBlock() uint64 { + if c.CliquePeriod == 0 { + return CLIQUE_PERIOD_DEFAULT + } + return c.CliquePeriod +} + type Eth1Genesis struct { Genesis *core.Genesis DepositAddress common.Address @@ -144,7 +178,7 @@ func BuildEth1Genesis(ttd *big.Int, genesisTime uint64, consensus Eth1Consensus) BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), ArrowGlacierBlock: big.NewInt(0), - MergeForkBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), TerminalTotalDifficulty: ttd, Clique: nil, }, @@ -188,7 +222,7 @@ func (conf *Eth1Genesis) ToParams(depositAddress [20]byte) hivesim.Params { "HIVE_FORK_BERLIN": conf.Genesis.Config.BerlinBlock.String(), "HIVE_FORK_LONDON": conf.Genesis.Config.LondonBlock.String(), "HIVE_FORK_ARROWGLACIER": conf.Genesis.Config.ArrowGlacierBlock.String(), - "HIVE_MERGE_BLOCK_ID": conf.Genesis.Config.MergeForkBlock.String(), + "HIVE_MERGE_BLOCK_ID": conf.Genesis.Config.MergeNetsplitBlock.String(), "HIVE_TERMINAL_TOTAL_DIFFICULTY": conf.Genesis.Config.TerminalTotalDifficulty.String(), } if conf.Genesis.Config.Clique != nil { diff --git a/simulators/eth2/engine/setup/files.go b/simulators/eth2/engine/setup/files.go index 871217ba9e..814adf443b 100644 --- a/simulators/eth2/engine/setup/files.go +++ b/simulators/eth2/engine/setup/files.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/hive/hivesim" "github.com/protolambda/zrnt/eth2/beacon/common" "github.com/protolambda/ztyp/codec" @@ -67,6 +68,16 @@ func ConsensusConfigsBundle(spec *common.Spec, genesis *core.Genesis, valCount u ), nil } +func ChainBundle(chain []*types.Block) (hivesim.StartOption, error) { + var buf bytes.Buffer + for _, block := range chain { + if err := block.EncodeRLP(&buf); err != nil { + return nil, err + } + } + return hivesim.WithDynamicFile("/chain.rlp", bytesSource(buf.Bytes())), nil +} + func KeysBundle(keys []*KeyDetails) hivesim.StartOption { opts := make([]hivesim.StartOption, 0, len(keys)*2) for _, k := range keys { diff --git a/simulators/eth2/engine/verification.go b/simulators/eth2/engine/verification.go index 87be9654a4..487498cd55 100644 --- a/simulators/eth2/engine/verification.go +++ b/simulators/eth2/engine/verification.go @@ -5,16 +5,20 @@ import ( "context" "fmt" "math/big" + "net/http" "time" ethcommon "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" "github.com/protolambda/eth2api" "github.com/protolambda/eth2api/client/beaconapi" "github.com/protolambda/zrnt/eth2/beacon/altair" "github.com/protolambda/zrnt/eth2/beacon/bellatrix" "github.com/protolambda/zrnt/eth2/beacon/common" "github.com/protolambda/zrnt/eth2/beacon/phase0" + "github.com/protolambda/ztyp/tree" ) // Interface to specify on which slot the verification will be performed @@ -222,6 +226,75 @@ func VerifyProposers(t *Testnet, ctx context.Context, vs VerificationSlot, allow return nil } +func VerifyELBlockLabels(t *Testnet, ctx context.Context) error { + for i := 0; i < len(t.verificationExecution()); i++ { + el := t.verificationExecution()[i] + bn := t.verificationBeacons()[i] + // Get the head + var headInfo eth2api.BeaconBlockHeaderAndInfo + if exists, err := beaconapi.BlockHeader(ctx, bn.API, eth2api.BlockHead, &headInfo); err != nil { + return err + } else if !exists { + return fmt.Errorf("beacon %d: head info not found", i) + } + + // Get the checkpoints + var checkpoints eth2api.FinalityCheckpoints + if exists, err := beaconapi.FinalityCheckpoints(ctx, bn.API, eth2api.StateIdRoot(headInfo.Header.Message.StateRoot), &checkpoints); err != nil || !exists { + if exists, err = beaconapi.FinalityCheckpoints(ctx, bn.API, eth2api.StateIdSlot(headInfo.Header.Message.Slot), &checkpoints); err != nil { + return err + } else if !exists { + return fmt.Errorf("beacon %d: finality checkpoints not found", i) + } + } + blockLabels := map[string]tree.Root{ + "latest": headInfo.Root, + "finalized": checkpoints.Finalized.Root, + "safe": checkpoints.CurrentJustified.Root, + } + + for label, root := range blockLabels { + // Get the beacon block + var ( + versionedBlock eth2api.VersionedSignedBeaconBlock + expectedExec ethcommon.Hash + ) + if exists, err := beaconapi.BlockV2(ctx, bn.API, eth2api.BlockIdRoot(root), &versionedBlock); err != nil { + return err + } else if !exists { + return fmt.Errorf("beacon %d: beacon block to query %s not found", i, label) + } + switch versionedBlock.Version { + case "phase0": + expectedExec = ethcommon.Hash{} + case "altair": + expectedExec = ethcommon.Hash{} + case "bellatrix": + block := versionedBlock.Data.(*bellatrix.SignedBeaconBlock) + expectedExec = ethcommon.BytesToHash(block.Message.Body.ExecutionPayload.BlockHash[:]) + } + + // Get the el block and compare + rpcAddr, _ := el.UserRPCAddress() + rpcClient, _ := rpc.DialHTTPWithClient(rpcAddr, &http.Client{}) + var h types.Header + + if err := rpcClient.CallContext(ctx, &h, "eth_getBlockByNumber", label, false); err != nil { + if expectedExec != (ethcommon.Hash{}) { + return err + } + } else { + if h.Hash() != expectedExec { + return fmt.Errorf("beacon %d: Execution hash found in checkpoint block (%s) does not match what the el returns: %v != %v", i, label, expectedExec, h.Hash()) + } + fmt.Printf("beacon %d: Execution hash matches beacon checkpoint block (%s) information: %v\n", i, label, h.Hash()) + } + + } + } + return nil +} + func VerifyELHeads(t *Testnet, ctx context.Context) error { client := ethclient.NewClient(t.verificationExecution()[0].RPC()) head, err := client.HeaderByNumber(ctx, nil)