From 79a3c9a9a9811e00e49f52d3bd4e049ee751e3dc Mon Sep 17 00:00:00 2001 From: corver <29249923+corverroos@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:32:56 +0200 Subject: [PATCH] ci(e2e): add slashing testcase (#2544) Add support for `manifest.evidence` that submits a double signing evidence and asserts the validator is slashed. Also speed up e2e by doing stuff in parralel. issue: #2490 Co-authored-by: Christian <649785+chmllr@users.noreply.github.com> --- e2e/app/evidence.go | 182 ++++++++++++++++++++++++++++++++++++++ e2e/app/gaspump.go | 8 +- e2e/app/portalregistry.go | 2 +- e2e/app/run.go | 79 ++++++++--------- e2e/cmd/cmd.go | 2 +- e2e/manifests/ci.toml | 1 + 6 files changed, 227 insertions(+), 47 deletions(-) create mode 100644 e2e/app/evidence.go diff --git a/e2e/app/evidence.go b/e2e/app/evidence.go new file mode 100644 index 000000000..3e560f050 --- /dev/null +++ b/e2e/app/evidence.go @@ -0,0 +1,182 @@ +package app + +import ( + "context" + "time" + + "github.com/omni-network/omni/e2e/types" + "github.com/omni-network/omni/lib/cchain/provider" + "github.com/omni-network/omni/lib/errors" + "github.com/omni-network/omni/lib/log" + + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/crypto/tmhash" + "github.com/cometbft/cometbft/evidence" + e2e "github.com/cometbft/cometbft/test/e2e/pkg" + cmttypes "github.com/cometbft/cometbft/types" +) + +// awaitSlashed returns nil when the provided validator is slashed. +func awaitSlashed(ctx context.Context, def Definition, valAddr crypto.Address) error { + client, err := def.Testnet.BroadcastNode().Client() + if err != nil { + return errors.Wrap(err, "broadcast client") + } + + cprov := provider.NewABCI(client, def.Testnet.Network) + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + for { + if err := ctx.Err(); err != nil { + return errors.Wrap(err, "timeout") + } + + infos, err := cprov.SDKSigningInfos(ctx) + if err != nil { + return errors.Wrap(err, "signing infos") + } + + for _, info := range infos { + if addr, err := info.ConsensusCmtAddr(); err != nil { + return errors.Wrap(err, "consensus address") + } else if addr.String() != valAddr.String() { + continue + } + + // Ensure jailed + if info.Jailed() { + log.Info(ctx, "Validator slashed", "address", valAddr) + return nil + } + } + } +} + +// injectEvidence takes a running testnet and generates an +// DuplicateVoteEvidence against the last validator and +// broadcasts it via the broadcast node rpc endpoint `/broadcast_evidence`. +// It returns the address of the validator that was slashed. +// +// This was copied from cometbft/test/e2e/runner/evidence.go. +func injectEvidence(ctx context.Context, testnet types.Testnet) (crypto.Address, error) { + chainID := testnet.Network.Static().OmniConsensusChainIDStr() + + client, err := testnet.BroadcastNode().Client() + if err != nil { + return nil, errors.Wrap(err, "client") + } + + // request the latest block and validator set from the node + blockRes, err := client.Block(ctx, nil) + if err != nil { + return nil, errors.Wrap(err, "block") + } + evidenceHeight := blockRes.Block.Height + waitHeight := blockRes.Block.Height + 3 + + nValidators := 100 + valRes, err := client.Validators(ctx, &evidenceHeight, nil, &nValidators) + if err != nil { + return nil, errors.Wrap(err, "validators") + } + + valSet, err := cmttypes.ValidatorSetFromExistingValidators(valRes.Validators) + if err != nil { + return nil, errors.Wrap(err, "valset") + } + + // Get the private keys of all the validators in the network + privVals := getPrivateValidatorKeys(testnet.Testnet) + + // Slash the last validator + valIdx := len(privVals) - 1 + dve, err := generateDuplicateVoteEvidence(privVals, valIdx, evidenceHeight, valSet, chainID, blockRes.Block.Time) + if err != nil { + return nil, err + } + + // Ensure it is valid + if err := evidence.VerifyDuplicateVote(dve, chainID, valSet); err != nil { + return nil, errors.Wrap(err, "verify evidence") + } + + // Wait for the node to reach the height above the forged height so that + // it is able to validate the evidence + _, err = waitForNode(ctx, testnet.BroadcastNode(), waitHeight, time.Minute) + if err != nil { + return nil, err + } + + _, err = client.BroadcastEvidence(ctx, dve) + if err != nil { + return nil, errors.Wrap(err, "broadcast evidence") + } + + log.Info(ctx, "Injected double signing evidence", "evidence_height", evidenceHeight, "submit_height", waitHeight) + + return privVals[valIdx].PrivKey.PubKey().Address(), nil +} + +func getPrivateValidatorKeys(testnet *e2e.Testnet) []cmttypes.MockPV { + var privVals []cmttypes.MockPV + for _, node := range testnet.Nodes { + if node.Mode == e2e.ModeValidator { + // Create mock private validators from the validators private key. MockPV is + // stateless which means we can double vote and do other funky stuff + privVals = append(privVals, cmttypes.NewMockPVWithParams(node.PrivvalKey, false, false)) + } + } + + return privVals +} + +// generateDuplicateVoteEvidence returns duplicate vote evidence against the valIdx validator. +// This was copied from cometbft/test/e2e/runner/evidence.go. +func generateDuplicateVoteEvidence( + privVals []cmttypes.MockPV, + valIdx int, + height int64, + vals *cmttypes.ValidatorSet, + chainID string, + time time.Time, +) (*cmttypes.DuplicateVoteEvidence, error) { + voteA, err := cmttypes.MakeVote(privVals[valIdx], chainID, int32(valIdx), height, 0, 2, makeRandomBlockID(), time) //nolint:gosec // Overflow not possible + if err != nil { + return nil, errors.Wrap(err, "make vote") + } + voteB, err := cmttypes.MakeVote(privVals[valIdx], chainID, int32(valIdx), height, 0, 2, makeRandomBlockID(), time) //nolint:gosec // Overflow not possible + if err != nil { + return nil, errors.Wrap(err, "make vote") + } + ev, err := cmttypes.NewDuplicateVoteEvidence(voteA, voteB, time, vals) + if err != nil { + return nil, errors.Wrap(err, "new evidence") + } + + return ev, nil +} + +// makeRandomBlockID was copied from cometbft/test/e2e/runner/evidence.go. +func makeRandomBlockID() cmttypes.BlockID { + return makeBlockID(crypto.CRandBytes(tmhash.Size), 100, crypto.CRandBytes(tmhash.Size)) +} + +// makeBlockID was copied from cometbft/test/e2e/runner/evidence.go. +func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) cmttypes.BlockID { + var ( + h = make([]byte, tmhash.Size) + psH = make([]byte, tmhash.Size) + ) + copy(h, hash) + copy(psH, partSetHash) + + return cmttypes.BlockID{ + Hash: h, + PartSetHeader: cmttypes.PartSetHeader{ + Total: partSetSize, + Hash: psH, + }, + } +} diff --git a/e2e/app/gaspump.go b/e2e/app/gaspump.go index de6f82b20..1e5dac28e 100644 --- a/e2e/app/gaspump.go +++ b/e2e/app/gaspump.go @@ -21,8 +21,12 @@ import ( "github.com/ethereum/go-ethereum/params" ) -// DeployGasApp deploys OmniGasPump and OmniGasStation contracts. -func DeployGasApp(ctx context.Context, def Definition) error { +// DeployEphemeralGasApp deploys OmniGasPump and OmniGasStation contracts to ephemeral networks. +func DeployEphemeralGasApp(ctx context.Context, def Definition) error { + if !def.Testnet.Network.IsEphemeral() { + return nil + } + if err := deployGasPumps(ctx, def); err != nil { return errors.Wrap(err, "deploy gas pumps") } diff --git a/e2e/app/portalregistry.go b/e2e/app/portalregistry.go index cd5224912..db5f58fd4 100644 --- a/e2e/app/portalregistry.go +++ b/e2e/app/portalregistry.go @@ -189,7 +189,7 @@ func startAddingMockPortals(ctx context.Context, def Definition) func() error { return } - ticker := time.NewTicker(time.Second) + ticker := time.NewTicker(time.Second * 10) defer ticker.Stop() chainID := uint64(999000) diff --git a/e2e/app/run.go b/e2e/app/run.go index a1038490f..d979ce05f 100644 --- a/e2e/app/run.go +++ b/e2e/app/run.go @@ -21,6 +21,8 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + + "golang.org/x/sync/errgroup" ) const ( @@ -83,49 +85,34 @@ func Deploy(ctx context.Context, def Definition, cfg DeployConfig) (*pingpong.XD contracts.UseStagingOmniRPC(def.Testnet.BroadcastOmniEVM().ExternalRPC) - if err := fundAnvil(ctx, def); err != nil { - return nil, err - } - - if err := deployAllCreate3(ctx, def); err != nil { - return nil, err + // Prep for deploying contracts. + var eg1 errgroup.Group + eg1.Go(func() error { return fundAnvil(ctx, def) }) + eg1.Go(func() error { return deployAllCreate3(ctx, def) }) + if err := eg1.Wait(); err != nil { + return nil, errors.Wrap(err, "deploy prep") } + // Deploy portals if err := def.Netman().DeployPortals(ctx, genesisValSetID, genesisVals); err != nil { - return nil, err - } - logRPCs(ctx, def) - - if err := initPortalRegistry(ctx, def); err != nil { - return nil, err - } - - if err := allowStagingValidators(ctx, def); err != nil { - return nil, err - } - - if def.Testnet.Network.IsEphemeral() { - if err := DeployGasApp(ctx, def); err != nil { - return nil, err - } + return nil, errors.Wrap(err, "deploy portals") } - if err := DeployBridge(ctx, def); err != nil { - return nil, errors.Wrap(err, "setup token bridge") - } - - if err := maybeSubmitNetworkUpgrade(ctx, def); err != nil { - return nil, err - } - - if err := FundValidatorsForTesting(ctx, def); err != nil { - return nil, err - } + logRPCs(ctx, def) + // Deploy other contracts (and other on-chain setup) + var eg2 errgroup.Group + eg2.Go(func() error { return initPortalRegistry(ctx, def) }) + eg2.Go(func() error { return allowStagingValidators(ctx, def) }) + eg2.Go(func() error { return DeployEphemeralGasApp(ctx, def) }) + eg2.Go(func() error { return DeployBridge(ctx, def) }) + eg2.Go(func() error { return maybeSubmitNetworkUpgrade(ctx, def) }) + eg2.Go(func() error { return FundValidatorsForTesting(ctx, def) }) if def.Manifest.DeploySolve { - if err := solve.DeployContracts(ctx, NetworkFromDef(def), def.Backends()); err != nil { - return nil, errors.Wrap(err, "deploy solve contracts") - } + eg2.Go(func() error { return solve.DeployContracts(ctx, NetworkFromDef(def), def.Backends()) }) + } + if err := eg2.Wait(); err != nil { + return nil, errors.Wrap(err, "deploy other contracts") } err = waitForSupportedChains(ctx, def) @@ -181,12 +168,11 @@ func E2ETest(ctx context.Context, def Definition, cfg E2ETestConfig) error { return err } - if err := testGasPumps(ctx, def); err != nil { - return errors.Wrap(err, "test gas app") - } - - if err := testBridge(ctx, def); err != nil { - return errors.Wrap(err, "test bridge") + var eg errgroup.Group + eg.Go(func() error { return testGasPumps(ctx, def) }) + eg.Go(func() error { return testBridge(ctx, def) }) + if err := eg.Wait(); err != nil { + return errors.Wrap(err, "test xdapps") } stopReceiptMonitor := StartMonitoringReceipts(ctx, def) @@ -213,7 +199,14 @@ func E2ETest(ctx context.Context, def Definition, cfg E2ETestConfig) error { } if def.Testnet.Evidence > 0 { - return errors.New("evidence injection not supported yet") + valAddr, err := injectEvidence(ctx, def.Testnet) + if err != nil { + return errors.Wrap(err, "inject evidence") + } + + if err := awaitSlashed(ctx, def, valAddr); err != nil { + return errors.Wrap(err, "await slashed") + } } // Wait for: diff --git a/e2e/cmd/cmd.go b/e2e/cmd/cmd.go index 5d282dc31..1e4ec1d3e 100644 --- a/e2e/cmd/cmd.go +++ b/e2e/cmd/cmd.go @@ -257,7 +257,7 @@ func newDeployGasAppCmd(def *app.Definition) *cobra.Command { return errors.New("only permanent networks") } - return app.DeployGasApp(cmd.Context(), *def) + return app.DeployEphemeralGasApp(cmd.Context(), *def) }, } diff --git a/e2e/manifests/ci.toml b/e2e/manifests/ci.toml index 16436264a..db58591cb 100644 --- a/e2e/manifests/ci.toml +++ b/e2e/manifests/ci.toml @@ -4,6 +4,7 @@ anvil_chains = ["mock_l2", "mock_l1"] multi_omni_evms = true network_upgrade_height = 15 pingpong_n = 5 # Increased ping pong to span validator updates +evidence = 1 # Slash a validator for double signing [node.validator01] [node.validator02]