Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add light client attack to e2e tests #1249

Merged
merged 6 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
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(
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we somehow get the information about cometmock methods into the interchain-security repo?

Maybe just keep a .md with all the available methods?

I'm trying to think how we could check what the invoked method actually does and need some advice on how to go about it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on this, an easily accessible doc outlining what's going on under the hood for each method would be nice

Copy link
Contributor Author

@p-offtermatt p-offtermatt Sep 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, though I am a bit worried about these things going out of date if they are not linked to anything. wdyt about just linking that part of the CometMock repo? It documents each method. I've added a link next to the CometMock flag, but not sure where else this would be good to have.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linking to the cometmock repo seems solid to me 👍

Might be helpful to have a methods.md file or similar where you can link each method used in this repo to the appropriate md section in the cometmock repo

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
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
// stepsSubmitEquivocationProposal will cause the double sign slash to be executed
p-offtermatt marked this conversation as resolved.
Show resolved Hide resolved
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,
},
},
},
},
}
}
Loading