Skip to content

Commit

Permalink
test: Add light client attack to e2e tests (#1249)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
p-offtermatt committed Sep 8, 2023
1 parent 24b89ed commit 57b96ca
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 12 deletions.
93 changes: 84 additions & 9 deletions tests/e2e/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1639,20 +1639,20 @@ 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 {
valAddressString = tr.validatorConfigs[validator].consumerPrivValidatorKey
} else {
valAddressString = tr.validatorConfigs[validator].privValidatorKey
}
validatorAddress = tr.getValidatorKeyAddressFromString(valAddressString)
validatorPrivateKeyAddress = tr.getValidatorKeyAddressFromString(valAddressString)
}
return validatorAddress
return validatorPrivateKeyAddress
}

type unjailValidatorAction struct {
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
14 changes: 13 additions & 1 deletion tests/e2e/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)

Expand All @@ -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"},
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 17 additions & 2 deletions tests/e2e/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
130 changes: 130 additions & 0 deletions tests/e2e/steps_light_client_attack.go
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
},
}
}

0 comments on commit 57b96ca

Please sign in to comment.