Skip to content

Commit

Permalink
ci(e2e): add slashing testcase (#2544)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
corverroos and chmllr authored Nov 25, 2024
1 parent 63255d8 commit 79a3c9a
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 47 deletions.
182 changes: 182 additions & 0 deletions e2e/app/evidence.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
8 changes: 6 additions & 2 deletions e2e/app/gaspump.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
2 changes: 1 addition & 1 deletion e2e/app/portalregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
79 changes: 36 additions & 43 deletions e2e/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion e2e/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
}

Expand Down
1 change: 1 addition & 0 deletions e2e/manifests/ci.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 79a3c9a

Please sign in to comment.