feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
[package]
|
||||
name = "pezkuwi-test-malus"
|
||||
description = "Misbehaving nodes for local testnets, system and Simnet tests."
|
||||
version = "1.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "malus"
|
||||
path = "src/malus.rs"
|
||||
|
||||
# Use artifact dependencies once stable.
|
||||
# See https://github.com/rust-lang/cargo/issues/9096.
|
||||
[[bin]]
|
||||
name = "pezkuwi-execute-worker"
|
||||
path = "../../src/bin/execute-worker.rs"
|
||||
# Prevent rustdoc error. Already documented from top-level Cargo.toml.
|
||||
doc = false
|
||||
[[bin]]
|
||||
name = "pezkuwi-prepare-worker"
|
||||
path = "../../src/bin/prepare-worker.rs"
|
||||
# Prevent rustdoc error. Already documented from top-level Cargo.toml.
|
||||
doc = false
|
||||
|
||||
[dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
clap = { features = ["derive"], workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
gum = { workspace = true, default-features = true }
|
||||
pezkuwi-cli = { features = [
|
||||
"malus",
|
||||
"pezkuwichain-native",
|
||||
"zagros-native",
|
||||
], workspace = true, default-features = true }
|
||||
pezkuwi-erasure-coding = { workspace = true, default-features = true }
|
||||
pezkuwi-node-network-protocol = { workspace = true, default-features = true }
|
||||
pezkuwi-node-primitives = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem-types = { workspace = true, default-features = true }
|
||||
pezkuwi-node-subsystem-util = { workspace = true, default-features = true }
|
||||
pezkuwi-primitives = { workspace = true, default-features = true }
|
||||
rand = { workspace = true, default-features = true }
|
||||
sp-core = { workspace = true, default-features = true }
|
||||
|
||||
# Required for worker binaries to build.
|
||||
pezkuwi-node-core-pvf-common = { workspace = true, default-features = true }
|
||||
pezkuwi-node-core-pvf-execute-worker = { workspace = true, default-features = true }
|
||||
pezkuwi-node-core-pvf-prepare-worker = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = { features = ["thread-pool"], workspace = true }
|
||||
pezkuwi-node-subsystem-test-helpers = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
substrate-build-script-utils = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
fast-runtime = ["pezkuwi-cli/fast-runtime"]
|
||||
runtime-benchmarks = [
|
||||
"gum/runtime-benchmarks",
|
||||
"pezkuwi-cli/runtime-benchmarks",
|
||||
"pezkuwi-erasure-coding/runtime-benchmarks",
|
||||
"pezkuwi-node-core-pvf-common/runtime-benchmarks",
|
||||
"pezkuwi-node-core-pvf-execute-worker/runtime-benchmarks",
|
||||
"pezkuwi-node-core-pvf-prepare-worker/runtime-benchmarks",
|
||||
"pezkuwi-node-network-protocol/runtime-benchmarks",
|
||||
"pezkuwi-node-primitives/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem-test-helpers/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem-types/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem-util/runtime-benchmarks",
|
||||
"pezkuwi-node-subsystem/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
# malus
|
||||
|
||||
Create nemesis nodes with alternate, at best faulty, at worst intentionally destructive behavior traits.
|
||||
|
||||
The first argument determines the behavior strain. The currently supported are:
|
||||
|
||||
* `suggest-garbage-candidate`
|
||||
* `back-garbage-candidate`
|
||||
* `dispute-ancestor`
|
||||
|
||||
## Integration test cases
|
||||
|
||||
To define integration tests create file
|
||||
in the toml format as used with [zombienet][zombienet]
|
||||
under `./integrationtests` describing the network to spawn and
|
||||
also the `zndsl` file (with `.zndsl` extension ) using the format
|
||||
defined in the [(DSL[(**D**omain **S**pecific **L**anguage)]) doc](https://paritytech.github.io/zombienet/cli/test-dsl-definition-spec.html).
|
||||
|
||||
## Usage
|
||||
|
||||
> Assumes you already gained permissions, ping in element `@javier:matrix.parity.io` to get access.
|
||||
> and you have cloned the [zombienet][zombienet] repo.
|
||||
|
||||
To launch a test case in the development cluster use (e.g. for the ./node/malus/integrationtests/0001-dispute-valid-block.toml):
|
||||
|
||||
```sh
|
||||
# declare the containers pulled in by zombie-net test definitions
|
||||
export MALUS_IMAGE=docker.io/paritypr/malus:4131-ccd09bbf
|
||||
export ZOMBIENET_INTEGRATION_TEST_IMAGE=docker.io/paritypr/synth-wave:4131-0.9.12-ccd09bbf-29a1ac18
|
||||
export COL_IMAGE=docker.io/paritypr/colander:4131-ccd09bbf
|
||||
|
||||
# login chore, once, with the values as provided in the above guide
|
||||
gcloud auth login
|
||||
gcloud config set project "parity-zombienet"
|
||||
gcloud container clusters get-credentials "parity-zombienet" --zone "europe-west3-b" --project parity-zombienet
|
||||
|
||||
# launching the actual test
|
||||
cd zombienet
|
||||
npm run build
|
||||
node dist/cli.js test <path to pezkuwi repo>/node/malus/integrationtests/0001-dispute-valid-block.zndsl
|
||||
|
||||
# Access logs (in google cloud storage)
|
||||
gsutil ls gs://zombienet-logs/zombie-<namespace uniqueId>/logs/
|
||||
```
|
||||
|
||||
This will also teardown the namespace after completion.
|
||||
|
||||
## Container Image Building Note
|
||||
|
||||
In order to build the container image you need to have the latest changes from
|
||||
PezkuwiChain and Substrate master branches.
|
||||
|
||||
```sh
|
||||
pwd # run this from the current dir
|
||||
podman build -t paritypr/malus:v1 -f Containerfile ../../..
|
||||
```
|
||||
|
||||
[zombienet]: https://github.com/paritytech/zombienet
|
||||
[gke]: (https://github.com/paritytech/gurke/blob/main/docs/How-to-setup-access-to-gke-k8s-cluster.md)
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
fn main() {
|
||||
substrate_build_script_utils::generate_cargo_keys();
|
||||
// For the node/worker version check, make sure we always rebuild the node and binary workers
|
||||
// when the version changes.
|
||||
substrate_build_script_utils::rerun_if_git_head_changed();
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#
|
||||
### Builder stage
|
||||
#
|
||||
|
||||
FROM rust as builder
|
||||
|
||||
WORKDIR /usr/src/polkadot-malus
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
ca-certificates \
|
||||
clang \
|
||||
curl \
|
||||
cmake \
|
||||
libssl1.1 \
|
||||
libssl-dev \
|
||||
pkg-config
|
||||
|
||||
RUN export PATH="$PATH:$HOME/.cargo/bin" && \
|
||||
rustup toolchain install nightly && \
|
||||
rustup target add wasm32-unknown-unknown --toolchain nightly && \
|
||||
rustup default stable
|
||||
|
||||
COPY polkadot/ /usr/src/polkadot-malus/polkadot/
|
||||
COPY substrate/ /usr/src/polkadot-malus/substrate/
|
||||
|
||||
WORKDIR /usr/src/polkadot-malus/polkadot
|
||||
|
||||
RUN cargo build -p polkadot-test-malus --release
|
||||
RUN cp -v /usr/src/polkadot-malus/polkadot/target/release/malus /usr/local/bin
|
||||
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/malus $VARIANT --version
|
||||
|
||||
#
|
||||
### Runtime
|
||||
#
|
||||
|
||||
FROM debian:buster-slim as runtime
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl tini
|
||||
|
||||
COPY --from=builder /usr/src/polkadot-malus/polkadot/target/release/malus /usr/local/bin
|
||||
# Non-root user for security purposes.
|
||||
#
|
||||
# UIDs below 10,000 are a security risk, as a container breakout could result
|
||||
# in the container being ran as a more privileged user on the host kernel with
|
||||
# the same UID.
|
||||
#
|
||||
# Static GID/UID is also useful for chown'ing files outside the container where
|
||||
# such a user does not exist.
|
||||
RUN groupadd --gid 10001 nonroot && \
|
||||
useradd --home-dir /home/nonroot \
|
||||
--create-home \
|
||||
--shell /bin/bash \
|
||||
--gid nonroot \
|
||||
--groups nonroot \
|
||||
--uid 10000 nonroot
|
||||
WORKDIR /home/nonroot/polkadot-malus
|
||||
|
||||
RUN chown -R nonroot. /home/nonroot
|
||||
|
||||
# Use the non-root user to run our application
|
||||
# Tell run test script that it runs in container
|
||||
USER nonroot
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/malus --version
|
||||
# Tini allows us to avoid several Docker edge cases, see https://github.com/krallin/tini.
|
||||
ENTRYPOINT ["tini", "--", "/usr/local/bin/malus"]
|
||||
|
||||
|
||||
|
||||
|
||||
FROM rust:1.54.0 as planner
|
||||
WORKDIR /usr/src/polkadot-malus
|
||||
# We only pay the installation cost once,
|
||||
# it will be cached from the second build onwards
|
||||
RUN cargo install cargo-chef
|
||||
COPY polkadot/ /usr/src/polkadot-malus/polkadot/
|
||||
COPY substrate/ /usr/src/polkadot-malus/substrate/
|
||||
WORKDIR /usr/src/polkadot-malus/polkadot
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
|
||||
FROM rust:1.54.0 as cacher
|
||||
WORKDIR /usr/src/polkadot-malus/polkadot
|
||||
RUN cargo install cargo-chef
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
ca-certificates \
|
||||
clang \
|
||||
curl \
|
||||
cmake \
|
||||
libssl1.1 \
|
||||
libssl-dev \
|
||||
pkg-config
|
||||
RUN export PATH="$PATH:$HOME/.cargo/bin" && \
|
||||
rustup toolchain install nightly && \
|
||||
rustup target add wasm32-unknown-unknown --toolchain nightly && \
|
||||
rustup default stable
|
||||
COPY --from=planner /usr/src/polkadot-malus/polkadot/recipe.json recipe.json
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
|
||||
FROM rust:1.54.0 as builder
|
||||
WORKDIR /usr/src/polkadot-malus
|
||||
COPY polkadot/ /usr/src/polkadot-malus/polkadot/
|
||||
COPY substrate/ /usr/src/polkadot-malus/substrate/
|
||||
# Copy over the cached dependencies
|
||||
WORKDIR /usr/src/polkadot-malus/polkadot
|
||||
COPY --from=cacher /usr/src/polkadot-malus/polkadot/target target
|
||||
COPY --from=cacher $CARGO_HOME $CARGO_HOME
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
ca-certificates \
|
||||
clang \
|
||||
curl \
|
||||
cmake \
|
||||
libssl1.1 \
|
||||
libssl-dev \
|
||||
pkg-config
|
||||
RUN export PATH="$PATH:$HOME/.cargo/bin" && \
|
||||
rustup toolchain install nightly && \
|
||||
rustup target add wasm32-unknown-unknown --toolchain nightly && \
|
||||
rustup default stable
|
||||
RUN cargo build -p polkadot-test-malus --release
|
||||
|
||||
|
||||
FROM debian:buster-slim as runtime
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl tini
|
||||
COPY --from=builder /usr/src/polkadot-malus/polkadot/target/release/malus /usr/local/bin
|
||||
# Non-root user for security purposes.
|
||||
#
|
||||
# UIDs below 10,000 are a security risk, as a container breakout could result
|
||||
# in the container being ran as a more privileged user on the host kernel with
|
||||
# the same UID.
|
||||
#
|
||||
# Static GID/UID is also useful for chown'ing files outside the container where
|
||||
# such a user does not exist.
|
||||
RUN groupadd --gid 10001 nonroot && \
|
||||
useradd --home-dir /home/nonroot \
|
||||
--create-home \
|
||||
--shell /bin/bash \
|
||||
--gid nonroot \
|
||||
--groups nonroot \
|
||||
--uid 10000 nonroot
|
||||
WORKDIR /home/nonroot/polkadot-malus
|
||||
RUN chown -R nonroot. /home/nonroot
|
||||
# Use the non-root user to run our application
|
||||
# Tell run test script that it runs in container
|
||||
USER nonroot
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/malus --version
|
||||
# Tini allows us to avoid several Docker edge cases, see https://github.com/krallin/tini.
|
||||
ENTRYPOINT ["/usr/local/bin/malus"]
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
podman build -t paritypr/malus:v1 -f Containerfile ../../../..
|
||||
@@ -0,0 +1,66 @@
|
||||
#
|
||||
### Builder stage
|
||||
#
|
||||
|
||||
FROM rust as builder
|
||||
|
||||
WORKDIR /usr/src/polkadot-malus
|
||||
COPY polkadot/ /usr/src/polkadot-malus/polkadot/
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
ca-certificates \
|
||||
clang \
|
||||
curl \
|
||||
cmake \
|
||||
libssl1.1 \
|
||||
libssl-dev \
|
||||
pkg-config
|
||||
|
||||
RUN export PATH="$PATH:$HOME/.cargo/bin" && \
|
||||
rustup toolchain install nightly && \
|
||||
rustup target add wasm32-unknown-unknown --toolchain nightly && \
|
||||
rustup default stable
|
||||
|
||||
|
||||
WORKDIR /usr/src/polkadot-malus/polkadot
|
||||
|
||||
RUN cargo build -p polkadot-test-malus --release --verbose
|
||||
RUN cp -v /usr/src/polkadot-malus/polkadot/target/release/malus /usr/local/bin
|
||||
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/malus --version
|
||||
|
||||
#
|
||||
### Runtime
|
||||
#
|
||||
|
||||
FROM debian:buster-slim as runtime
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl tini
|
||||
|
||||
COPY --from=builder /usr/src/polkadot-malus/polkadot/target/release/malus /usr/local/bin
|
||||
# Non-root user for security purposes.
|
||||
#
|
||||
# UIDs below 10,000 are a security risk, as a container breakout could result
|
||||
# in the container being ran as a more privileged user on the host kernel with
|
||||
# the same UID.
|
||||
#
|
||||
# Static GID/UID is also useful for chown'ing files outside the container where
|
||||
# such a user does not exist.
|
||||
RUN groupadd --gid 10001 nonroot && \
|
||||
useradd --home-dir /home/nonroot \
|
||||
--create-home \
|
||||
--shell /bin/bash \
|
||||
--gid nonroot \
|
||||
--groups nonroot \
|
||||
--uid 10000 nonroot
|
||||
WORKDIR /home/nonroot/polkadot-malus
|
||||
|
||||
RUN chown -R nonroot. /home/nonroot
|
||||
|
||||
# Use the non-root user to run our application
|
||||
USER nonroot
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/malus --version
|
||||
# Tini allows us to avoid several Docker edge cases, see https://github.com/krallin/tini.
|
||||
ENTRYPOINT ["tini", "--", "/usr/local/bin/malus"]
|
||||
@@ -0,0 +1,66 @@
|
||||
#
|
||||
### Builder stage
|
||||
#
|
||||
|
||||
FROM rust as builder
|
||||
|
||||
WORKDIR /usr/src/polkadot
|
||||
COPY polkadot/ /usr/src/polkadot
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
ca-certificates \
|
||||
clang \
|
||||
curl \
|
||||
cmake \
|
||||
libssl1.1 \
|
||||
libssl-dev \
|
||||
pkg-config
|
||||
|
||||
RUN export PATH="$PATH:$HOME/.cargo/bin" && \
|
||||
rustup toolchain install nightly && \
|
||||
rustup target add wasm32-unknown-unknown --toolchain nightly && \
|
||||
rustup default stable
|
||||
|
||||
|
||||
WORKDIR /usr/src/polkadot
|
||||
|
||||
RUN cargo build --release --bin polkadot --features disputes --verbose
|
||||
RUN cp -v /usr/src/polkadot/target/release/polkadot /usr/local/bin
|
||||
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/polkadot --version
|
||||
|
||||
#
|
||||
### Runtime
|
||||
#
|
||||
|
||||
FROM debian:buster-slim as runtime
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl tini
|
||||
|
||||
COPY --from=builder /usr/src/polkadot/target/release/polkadot /usr/local/bin
|
||||
# Non-root user for security purposes.
|
||||
#
|
||||
# UIDs below 10,000 are a security risk, as a container breakout could result
|
||||
# in the container being ran as a more privileged user on the host kernel with
|
||||
# the same UID.
|
||||
#
|
||||
# Static GID/UID is also useful for chown'ing files outside the container where
|
||||
# such a user does not exist.
|
||||
RUN groupadd --gid 10001 nonroot && \
|
||||
useradd --home-dir /home/nonroot \
|
||||
--create-home \
|
||||
--shell /bin/bash \
|
||||
--gid nonroot \
|
||||
--groups nonroot \
|
||||
--uid 10000 nonroot
|
||||
WORKDIR /home/nonroot/polkadot
|
||||
|
||||
RUN chown -R nonroot. /home/nonroot
|
||||
|
||||
# Use the non-root user to run our application
|
||||
USER nonroot
|
||||
# check if executable works in this container
|
||||
RUN /usr/local/bin/polkadot --version
|
||||
# Tini allows us to avoid several Docker edge cases, see https://github.com/krallin/tini.
|
||||
ENTRYPOINT ["tini", "--", "/usr/local/bin/polkadot"]
|
||||
@@ -0,0 +1,42 @@
|
||||
[settings]
|
||||
timeout = 1000
|
||||
|
||||
[relaychain.genesis.runtimeGenesis.patch.configuration.config.scheduler_params]
|
||||
max_validators_per_core = 1
|
||||
|
||||
[relaychain]
|
||||
default_image = "{{ZOMBIENET_INTEGRATION_TEST_IMAGE}}"
|
||||
chain = "zagros-local"
|
||||
command = "pezkuwi"
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "alice"
|
||||
validator = true
|
||||
extra_args = ["--alice", "-lteyrchain=debug"]
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "bob"
|
||||
validator = true
|
||||
extra_args = ["--bob", "-lteyrchain=debug"]
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "charlie"
|
||||
validator = true
|
||||
extra_args = ["--charlie", "-lteyrchain=debug"]
|
||||
|
||||
[[relaychain.nodes]]
|
||||
name = "dave"
|
||||
validator = true
|
||||
command = "malus dispute-ancestor"
|
||||
extra_args = ["--dave", "-lteyrchain=debug"]
|
||||
image = "{{MALUS_IMAGE}}"
|
||||
autoConnectApi = false
|
||||
|
||||
[[teyrchains]]
|
||||
id = 100
|
||||
|
||||
[teyrchains.collator]
|
||||
name = "collator01"
|
||||
image = "{{COL_IMAGE}}"
|
||||
command = "adder-collator"
|
||||
args = ["-lteyrchain=debug"]
|
||||
@@ -0,0 +1,29 @@
|
||||
Description: Disputes
|
||||
Network: ./0001-dispute-valid-block.toml
|
||||
Creds: config
|
||||
|
||||
|
||||
alice: is up
|
||||
bob: is up
|
||||
charlie: is up
|
||||
dave: is up
|
||||
alice: reports node_roles is 4
|
||||
bob: reports node_roles is 4
|
||||
alice: reports sub_libp2p_is_major_syncing is 0
|
||||
alice: reports block height is at least 2 within 15 seconds
|
||||
alice: reports peers count is at least 2
|
||||
bob: reports block height is at least 2
|
||||
bob: reports peers count is at least 2
|
||||
charlie: reports block height is at least 2
|
||||
charlie: reports peers count is at least 2
|
||||
alice: reports polkadot_teyrchain_candidate_disputes_total is at least 1 within 250 seconds
|
||||
bob: reports polkadot_teyrchain_candidate_disputes_total is at least 1 within 90 seconds
|
||||
charlie: reports polkadot_teyrchain_candidate_disputes_total is at least 1 within 90 seconds
|
||||
alice: reports polkadot_teyrchain_candidate_dispute_votes{validity="valid"} is at least 1 within 90 seconds
|
||||
bob: reports polkadot_teyrchain_candidate_dispute_votes{validity="valid"} is at least 2 within 90 seconds
|
||||
charlie: reports polkadot_teyrchain_candidate_dispute_votes{validity="valid"} is at least 2 within 90 seconds
|
||||
alice: reports polkadot_teyrchain_candidate_dispute_concluded{validity="valid"} is at least 1 within 90 seconds
|
||||
alice: reports polkadot_teyrchain_candidate_dispute_concluded{validity="invalid"} is 0 within 90 seconds
|
||||
bob: reports polkadot_teyrchain_candidate_dispute_concluded{validity="valid"} is at least 1 within 90 seconds
|
||||
charlie: reports polkadot_teyrchain_candidate_dispute_concluded{validity="valid"} is at least 1 within 90 seconds
|
||||
charlie: reports polkadot_teyrchain_candidate_dispute_concluded{validity="valid"} is at least 1 within 90 seconds
|
||||
@@ -0,0 +1,333 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A small set of wrapping types to cover most of our adversary test cases.
|
||||
//!
|
||||
//! This allows types with internal mutability to synchronize across
|
||||
//! multiple subsystems and intercept or replace incoming and outgoing
|
||||
//! messages on the overseer level.
|
||||
|
||||
use pezkuwi_node_subsystem::*;
|
||||
pub use pezkuwi_node_subsystem::{messages::*, overseer, FromOrchestra};
|
||||
use std::{collections::VecDeque, future::Future, pin::Pin};
|
||||
|
||||
/// Filter incoming and outgoing messages.
|
||||
pub trait MessageInterceptor<Sender>: Send + Sync + Clone + 'static
|
||||
where
|
||||
Sender: overseer::SubsystemSender<<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
/// The message type the original subsystem handles incoming.
|
||||
type Message: overseer::AssociateOutgoing + Send + 'static;
|
||||
|
||||
/// Filter messages that are to be received by
|
||||
/// the subsystem.
|
||||
///
|
||||
/// For non-trivial cases, the `sender` can be used to send
|
||||
/// multiple messages after doing some additional processing.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
/// Specifies if we need to replace some outgoing message with another (potentially empty)
|
||||
/// message
|
||||
fn need_intercept_outgoing(
|
||||
&self,
|
||||
_msg: &<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
/// Send modified message instead of the original one
|
||||
fn intercept_outgoing(
|
||||
&self,
|
||||
_msg: &<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages,
|
||||
) -> Option<<Self::Message as overseer::AssociateOutgoing>::OutgoingMessages> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A sender with the outgoing messages filtered.
|
||||
#[derive(Clone)]
|
||||
pub struct InterceptedSender<Sender, Fil> {
|
||||
inner: Sender,
|
||||
message_filter: Fil,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<OutgoingMessage, Sender, Fil> overseer::SubsystemSender<OutgoingMessage> for InterceptedSender<Sender, Fil>
|
||||
where
|
||||
OutgoingMessage: overseer::AssociateOutgoing + Send + 'static + TryFrom<overseer::AllMessages>,
|
||||
Sender: overseer::SubsystemSender<OutgoingMessage>
|
||||
+ overseer::SubsystemSender<
|
||||
<
|
||||
<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages
|
||||
>,
|
||||
Fil: MessageInterceptor<Sender>,
|
||||
<Fil as MessageInterceptor<Sender>>::Message: overseer::AssociateOutgoing,
|
||||
<
|
||||
<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages:
|
||||
From<OutgoingMessage> + Send + Sync,
|
||||
<OutgoingMessage as TryFrom<overseer::AllMessages>>::Error: std::fmt::Debug,
|
||||
{
|
||||
async fn send_message(&mut self, msg: OutgoingMessage) {
|
||||
self.send_message_with_priority::<overseer::NormalPriority>(msg).await;
|
||||
}
|
||||
|
||||
async fn send_message_with_priority<P: Priority>(&mut self, msg: OutgoingMessage) {
|
||||
let msg = <
|
||||
<<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages as From<OutgoingMessage>>::from(msg);
|
||||
if self.message_filter.need_intercept_outgoing(&msg) {
|
||||
if let Some(msg) = self.message_filter.intercept_outgoing(&msg) {
|
||||
self.inner.send_message(msg).await;
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.inner.send_message(msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn try_send_message(
|
||||
&mut self,
|
||||
msg: OutgoingMessage,
|
||||
) -> Result<(), pezkuwi_node_subsystem_util::metered::TrySendError<OutgoingMessage>> {
|
||||
self.try_send_message_with_priority::<overseer::NormalPriority>(msg)
|
||||
}
|
||||
|
||||
fn try_send_message_with_priority<P: Priority>(&mut self, msg: OutgoingMessage) -> Result<(), TrySendError<OutgoingMessage>> {
|
||||
let msg = <
|
||||
<<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages as From<OutgoingMessage>>::from(msg);
|
||||
if self.message_filter.need_intercept_outgoing(&msg) {
|
||||
if let Some(real_msg) = self.message_filter.intercept_outgoing(&msg) {
|
||||
let orig_msg : OutgoingMessage = msg.into().try_into().expect("must be able to recover the original message");
|
||||
self.inner.try_send_message(real_msg).map_err(|e| {
|
||||
match e {
|
||||
TrySendError::Full(_) => TrySendError::Full(orig_msg),
|
||||
TrySendError::Closed(_) => TrySendError::Closed(orig_msg),
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
// No message to send after intercepting
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
else {
|
||||
let orig_msg : OutgoingMessage = msg.into().try_into().expect("must be able to recover the original message");
|
||||
self.inner.try_send_message(orig_msg)
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_messages<T>(&mut self, msgs: T)
|
||||
where
|
||||
T: IntoIterator<Item = OutgoingMessage> + Send,
|
||||
T::IntoIter: Send,
|
||||
{
|
||||
for msg in msgs {
|
||||
self.send_message(msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn send_unbounded_message(&mut self, msg: OutgoingMessage) {
|
||||
let msg = <
|
||||
<<Fil as MessageInterceptor<Sender>>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages as From<OutgoingMessage>>::from(msg);
|
||||
if self.message_filter.need_intercept_outgoing(&msg) {
|
||||
if let Some(msg) = self.message_filter.intercept_outgoing(&msg) {
|
||||
self.inner.send_unbounded_message(msg);
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.inner.send_unbounded_message(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A subsystem context, that filters the outgoing messages.
|
||||
pub struct InterceptedContext<Context, Fil>
|
||||
where
|
||||
Context: overseer::SubsystemContext<Error=SubsystemError, Signal=OverseerSignal>,
|
||||
Fil: MessageInterceptor<<Context as overseer::SubsystemContext>::Sender>,
|
||||
<Context as overseer::SubsystemContext>::Sender:
|
||||
overseer::SubsystemSender<
|
||||
<
|
||||
<
|
||||
Fil as MessageInterceptor<<Context as overseer::SubsystemContext>::Sender>
|
||||
>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages,
|
||||
>,
|
||||
{
|
||||
inner: Context,
|
||||
message_filter: Fil,
|
||||
sender: InterceptedSender<<Context as overseer::SubsystemContext>::Sender, Fil>,
|
||||
message_buffer: VecDeque<FromOrchestra<<Context as overseer::SubsystemContext>::Message>>,
|
||||
}
|
||||
|
||||
impl<Context, Fil> InterceptedContext<Context, Fil>
|
||||
where
|
||||
Context: overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal>,
|
||||
Fil: MessageInterceptor<
|
||||
<Context as overseer::SubsystemContext>::Sender,
|
||||
Message = <Context as overseer::SubsystemContext>::Message,
|
||||
>,
|
||||
<Context as overseer::SubsystemContext>::Message: overseer::AssociateOutgoing,
|
||||
<Context as overseer::SubsystemContext>::Sender: overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages
|
||||
>
|
||||
{
|
||||
pub fn new(mut inner: Context, message_filter: Fil) -> Self {
|
||||
let sender = InterceptedSender::<<Context as overseer::SubsystemContext>::Sender, Fil> {
|
||||
inner: inner.sender().clone(),
|
||||
message_filter: message_filter.clone(),
|
||||
};
|
||||
Self { inner, message_filter, sender, message_buffer: VecDeque::new() }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<Context, Fil> overseer::SubsystemContext for InterceptedContext<Context, Fil>
|
||||
where
|
||||
Context: overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal>,
|
||||
<Context as overseer::SubsystemContext>::Message:
|
||||
overseer::AssociateOutgoing,
|
||||
<Context as overseer::SubsystemContext>::Sender:
|
||||
overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages
|
||||
>,
|
||||
InterceptedSender<<Context as overseer::SubsystemContext>::Sender, Fil>:
|
||||
overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages
|
||||
>,
|
||||
Fil: MessageInterceptor<
|
||||
<Context as overseer::SubsystemContext>::Sender,
|
||||
Message = <Context as overseer::SubsystemContext>::Message,
|
||||
>,
|
||||
{
|
||||
type Message = <Context as overseer::SubsystemContext>::Message;
|
||||
type Sender = InterceptedSender<<Context as overseer::SubsystemContext>::Sender, Fil>;
|
||||
type Error = SubsystemError;
|
||||
type OutgoingMessages = <<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing>::OutgoingMessages;
|
||||
type Signal = OverseerSignal;
|
||||
|
||||
async fn try_recv(&mut self) -> Result<Option<FromOrchestra<Self::Message>>, ()> {
|
||||
loop {
|
||||
match self.inner.try_recv().await? {
|
||||
None => return Ok(None),
|
||||
Some(msg) =>
|
||||
if let Some(msg) =
|
||||
self.message_filter.intercept_incoming(self.inner.sender(), msg)
|
||||
{
|
||||
return Ok(Some(msg))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> SubsystemResult<FromOrchestra<Self::Message>> {
|
||||
if let Some(msg) = self.message_buffer.pop_front() {
|
||||
return Ok(msg)
|
||||
}
|
||||
loop {
|
||||
let msg = self.inner.recv().await?;
|
||||
if let Some(msg) = self.message_filter.intercept_incoming(self.inner.sender(), msg) {
|
||||
return Ok(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_signal(&mut self) -> SubsystemResult<Self::Signal> {
|
||||
loop {
|
||||
let msg = self.inner.recv().await?;
|
||||
if let Some(msg) = self.message_filter.intercept_incoming(self.inner.sender(), msg) {
|
||||
if let FromOrchestra::Signal(sig) = msg {
|
||||
return Ok(sig)
|
||||
} else {
|
||||
self.message_buffer.push_back(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
s: Pin<Box<dyn Future<Output = ()> + Send>>,
|
||||
) -> SubsystemResult<()> {
|
||||
self.inner.spawn(name, s)
|
||||
}
|
||||
|
||||
fn spawn_blocking(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
s: Pin<Box<dyn Future<Output = ()> + Send>>,
|
||||
) -> SubsystemResult<()> {
|
||||
self.inner.spawn_blocking(name, s)
|
||||
}
|
||||
|
||||
fn sender(&mut self) -> &mut Self::Sender {
|
||||
&mut self.sender
|
||||
}
|
||||
}
|
||||
|
||||
/// A subsystem to which incoming and outgoing filters are applied.
|
||||
pub struct InterceptedSubsystem<Sub, Interceptor> {
|
||||
pub subsystem: Sub,
|
||||
pub message_interceptor: Interceptor,
|
||||
}
|
||||
|
||||
impl<Sub, Interceptor> InterceptedSubsystem<Sub, Interceptor> {
|
||||
pub fn new(subsystem: Sub, message_interceptor: Interceptor) -> Self {
|
||||
Self { subsystem, message_interceptor }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Context, Sub, Interceptor> overseer::Subsystem<Context, SubsystemError> for InterceptedSubsystem<Sub, Interceptor>
|
||||
where
|
||||
Context:
|
||||
overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal> + Sync + Send,
|
||||
InterceptedContext<Context, Interceptor>:
|
||||
overseer::SubsystemContext<Error=SubsystemError,Signal=OverseerSignal>,
|
||||
Sub:
|
||||
overseer::Subsystem<InterceptedContext<Context, Interceptor>, SubsystemError>,
|
||||
Interceptor:
|
||||
MessageInterceptor<
|
||||
<Context as overseer::SubsystemContext>::Sender,
|
||||
Message = <Context as overseer::SubsystemContext>::Message,
|
||||
>,
|
||||
<Context as overseer::SubsystemContext>::Message:
|
||||
overseer::AssociateOutgoing,
|
||||
<Context as overseer::SubsystemContext>::Sender:
|
||||
overseer::SubsystemSender<
|
||||
<<Context as overseer::SubsystemContext>::Message as overseer::AssociateOutgoing
|
||||
>::OutgoingMessages
|
||||
>,
|
||||
{
|
||||
fn start(self, ctx: Context) -> SpawnedSubsystem {
|
||||
let ctx = InterceptedContext::new(ctx, self.message_interceptor);
|
||||
overseer::Subsystem::<InterceptedContext<Context, Interceptor>, SubsystemError>::start(
|
||||
self.subsystem,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malus or nemesis node launch code.
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre;
|
||||
|
||||
pub(crate) mod interceptor;
|
||||
pub(crate) mod shared;
|
||||
|
||||
mod variants;
|
||||
|
||||
use variants::*;
|
||||
|
||||
/// Define the different variants of behavior.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(about = "Malus - the nemesis of pezkuwi.", version, rename_all = "kebab-case")]
|
||||
enum NemesisVariant {
|
||||
/// Suggest a candidate with an invalid proof of validity.
|
||||
SuggestGarbageCandidate(SuggestGarbageCandidateOptions),
|
||||
/// Support disabled validators in backing and statement distribution.
|
||||
SupportDisabled(SupportDisabledOptions),
|
||||
/// Back a candidate with a specifically crafted proof of validity.
|
||||
BackGarbageCandidate(BackGarbageCandidateOptions),
|
||||
/// Delayed disputing of ancestors that are perfectly fine.
|
||||
DisputeAncestor(DisputeAncestorOptions),
|
||||
/// Delayed disputing of finalized candidates.
|
||||
DisputeFinalizedCandidates(DisputeFinalizedCandidatesOptions),
|
||||
/// Spam many request statements instead of sending a single one.
|
||||
SpamStatementRequests(SpamStatementRequestsOptions),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[allow(missing_docs)]
|
||||
struct MalusCli {
|
||||
#[command(subcommand)]
|
||||
pub variant: NemesisVariant,
|
||||
/// Sets the minimum delay between the best and finalized block.
|
||||
pub finality_delay: Option<u32>,
|
||||
}
|
||||
|
||||
impl MalusCli {
|
||||
/// Launch a malus node.
|
||||
fn launch(self) -> eyre::Result<()> {
|
||||
let finality_delay = self.finality_delay;
|
||||
match self.variant {
|
||||
NemesisVariant::BackGarbageCandidate(opts) => {
|
||||
let BackGarbageCandidateOptions { percentage, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, BackGarbageCandidates { percentage }, finality_delay)?
|
||||
},
|
||||
NemesisVariant::SuggestGarbageCandidate(opts) => {
|
||||
let SuggestGarbageCandidateOptions { percentage, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, SuggestGarbageCandidates { percentage }, finality_delay)?
|
||||
},
|
||||
NemesisVariant::SupportDisabled(opts) => {
|
||||
let SupportDisabledOptions { cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, SupportDisabled, finality_delay)?
|
||||
},
|
||||
NemesisVariant::DisputeAncestor(opts) => {
|
||||
let DisputeAncestorOptions {
|
||||
fake_validation,
|
||||
fake_validation_error,
|
||||
percentage,
|
||||
cli,
|
||||
} = opts;
|
||||
|
||||
pezkuwi_cli::run_node(
|
||||
cli,
|
||||
DisputeValidCandidates { fake_validation, fake_validation_error, percentage },
|
||||
finality_delay,
|
||||
)?
|
||||
},
|
||||
NemesisVariant::DisputeFinalizedCandidates(opts) => {
|
||||
let DisputeFinalizedCandidatesOptions { dispute_offset, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(
|
||||
cli,
|
||||
DisputeFinalizedCandidates { dispute_offset },
|
||||
finality_delay,
|
||||
)?
|
||||
},
|
||||
NemesisVariant::SpamStatementRequests(opts) => {
|
||||
let SpamStatementRequestsOptions { spam_factor, cli } = opts;
|
||||
|
||||
pezkuwi_cli::run_node(cli, SpamStatementRequests { spam_factor }, finality_delay)?
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let cli = MalusCli::parse();
|
||||
cli.launch()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn subcommand_works() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-ancestor",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeAncestor(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_works_suggest_garbage() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"suggest-garbage-candidate",
|
||||
"--percentage",
|
||||
"100",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::SuggestGarbageCandidate(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_works_dispute_ancestor() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-ancestor",
|
||||
"--percentage",
|
||||
"100",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeAncestor(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_works_back_garbage() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"back-garbage-candidate",
|
||||
"--percentage",
|
||||
"100",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::BackGarbageCandidate(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn validate_range_for_percentage() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"suggest-garbage-candidate",
|
||||
"--percentage",
|
||||
"101",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeAncestor(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispute_finalized_candidates_works() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-finalized-candidates",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeFinalizedCandidates(run),
|
||||
..
|
||||
} => {
|
||||
assert!(run.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispute_finalized_offset_value_works() {
|
||||
let cli = MalusCli::try_parse_from(IntoIterator::into_iter([
|
||||
"malus",
|
||||
"dispute-finalized-candidates",
|
||||
"--dispute-offset",
|
||||
"13",
|
||||
"--bob",
|
||||
]))
|
||||
.unwrap();
|
||||
assert_matches::assert_matches!(cli, MalusCli {
|
||||
variant: NemesisVariant::DisputeFinalizedCandidates(opts),
|
||||
..
|
||||
} => {
|
||||
assert_eq!(opts.dispute_offset, 13); // This line checks that dispute_offset is correctly set to 13
|
||||
assert!(opts.cli.run.base.bob);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use futures::prelude::*;
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
pub const MALUS: &str = "MALUS";
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) const MALICIOUS_POV: &[u8] = "😈😈pov_looks_valid_to_me😈😈".as_bytes();
|
||||
|
||||
/// Launch a service task for each item in the provided queue.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn launch_processing_task<X, F, U, Q, S>(spawner: &S, queue: Q, action: F)
|
||||
where
|
||||
F: Fn(X) -> U + Send + 'static,
|
||||
U: Future<Output = ()> + Send + 'static,
|
||||
Q: Stream<Item = X> + Send + 'static,
|
||||
X: Send,
|
||||
S: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
let spawner2: S = spawner.clone();
|
||||
spawner.spawn(
|
||||
"nemesis-queue-processor",
|
||||
Some("malus"),
|
||||
Box::pin(async move {
|
||||
let spawner3 = spawner2.clone();
|
||||
queue
|
||||
.for_each(move |input| {
|
||||
spawner3.spawn("nemesis-task", Some("malus"), Box::pin(action(input)));
|
||||
async move { () }
|
||||
})
|
||||
.await;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use super::*;
|
||||
|
||||
use pezkuwi_node_subsystem_test_helpers::*;
|
||||
|
||||
use pezkuwi_node_subsystem::{
|
||||
messages::AvailabilityStoreMessage,
|
||||
overseer::{dummy::DummySubsystem, gen::TimeoutExt, Subsystem, AssociateOutgoing},
|
||||
SubsystemError,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BlackHoleInterceptor;
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for BlackHoleInterceptor
|
||||
where
|
||||
Sender: overseer::AvailabilityStoreSenderTrait
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
type Message = AvailabilityStoreMessage;
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication { msg: _msg } => None,
|
||||
// to conclude the test cleanly
|
||||
sig => Some(sig),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PassInterceptor;
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for PassInterceptor
|
||||
where
|
||||
Sender: overseer::AvailabilityStoreSenderTrait
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
type Message = AvailabilityStoreMessage;
|
||||
}
|
||||
|
||||
async fn overseer_send<T: Into<AllMessages>>(overseer: &mut TestSubsystemContextHandle<T>, msg: T) {
|
||||
overseer.send(FromOrchestra::Communication { msg }).await;
|
||||
}
|
||||
|
||||
use sp_core::testing::TaskExecutor;
|
||||
|
||||
fn launch_harness<F, M, Sub, G>(test_gen: G)
|
||||
where
|
||||
F: Future<Output = TestSubsystemContextHandle<M>> + Send,
|
||||
M: AssociateOutgoing + std::fmt::Debug + Send + 'static,
|
||||
// <M as AssociateOutgoing>::OutgoingMessages: From<M>,
|
||||
Sub: Subsystem<TestSubsystemContext<M, SpawnGlue<TaskExecutor>>, SubsystemError>,
|
||||
G: Fn(TestSubsystemContextHandle<M>) -> (F, Sub),
|
||||
{
|
||||
let pool = TaskExecutor::new();
|
||||
let (context, overseer) = make_subsystem_context(pool);
|
||||
|
||||
let (test_fut, subsystem) = test_gen(overseer);
|
||||
let subsystem = async move {
|
||||
subsystem.start(context).future.await.unwrap();
|
||||
};
|
||||
futures::pin_mut!(test_fut);
|
||||
futures::pin_mut!(subsystem);
|
||||
|
||||
futures::executor::block_on(futures::future::join(
|
||||
async move {
|
||||
let mut overseer = test_fut.await;
|
||||
overseer.send(FromOrchestra::Signal(OverseerSignal::Conclude)).await;
|
||||
},
|
||||
subsystem,
|
||||
))
|
||||
.1;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_test_intercept() {
|
||||
launch_harness(|mut overseer| {
|
||||
let sub = DummySubsystem;
|
||||
|
||||
let sub_intercepted = InterceptedSubsystem::new(sub, BlackHoleInterceptor);
|
||||
|
||||
(
|
||||
async move {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
overseer_send(
|
||||
&mut overseer,
|
||||
AvailabilityStoreMessage::QueryChunk(Default::default(), 0.into(), tx),
|
||||
)
|
||||
.await;
|
||||
let _ = rx.timeout(std::time::Duration::from_millis(100)).await.unwrap();
|
||||
overseer
|
||||
},
|
||||
sub_intercepted,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integrity_test_pass() {
|
||||
launch_harness(|mut overseer| {
|
||||
let sub = DummySubsystem;
|
||||
|
||||
let sub_intercepted = InterceptedSubsystem::new(sub, PassInterceptor);
|
||||
|
||||
(
|
||||
async move {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
overseer_send(
|
||||
&mut overseer,
|
||||
AvailabilityStoreMessage::QueryChunk(Default::default(), 0.into(), tx),
|
||||
)
|
||||
.await;
|
||||
let _ = rx.timeout(std::time::Duration::from_millis(100)).await.unwrap();
|
||||
overseer
|
||||
},
|
||||
sub_intercepted,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! This variant of Malus backs/approves all malicious candidates crafted by
|
||||
//! `suggest-garbage-candidate` variant and behaves honestly with other
|
||||
//! candidates.
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
use crate::{
|
||||
interceptor::*,
|
||||
variants::{FakeCandidateValidation, FakeCandidateValidationError, ReplaceValidationResult},
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct BackGarbageCandidateOptions {
|
||||
/// Determines the percentage of garbage candidates that should be backed.
|
||||
/// Defaults to 100% of garbage candidates being backed.
|
||||
#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
pub percentage: u8,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// Generates an overseer that replaces the candidate validation subsystem with our malicious
|
||||
/// variant.
|
||||
pub(crate) struct BackGarbageCandidates {
|
||||
/// The probability of behaving maliciously.
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
impl OverseerGen for BackGarbageCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
let validation_filter = ReplaceValidationResult::new(
|
||||
FakeCandidateValidation::BackingAndApprovalValid,
|
||||
FakeCandidateValidationError::InvalidOutputs,
|
||||
f64::from(self.percentage),
|
||||
);
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_candidate_validation(move |cv_subsystem| {
|
||||
InterceptedSubsystem::new(cv_subsystem, validation_filter)
|
||||
})
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Implements common code for nemesis. Currently, only `ReplaceValidationResult`
|
||||
//! interceptor is implemented.
|
||||
use crate::{
|
||||
interceptor::*,
|
||||
shared::{MALICIOUS_POV, MALUS},
|
||||
};
|
||||
|
||||
use pezkuwi_node_primitives::{InvalidCandidate, ValidationResult};
|
||||
|
||||
use pezkuwi_primitives::{
|
||||
CandidateCommitments, CandidateDescriptorV2 as CandidateDescriptor,
|
||||
CandidateReceiptV2 as CandidateReceipt, PersistedValidationData, PvfExecKind,
|
||||
};
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use rand::distributions::{Bernoulli, Distribution};
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
#[non_exhaustive]
|
||||
pub enum FakeCandidateValidation {
|
||||
Disabled,
|
||||
BackingInvalid,
|
||||
ApprovalInvalid,
|
||||
BackingAndApprovalInvalid,
|
||||
BackingValid,
|
||||
ApprovalValid,
|
||||
BackingAndApprovalValid,
|
||||
}
|
||||
|
||||
impl FakeCandidateValidation {
|
||||
fn misbehaves_valid(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
BackingValid | ApprovalValid | BackingAndApprovalValid => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn misbehaves_invalid(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
BackingInvalid | ApprovalInvalid | BackingAndApprovalInvalid => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn includes_backing(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
BackingInvalid | BackingAndApprovalInvalid | BackingValid | BackingAndApprovalValid =>
|
||||
true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn includes_approval(&self) -> bool {
|
||||
use FakeCandidateValidation::*;
|
||||
|
||||
match *self {
|
||||
ApprovalInvalid |
|
||||
BackingAndApprovalInvalid |
|
||||
ApprovalValid |
|
||||
BackingAndApprovalValid => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_misbehave(&self, timeout: PvfExecKind) -> bool {
|
||||
match timeout {
|
||||
PvfExecKind::Backing => self.includes_backing(),
|
||||
PvfExecKind::Approval => self.includes_approval(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Candidate invalidity details
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
pub enum FakeCandidateValidationError {
|
||||
/// Validation outputs check doesn't pass.
|
||||
InvalidOutputs,
|
||||
/// Failed to execute.`validate_block`. This includes function panicking.
|
||||
ExecutionError,
|
||||
/// Execution timeout.
|
||||
Timeout,
|
||||
/// Validation input is over the limit.
|
||||
ParamsTooLarge,
|
||||
/// Code size is over the limit.
|
||||
CodeTooLarge,
|
||||
/// PoV does not decompress correctly.
|
||||
POVDecompressionFailure,
|
||||
/// Validation function returned invalid data.
|
||||
BadReturn,
|
||||
/// Invalid relay chain parent.
|
||||
BadParent,
|
||||
/// POV hash does not match.
|
||||
POVHashMismatch,
|
||||
/// Bad collator signature.
|
||||
BadSignature,
|
||||
/// Para head hash does not match.
|
||||
ParaHeadHashMismatch,
|
||||
/// Validation code hash does not match.
|
||||
CodeHashMismatch,
|
||||
}
|
||||
|
||||
impl Into<InvalidCandidate> for FakeCandidateValidationError {
|
||||
fn into(self) -> InvalidCandidate {
|
||||
match self {
|
||||
FakeCandidateValidationError::ExecutionError =>
|
||||
InvalidCandidate::ExecutionError("Malus".into()),
|
||||
FakeCandidateValidationError::InvalidOutputs => InvalidCandidate::InvalidOutputs,
|
||||
FakeCandidateValidationError::Timeout => InvalidCandidate::Timeout,
|
||||
FakeCandidateValidationError::ParamsTooLarge => InvalidCandidate::ParamsTooLarge(666),
|
||||
FakeCandidateValidationError::CodeTooLarge => InvalidCandidate::CodeTooLarge(666),
|
||||
FakeCandidateValidationError::POVDecompressionFailure =>
|
||||
InvalidCandidate::PoVDecompressionFailure,
|
||||
FakeCandidateValidationError::BadReturn => InvalidCandidate::BadReturn,
|
||||
FakeCandidateValidationError::BadParent => InvalidCandidate::BadParent,
|
||||
FakeCandidateValidationError::POVHashMismatch => InvalidCandidate::PoVHashMismatch,
|
||||
FakeCandidateValidationError::BadSignature => InvalidCandidate::BadSignature,
|
||||
FakeCandidateValidationError::ParaHeadHashMismatch =>
|
||||
InvalidCandidate::ParaHeadHashMismatch,
|
||||
FakeCandidateValidationError::CodeHashMismatch => InvalidCandidate::CodeHashMismatch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// An interceptor which fakes validation result with a preconfigured result.
|
||||
/// Replaces `CandidateValidationSubsystem`.
|
||||
pub struct ReplaceValidationResult {
|
||||
fake_validation: FakeCandidateValidation,
|
||||
fake_validation_error: FakeCandidateValidationError,
|
||||
distribution: Bernoulli,
|
||||
}
|
||||
|
||||
impl ReplaceValidationResult {
|
||||
pub fn new(
|
||||
fake_validation: FakeCandidateValidation,
|
||||
fake_validation_error: FakeCandidateValidationError,
|
||||
percentage: f64,
|
||||
) -> Self {
|
||||
let distribution = Bernoulli::new(percentage / 100.0)
|
||||
.expect("Invalid probability! Percentage must be in range [0..=100].");
|
||||
Self { fake_validation, fake_validation_error, distribution }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_fake_candidate_commitments(
|
||||
persisted_validation_data: &PersistedValidationData,
|
||||
) -> CandidateCommitments {
|
||||
// Backing rejects candidates which output the same head as the parent,
|
||||
// therefore we must create a new head which is not equal to the parent.
|
||||
let mut head_data = persisted_validation_data.parent_head.clone();
|
||||
if head_data.0.is_empty() {
|
||||
head_data.0.push(0);
|
||||
} else {
|
||||
head_data.0[0] = head_data.0[0].wrapping_add(1);
|
||||
};
|
||||
|
||||
CandidateCommitments {
|
||||
upward_messages: Default::default(),
|
||||
horizontal_messages: Default::default(),
|
||||
new_validation_code: None,
|
||||
head_data,
|
||||
processed_downward_messages: 0,
|
||||
hrmp_watermark: persisted_validation_data.relay_parent_number,
|
||||
}
|
||||
}
|
||||
|
||||
// Create and send validation response. This function needs the persistent validation data.
|
||||
fn create_validation_response(
|
||||
persisted_validation_data: PersistedValidationData,
|
||||
descriptor: CandidateDescriptor,
|
||||
response_sender: oneshot::Sender<Result<ValidationResult, ValidationFailed>>,
|
||||
) {
|
||||
let commitments = create_fake_candidate_commitments(&persisted_validation_data);
|
||||
|
||||
// Craft the new malicious candidate.
|
||||
let candidate_receipt = CandidateReceipt { descriptor, commitments_hash: commitments.hash() };
|
||||
|
||||
let result = Ok(ValidationResult::Valid(commitments, persisted_validation_data));
|
||||
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
para_id = ?candidate_receipt.descriptor.para_id(),
|
||||
candidate_hash = ?candidate_receipt.hash(),
|
||||
"ValidationResult: {:?}",
|
||||
&result
|
||||
);
|
||||
|
||||
response_sender.send(result).unwrap();
|
||||
}
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for ReplaceValidationResult
|
||||
where
|
||||
Sender: overseer::CandidateValidationSenderTrait + Clone + Send + 'static,
|
||||
{
|
||||
type Message = CandidateValidationMessage;
|
||||
|
||||
// Capture all (approval and backing) candidate validation requests and depending on
|
||||
// configuration fail them.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
// Message sent by the approval voting subsystem
|
||||
FromOrchestra::Communication {
|
||||
msg:
|
||||
CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
..
|
||||
},
|
||||
} => {
|
||||
match self.fake_validation {
|
||||
x if x.misbehaves_valid() && x.should_misbehave(exec_kind.into()) => {
|
||||
// Behave normally if the `PoV` is not known to be malicious.
|
||||
if pov.block_data.0.as_slice() != MALICIOUS_POV {
|
||||
return Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Create the fake response with probability `p` if the `PoV` is malicious,
|
||||
// where 'p' defaults to 100% for suggest-garbage-candidate variant.
|
||||
let behave_maliciously = self.distribution.sample(&mut rand::thread_rng());
|
||||
match behave_maliciously {
|
||||
true => {
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
?behave_maliciously,
|
||||
"😈 Creating malicious ValidationResult::Valid message with fake candidate commitments.",
|
||||
);
|
||||
|
||||
create_validation_response(
|
||||
validation_data,
|
||||
candidate_receipt.descriptor,
|
||||
response_sender,
|
||||
);
|
||||
None
|
||||
},
|
||||
false => {
|
||||
// Behave normally with probability `(1-p)` for a malicious `PoV`.
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
?behave_maliciously,
|
||||
"😈 Passing CandidateValidationMessage::ValidateFromExhaustive to the candidate validation subsystem.",
|
||||
);
|
||||
|
||||
Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
x if x.misbehaves_invalid() && x.should_misbehave(exec_kind.into()) => {
|
||||
// Set the validation result to invalid with probability `p` and trigger a
|
||||
// dispute
|
||||
let behave_maliciously = self.distribution.sample(&mut rand::thread_rng());
|
||||
match behave_maliciously {
|
||||
true => {
|
||||
let validation_result =
|
||||
ValidationResult::Invalid(self.fake_validation_error.into());
|
||||
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
?behave_maliciously,
|
||||
para_id = ?candidate_receipt.descriptor.para_id(),
|
||||
"😈 Maliciously sending invalid validation result: {:?}.",
|
||||
&validation_result,
|
||||
);
|
||||
|
||||
// We're not even checking the candidate, this makes us appear
|
||||
// faster than honest validators.
|
||||
response_sender.send(Ok(validation_result)).unwrap();
|
||||
None
|
||||
},
|
||||
false => {
|
||||
// Behave normally with probability `(1-p)`
|
||||
gum::info!(target: MALUS, "😈 'Decided' to not act maliciously.",);
|
||||
|
||||
Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
// Handle FakeCandidateValidation::Disabled
|
||||
_ => Some(FromOrchestra::Communication {
|
||||
msg: CandidateValidationMessage::ValidateFromExhaustive {
|
||||
validation_data,
|
||||
validation_code,
|
||||
candidate_receipt,
|
||||
pov,
|
||||
executor_params,
|
||||
exec_kind,
|
||||
response_sender,
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
msg => Some(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node variant that attempts to dispute finalized candidates.
|
||||
//!
|
||||
//! This malus variant behaves honestly in backing and approval voting.
|
||||
//! The maliciousness comes from emitting an extra dispute statement on top of the other ones.
|
||||
//!
|
||||
//! Some extra quirks which generally should be insignificant:
|
||||
//! - The malus node will not dispute at session boundaries
|
||||
//! - The malus node will not dispute blocks it backed itself
|
||||
//! - Be cautious about the size of the network to make sure disputes are not auto-confirmed
|
||||
//! (7 validators is the smallest network size as it needs [(7-1)//3]+1 = 3 votes to get
|
||||
//! confirmed but it only gets 1 from backing and 1 from malus so 2 in total)
|
||||
//!
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, OverseerSignal, RuntimeApiSubsystemClient};
|
||||
use pezkuwi_node_subsystem_util::request_candidate_events;
|
||||
use pezkuwi_primitives::CandidateEvent;
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
// Filter wrapping related types.
|
||||
use crate::{interceptor::*, shared::MALUS};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Wraps around ApprovalVotingSubsystem and replaces it.
|
||||
/// Listens to finalization messages and if possible triggers disputes for their ancestors.
|
||||
#[derive(Clone)]
|
||||
struct AncestorDisputer<Spawner> {
|
||||
spawner: Spawner, //stores the actual ApprovalVotingSubsystem spawner
|
||||
dispute_offset: u32, /* relative depth of the disputed block to the finalized block,
|
||||
* 0=finalized, 1=parent of finalized etc */
|
||||
}
|
||||
|
||||
impl<Sender, Spawner> MessageInterceptor<Sender> for AncestorDisputer<Spawner>
|
||||
where
|
||||
Sender: overseer::ApprovalVotingSenderTrait + Clone + Send + 'static,
|
||||
Spawner: overseer::gen::Spawner + Clone + 'static,
|
||||
{
|
||||
type Message = ApprovalVotingMessage;
|
||||
|
||||
/// Intercept incoming `OverseerSignal::BlockFinalized' and pass the rest as normal.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(OverseerSignal::BlockFinalized(
|
||||
finalized_hash,
|
||||
finalized_height,
|
||||
)) => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
"😈 Block Finalization Interception! Block: {:?}", finalized_hash,
|
||||
);
|
||||
|
||||
//Ensure that the chain is long enough for the target ancestor to exist
|
||||
if finalized_height <= self.dispute_offset {
|
||||
return Some(FromOrchestra::Signal(OverseerSignal::BlockFinalized(
|
||||
finalized_hash,
|
||||
finalized_height,
|
||||
)));
|
||||
}
|
||||
|
||||
let dispute_offset = self.dispute_offset;
|
||||
let mut sender = subsystem_sender.clone();
|
||||
self.spawner.spawn_blocking(
|
||||
"malus-dispute-finalized-block",
|
||||
Some("malus"),
|
||||
Box::pin(async move {
|
||||
// Query chain for the block hash at the target depth
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender
|
||||
.send_message(ChainApiMessage::FinalizedBlockHash(
|
||||
finalized_height - dispute_offset,
|
||||
tx,
|
||||
))
|
||||
.await;
|
||||
let disputable_hash = match rx.await {
|
||||
Ok(Ok(Some(hash))) => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
"😈 Time to search {:?}`th ancestor! Block: {:?}", dispute_offset, hash,
|
||||
);
|
||||
hash
|
||||
},
|
||||
_ => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
"😈 Seems the target is not yet finalized! Nothing to dispute."
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
|
||||
// Fetch all candidate events for the target ancestor
|
||||
let events =
|
||||
request_candidate_events(disputable_hash, &mut sender).await.await;
|
||||
let events = match events {
|
||||
Ok(Ok(events)) => events,
|
||||
Ok(Err(e)) => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 Failed to fetch candidate events: {:?}", e
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
Err(e) => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 Failed to fetch candidate events: {:?}", e
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
|
||||
// Extract a token candidate from the events to use for disputing
|
||||
let event = events.iter().find(|event| {
|
||||
matches!(event, CandidateEvent::CandidateIncluded(_, _, _, _))
|
||||
});
|
||||
let candidate = match event {
|
||||
Some(CandidateEvent::CandidateIncluded(candidate, _, _, _)) =>
|
||||
candidate,
|
||||
_ => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 No candidate included event found! Nothing to dispute."
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
|
||||
// Extract the candidate hash from the candidate
|
||||
let candidate_hash = candidate.hash();
|
||||
|
||||
// Fetch the session index for the candidate
|
||||
let (tx, rx) = oneshot::channel();
|
||||
sender
|
||||
.send_message(RuntimeApiMessage::Request(
|
||||
disputable_hash,
|
||||
RuntimeApiRequest::SessionIndexForChild(tx),
|
||||
))
|
||||
.await;
|
||||
let session_index = match rx.await {
|
||||
Ok(Ok(session_index)) => session_index,
|
||||
_ => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
"😈 Failed to fetch session index for candidate."
|
||||
);
|
||||
return; // Early return from the async block
|
||||
},
|
||||
};
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Disputing candidate with hash: {:?} in session {:?}", candidate_hash, session_index,
|
||||
);
|
||||
|
||||
// Start dispute
|
||||
sender.send_unbounded_message(
|
||||
DisputeCoordinatorMessage::IssueLocalStatement(
|
||||
session_index,
|
||||
candidate_hash,
|
||||
candidate.clone(),
|
||||
false, // indicates candidate is invalid -> dispute starts
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Passthrough the finalization signal as usual (using it as hook only)
|
||||
Some(FromOrchestra::Signal(OverseerSignal::BlockFinalized(
|
||||
finalized_hash,
|
||||
finalized_height,
|
||||
)))
|
||||
},
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct DisputeFinalizedCandidatesOptions {
|
||||
/// relative depth of the disputed block to the finalized block, 0=finalized, 1=parent of
|
||||
/// finalized etc
|
||||
#[clap(long, ignore_case = true, default_value_t = 2, value_parser = clap::value_parser!(u32).range(0..=50))]
|
||||
pub dispute_offset: u32,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// DisputeFinalizedCandidates implementation wrapper which implements `OverseerGen` glue.
|
||||
pub(crate) struct DisputeFinalizedCandidates {
|
||||
/// relative depth of the disputed block to the finalized block, 0=finalized, 1=parent of
|
||||
/// finalized etc
|
||||
pub dispute_offset: u32,
|
||||
}
|
||||
|
||||
impl OverseerGen for DisputeFinalizedCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Started Malus node that disputes finalized blocks after they are {:?} finalizations deep.",
|
||||
&self.dispute_offset,
|
||||
);
|
||||
|
||||
let ancestor_disputer = AncestorDisputer {
|
||||
spawner: SpawnGlue(args.spawner.clone()),
|
||||
dispute_offset: self.dispute_offset,
|
||||
};
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_approval_voting(move |cb| InterceptedSubsystem::new(cb, ancestor_disputer))
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node that replaces approvals with invalid disputes
|
||||
//! against valid candidates. Additionally, the malus node can be configured to
|
||||
//! fake candidate validation and return a static result for candidate checking.
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
// Filter wrapping related types.
|
||||
use super::common::{FakeCandidateValidation, FakeCandidateValidationError};
|
||||
use crate::{interceptor::*, variants::ReplaceValidationResult};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct DisputeAncestorOptions {
|
||||
/// Malicious candidate validation subsystem configuration. When enabled, node PVF execution is
|
||||
/// skipped during backing and/or approval and it's result can by specified by this option and
|
||||
/// `--fake-validation-error` for invalid candidate outcomes.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value_t = FakeCandidateValidation::BackingAndApprovalInvalid)]
|
||||
pub fake_validation: FakeCandidateValidation,
|
||||
|
||||
/// Applies only when `--fake-validation` is configured to reject candidates as invalid. It
|
||||
/// allows to specify the exact error to return from the malicious candidate validation
|
||||
/// subsystem.
|
||||
#[arg(long, value_enum, ignore_case = true, default_value_t = FakeCandidateValidationError::InvalidOutputs)]
|
||||
pub fake_validation_error: FakeCandidateValidationError,
|
||||
|
||||
/// Determines the percentage of candidates that should be disputed. Allows for fine-tuning
|
||||
/// the intensity of the behavior of the malicious node. Value must be in the range [0..=100].
|
||||
#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
pub percentage: u8,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
pub(crate) struct DisputeValidCandidates {
|
||||
/// Fake validation config (applies to disputes as well).
|
||||
pub fake_validation: FakeCandidateValidation,
|
||||
/// Fake validation error config.
|
||||
pub fake_validation_error: FakeCandidateValidationError,
|
||||
/// The probability of behaving maliciously.
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
impl OverseerGen for DisputeValidCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
let validation_filter = ReplaceValidationResult::new(
|
||||
self.fake_validation,
|
||||
self.fake_validation_error,
|
||||
f64::from(self.percentage),
|
||||
);
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_candidate_validation(move |cv_subsystem| {
|
||||
InterceptedSubsystem::new(cv_subsystem, validation_filter)
|
||||
})
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collection of behavior variants.
|
||||
|
||||
mod back_garbage_candidate;
|
||||
mod common;
|
||||
mod dispute_finalized_candidates;
|
||||
mod dispute_valid_candidates;
|
||||
mod spam_statement_requests;
|
||||
mod suggest_garbage_candidate;
|
||||
mod support_disabled;
|
||||
|
||||
pub(crate) use self::{
|
||||
back_garbage_candidate::{BackGarbageCandidateOptions, BackGarbageCandidates},
|
||||
dispute_finalized_candidates::{DisputeFinalizedCandidates, DisputeFinalizedCandidatesOptions},
|
||||
dispute_valid_candidates::{DisputeAncestorOptions, DisputeValidCandidates},
|
||||
spam_statement_requests::{SpamStatementRequests, SpamStatementRequestsOptions},
|
||||
suggest_garbage_candidate::{SuggestGarbageCandidateOptions, SuggestGarbageCandidates},
|
||||
support_disabled::{SupportDisabled, SupportDisabledOptions},
|
||||
};
|
||||
pub(crate) use common::*;
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node variant that attempts spam statement requests.
|
||||
//!
|
||||
//! This malus variant behaves honestly in everything except when propagating statement distribution
|
||||
//! requests through the network bridge subsystem. Instead of sending a single request when it needs
|
||||
//! something it attempts to spam the peer with multiple requests.
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_network_protocol::request_response::{outgoing::Requests, OutgoingRequest};
|
||||
use pezkuwi_node_subsystem::{messages::NetworkBridgeTxMessage, SpawnGlue};
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
// Filter wrapping related types.
|
||||
use crate::{interceptor::*, shared::MALUS};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Wraps around network bridge and replaces it.
|
||||
#[derive(Clone)]
|
||||
struct RequestSpammer {
|
||||
spam_factor: u32, // How many statement distribution requests to send.
|
||||
}
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for RequestSpammer
|
||||
where
|
||||
Sender: overseer::NetworkBridgeTxSenderTrait + Clone + Send + 'static,
|
||||
{
|
||||
type Message = NetworkBridgeTxMessage;
|
||||
|
||||
/// Intercept NetworkBridgeTxMessage::SendRequests with Requests::AttestedCandidateV2 inside and
|
||||
/// duplicate that request
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication {
|
||||
msg: NetworkBridgeTxMessage::SendRequests(requests, if_disconnected),
|
||||
} => {
|
||||
let mut new_requests = Vec::new();
|
||||
|
||||
for request in requests {
|
||||
match request {
|
||||
Requests::AttestedCandidateV2(ref req) => {
|
||||
// Temporarily store peer and payload for duplication
|
||||
let peer_to_duplicate = req.peer.clone();
|
||||
let payload_to_duplicate = req.payload.clone();
|
||||
// Push the original request
|
||||
new_requests.push(request);
|
||||
|
||||
// Duplicate for spam purposes
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Duplicating AttestedCandidateV2 request extra {:?} times to peer: {:?}.", self.spam_factor, peer_to_duplicate,
|
||||
);
|
||||
new_requests.extend((0..self.spam_factor - 1).map(|_| {
|
||||
let (new_outgoing_request, _) = OutgoingRequest::new(
|
||||
peer_to_duplicate.clone(),
|
||||
payload_to_duplicate.clone(),
|
||||
);
|
||||
Requests::AttestedCandidateV2(new_outgoing_request)
|
||||
}));
|
||||
},
|
||||
_ => {
|
||||
new_requests.push(request);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Passthrough the message with a potentially modified number of requests
|
||||
Some(FromOrchestra::Communication {
|
||||
msg: NetworkBridgeTxMessage::SendRequests(new_requests, if_disconnected),
|
||||
})
|
||||
},
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SpamStatementRequestsOptions {
|
||||
/// How many statement distribution requests to send.
|
||||
#[clap(long, ignore_case = true, default_value_t = 1000, value_parser = clap::value_parser!(u32).range(0..=10000000))]
|
||||
pub spam_factor: u32,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// SpamStatementRequests implementation wrapper which implements `OverseerGen` glue.
|
||||
pub(crate) struct SpamStatementRequests {
|
||||
/// How many statement distribution requests to send.
|
||||
pub spam_factor: u32,
|
||||
}
|
||||
|
||||
impl OverseerGen for SpamStatementRequests {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Started Malus node that duplicates each statement distribution request spam_factor = {:?} times.",
|
||||
&self.spam_factor,
|
||||
);
|
||||
|
||||
let request_spammer = RequestSpammer { spam_factor: self.spam_factor };
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_network_bridge_tx(move |cb| InterceptedSubsystem::new(cb, request_spammer))
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! A malicious node that stores bogus availability chunks, preventing others from
|
||||
//! doing approval voting. This should lead to disputes depending if the validator
|
||||
//! has fetched a malicious chunk.
|
||||
//!
|
||||
//! Attention: For usage with `zombienet` only!
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_primitives::{AvailableData, BlockData, PoV};
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use pezkuwi_primitives::{CandidateDescriptorV2, CandidateReceiptV2, CoreIndex};
|
||||
|
||||
use pezkuwi_node_subsystem_util::request_validators;
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
use rand::distributions::{Bernoulli, Distribution};
|
||||
|
||||
// Filter wrapping related types.
|
||||
use crate::{
|
||||
interceptor::*,
|
||||
shared::{MALICIOUS_POV, MALUS},
|
||||
variants::{
|
||||
create_fake_candidate_commitments, FakeCandidateValidation, FakeCandidateValidationError,
|
||||
ReplaceValidationResult,
|
||||
},
|
||||
};
|
||||
|
||||
// Import extra types relevant to the particular
|
||||
// subsystem.
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Replace outgoing approval messages with disputes.
|
||||
#[derive(Clone)]
|
||||
struct NoteCandidate<Spawner> {
|
||||
spawner: Spawner,
|
||||
percentage: f64,
|
||||
}
|
||||
|
||||
impl<Sender, Spawner> MessageInterceptor<Sender> for NoteCandidate<Spawner>
|
||||
where
|
||||
Sender: overseer::CandidateBackingSenderTrait + Clone + Send + 'static,
|
||||
Spawner: overseer::gen::Spawner + Clone + 'static,
|
||||
{
|
||||
type Message = CandidateBackingMessage;
|
||||
|
||||
/// Intercept incoming `Second` requests from the `collator-protocol` subsystem.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication {
|
||||
msg:
|
||||
CandidateBackingMessage::Second(
|
||||
relay_parent,
|
||||
ref candidate,
|
||||
ref validation_data,
|
||||
ref _pov,
|
||||
),
|
||||
} => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
candidate_hash = ?candidate.hash(),
|
||||
?relay_parent,
|
||||
"Received request to second candidate",
|
||||
);
|
||||
|
||||
// Need to draw value from Bernoulli distribution with given probability of success
|
||||
// defined by the clap parameter. Note that clap parameter must be f64 since this is
|
||||
// expected by the Bernoulli::new() function. It must be converted from u8, due to
|
||||
// the lack of support for the .range() call on u64 in the clap crate.
|
||||
let distribution = Bernoulli::new(self.percentage / 100.0)
|
||||
.expect("Invalid probability! Percentage must be in range [0..=100].");
|
||||
|
||||
// Draw a random boolean from the Bernoulli distribution with probability of true
|
||||
// equal to `p`. We use `rand::thread_rng` as the source of randomness.
|
||||
let generate_malicious_candidate = distribution.sample(&mut rand::thread_rng());
|
||||
|
||||
if generate_malicious_candidate {
|
||||
gum::debug!(target: MALUS, "😈 Suggesting malicious candidate.",);
|
||||
|
||||
let pov = PoV { block_data: BlockData(MALICIOUS_POV.into()) };
|
||||
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut new_sender = subsystem_sender.clone();
|
||||
let _candidate = candidate.clone();
|
||||
let validation_data = validation_data.clone();
|
||||
self.spawner.spawn_blocking(
|
||||
"malus-get-validation-data",
|
||||
Some("malus"),
|
||||
Box::pin(async move {
|
||||
gum::trace!(target: MALUS, "Requesting validators");
|
||||
let n_validators = request_validators(relay_parent, &mut new_sender)
|
||||
.await
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.len();
|
||||
gum::trace!(target: MALUS, "Validators {}", n_validators);
|
||||
|
||||
let validation_code = {
|
||||
let validation_code_hash =
|
||||
_candidate.descriptor().validation_code_hash();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
new_sender
|
||||
.send_message(RuntimeApiMessage::Request(
|
||||
relay_parent,
|
||||
RuntimeApiRequest::ValidationCodeByHash(
|
||||
validation_code_hash,
|
||||
tx,
|
||||
),
|
||||
))
|
||||
.await;
|
||||
|
||||
let code = rx.await.expect("Querying the RuntimeApi should work");
|
||||
match code {
|
||||
Err(e) => {
|
||||
gum::error!(
|
||||
target: MALUS,
|
||||
?validation_code_hash,
|
||||
error = %e,
|
||||
"Failed to fetch validation code",
|
||||
);
|
||||
|
||||
sender.send(None).expect("channel is still open");
|
||||
return;
|
||||
},
|
||||
Ok(None) => {
|
||||
gum::debug!(
|
||||
target: MALUS,
|
||||
?validation_code_hash,
|
||||
"Could not find validation code on chain",
|
||||
);
|
||||
|
||||
sender.send(None).expect("channel is still open");
|
||||
return;
|
||||
},
|
||||
Ok(Some(c)) => c,
|
||||
}
|
||||
};
|
||||
|
||||
sender
|
||||
.send(Some((validation_data, validation_code, n_validators)))
|
||||
.expect("channel is still open");
|
||||
}),
|
||||
);
|
||||
|
||||
let (validation_data, validation_code, n_validators) =
|
||||
receiver.recv().unwrap()?;
|
||||
|
||||
let validation_data_hash = validation_data.hash();
|
||||
let validation_code_hash = validation_code.hash();
|
||||
let validation_data_relay_parent_number = validation_data.relay_parent_number;
|
||||
|
||||
gum::trace!(
|
||||
target: MALUS,
|
||||
candidate_hash = ?candidate.hash(),
|
||||
?relay_parent,
|
||||
?n_validators,
|
||||
?validation_data_hash,
|
||||
?validation_code_hash,
|
||||
?validation_data_relay_parent_number,
|
||||
"Fetched validation data."
|
||||
);
|
||||
|
||||
let malicious_available_data = AvailableData {
|
||||
pov: Arc::new(pov.clone()),
|
||||
validation_data: validation_data.clone(),
|
||||
};
|
||||
|
||||
let pov_hash = pov.hash();
|
||||
let erasure_root = {
|
||||
let chunks = pezkuwi_erasure_coding::obtain_chunks_v1(
|
||||
n_validators as usize,
|
||||
&malicious_available_data,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let branches = pezkuwi_erasure_coding::branches(chunks.as_ref());
|
||||
branches.root()
|
||||
};
|
||||
|
||||
let malicious_commitments = create_fake_candidate_commitments(
|
||||
&malicious_available_data.validation_data,
|
||||
);
|
||||
|
||||
let malicious_candidate = CandidateReceiptV2 {
|
||||
descriptor: CandidateDescriptorV2::new(
|
||||
candidate.descriptor.para_id(),
|
||||
relay_parent,
|
||||
candidate.descriptor.core_index().unwrap_or(CoreIndex(0)),
|
||||
candidate.descriptor.session_index().unwrap_or(0),
|
||||
validation_data_hash,
|
||||
pov_hash,
|
||||
erasure_root,
|
||||
malicious_commitments.head_data.hash(),
|
||||
validation_code_hash,
|
||||
),
|
||||
commitments_hash: malicious_commitments.hash(),
|
||||
};
|
||||
let malicious_candidate_hash = malicious_candidate.hash();
|
||||
|
||||
let message = FromOrchestra::Communication {
|
||||
msg: CandidateBackingMessage::Second(
|
||||
relay_parent,
|
||||
malicious_candidate,
|
||||
validation_data,
|
||||
pov,
|
||||
),
|
||||
};
|
||||
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
candidate_hash = ?candidate.hash(),
|
||||
"😈 Intercepted CandidateBackingMessage::Second and created malicious candidate with hash: {:?}",
|
||||
&malicious_candidate_hash
|
||||
);
|
||||
Some(message)
|
||||
} else {
|
||||
Some(msg)
|
||||
}
|
||||
},
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SuggestGarbageCandidateOptions {
|
||||
/// Determines the percentage of malicious candidates that are suggested by malus,
|
||||
/// based on the total number of intercepted CandidateBacking
|
||||
/// Must be in the range [0..=100].
|
||||
#[clap(short, long, ignore_case = true, default_value_t = 100, value_parser = clap::value_parser!(u8).range(0..=100))]
|
||||
pub percentage: u8,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// Garbage candidate implementation wrapper which implements `OverseerGen` glue.
|
||||
pub(crate) struct SuggestGarbageCandidates {
|
||||
/// The probability of behaving maliciously.
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
impl OverseerGen for SuggestGarbageCandidates {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
gum::info!(
|
||||
target: MALUS,
|
||||
"😈 Started Malus node with a {:?} percent chance of behaving maliciously for a given candidate.",
|
||||
&self.percentage,
|
||||
);
|
||||
|
||||
let note_candidate = NoteCandidate {
|
||||
spawner: SpawnGlue(args.spawner.clone()),
|
||||
percentage: f64::from(self.percentage),
|
||||
};
|
||||
let fake_valid_probability = 100.0;
|
||||
let validation_filter = ReplaceValidationResult::new(
|
||||
FakeCandidateValidation::BackingAndApprovalValid,
|
||||
FakeCandidateValidationError::InvalidOutputs,
|
||||
fake_valid_probability,
|
||||
);
|
||||
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_candidate_backing(move |cb| InterceptedSubsystem::new(cb, note_candidate))
|
||||
.replace_candidate_validation(move |cb| InterceptedSubsystem::new(cb, validation_filter))
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// Pezkuwi is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! This variant of Malus overrides the `disabled_validators` runtime API
|
||||
//! to always return an empty set of disabled validators.
|
||||
|
||||
use pezkuwi_cli::{
|
||||
service::{
|
||||
AuxStore, Error, ExtendedOverseerGenArgs, Overseer, OverseerConnector, OverseerGen,
|
||||
OverseerGenArgs, OverseerHandle,
|
||||
},
|
||||
validator_overseer_builder, Cli,
|
||||
};
|
||||
use pezkuwi_node_subsystem::SpawnGlue;
|
||||
use pezkuwi_node_subsystem_types::{ChainApiBackend, RuntimeApiSubsystemClient};
|
||||
use sp_core::traits::SpawnNamed;
|
||||
|
||||
use crate::interceptor::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
#[allow(missing_docs)]
|
||||
pub struct SupportDisabledOptions {
|
||||
#[clap(flatten)]
|
||||
pub cli: Cli,
|
||||
}
|
||||
|
||||
/// Generates an overseer with a custom runtime API subsystem.
|
||||
pub(crate) struct SupportDisabled;
|
||||
|
||||
impl OverseerGen for SupportDisabled {
|
||||
fn generate<Spawner, RuntimeClient>(
|
||||
&self,
|
||||
connector: OverseerConnector,
|
||||
args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
|
||||
ext_args: Option<ExtendedOverseerGenArgs>,
|
||||
) -> Result<(Overseer<SpawnGlue<Spawner>, Arc<RuntimeClient>>, OverseerHandle), Error>
|
||||
where
|
||||
RuntimeClient: RuntimeApiSubsystemClient + ChainApiBackend + AuxStore + 'static,
|
||||
Spawner: 'static + SpawnNamed + Clone + Unpin,
|
||||
{
|
||||
validator_overseer_builder(
|
||||
args,
|
||||
ext_args.expect("Extended arguments required to build validator overseer are provided"),
|
||||
)?
|
||||
.replace_runtime_api(move |ra_subsystem| {
|
||||
InterceptedSubsystem::new(ra_subsystem, IgnoreDisabled)
|
||||
})
|
||||
.build_with_connector(connector)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct IgnoreDisabled;
|
||||
|
||||
impl<Sender> MessageInterceptor<Sender> for IgnoreDisabled
|
||||
where
|
||||
Sender: overseer::RuntimeApiSenderTrait + Clone + Send + 'static,
|
||||
{
|
||||
type Message = RuntimeApiMessage;
|
||||
|
||||
/// Intercept incoming runtime api requests.
|
||||
fn intercept_incoming(
|
||||
&self,
|
||||
_subsystem_sender: &mut Sender,
|
||||
msg: FromOrchestra<Self::Message>,
|
||||
) -> Option<FromOrchestra<Self::Message>> {
|
||||
match msg {
|
||||
FromOrchestra::Communication {
|
||||
msg:
|
||||
RuntimeApiMessage::Request(_relay_parent, RuntimeApiRequest::DisabledValidators(tx)),
|
||||
} => {
|
||||
let _ = tx.send(Ok(Vec::new()));
|
||||
None
|
||||
},
|
||||
FromOrchestra::Communication { msg } => Some(FromOrchestra::Communication { msg }),
|
||||
FromOrchestra::Signal(signal) => Some(FromOrchestra::Signal(signal)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user