From 57b96ca8d82052b34f49ec57d1160cffe54cb667 Mon Sep 17 00:00:00 2001 From: Philip Offtermatt <57488781+p-offtermatt@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:01:29 +0200 Subject: [PATCH] test: Add light client attack to e2e tests (#1249) * Add light client attack to e2e tests * Add details about the attack types * Rename validatorAddress to validatorPrivateKeyAddress * Add URL to CometMock to flag * Improve wording for light client attack run * Add submitting equivocation proposals and change consu->consumerName --- tests/e2e/actions.go | 93 ++++++++++++++++-- tests/e2e/main.go | 14 ++- tests/e2e/steps.go | 19 +++- tests/e2e/steps_light_client_attack.go | 130 +++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 tests/e2e/steps_light_client_attack.go diff --git a/tests/e2e/actions.go b/tests/e2e/actions.go index fccc27a957..c05b21bfef 100644 --- a/tests/e2e/actions.go +++ b/tests/e2e/actions.go @@ -1606,10 +1606,10 @@ func (tr TestRun) setValidatorDowntime(chain chainID, validator validatorID, dow if tr.useCometmock { // send set_signing_status either to down or up for validator - validatorAddress := tr.GetValidatorAddress(chain, validator) + validatorPrivateKeyAddress := tr.GetValidatorPrivateKeyAddress(chain, validator) method := "set_signing_status" - params := fmt.Sprintf(`{"private_key_address":"%s","status":"%s"}`, validatorAddress, lastArg) + params := fmt.Sprintf(`{"private_key_address":"%s","status":"%s"}`, validatorPrivateKeyAddress, lastArg) address := tr.getQueryNodeRPCAddress(chain) tr.curlJsonRPCRequest(method, params, address) @@ -1639,10 +1639,10 @@ func (tr TestRun) setValidatorDowntime(chain chainID, validator validatorID, dow } } -func (tr TestRun) GetValidatorAddress(chain chainID, validator validatorID) string { - var validatorAddress string +func (tr TestRun) GetValidatorPrivateKeyAddress(chain chainID, validator validatorID) string { + var validatorPrivateKeyAddress string if chain == chainID("provi") { - validatorAddress = tr.getValidatorKeyAddressFromString(tr.validatorConfigs[validator].privValidatorKey) + validatorPrivateKeyAddress = tr.getValidatorKeyAddressFromString(tr.validatorConfigs[validator].privValidatorKey) } else { var valAddressString string if tr.validatorConfigs[validator].useConsumerKey { @@ -1650,9 +1650,9 @@ func (tr TestRun) GetValidatorAddress(chain chainID, validator validatorID) stri } else { valAddressString = tr.validatorConfigs[validator].privValidatorKey } - validatorAddress = tr.getValidatorKeyAddressFromString(valAddressString) + validatorPrivateKeyAddress = tr.getValidatorKeyAddressFromString(valAddressString) } - return validatorAddress + return validatorPrivateKeyAddress } type unjailValidatorAction struct { @@ -1813,10 +1813,10 @@ func (tr TestRun) invokeDoublesignSlash( } tr.waitBlocks("provi", 10, 2*time.Minute) } else { // tr.useCometMock - validatorAddress := tr.GetValidatorAddress(action.chain, action.validator) + validatorPrivateKeyAddress := tr.GetValidatorPrivateKeyAddress(action.chain, action.validator) method := "cause_double_sign" - params := fmt.Sprintf(`{"private_key_address":"%s"}`, validatorAddress) + params := fmt.Sprintf(`{"private_key_address":"%s"}`, validatorPrivateKeyAddress) address := tr.getQueryNodeRPCAddress(action.chain) @@ -1826,6 +1826,81 @@ func (tr TestRun) invokeDoublesignSlash( } } +// Cause light client attack evidence for a certain validator to appear on the given chain. +// The evidence will look like the validator equivocated to a light client. +// See https://github.com/cometbft/cometbft/tree/main/spec/light-client/accountability +// for more information about light client attacks. +type lightClientEquivocationAttackAction struct { + validator validatorID + chain chainID +} + +func (tr TestRun) lightClientEquivocationAttack( + action lightClientEquivocationAttackAction, + verbose bool, +) { + tr.lightClientAttack(action.validator, action.chain, LightClientEquivocationAttack) +} + +// Cause light client attack evidence for a certain validator to appear on the given chain. +// The evidence will look like the validator tried to perform an amnesia attack. +// See https://github.com/cometbft/cometbft/tree/main/spec/light-client/accountability +// for more information about light client attacks. +type lightClientAmnesiaAttackAction struct { + validator validatorID + chain chainID +} + +func (tr TestRun) lightClientAmnesiaAttack( + action lightClientAmnesiaAttackAction, + verbose bool, +) { + tr.lightClientAttack(action.validator, action.chain, LightClientAmnesiaAttack) +} + +// Cause light client attack evidence for a certain validator to appear on the given chain. +// The evidence will look like the validator tried to perform a lunatic attack. +// See https://github.com/cometbft/cometbft/tree/main/spec/light-client/accountability +// for more information about light client attacks. +type lightClientLunaticAttackAction struct { + validator validatorID + chain chainID +} + +func (tr TestRun) lightClientLunaticAttack( + action lightClientLunaticAttackAction, + verbose bool, +) { + tr.lightClientAttack(action.validator, action.chain, LightClientLunaticAttack) +} + +type LightClientAttackType string + +const ( + LightClientEquivocationAttack LightClientAttackType = "Equivocation" + LightClientAmnesiaAttack LightClientAttackType = "Amnesia" + LightClientLunaticAttack LightClientAttackType = "Lunatic" +) + +func (tr TestRun) lightClientAttack( + validator validatorID, + chain chainID, + attackType LightClientAttackType, +) { + if !tr.useCometmock { + log.Fatal("light client attack is only supported with CometMock") + } + validatorPrivateKeyAddress := tr.GetValidatorPrivateKeyAddress(chain, validator) + + method := "cause_light_client_attack" + params := fmt.Sprintf(`{"private_key_address":"%s", "misbehaviour_type": "%s"}`, validatorPrivateKeyAddress, attackType) + + address := tr.getQueryNodeRPCAddress(chain) + + tr.curlJsonRPCRequest(method, params, address) + tr.waitBlocks(chain, 1, 10*time.Second) +} + type assignConsumerPubKeyAction struct { chain chainID validator validatorID diff --git a/tests/e2e/main.go b/tests/e2e/main.go index e9336422ae..1998e95a2e 100644 --- a/tests/e2e/main.go +++ b/tests/e2e/main.go @@ -39,7 +39,7 @@ var ( parallel = flag.Bool("parallel", false, "run all tests in parallel") localSdkPath = flag.String("local-sdk-path", "", "path of a local sdk version to build and reference in integration tests") - useCometmock = flag.Bool("use-cometmock", false, "use cometmock instead of CometBFT") + useCometmock = flag.Bool("use-cometmock", false, "use cometmock instead of CometBFT. see https://github.com/informalsystems/CometMock") useGorelayer = flag.Bool("use-gorelayer", false, "use go relayer instead of Hermes") ) @@ -56,6 +56,12 @@ var ( description: `This is like the happy path, but skips steps that involve starting or stopping nodes for the same chain outside of the chain setup or teardown. This is suited for CometMock+Gorelayer testing`, + }, + "light-client-attack": { + testRun: DefaultTestRun(), steps: lightClientAttackSteps, + description: `This is like the short happy path, but will slash validators for LightClientAttackEvidence instead of DuplicateVoteEvidence. +This is suited for CometMock+Gorelayer testing, but currently does not work with CometBFT, +since causing light client attacks is not implemented.`, }, "happy-path": {testRun: DefaultTestRun(), steps: happyPathSteps, description: "happy path tests"}, "changeover": {testRun: ChangeoverTestRun(), steps: changeoverSteps, description: "changeover tests"}, @@ -238,6 +244,12 @@ func (tr *TestRun) runStep(step Step, verbose bool) { tr.unjailValidator(action, verbose) case doublesignSlashAction: tr.invokeDoublesignSlash(action, verbose) + case lightClientAmnesiaAttackAction: + tr.lightClientAmnesiaAttack(action, verbose) + case lightClientEquivocationAttackAction: + tr.lightClientEquivocationAttack(action, verbose) + case lightClientLunaticAttackAction: + tr.lightClientLunaticAttack(action, verbose) case registerRepresentativeAction: tr.registerRepresentative(action, verbose) case assignConsumerPubKeyAction: diff --git a/tests/e2e/steps.go b/tests/e2e/steps.go index b33d19783a..6fb284c07a 100644 --- a/tests/e2e/steps.go +++ b/tests/e2e/steps.go @@ -39,9 +39,24 @@ var shortHappyPathSteps = concatSteps( stepsDowntime("consu"), stepsRejectEquivocationProposal("consu", 2), // prop to tombstone bob is rejected stepsDoubleSignOnProviderAndConsumer("consu"), // carol double signs on provider, bob double signs on consumer + stepsSubmitEquivocationProposal("consu", 2), // now prop to tombstone bob is submitted and accepted stepsStartRelayer(), - stepsConsumerRemovalPropNotPassing("consu", 2), // submit removal prop but vote no on it - chain should stay - stepsStopChain("consu", 3), // stop chain + stepsConsumerRemovalPropNotPassing("consu", 3), // submit removal prop but vote no on it - chain should stay + stepsStopChain("consu", 4), // stop chain +) + +var lightClientAttackSteps = concatSteps( + stepsStartChains([]string{"consu"}, false), + stepsDelegate("consu"), + stepsUnbond("consu"), + stepsRedelegateShort("consu"), + stepsDowntime("consu"), + stepsRejectEquivocationProposal("consu", 2), // prop to tombstone bob is rejected + stepsLightClientAttackOnProviderAndConsumer("consu"), // carol double signs on provider, bob double signs on consumer + stepsSubmitEquivocationProposal("consu", 2), // now prop to tombstone bob is submitted and accepted + stepsStartRelayer(), + stepsConsumerRemovalPropNotPassing("consu", 3), // submit removal prop but vote no on it - chain should stay + stepsStopChain("consu", 4), // stop chain ) var slashThrottleSteps = concatSteps( diff --git a/tests/e2e/steps_light_client_attack.go b/tests/e2e/steps_light_client_attack.go new file mode 100644 index 0000000000..f00d5f5bd6 --- /dev/null +++ b/tests/e2e/steps_light_client_attack.go @@ -0,0 +1,130 @@ +package main + +// Steps that make carol double sign on the provider, and bob double sign on a single consumer +func stepsLightClientAttackOnProviderAndConsumer(consumerName string) []Step { + return []Step{ + { + // provider double sign + action: lightClientEquivocationAttackAction{ + chain: chainID("provi"), + validator: validatorID("carol"), + }, + state: State{ + // slash on provider + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, // from 500 to 0 + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 495, // not tombstoned on consumerName yet + }, + }, + }, + }, + { + // relay power change to consumerName + action: relayPacketsAction{ + chainA: chainID("provi"), + chainB: chainID(consumerName), + port: "provider", + channel: 0, // consumerName channel + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, // tombstoning visible on consumerName + }, + }, + }, + }, + { + // consumer double sign + // provider will only log the double sign slash + // stepsSubmitEquivocationProposal will cause the double sign slash to be executed + action: lightClientEquivocationAttackAction{ + chain: chainID(consumerName), + validator: validatorID("bob"), + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + }, + }, + { + action: relayPacketsAction{ + chainA: chainID("provi"), + chainB: chainID(consumerName), + port: "provider", + channel: 0, + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + }, + }, + { + // consumer learns about the double sign + action: relayPacketsAction{ + chainA: chainID("provi"), + chainB: chainID(consumerName), + port: "provider", + channel: 0, + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + }, + }, + } +}