From 7ccf41db055a3921ecbcb5195fba66d6e2e6847c Mon Sep 17 00:00:00 2001 From: btwiuse <54848194+btwiuse@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:11:20 +0800 Subject: [PATCH] Revert "Delete staking miner (#1480)" This reverts commit 4b8bd9060e66932f5e038e16478c089191b19723. --- .../workflows/release-50_publish-docker.yml | 4 +- .gitlab/pipeline/build.yml | 22 +- .gitlab/pipeline/publish.yml | 18 + Cargo.lock | 120 +++- Cargo.toml | 1 + .../staking-miner_builder.Dockerfile | 46 ++ .../staking-miner_injected.Dockerfile | 43 ++ docker/scripts/staking-miner/README.md | 37 + .../scripts/staking-miner/build-injected.sh | 13 + docker/scripts/staking-miner/build.sh | 13 + .../staking-miner_Dockerfile.README.md | 3 + .../staking-miner_builder.Dockerfile | 43 ++ docker/scripts/staking-miner/test-build.sh | 18 + polkadot/utils/staking-miner/.gitignore | 2 + polkadot/utils/staking-miner/Cargo.toml | 54 ++ polkadot/utils/staking-miner/README.md | 81 +++ polkadot/utils/staking-miner/src/dry_run.rs | 166 +++++ .../staking-miner/src/emergency_solution.rs | 65 ++ polkadot/utils/staking-miner/src/main.rs | 665 ++++++++++++++++++ polkadot/utils/staking-miner/src/monitor.rs | 478 +++++++++++++ polkadot/utils/staking-miner/src/opts.rs | 366 ++++++++++ polkadot/utils/staking-miner/src/prelude.rs | 55 ++ polkadot/utils/staking-miner/src/rpc.rs | 182 +++++ .../staking-miner/src/runtime_versions.rs | 90 +++ polkadot/utils/staking-miner/src/signer.rs | 84 +++ polkadot/utils/staking-miner/tests/cli.rs | 49 ++ .../election-provider-multi-phase/src/lib.rs | 3 +- 27 files changed, 2711 insertions(+), 10 deletions(-) create mode 100644 docker/dockerfiles/staking-miner/staking-miner_builder.Dockerfile create mode 100644 docker/dockerfiles/staking-miner/staking-miner_injected.Dockerfile create mode 100644 docker/scripts/staking-miner/README.md create mode 100755 docker/scripts/staking-miner/build-injected.sh create mode 100755 docker/scripts/staking-miner/build.sh create mode 100644 docker/scripts/staking-miner/staking-miner_Dockerfile.README.md create mode 100644 docker/scripts/staking-miner/staking-miner_builder.Dockerfile create mode 100755 docker/scripts/staking-miner/test-build.sh create mode 100644 polkadot/utils/staking-miner/.gitignore create mode 100644 polkadot/utils/staking-miner/Cargo.toml create mode 100644 polkadot/utils/staking-miner/README.md create mode 100644 polkadot/utils/staking-miner/src/dry_run.rs create mode 100644 polkadot/utils/staking-miner/src/emergency_solution.rs create mode 100644 polkadot/utils/staking-miner/src/main.rs create mode 100644 polkadot/utils/staking-miner/src/monitor.rs create mode 100644 polkadot/utils/staking-miner/src/opts.rs create mode 100644 polkadot/utils/staking-miner/src/prelude.rs create mode 100644 polkadot/utils/staking-miner/src/rpc.rs create mode 100644 polkadot/utils/staking-miner/src/runtime_versions.rs create mode 100644 polkadot/utils/staking-miner/src/signer.rs create mode 100644 polkadot/utils/staking-miner/tests/cli.rs diff --git a/.github/workflows/release-50_publish-docker.yml b/.github/workflows/release-50_publish-docker.yml index 15631c172b9f..8e8022d7a4a8 100644 --- a/.github/workflows/release-50_publish-docker.yml +++ b/.github/workflows/release-50_publish-docker.yml @@ -167,8 +167,8 @@ jobs: echo "tag=latest" >> $GITHUB_OUTPUT echo "release=${release}" >> $GITHUB_OUTPUT - - name: Build Injected Container image for polkadot rc - if: ${{ env.BINARY == 'polkadot' }} + - name: Build Injected Container image for polkadot/staking-miner + if: ${{ env.BINARY == 'polkadot' || env.BINARY == 'staking-miner' }} env: ARTIFACTS_FOLDER: ./release-artifacts IMAGE_NAME: ${{ env.BINARY }} diff --git a/.gitlab/pipeline/build.yml b/.gitlab/pipeline/build.yml index 029c0f6a3cdd..d64ae5642ab5 100644 --- a/.gitlab/pipeline/build.yml +++ b/.gitlab/pipeline/build.yml @@ -83,6 +83,26 @@ build-malus: - echo "polkadot-test-malus = $(cat ./artifacts/VERSION) (EXTRATAG = $(cat ./artifacts/EXTRATAG))" - cp -r ./docker/* ./artifacts +build-staking-miner: + stage: build + extends: + - .docker-env + - .common-refs + # - .collect-artifacts + # DAG + needs: + - job: build-malus + artifacts: false + script: + - time cargo build -q --locked --release --package staging-staking-miner + # # pack artifacts + # - mkdir -p ./artifacts + # - mv ./target/release/staking-miner ./artifacts/. + # - echo -n "${CI_COMMIT_REF_NAME}" > ./artifacts/VERSION + # - echo -n "${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}" > ./artifacts/EXTRATAG + # - echo "staking-miner = $(cat ./artifacts/VERSION) (EXTRATAG = $(cat ./artifacts/EXTRATAG))" + # - cp -r ./scripts/* ./artifacts + build-rustdoc: stage: build extends: @@ -347,7 +367,7 @@ build-subkey-linux: extends: .build-subkey # DAG needs: - - job: build-malus + - job: build-staking-miner artifacts: false # tbd # build-subkey-macos: diff --git a/.gitlab/pipeline/publish.yml b/.gitlab/pipeline/publish.yml index a03d407c0409..7f1ae56f97bb 100644 --- a/.gitlab/pipeline/publish.yml +++ b/.gitlab/pipeline/publish.yml @@ -336,6 +336,24 @@ build-push-image-substrate-pr: # # this artifact is used in zombienet-tests job # dotenv: ./artifacts/malus.env +# publish-staking-miner-image: +# stage: publish +# extends: +# - .kubernetes-env +# - .build-push-image +# - .publish-refs +# variables: +# CI_IMAGE: ${BUILDAH_IMAGE} +# # scripts/ci/dockerfiles/staking-miner/staking-miner_injected.Dockerfile +# DOCKERFILE: ci/dockerfiles/staking-miner/staking-miner_injected.Dockerfile +# IMAGE_NAME: docker.io/paritytech/staking-miner +# GIT_STRATEGY: none +# DOCKER_USER: ${Docker_Hub_User_Parity} +# DOCKER_PASS: ${Docker_Hub_Pass_Parity} +# needs: +# - job: build-staking-miner +# artifacts: true + # substrate # publish-substrate-image-pr: diff --git a/Cargo.lock b/Cargo.lock index d3811ed4ad6c..527602ddf4b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,7 +494,7 @@ dependencies = [ "ark-ff", "ark-std", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -4840,6 +4840,12 @@ dependencies = [ "futures", ] +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + [[package]] name = "expander" version = "0.0.4" @@ -7690,6 +7696,15 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matches" version = "0.1.10" @@ -8534,6 +8549,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.4.1" @@ -8763,6 +8788,12 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "3.5.0" @@ -15127,7 +15158,7 @@ dependencies = [ "substrate-test-runtime", "tempfile", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.2.25", "wat", ] @@ -15826,7 +15857,7 @@ dependencies = [ "thiserror", "tracing", "tracing-log", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -16416,6 +16447,18 @@ dependencies = [ "libc", ] +[[package]] +name = "signal-hook-tokio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + [[package]] name = "signature" version = "1.6.4" @@ -17534,7 +17577,7 @@ dependencies = [ "sp-std", "tracing", "tracing-core", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -17817,6 +17860,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "staging-staking-miner" +version = "1.0.0" +dependencies = [ + "assert_cmd", + "clap 4.4.2", + "exitcode", + "frame-election-provider-support", + "frame-remote-externalities", + "frame-support", + "frame-system", + "futures-util", + "jsonrpsee", + "log", + "pallet-balances", + "pallet-election-provider-multi-phase", + "pallet-staking", + "pallet-transaction-payment", + "parity-scale-codec", + "paste", + "polkadot-core-primitives", + "polkadot-runtime", + "polkadot-runtime-common", + "sc-transaction-pool-api", + "serde", + "serde_json", + "signal-hook", + "signal-hook-tokio", + "sp-core", + "sp-npos-elections", + "sp-runtime", + "sp-state-machine", + "sp-version", + "staging-kusama-runtime", + "sub-tokens", + "thiserror", + "tokio", + "tracing-subscriber 0.3.17", + "westend-runtime", +] + [[package]] name = "staging-xcm" version = "1.0.0" @@ -17991,6 +18075,14 @@ dependencies = [ "webrtc-util", ] +[[package]] +name = "sub-tokens" +version = "0.1.0" +source = "git+https://github.com/paritytech/substrate-debug-kit?branch=master#e12503ab781e913735dc389865a3b8b4a6c6399d" +dependencies = [ + "separator", +] + [[package]] name = "subkey" version = "3.0.0" @@ -19070,7 +19162,7 @@ dependencies = [ "ansi_term", "chrono", "lazy_static", - "matchers", + "matchers 0.0.1", "parking_lot 0.11.2", "regex", "serde", @@ -19084,6 +19176,24 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers 0.1.0", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "trie-bench" version = "0.38.0" diff --git a/Cargo.toml b/Cargo.toml index d1078e3c86a8..4664ee41f238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,6 +171,7 @@ members = [ "polkadot/statement-table", "polkadot/utils/generate-bags", "polkadot/utils/remote-ext-tests/bags-list", + "polkadot/utils/staking-miner", "polkadot/xcm", "polkadot/xcm/pallet-xcm", "polkadot/xcm/pallet-xcm-benchmarks", diff --git a/docker/dockerfiles/staking-miner/staking-miner_builder.Dockerfile b/docker/dockerfiles/staking-miner/staking-miner_builder.Dockerfile new file mode 100644 index 000000000000..a1932095fd4c --- /dev/null +++ b/docker/dockerfiles/staking-miner/staking-miner_builder.Dockerfile @@ -0,0 +1,46 @@ +FROM paritytech/ci-linux:production as builder + +# metadata +ARG VCS_REF +ARG BUILD_DATE +ARG IMAGE_NAME="staking-miner" +ARG PROFILE=release + +LABEL description="This is the build stage. Here we create the binary." + +WORKDIR /app +COPY . /app +RUN cargo build --locked --$PROFILE --package staking-miner + +# ===== SECOND STAGE ====== + +FROM docker.io/library/ubuntu:20.04 +LABEL description="This is the 2nd stage: a very small image where we copy the binary." +LABEL io.parity.image.authors="devops-team@parity.io" \ + io.parity.image.vendor="Parity Technologies" \ + io.parity.image.title="${IMAGE_NAME}" \ + io.parity.image.description="${IMAGE_NAME} for substrate based chains" \ + io.parity.image.source="https://github.com/paritytech/polkadot/blob/${VCS_REF}/scripts/ci/dockerfiles/${IMAGE_NAME}/${IMAGE_NAME}_builder.Dockerfile" \ + io.parity.image.revision="${VCS_REF}" \ + io.parity.image.created="${BUILD_DATE}" \ + io.parity.image.documentation="https://github.com/paritytech/polkadot/" + +ARG PROFILE=release +COPY --from=builder /app/target/$PROFILE/staking-miner /usr/local/bin + +RUN useradd -u 1000 -U -s /bin/sh miner && \ + rm -rf /usr/bin /usr/sbin + +# show backtraces +ENV RUST_BACKTRACE 1 + +USER miner + +ENV SEED="" +ENV URI="wss://rpc.polkadot.io" +ENV RUST_LOG="info" + +# check if the binary works in this container +RUN /usr/local/bin/staking-miner --version + +ENTRYPOINT [ "/usr/local/bin/staking-miner" ] diff --git a/docker/dockerfiles/staking-miner/staking-miner_injected.Dockerfile b/docker/dockerfiles/staking-miner/staking-miner_injected.Dockerfile new file mode 100644 index 000000000000..4901ab4a3736 --- /dev/null +++ b/docker/dockerfiles/staking-miner/staking-miner_injected.Dockerfile @@ -0,0 +1,43 @@ +FROM docker.io/library/ubuntu:20.04 + +# metadata +ARG VCS_REF +ARG BUILD_DATE +ARG IMAGE_NAME="staking-miner" + +LABEL io.parity.image.authors="devops-team@parity.io" \ + io.parity.image.vendor="Parity Technologies" \ + io.parity.image.title="${IMAGE_NAME}" \ + io.parity.image.description="${IMAGE_NAME} for substrate based chains" \ + io.parity.image.source="https://github.com/paritytech/polkadot/blob/${VCS_REF}/scripts/ci/dockerfiles/${IMAGE_NAME}/${IMAGE_NAME}_injected.Dockerfile" \ + io.parity.image.revision="${VCS_REF}" \ + io.parity.image.created="${BUILD_DATE}" \ + io.parity.image.documentation="https://github.com/paritytech/polkadot/" + +# show backtraces +ENV RUST_BACKTRACE 1 + +# install tools and dependencies +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + libssl1.1 \ + ca-certificates && \ +# apt cleanup + apt-get autoremove -y && \ + apt-get clean && \ + find /var/lib/apt/lists/ -type f -not -name lock -delete; \ + useradd -u 1000 -U -s /bin/sh miner + +# add binary to docker image +COPY ./staking-miner /usr/local/bin + +USER miner + +ENV SEED="" +ENV URI="wss://rpc.polkadot.io" +ENV RUST_LOG="info" + +# check if the binary works in this container +RUN /usr/local/bin/staking-miner --version + +ENTRYPOINT [ "/usr/local/bin/staking-miner" ] diff --git a/docker/scripts/staking-miner/README.md b/docker/scripts/staking-miner/README.md new file mode 100644 index 000000000000..3610e1130316 --- /dev/null +++ b/docker/scripts/staking-miner/README.md @@ -0,0 +1,37 @@ +# staking-miner container image + +## Build using the Builder + +``` +./build.sh +``` + +## Build the injected Image + +You first need a valid Linux binary to inject. Let's assume this binary is located in `BIN_FOLDER`. + +``` +./build-injected.sh "$BIN_FOLDER" +``` + +## Test + +Here is how to test the image. We can generate a valid seed but the staking-miner will quickly notice that our +account is not funded and "does not exist". + +You may pass any ENV supported by the binary and must provide at least a few such as `SEED` and `URI`: +``` +ENV SEED="" +ENV URI="wss://rpc.polkadot.io:443" +ENV RUST_LOG="info" +``` + +``` +export SEED=$(subkey generate -n polkadot --output-type json | jq -r .secretSeed) +podman run --rm -it \ + -e URI="wss://rpc.polkadot.io:443" \ + -e RUST_LOG="info" \ + -e SEED \ + localhost/parity/staking-miner \ + dry-run seq-phragmen +``` diff --git a/docker/scripts/staking-miner/build-injected.sh b/docker/scripts/staking-miner/build-injected.sh new file mode 100755 index 000000000000..efe323b5fed8 --- /dev/null +++ b/docker/scripts/staking-miner/build-injected.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Sample call: +# $0 /path/to/folder_with_staking-miner_binary +# This script replace the former dedicated staking-miner "injected" Dockerfile +# and shows how to use the generic binary_injected.dockerfile + +PROJECT_ROOT=`git rev-parse --show-toplevel` + +export BINARY=staking-miner +export ARTIFACTS_FOLDER=$1 + +$PROJECT_ROOT/docker/scripts/build-injected.sh diff --git a/docker/scripts/staking-miner/build.sh b/docker/scripts/staking-miner/build.sh new file mode 100755 index 000000000000..c2b6ab77e531 --- /dev/null +++ b/docker/scripts/staking-miner/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# Sample call: +# $0 /path/to/folder_with_staking-miner_binary +# This script replace the former dedicated staking-miner "injected" Dockerfile +# and shows how to use the generic binary_injected.dockerfile + +PROJECT_ROOT=`git rev-parse --show-toplevel` +ENGINE=podman + +echo "Building the staking-miner using the Builder image" +echo "PROJECT_ROOT=$PROJECT_ROOT" +$ENGINE build -t staking-miner -f "${PROJECT_ROOT}/docker/dockerfiles/staking-miner/staking-miner_builder.Dockerfile" "$PROJECT_ROOT" diff --git a/docker/scripts/staking-miner/staking-miner_Dockerfile.README.md b/docker/scripts/staking-miner/staking-miner_Dockerfile.README.md new file mode 100644 index 000000000000..ce424c42f479 --- /dev/null +++ b/docker/scripts/staking-miner/staking-miner_Dockerfile.README.md @@ -0,0 +1,3 @@ +# Staking-miner Docker image + +## [GitHub](https://github.com/paritytech/polkadot/tree/master/utils/staking-miner) diff --git a/docker/scripts/staking-miner/staking-miner_builder.Dockerfile b/docker/scripts/staking-miner/staking-miner_builder.Dockerfile new file mode 100644 index 000000000000..0ae77f36c79d --- /dev/null +++ b/docker/scripts/staking-miner/staking-miner_builder.Dockerfile @@ -0,0 +1,43 @@ +FROM paritytech/ci-linux:production as builder + +# metadata +ARG VCS_REF +ARG BUILD_DATE +ARG IMAGE_NAME="staking-miner" +ARG PROFILE=production + +LABEL description="This is the build stage. Here we create the binary." + +WORKDIR /app +COPY . /app +RUN cargo build --locked --profile $PROFILE --package staking-miner + +# ===== SECOND STAGE ====== + +FROM docker.io/parity/base-bin:latest +LABEL description="This is the 2nd stage: a very small image where we copy the binary." +LABEL io.parity.image.authors="devops-team@parity.io" \ + io.parity.image.vendor="Parity Technologies" \ + io.parity.image.title="${IMAGE_NAME}" \ + io.parity.image.description="${IMAGE_NAME} for substrate based chains" \ + io.parity.image.source="https://github.com/paritytech/polkadot/blob/${VCS_REF}/scripts/ci/dockerfiles/${IMAGE_NAME}/${IMAGE_NAME}_builder.Dockerfile" \ + io.parity.image.revision="${VCS_REF}" \ + io.parity.image.created="${BUILD_DATE}" \ + io.parity.image.documentation="https://github.com/paritytech/polkadot/" + +ARG PROFILE=release +COPY --from=builder /app/target/$PROFILE/staking-miner /usr/local/bin + +# show backtraces +ENV RUST_BACKTRACE 1 + +USER parity + +ENV SEED="" +ENV URI="wss://rpc.polkadot.io" +ENV RUST_LOG="info" + +# check if the binary works in this container +RUN /usr/local/bin/staking-miner --version + +ENTRYPOINT [ "/usr/local/bin/staking-miner" ] diff --git a/docker/scripts/staking-miner/test-build.sh b/docker/scripts/staking-miner/test-build.sh new file mode 100755 index 000000000000..0ce74e2df296 --- /dev/null +++ b/docker/scripts/staking-miner/test-build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +TMP=$(mktemp -d) +ENGINE=${ENGINE:-podman} + +# You need to build an injected image first + +# Fetch some binaries +$ENGINE run --user root --rm -i \ + -v "$TMP:/export" \ + --entrypoint /bin/bash \ + parity/staking-miner -c \ + 'cp "$(which staking-miner)" /export' + +echo "Checking binaries we got:" +tree $TMP + +./build-injected.sh $TMP diff --git a/polkadot/utils/staking-miner/.gitignore b/polkadot/utils/staking-miner/.gitignore new file mode 100644 index 000000000000..db7cff848330 --- /dev/null +++ b/polkadot/utils/staking-miner/.gitignore @@ -0,0 +1,2 @@ +*.key +*.bin diff --git a/polkadot/utils/staking-miner/Cargo.toml b/polkadot/utils/staking-miner/Cargo.toml new file mode 100644 index 000000000000..4b012e3ac73f --- /dev/null +++ b/polkadot/utils/staking-miner/Cargo.toml @@ -0,0 +1,54 @@ +[[bin]] +name = "staging-staking-miner" +path = "src/main.rs" + +[package] +name = "staging-staking-miner" +version = "1.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.6.1" } +clap = { version = "4.4.2", features = ["derive", "env"] } +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } +jsonrpsee = { version = "0.16.2", features = ["ws-client", "macros"] } +log = "0.4.17" +paste = "1.0.7" +serde = "1.0.188" +serde_json = "1.0" +thiserror = "1.0.48" +tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread", "sync"] } +remote-externalities = { package = "frame-remote-externalities" , path = "../../../substrate/utils/frame/remote-externalities" } +signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } +sp-core = { path = "../../../substrate/primitives/core" } +sp-version = { path = "../../../substrate/primitives/version" } +sp-state-machine = { path = "../../../substrate/primitives/state-machine" } +sp-runtime = { path = "../../../substrate/primitives/runtime" } +sp-npos-elections = { path = "../../../substrate/primitives/npos-elections" } +sc-transaction-pool-api = { path = "../../../substrate/client/transaction-pool/api" } + +frame-system = { path = "../../../substrate/frame/system" } +frame-support = { path = "../../../substrate/frame/support" } +frame-election-provider-support = { path = "../../../substrate/frame/election-provider-support" } +pallet-election-provider-multi-phase = { path = "../../../substrate/frame/election-provider-multi-phase" } +pallet-staking = { path = "../../../substrate/frame/staking" } +pallet-balances = { path = "../../../substrate/frame/balances" } +pallet-transaction-payment = { path = "../../../substrate/frame/transaction-payment" } + +core-primitives = { package = "polkadot-core-primitives", path = "../../core-primitives" } + +runtime-common = { package = "polkadot-runtime-common", path = "../../runtime/common" } +polkadot-runtime = { path = "../../runtime/polkadot" } +kusama-runtime = { package = "staging-kusama-runtime", path = "../../runtime/kusama" } +westend-runtime = { path = "../../runtime/westend" } +exitcode = "1.1" + +sub-tokens = { git = "https://github.com/paritytech/substrate-debug-kit", branch = "master" } +signal-hook = "0.3" +futures-util = "0.3" + +[dev-dependencies] +assert_cmd = "2.0.4" diff --git a/polkadot/utils/staking-miner/README.md b/polkadot/utils/staking-miner/README.md new file mode 100644 index 000000000000..90a00eeac089 --- /dev/null +++ b/polkadot/utils/staking-miner/README.md @@ -0,0 +1,81 @@ +# Staking Miner + +Substrate chains validators compute a basic solution for the NPoS election. The optimization of the solution is +computing-intensive and can be delegated to the `staking-miner`. The `staking-miner` does not act as validator and +focuses solely on the optimization of the solution. + +The staking miner connects to a specified chain and keeps listening to new Signed phase of the +[pallet-election-provider-multi-phase](https://crates.parity.io/pallet_election_provider_multi_phase/index.html) in +order to submit solutions to the NPoS election. When the correct time comes, it computes its solution and submit it to +the chain. The default miner algorithm is +[sequential-phragmen](https://crates.parity.io/sp_npos_elections/phragmen/fn.seq_phragmen_core.html)] with a +configurable number of balancing iterations that improve the score. + +Running the staking-miner requires passing the seed of a funded account in order to pay the fees for the transactions +that will be sent. The same account's balance is used to reserve deposits as well. The best solution in each round is +rewarded. All correct solutions will get their bond back. Any invalid solution will lose their bond. + +You can check the help with: +``` +staking-miner --help +``` + +## Building + +You can build from the root of the Polkadot repository using: +``` +cargo build --profile production --locked --package staking-miner --bin staking-miner +``` + +## Docker + +There are 2 options to build a staking-miner Docker image: +- injected binary: the binary is first built on a Linux host and then injected into a Docker base image. This method + only works if you have a Linux host or access to a pre-built binary from a Linux host. +- multi-stage: the binary is entirely built within the multi-stage Docker image. There is no requirement on the host in + terms of OS and the host does not even need to have any Rust toolchain installed. + +### Building the injected image + +First build the binary as documented [above](#building). You may then inject the binary into a Docker base image: +`parity/base-bin` (running the command from the root of the Polkadot repository): +``` +TODO: UPDATE THAT +docker build -t staking-miner -f scripts/ci/dockerfiles/staking-miner/staking-miner_injected.Dockerfile target/release +``` + +### Building the multi-stage image + +Unlike the injected image that requires a Linux pre-built binary, this option does not requires a Linux host, nor Rust +to be installed. The trade-off however is that it takes a little longer to build and this option is less ideal for CI +tasks. You may build the multi-stage image the root of the Polkadot repository with: +``` +TODO: UPDATE THAT +docker build -t staking-miner -f docker/dockerfiles/staking-miner/staking-miner_builder.Dockerfile . +``` + +### Running + +A Docker container, especially one holding one of your `SEED` should be kept as secure as possible. While it won't +prevent a malicious actor to read your `SEED` if they gain access to your container, it is nonetheless recommended +running this container in `read-only` mode: + +``` +# The following line starts with an extra space on purpose: + SEED=0x1234... + +docker run --rm -i \ + --name staking-miner \ + --read-only \ + -e RUST_LOG=info \ + -e SEED=$SEED \ + -e URI=wss://your-node:9944 \ + staking-miner dry-run +``` + +### Test locally + +Make sure you've built Polkadot, then: + +1. `cargo run -p polkadot --features fast-runtime -- --chain polkadot-dev --tmp --alice -lruntime=debug` +2. `cargo run -p staking-miner -- --uri ws://localhost:9944 monitor --seed-or-path //Alice phrag-mms` diff --git a/polkadot/utils/staking-miner/src/dry_run.rs b/polkadot/utils/staking-miner/src/dry_run.rs new file mode 100644 index 000000000000..7e46f630a1f5 --- /dev/null +++ b/polkadot/utils/staking-miner/src/dry_run.rs @@ -0,0 +1,166 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! The dry-run command. + +use crate::{opts::DryRunConfig, prelude::*, rpc::*, signer::Signer, Error, SharedRpcClient}; +use codec::Encode; +use frame_support::traits::Currency; +use sp_core::Bytes; +use sp_npos_elections::ElectionScore; + +/// Forcefully create the snapshot. This can be used to compute the election at anytime. +fn force_create_snapshot(ext: &mut Ext) -> Result<(), Error> { + ext.execute_with(|| { + if >::exists() { + log::info!(target: LOG_TARGET, "snapshot already exists."); + Ok(()) + } else { + log::info!(target: LOG_TARGET, "creating a fake snapshot now."); + >::create_snapshot().map(|_| ()).map_err(Into::into) + } + }) +} + +/// Helper method to print the encoded size of the snapshot. +async fn print_info( + rpc: &SharedRpcClient, + ext: &mut Ext, + raw_solution: &EPM::RawSolution>, + extrinsic: &Bytes, +) where + ::Currency: Currency, +{ + ext.execute_with(|| { + log::info!( + target: LOG_TARGET, + "Snapshot Metadata: {:?}", + >::snapshot_metadata() + ); + log::info!( + target: LOG_TARGET, + "Snapshot Encoded Length: {:?}", + >::snapshot() + .expect("snapshot must exist before calling `measure_snapshot_size`") + .encode() + .len() + ); + + let snapshot_size = + >::snapshot_metadata().expect("snapshot must exist by now; qed."); + let deposit = EPM::Pallet::::deposit_for(raw_solution, snapshot_size); + + let score = { + let ElectionScore { minimal_stake, sum_stake, sum_stake_squared } = raw_solution.score; + [Token::from(minimal_stake), Token::from(sum_stake), Token::from(sum_stake_squared)] + }; + + log::info!( + target: LOG_TARGET, + "solution score {:?} / deposit {:?} / length {:?}", + score, + Token::from(deposit), + raw_solution.encode().len(), + ); + }); + + let info = rpc.payment_query_info(&extrinsic, None).await; + + log::info!( + target: LOG_TARGET, + "payment_queryInfo: (fee = {}) {:?}", + info.as_ref() + .map(|d| Token::from(d.partial_fee)) + .unwrap_or_else(|_| Token::from(0)), + info, + ); +} + +/// Find the stake threshold in order to have at most `count` voters. +#[allow(unused)] +fn find_threshold(ext: &mut Ext, count: usize) { + ext.execute_with(|| { + let mut voters = >::snapshot() + .expect("snapshot must exist before calling `measure_snapshot_size`") + .voters; + voters.sort_by_key(|(_voter, weight, _targets)| std::cmp::Reverse(*weight)); + match voters.get(count) { + Some(threshold_voter) => println!("smallest allowed voter is {:?}", threshold_voter), + None => { + println!("requested truncation to {} voters but had only {}", count, voters.len()); + println!("smallest current voter: {:?}", voters.last()); + }, + } + }) +} + +macro_rules! dry_run_cmd_for { ($runtime:ident) => { paste::paste! { + /// Execute the dry-run command. + pub(crate) async fn []( + rpc: SharedRpcClient, + config: DryRunConfig, + signer: Signer, + ) -> Result<(), Error<$crate::[<$runtime _runtime_exports>]::Runtime>> { + use $crate::[<$runtime _runtime_exports>]::*; + let pallets = if config.force_snapshot { + vec!["Staking".to_string(), "BagsList".to_string()] + } else { + Default::default() + }; + let mut ext = crate::create_election_ext::(rpc.clone(), config.at, pallets).await?; + if config.force_snapshot { + force_create_snapshot::(&mut ext)?; + }; + + log::debug!(target: LOG_TARGET, "solving with {:?}", config.solver); + let raw_solution = crate::mine_with::(&config.solver, &mut ext, false)?; + + let nonce = crate::get_account_info::(&rpc, &signer.account, config.at) + .await? + .map(|i| i.nonce) + .expect("signer account is checked to exist upon startup; it can only die if it \ + transfers funds out of it, or get slashed. If it does not exist at this point, \ + it is likely due to a bug, or the signer got slashed. Terminating." + ); + let tip = 0 as Balance; + let era = sp_runtime::generic::Era::Immortal; + let extrinsic = ext.execute_with(|| create_uxt(raw_solution.clone(), signer.clone(), nonce, tip, era)); + + let bytes = sp_core::Bytes(extrinsic.encode().to_vec()); + print_info::(&rpc, &mut ext, &raw_solution, &bytes).await; + + let feasibility_result = ext.execute_with(|| { + EPM::Pallet::::feasibility_check(raw_solution.clone(), EPM::ElectionCompute::Signed) + }); + log::info!(target: LOG_TARGET, "feasibility result is {:?}", feasibility_result.map(|_| ())); + + let dispatch_result = ext.execute_with(|| { + // manually tweak the phase. + EPM::CurrentPhase::::put(EPM::Phase::Signed); + EPM::Pallet::::submit(frame_system::RawOrigin::Signed(signer.account).into(), Box::new(raw_solution)) + }); + log::info!(target: LOG_TARGET, "dispatch result is {:?}", dispatch_result); + + let dry_run_fut = rpc.dry_run(&bytes, None); + let outcome: sp_runtime::ApplyExtrinsicResult = await_request_and_decode(dry_run_fut).await.map_err::, _>(Into::into)?; + log::info!(target: LOG_TARGET, "dry-run outcome is {:?}", outcome); + Ok(()) + } +}}} + +dry_run_cmd_for!(polkadot); +dry_run_cmd_for!(kusama); +dry_run_cmd_for!(westend); diff --git a/polkadot/utils/staking-miner/src/emergency_solution.rs b/polkadot/utils/staking-miner/src/emergency_solution.rs new file mode 100644 index 000000000000..9ea9f90756e2 --- /dev/null +++ b/polkadot/utils/staking-miner/src/emergency_solution.rs @@ -0,0 +1,65 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! The emergency-solution command. + +use crate::{prelude::*, EmergencySolutionConfig, Error, SharedRpcClient}; +use codec::Encode; +use std::io::Write; + +macro_rules! emergency_solution_cmd_for { ($runtime:ident) => { paste::paste! { + /// Execute the emergency-solution command. + pub(crate) async fn []( + client: SharedRpcClient, + config: EmergencySolutionConfig, + ) -> Result<(), Error<$crate::[<$runtime _runtime_exports>]::Runtime>> { + use $crate::[<$runtime _runtime_exports>]::*; + + let mut ext = crate::create_election_ext::(client, config.at, vec![]).await?; + let raw_solution = crate::mine_with::(&config.solver, &mut ext, false)?; + + ext.execute_with(|| { + assert!(EPM::Pallet::::current_phase().is_emergency()); + + log::info!(target: LOG_TARGET, "mined solution with {:?}", &raw_solution.score); + + let ready_solution = EPM::Pallet::::feasibility_check(raw_solution, EPM::ElectionCompute::Signed)?; + let encoded_size = ready_solution.encoded_size(); + let score = ready_solution.score; + let mut supports = ready_solution.supports.into_inner(); + // maybe truncate. + if let Some(take) = config.take { + log::info!(target: LOG_TARGET, "truncating {} winners to {}", supports.len(), take); + supports.sort_unstable_by_key(|(_, s)| s.total); + supports.truncate(take); + } + + // write to file and stdout. + let encoded_support = supports.encode(); + let mut supports_file = std::fs::File::create("solution.supports.bin")?; + supports_file.write_all(&encoded_support)?; + + log::info!(target: LOG_TARGET, "ReadySolution: size {:?} / score = {:?}", encoded_size, score); + log::trace!(target: LOG_TARGET, "Supports: {}", sp_core::hexdisplay::HexDisplay::from(&encoded_support)); + + Ok(()) + }) + } +}}} + +emergency_solution_cmd_for!(polkadot); +emergency_solution_cmd_for!(kusama); +emergency_solution_cmd_for!(westend); diff --git a/polkadot/utils/staking-miner/src/main.rs b/polkadot/utils/staking-miner/src/main.rs new file mode 100644 index 000000000000..90b2c7366a1b --- /dev/null +++ b/polkadot/utils/staking-miner/src/main.rs @@ -0,0 +1,665 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! # Polkadot Staking Miner. +//! +//! Simple bot capable of monitoring a polkadot (and cousins) chain and submitting solutions to the +//! `pallet-election-provider-multi-phase`. See `--help` for more details. +//! +//! # Implementation Notes: +//! +//! - First draft: Be aware that this is the first draft and there might be bugs, or undefined +//! behaviors. Don't attach this bot to an account with lots of funds. +//! - Quick to crash: The bot is written so that it only continues to work if everything goes well. +//! In case of any failure (RPC, logic, IO), it will crash. This was a decision to simplify the +//! development. It is intended to run this bot with a `restart = true` way, so that it reports it +//! crash, but resumes work thereafter. + +// Silence erroneous warning about unsafe not being required whereas it is +// see https://github.com/rust-lang/rust/issues/49112 +#![allow(unused_unsafe)] + +mod dry_run; +mod emergency_solution; +mod monitor; +mod opts; +mod prelude; +mod rpc; +mod runtime_versions; +mod signer; + +pub(crate) use prelude::*; +pub(crate) use signer::get_account_info; + +use crate::opts::*; +use clap::Parser; +use frame_election_provider_support::NposSolver; +use frame_support::traits::Get; +use futures_util::StreamExt; +use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; +use remote_externalities::{Builder, Mode, OnlineConfig, Transport}; +use rpc::{RpcApiClient, SharedRpcClient}; +use runtime_versions::RuntimeVersions; +use signal_hook::consts::signal::*; +use signal_hook_tokio::Signals; +use sp_npos_elections::BalancingConfig; +use std::{ops::Deref, sync::Arc, time::Duration}; +use tracing_subscriber::{fmt, EnvFilter}; + +pub(crate) enum AnyRuntime { + Polkadot, + Kusama, + Westend, +} + +pub(crate) static mut RUNTIME: AnyRuntime = AnyRuntime::Polkadot; + +macro_rules! construct_runtime_prelude { + ($runtime:ident) => { paste::paste! { + pub(crate) mod [<$runtime _runtime_exports>] { + pub(crate) use crate::prelude::EPM; + pub(crate) use [<$runtime _runtime>]::*; + pub(crate) use crate::monitor::[] as monitor_cmd; + pub(crate) use crate::dry_run::[] as dry_run_cmd; + pub(crate) use crate::emergency_solution::[] as emergency_solution_cmd; + pub(crate) use private::{[] as create_uxt}; + + mod private { + use super::*; + pub(crate) fn []( + raw_solution: EPM::RawSolution>, + signer: crate::signer::Signer, + nonce: crate::prelude::Nonce, + tip: crate::prelude::Balance, + era: sp_runtime::generic::Era, + ) -> UncheckedExtrinsic { + use codec::Encode as _; + use sp_core::Pair as _; + use sp_runtime::traits::StaticLookup as _; + + let crate::signer::Signer { account, pair, .. } = signer; + + let local_call = EPMCall::::submit { raw_solution: Box::new(raw_solution) }; + let call: RuntimeCall = as std::convert::TryInto>::try_into(local_call) + .expect("election provider pallet must exist in the runtime, thus \ + inner call can be converted, qed." + ); + + let extra: SignedExtra = crate::[](nonce, tip, era); + let raw_payload = SignedPayload::new(call, extra).expect("creating signed payload infallible; qed."); + let signature = raw_payload.using_encoded(|payload| { + pair.sign(payload) + }); + let (call, extra, _) = raw_payload.deconstruct(); + let address = ::Lookup::unlookup(account); + let extrinsic = UncheckedExtrinsic::new_signed(call, address, signature.into(), extra); + log::debug!( + target: crate::LOG_TARGET, "constructed extrinsic {} with length {}", + sp_core::hexdisplay::HexDisplay::from(&extrinsic.encode()), + extrinsic.encode().len(), + ); + extrinsic + } + } + }} + }; +} + +// NOTE: we might be able to use some code from the bridges repo here. +fn signed_ext_builder_polkadot( + nonce: Nonce, + tip: Balance, + era: sp_runtime::generic::Era, +) -> polkadot_runtime_exports::SignedExtra { + use polkadot_runtime_exports::Runtime; + ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(era), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + runtime_common::claims::PrevalidateAttests::::new(), + ) +} + +fn signed_ext_builder_kusama( + nonce: Nonce, + tip: Balance, + era: sp_runtime::generic::Era, +) -> kusama_runtime_exports::SignedExtra { + use kusama_runtime_exports::Runtime; + ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(era), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + ) +} + +fn signed_ext_builder_westend( + nonce: Nonce, + tip: Balance, + era: sp_runtime::generic::Era, +) -> westend_runtime_exports::SignedExtra { + use westend_runtime_exports::Runtime; + ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(era), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(tip), + ) +} + +construct_runtime_prelude!(polkadot); +construct_runtime_prelude!(kusama); +construct_runtime_prelude!(westend); + +// NOTE: this is no longer used extensively, most of the per-runtime stuff us delegated to +// `construct_runtime_prelude` and macro's the import directly from it. A part of the code is also +// still generic over `T`. My hope is to still make everything generic over a `Runtime`, but sadly +// that is not currently possible as each runtime has its unique `Call`, and all Calls are not +// sharing any generic trait. In other words, to create the `UncheckedExtrinsic` of each chain, you +// need the concrete `Call` of that chain as well. +#[macro_export] +macro_rules! any_runtime { + ($($code:tt)*) => { + unsafe { + match $crate::RUNTIME { + $crate::AnyRuntime::Polkadot => { + #[allow(unused)] + use $crate::polkadot_runtime_exports::*; + $($code)* + }, + $crate::AnyRuntime::Kusama => { + #[allow(unused)] + use $crate::kusama_runtime_exports::*; + $($code)* + }, + $crate::AnyRuntime::Westend => { + #[allow(unused)] + use $crate::westend_runtime_exports::*; + $($code)* + } + } + } + } +} + +/// Same as [`any_runtime`], but instead of returning a `Result`, this simply returns `()`. Useful +/// for situations where the result is not useful and un-ergonomic to handle. +#[macro_export] +macro_rules! any_runtime_unit { + ($($code:tt)*) => { + unsafe { + match $crate::RUNTIME { + $crate::AnyRuntime::Polkadot => { + #[allow(unused)] + use $crate::polkadot_runtime_exports::*; + let _ = $($code)*; + }, + $crate::AnyRuntime::Kusama => { + #[allow(unused)] + use $crate::kusama_runtime_exports::*; + let _ = $($code)*; + }, + $crate::AnyRuntime::Westend => { + #[allow(unused)] + use $crate::westend_runtime_exports::*; + let _ = $($code)*; + } + } + } + } +} + +#[derive(frame_support::DebugNoBound, thiserror::Error)] +enum Error { + Io(#[from] std::io::Error), + JsonRpsee(#[from] jsonrpsee::core::Error), + RpcHelperError(#[from] rpc::RpcHelperError), + Codec(#[from] codec::Error), + Crypto(sp_core::crypto::SecretStringError), + RemoteExternalities(&'static str), + PalletMiner(EPM::unsigned::MinerError), + PalletElection(EPM::ElectionError), + PalletFeasibility(EPM::FeasibilityError), + AccountDoesNotExists, + IncorrectPhase, + AlreadySubmitted, + VersionMismatch, + StrategyNotSatisfied, + Other(String), +} + +impl From for Error { + fn from(e: sp_core::crypto::SecretStringError) -> Error { + Error::Crypto(e) + } +} + +impl From for Error { + fn from(e: EPM::unsigned::MinerError) -> Error { + Error::PalletMiner(e) + } +} + +impl From> for Error { + fn from(e: EPM::ElectionError) -> Error { + Error::PalletElection(e) + } +} + +impl From for Error { + fn from(e: EPM::FeasibilityError) -> Error { + Error::PalletFeasibility(e) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + as std::fmt::Debug>::fmt(self, f) + } +} + +frame_support::parameter_types! { + /// Number of balancing iterations for a solution algorithm. Set based on the [`Solvers`] CLI + /// config. + pub static BalanceIterations: usize = 10; + pub static Balancing: Option = Some( BalancingConfig { iterations: BalanceIterations::get(), tolerance: 0 } ); +} + +/// Build the Ext at hash with all the data of `ElectionProviderMultiPhase` and any additional +/// pallets. +async fn create_election_ext( + client: SharedRpcClient, + at: Option, + additional: Vec, +) -> Result> +where + T: EPM::Config, +{ + use frame_support::{storage::generator::StorageMap, traits::PalletInfo}; + use sp_core::hashing::twox_128; + + let mut pallets = vec![::PalletInfo::name::>() + .expect("Pallet always has name; qed.") + .to_string()]; + pallets.extend(additional); + Builder::::new() + .mode(Mode::Online(OnlineConfig { + transport: Transport::Uri(client.uri().to_owned()), + at, + pallets, + hashed_prefixes: vec![>::prefix_hash()], + hashed_keys: vec![[twox_128(b"System"), twox_128(b"Number")].concat()], + ..Default::default() + })) + .build() + .await + .map_err(|why| Error::::RemoteExternalities(why)) + .map(|rx| rx.inner_ext) +} + +/// Compute the election. It expects to NOT be `Phase::Off`. In other words, the snapshot must +/// exists on the given externalities. +fn mine_solution( + ext: &mut Ext, + do_feasibility: bool, +) -> Result>, Error> +where + T: EPM::Config, + S: NposSolver< + Error = <::Solver as NposSolver>::Error, + AccountId = <::Solver as NposSolver>::AccountId, + >, +{ + ext.execute_with(|| { + let (solution, _) = >::mine_solution().map_err::, _>(Into::into)?; + if do_feasibility { + let _ = >::feasibility_check( + solution.clone(), + EPM::ElectionCompute::Signed, + )?; + } + Ok(solution) + }) +} + +/// Mine a solution with the given `solver`. +fn mine_with( + solver: &Solver, + ext: &mut Ext, + do_feasibility: bool, +) -> Result>, Error> +where + T: EPM::Config, + T::Solver: NposSolver, +{ + use frame_election_provider_support::{PhragMMS, SequentialPhragmen}; + + match solver { + Solver::SeqPhragmen { iterations } => { + BalanceIterations::set(*iterations); + mine_solution::< + T, + SequentialPhragmen< + ::AccountId, + sp_runtime::Perbill, + Balancing, + >, + >(ext, do_feasibility) + }, + Solver::PhragMMS { iterations } => { + BalanceIterations::set(*iterations); + mine_solution::< + T, + PhragMMS<::AccountId, sp_runtime::Perbill, Balancing>, + >(ext, do_feasibility) + }, + } +} + +#[allow(unused)] +fn mine_dpos(ext: &mut Ext) -> Result<(), Error> { + ext.execute_with(|| { + use std::collections::BTreeMap; + use EPM::RoundSnapshot; + let RoundSnapshot { voters, .. } = EPM::Snapshot::::get().unwrap(); + let desired_targets = EPM::DesiredTargets::::get().unwrap(); + let mut candidates_and_backing = BTreeMap::::new(); + voters.into_iter().for_each(|(who, stake, targets)| { + if targets.is_empty() { + println!("target = {:?}", (who, stake, targets)); + return + } + let share: u128 = (stake as u128) / (targets.len() as u128); + for target in targets { + *candidates_and_backing.entry(target.clone()).or_default() += share + } + }); + + let mut candidates_and_backing = + candidates_and_backing.into_iter().collect::>(); + candidates_and_backing.sort_by_key(|(_, total_stake)| *total_stake); + let winners = candidates_and_backing + .into_iter() + .rev() + .take(desired_targets as usize) + .collect::>(); + let score = { + let min_staker = *winners.last().map(|(_, stake)| stake).unwrap(); + let sum_stake = winners.iter().fold(0u128, |acc, (_, stake)| acc + stake); + let sum_squared = winners.iter().fold(0u128, |acc, (_, stake)| acc + stake); + [min_staker, sum_stake, sum_squared] + }; + println!("mined a dpos-like solution with score = {:?}", score); + Ok(()) + }) +} + +pub(crate) async fn check_versions( + rpc: &SharedRpcClient, + print: bool, +) -> Result<(), Error> { + let linked_version = T::Version::get(); + let on_chain_version = rpc + .runtime_version(None) + .await + .expect("runtime version RPC should always work; qed"); + + let do_print = || { + log::info!( + target: LOG_TARGET, + "linked version {:?}", + (&linked_version.spec_name, &linked_version.spec_version) + ); + log::info!( + target: LOG_TARGET, + "on-chain version {:?}", + (&on_chain_version.spec_name, &on_chain_version.spec_version) + ); + }; + + if print { + do_print(); + } + + // we relax the checking here a bit, which should not cause any issues in production (a chain + // that messes up its spec name is highly unlikely), but it allows us to do easier testing. + if linked_version.spec_name != on_chain_version.spec_name || + linked_version.spec_version != on_chain_version.spec_version + { + if !print { + do_print(); + } + log::error!( + target: LOG_TARGET, + "VERSION MISMATCH: any transaction will fail with bad-proof" + ); + Err(Error::VersionMismatch) + } else { + Ok(()) + } +} + +/// Control how we exit the application +fn controlled_exit(code: i32) { + log::info!(target: LOG_TARGET, "Exiting application"); + std::process::exit(code); +} + +/// Handles the various signal and exit the application +/// when appropriate. +async fn handle_signals(mut signals: Signals) { + let mut keyboard_sig_count: u8 = 0; + while let Some(signal) = signals.next().await { + match signal { + // Interrupts come from the keyboard + SIGQUIT | SIGINT => { + if keyboard_sig_count >= 1 { + log::info!( + target: LOG_TARGET, + "Received keyboard termination signal #{}/{}, quitting...", + keyboard_sig_count + 1, + 2 + ); + controlled_exit(exitcode::OK); + } + keyboard_sig_count += 1; + log::warn!( + target: LOG_TARGET, + "Received keyboard termination signal #{}, if you keep doing that I will really quit", + keyboard_sig_count + ); + }, + + SIGKILL | SIGTERM => { + log::info!(target: LOG_TARGET, "Received SIGKILL | SIGTERM, quitting..."); + controlled_exit(exitcode::OK); + }, + _ => unreachable!(), + } + } +} + +#[tokio::main] +async fn main() { + fmt().with_env_filter(EnvFilter::from_default_env()).init(); + + let Opt { uri, command, connection_timeout, request_timeout } = Opt::parse(); + log::debug!(target: LOG_TARGET, "attempting to connect to {:?}", uri); + + let signals = Signals::new(&[SIGTERM, SIGINT, SIGQUIT]).expect("Failed initializing Signals"); + let handle = signals.handle(); + let signals_task = tokio::spawn(handle_signals(signals)); + + let rpc = loop { + match SharedRpcClient::new( + &uri, + Duration::from_secs(connection_timeout as u64), + Duration::from_secs(request_timeout as u64), + ) + .await + { + Ok(client) => break client, + Err(why) => { + log::warn!( + target: LOG_TARGET, + "failed to connect to client due to {:?}, retrying soon..", + why + ); + tokio::time::sleep(std::time::Duration::from_millis(2500)).await; + }, + } + }; + + let chain: String = rpc.system_chain().await.expect("system_chain infallible; qed."); + match chain.to_lowercase().as_str() { + "polkadot" | "development" => { + sp_core::crypto::set_default_ss58_version( + sp_core::crypto::Ss58AddressFormatRegistry::PolkadotAccount.into(), + ); + sub_tokens::dynamic::set_name("DOT"); + sub_tokens::dynamic::set_decimal_points(10_000_000_000); + // safety: this program will always be single threaded, thus accessing global static is + // safe. + unsafe { + RUNTIME = AnyRuntime::Polkadot; + } + }, + "kusama" | "kusama-dev" => { + sp_core::crypto::set_default_ss58_version( + sp_core::crypto::Ss58AddressFormatRegistry::KusamaAccount.into(), + ); + sub_tokens::dynamic::set_name("KSM"); + sub_tokens::dynamic::set_decimal_points(1_000_000_000_000); + // safety: this program will always be single threaded, thus accessing global static is + // safe. + unsafe { + RUNTIME = AnyRuntime::Kusama; + } + }, + "westend" => { + sp_core::crypto::set_default_ss58_version( + sp_core::crypto::Ss58AddressFormatRegistry::PolkadotAccount.into(), + ); + sub_tokens::dynamic::set_name("WND"); + sub_tokens::dynamic::set_decimal_points(1_000_000_000_000); + // safety: this program will always be single threaded, thus accessing global static is + // safe. + unsafe { + RUNTIME = AnyRuntime::Westend; + } + }, + _ => { + eprintln!("unexpected chain: {:?}", chain); + return + }, + } + log::info!(target: LOG_TARGET, "connected to chain {:?}", chain); + + any_runtime_unit! { + check_versions::(&rpc, true).await + }; + + let outcome = any_runtime! { + match command { + Command::Monitor(monitor_config) => + { + let signer_account = any_runtime! { + signer::signer_uri_from_string::(&monitor_config.seed_or_path , &rpc) + .await + .expect("Provided account is invalid, terminating.") + }; + monitor_cmd(rpc, monitor_config, signer_account).await + .map_err(|e| { + log::error!(target: LOG_TARGET, "Monitor error: {:?}", e); + })}, + Command::DryRun(dryrun_config) => { + let signer_account = any_runtime! { + signer::signer_uri_from_string::(&dryrun_config.seed_or_path , &rpc) + .await + .expect("Provided account is invalid, terminating.") + }; + dry_run_cmd(rpc, dryrun_config, signer_account).await + .map_err(|e| { + log::error!(target: LOG_TARGET, "DryRun error: {:?}", e); + })}, + Command::EmergencySolution(emergency_solution_config) => + emergency_solution_cmd(rpc, emergency_solution_config).await + .map_err(|e| { + log::error!(target: LOG_TARGET, "EmergencySolution error: {:?}", e); + }), + Command::Info(info_opts) => { + let remote_runtime_version = rpc.runtime_version(None).await.expect("runtime_version infallible; qed."); + + let builtin_version = any_runtime! { + Version::get() + }; + + let versions = RuntimeVersions::new(&remote_runtime_version, &builtin_version); + + if !info_opts.json { + println!("{}", versions); + } else { + let versions = serde_json::to_string_pretty(&versions).expect("Failed serializing version info"); + println!("{}", versions); + } + Ok(()) + } + } + }; + log::info!(target: LOG_TARGET, "round of execution finished. outcome = {:?}", outcome); + + handle.close(); + let _ = signals_task.await; +} + +#[cfg(test)] +mod tests { + use super::*; + + fn get_version() -> sp_version::RuntimeVersion { + T::Version::get() + } + + #[test] + fn any_runtime_works() { + unsafe { + RUNTIME = AnyRuntime::Polkadot; + } + let polkadot_version = any_runtime! { get_version::() }; + + unsafe { + RUNTIME = AnyRuntime::Kusama; + } + let kusama_version = any_runtime! { get_version::() }; + + assert_eq!(polkadot_version.spec_name, "polkadot".into()); + assert_eq!(kusama_version.spec_name, "kusama".into()); + } +} diff --git a/polkadot/utils/staking-miner/src/monitor.rs b/polkadot/utils/staking-miner/src/monitor.rs new file mode 100644 index 000000000000..607ecb6baa42 --- /dev/null +++ b/polkadot/utils/staking-miner/src/monitor.rs @@ -0,0 +1,478 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! The monitor command. + +use crate::{ + prelude::*, rpc::*, signer::Signer, Error, MonitorConfig, SharedRpcClient, SubmissionStrategy, +}; +use codec::Encode; +use jsonrpsee::core::Error as RpcError; +use sc_transaction_pool_api::TransactionStatus; +use sp_core::storage::StorageKey; +use sp_runtime::Perbill; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use EPM::{signed::SubmissionIndicesOf, SignedSubmissionOf}; + +/// Ensure that now is the signed phase. +async fn ensure_signed_phase>( + rpc: &SharedRpcClient, + at: B::Hash, +) -> Result<(), Error> { + let key = StorageKey(EPM::CurrentPhase::::hashed_key().to_vec()); + let phase = rpc + .get_storage_and_decode::>(&key, Some(at)) + .await + .map_err::, _>(Into::into)? + .unwrap_or_default(); + + if phase.is_signed() { + Ok(()) + } else { + Err(Error::IncorrectPhase) + } +} + +/// Ensure that our current `us` have not submitted anything previously. +async fn ensure_no_previous_solution( + rpc: &SharedRpcClient, + at: Hash, + us: &AccountId, +) -> Result<(), Error> +where + T: EPM::Config + frame_system::Config, + B: BlockT, +{ + let indices_key = StorageKey(EPM::SignedSubmissionIndices::::hashed_key().to_vec()); + + let indices: SubmissionIndicesOf = rpc + .get_storage_and_decode(&indices_key, Some(at)) + .await + .map_err::, _>(Into::into)? + .unwrap_or_default(); + + for (_score, _bn, idx) in indices { + let key = StorageKey(EPM::SignedSubmissionsMap::::hashed_key_for(idx)); + + if let Some(submission) = rpc + .get_storage_and_decode::>(&key, Some(at)) + .await + .map_err::, _>(Into::into)? + { + if &submission.who == us { + return Err(Error::AlreadySubmitted) + } + } + } + + Ok(()) +} + +/// `true` if `our_score` should pass the onchain `best_score` with the given strategy. +pub(crate) fn score_passes_strategy( + our_score: sp_npos_elections::ElectionScore, + best_score: sp_npos_elections::ElectionScore, + strategy: SubmissionStrategy, +) -> bool { + match strategy { + SubmissionStrategy::Always => true, + SubmissionStrategy::IfLeading => + our_score == best_score || + our_score.strict_threshold_better(best_score, Perbill::zero()), + SubmissionStrategy::ClaimBetterThan(epsilon) => + our_score.strict_threshold_better(best_score, epsilon), + SubmissionStrategy::ClaimNoWorseThan(epsilon) => + !best_score.strict_threshold_better(our_score, epsilon), + } +} + +/// Reads all current solutions and checks the scores according to the `SubmissionStrategy`. +async fn ensure_strategy_met( + rpc: &SharedRpcClient, + at: Hash, + score: sp_npos_elections::ElectionScore, + strategy: SubmissionStrategy, + max_submissions: u32, +) -> Result<(), Error> { + // don't care about current scores. + if matches!(strategy, SubmissionStrategy::Always) { + return Ok(()) + } + + let indices_key = StorageKey(EPM::SignedSubmissionIndices::::hashed_key().to_vec()); + + let indices: SubmissionIndicesOf = rpc + .get_storage_and_decode(&indices_key, Some(at)) + .await + .map_err::, _>(Into::into)? + .unwrap_or_default(); + + if indices.len() >= max_submissions as usize { + log::debug!(target: LOG_TARGET, "The submissions queue is full"); + } + + // default score is all zeros, any score is better than it. + let best_score = indices.last().map(|(score, _, _)| *score).unwrap_or_default(); + log::debug!(target: LOG_TARGET, "best onchain score is {:?}", best_score); + + if score_passes_strategy(score, best_score, strategy) { + Ok(()) + } else { + Err(Error::StrategyNotSatisfied) + } +} + +async fn get_latest_head( + rpc: &SharedRpcClient, + mode: &str, +) -> Result> { + if mode == "head" { + match rpc.block_hash(None).await { + Ok(Some(hash)) => Ok(hash), + Ok(None) => Err(Error::Other("Best head not found".into())), + Err(e) => Err(e.into()), + } + } else { + rpc.finalized_head().await.map_err(Into::into) + } +} + +macro_rules! monitor_cmd_for { ($runtime:tt) => { paste::paste! { + + /// The monitor command. + pub(crate) async fn []( + rpc: SharedRpcClient, + config: MonitorConfig, + signer: Signer, + ) -> Result<(), Error<$crate::[<$runtime _runtime_exports>]::Runtime>> { + use $crate::[<$runtime _runtime_exports>]::*; + type StakingMinerError = Error<$crate::[<$runtime _runtime_exports>]::Runtime>; + + let heads_subscription = || + if config.listen == "head" { + rpc.subscribe_new_heads() + } else { + rpc.subscribe_finalized_heads() + }; + + let mut subscription = heads_subscription().await?; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let submit_lock = Arc::new(Mutex::new(())); + + loop { + let at = tokio::select! { + maybe_rp = subscription.next() => { + match maybe_rp { + Some(Ok(r)) => r, + Some(Err(e)) => { + log::error!(target: LOG_TARGET, "subscription failed to decode Header {:?}, this is bug please file an issue", e); + return Err(e.into()); + } + // The subscription was dropped, should only happen if: + // - the connection was closed. + // - the subscription could not keep up with the server. + None => { + log::warn!(target: LOG_TARGET, "subscription to `subscribeNewHeads/subscribeFinalizedHeads` terminated. Retrying.."); + subscription = heads_subscription().await?; + continue + } + } + }, + maybe_err = rx.recv() => { + match maybe_err { + Some(err) => return Err(err), + None => unreachable!("at least one sender kept in the main loop should always return Some; qed"), + } + } + }; + + // Spawn task and non-recoverable errors are sent back to the main task + // such as if the connection has been closed. + tokio::spawn( + send_and_watch_extrinsic(rpc.clone(), tx.clone(), at, signer.clone(), config.clone(), submit_lock.clone()) + ); + } + + /// Construct extrinsic at given block and watch it. + async fn send_and_watch_extrinsic( + rpc: SharedRpcClient, + tx: mpsc::UnboundedSender, + at: Header, + signer: Signer, + config: MonitorConfig, + submit_lock: Arc>, + ) { + + async fn flatten( + handle: tokio::task::JoinHandle> + ) -> Result { + match handle.await { + Ok(Ok(result)) => Ok(result), + Ok(Err(err)) => Err(err), + Err(err) => panic!("tokio spawn task failed; kill task: {:?}", err), + } + } + + let hash = at.hash(); + log::trace!(target: LOG_TARGET, "new event at #{:?} ({:?})", at.number, hash); + + // block on this because if this fails there is no way to recover from + // that error i.e, upgrade/downgrade required. + if let Err(err) = crate::check_versions::(&rpc, false).await { + let _ = tx.send(err.into()); + return; + } + + let rpc1 = rpc.clone(); + let rpc2 = rpc.clone(); + let account = signer.account.clone(); + + let signed_phase_fut = tokio::spawn(async move { + ensure_signed_phase::(&rpc1, hash).await + }); + + tokio::time::sleep(std::time::Duration::from_secs(config.delay as u64)).await; + + let no_prev_sol_fut = tokio::spawn(async move { + ensure_no_previous_solution::(&rpc2, hash, &account).await + }); + + // Run the calls in parallel and return once all has completed or any failed. + if let Err(err) = tokio::try_join!(flatten(signed_phase_fut), flatten(no_prev_sol_fut)) { + log::debug!(target: LOG_TARGET, "Skipping block {}; {}", at.number, err); + return; + } + + let _lock = submit_lock.lock().await; + + let mut ext = match crate::create_election_ext::(rpc.clone(), Some(hash), vec![]).await { + Ok(ext) => ext, + Err(err) => { + log::debug!(target: LOG_TARGET, "Skipping block {}; {}", at.number, err); + return; + } + }; + + // mine a solution, and run feasibility check on it as well. + let raw_solution = match crate::mine_with::(&config.solver, &mut ext, true) { + Ok(r) => r, + Err(err) => { + let _ = tx.send(err.into()); + return; + } + }; + + let score = raw_solution.score; + log::info!(target: LOG_TARGET, "mined solution with {:?}", score); + + let nonce = match crate::get_account_info::(&rpc, &signer.account, Some(hash)).await { + Ok(maybe_account) => { + let acc = maybe_account.expect(crate::signer::SIGNER_ACCOUNT_WILL_EXIST); + acc.nonce + } + Err(err) => { + let _ = tx.send(err); + return; + } + }; + + let tip = 0 as Balance; + let period = ::BlockHashCount::get() / 2; + let current_block = at.number.saturating_sub(1); + let era = sp_runtime::generic::Era::mortal(period.into(), current_block.into()); + + log::trace!( + target: LOG_TARGET, "transaction mortality: {:?} -> {:?}", + era.birth(current_block.into()), + era.death(current_block.into()), + ); + + let extrinsic = ext.execute_with(|| create_uxt(raw_solution, signer.clone(), nonce, tip, era)); + let bytes = sp_core::Bytes(extrinsic.encode()); + + let rpc1 = rpc.clone(); + let rpc2 = rpc.clone(); + let rpc3 = rpc.clone(); + + let latest_head = match get_latest_head::(&rpc, &config.listen).await { + Ok(hash) => hash, + Err(e) => { + log::debug!(target: LOG_TARGET, "Skipping to submit at block {}; {}", at.number, e); + return; + } + }; + + let ensure_strategy_met_fut = tokio::spawn(async move { + ensure_strategy_met::( + &rpc1, + latest_head, + score, + config.submission_strategy, + SignedMaxSubmissions::get() + ).await + }); + + let ensure_signed_phase_fut = tokio::spawn(async move { + ensure_signed_phase::(&rpc2, latest_head).await + }); + + let account = signer.account.clone(); + let no_prev_sol_fut = tokio::spawn(async move { + ensure_no_previous_solution::(&rpc3, latest_head, &account).await + }); + + // Run the calls in parallel and return once all has completed or any failed. + if let Err(err) = tokio::try_join!( + flatten(ensure_strategy_met_fut), + flatten(ensure_signed_phase_fut), + flatten(no_prev_sol_fut), + ) { + log::debug!(target: LOG_TARGET, "Skipping to submit at block {}; {}", at.number, err); + return; + } + + let mut tx_subscription = match rpc.watch_extrinsic(&bytes).await { + Ok(sub) => sub, + Err(RpcError::RestartNeeded(e)) => { + let _ = tx.send(RpcError::RestartNeeded(e).into()); + return + }, + Err(why) => { + // This usually happens when we've been busy with mining for a few blocks, and + // now we're receiving the subscriptions of blocks in which we were busy. In + // these blocks, we still don't have a solution, so we re-compute a new solution + // and submit it with an outdated `Nonce`, which yields most often `Stale` + // error. NOTE: to improve this overall, and to be able to introduce an array of + // other fancy features, we should make this multi-threaded and do the + // computation outside of this callback. + log::warn!( + target: LOG_TARGET, + "failing to submit a transaction {:?}. ignore block: {}", + why, at.number + ); + return; + }, + }; + + while let Some(rp) = tx_subscription.next().await { + let status_update = match rp { + Ok(r) => r, + Err(e) => { + log::error!(target: LOG_TARGET, "subscription failed to decode TransactionStatus {:?}, this is a bug please file an issue", e); + let _ = tx.send(e.into()); + return; + }, + }; + + log::trace!(target: LOG_TARGET, "status update {:?}", status_update); + match status_update { + TransactionStatus::Ready | + TransactionStatus::Broadcast(_) | + TransactionStatus::Future => continue, + TransactionStatus::InBlock((hash, _)) => { + log::info!(target: LOG_TARGET, "included at {:?}", hash); + let key = StorageKey( + frame_support::storage::storage_prefix(b"System", b"Events").to_vec(), + ); + + let events = match rpc.get_storage_and_decode::< + Vec::Hash>>, + >(&key, Some(hash)) + .await { + Ok(rp) => rp.unwrap_or_default(), + Err(RpcHelperError::JsonRpsee(RpcError::RestartNeeded(e))) => { + let _ = tx.send(RpcError::RestartNeeded(e).into()); + return; + } + // Decoding or other RPC error => just terminate the task. + Err(e) => { + log::warn!(target: LOG_TARGET, "get_storage [key: {:?}, hash: {:?}] failed: {:?}; skip block: {}", + key, hash, e, at.number + ); + return; + } + }; + + log::info!(target: LOG_TARGET, "events at inclusion {:?}", events); + }, + TransactionStatus::Retracted(hash) => { + log::info!(target: LOG_TARGET, "Retracted at {:?}", hash); + }, + TransactionStatus::Finalized((hash, _)) => { + log::info!(target: LOG_TARGET, "Finalized at {:?}", hash); + break + }, + _ => { + log::warn!( + target: LOG_TARGET, + "Stopping listen due to other status {:?}", + status_update + ); + break + }, + }; + } + } + } +}}} + +monitor_cmd_for!(polkadot); +monitor_cmd_for!(kusama); +monitor_cmd_for!(westend); + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + fn score_passes_strategy_works() { + let s = |x| sp_npos_elections::ElectionScore { minimal_stake: x, ..Default::default() }; + let two = Perbill::from_percent(2); + + // anything passes Always + assert!(score_passes_strategy(s(0), s(0), SubmissionStrategy::Always)); + assert!(score_passes_strategy(s(5), s(0), SubmissionStrategy::Always)); + assert!(score_passes_strategy(s(5), s(10), SubmissionStrategy::Always)); + + // if leading + assert!(score_passes_strategy(s(0), s(0), SubmissionStrategy::IfLeading)); + assert!(score_passes_strategy(s(1), s(0), SubmissionStrategy::IfLeading)); + assert!(score_passes_strategy(s(2), s(0), SubmissionStrategy::IfLeading)); + assert!(!score_passes_strategy(s(5), s(10), SubmissionStrategy::IfLeading)); + assert!(!score_passes_strategy(s(9), s(10), SubmissionStrategy::IfLeading)); + assert!(score_passes_strategy(s(10), s(10), SubmissionStrategy::IfLeading)); + + // if better by 2% + assert!(!score_passes_strategy(s(50), s(100), SubmissionStrategy::ClaimBetterThan(two))); + assert!(!score_passes_strategy(s(100), s(100), SubmissionStrategy::ClaimBetterThan(two))); + assert!(!score_passes_strategy(s(101), s(100), SubmissionStrategy::ClaimBetterThan(two))); + assert!(!score_passes_strategy(s(102), s(100), SubmissionStrategy::ClaimBetterThan(two))); + assert!(score_passes_strategy(s(103), s(100), SubmissionStrategy::ClaimBetterThan(two))); + assert!(score_passes_strategy(s(150), s(100), SubmissionStrategy::ClaimBetterThan(two))); + + // if no less than 2% worse + assert!(!score_passes_strategy(s(50), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(!score_passes_strategy(s(97), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(98), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(99), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(100), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(101), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(102), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(103), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + assert!(score_passes_strategy(s(150), s(100), SubmissionStrategy::ClaimNoWorseThan(two))); + } +} diff --git a/polkadot/utils/staking-miner/src/opts.rs b/polkadot/utils/staking-miner/src/opts.rs new file mode 100644 index 000000000000..4cf4d0a76519 --- /dev/null +++ b/polkadot/utils/staking-miner/src/opts.rs @@ -0,0 +1,366 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +use crate::prelude::*; +use clap::Parser; +use sp_runtime::Perbill; +use std::str::FromStr; + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +#[command(author, version, about)] +pub(crate) struct Opt { + /// The `ws` node to connect to. + #[arg(long, short, default_value = DEFAULT_URI, env = "URI", global = true)] + pub uri: String, + + /// WS connection timeout in number of seconds. + #[arg(long, default_value_t = 60)] + pub connection_timeout: usize, + + /// WS request timeout in number of seconds. + #[arg(long, default_value_t = 60 * 10)] + pub request_timeout: usize, + + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) enum Command { + /// Monitor for the phase being signed, then compute. + Monitor(MonitorConfig), + + /// Just compute a solution now, and don't submit it. + DryRun(DryRunConfig), + + /// Provide a solution that can be submitted to the chain as an emergency response. + EmergencySolution(EmergencySolutionConfig), + + /// Return information about the current version + Info(InfoOpts), +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct MonitorConfig { + /// The path to a file containing the seed of the account. If the file is not found, the seed + /// is used as-is. + /// + /// Can also be provided via the `SEED` environment variable. + /// + /// WARNING: Don't use an account with a large stash for this. Based on how the bot is + /// configured, it might re-try and lose funds through transaction fees/deposits. + #[arg(long, short, env = "SEED")] + pub seed_or_path: String, + + /// They type of event to listen to. + /// + /// Typically, finalized is safer and there is no chance of anything going wrong, but it can be + /// slower. It is recommended to use finalized, if the duration of the signed phase is longer + /// than the the finality delay. + #[arg(long, default_value = "head", value_parser = ["head", "finalized"])] + pub listen: String, + + /// The solver algorithm to use. + #[command(subcommand)] + pub solver: Solver, + + /// Submission strategy to use. + /// + /// Possible options: + /// + /// `--submission-strategy if-leading`: only submit if leading. + /// + /// `--submission-strategy always`: always submit. + /// + /// `--submission-strategy "percent-better percent"`: submit if the submission is `n` percent + /// better. + /// + /// `--submission-strategy "no-worse-than percent"`: submit if submission is no more than + /// `n` percent worse. + #[clap(long, default_value = "if-leading")] + pub submission_strategy: SubmissionStrategy, + + /// Delay in number seconds to wait until starting mining a solution. + /// + /// At every block when a solution is attempted + /// a delay can be enforced to avoid submitting at + /// "same time" and risk potential races with other miners. + /// + /// When this is enabled and there are competing solutions, your solution might not be + /// submitted if the scores are equal. + #[arg(long, default_value_t = 0)] + pub delay: usize, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct DryRunConfig { + /// The path to a file containing the seed of the account. If the file is not found, the seed + /// is used as-is. + /// + /// Can also be provided via the `SEED` environment variable. + /// + /// WARNING: Don't use an account with a large stash for this. Based on how the bot is + /// configured, it might re-try and lose funds through transaction fees/deposits. + #[arg(long, short, env = "SEED")] + pub seed_or_path: String, + + /// The block hash at which scraping happens. If none is provided, the latest head is used. + #[arg(long)] + pub at: Option, + + /// The solver algorithm to use. + #[command(subcommand)] + pub solver: Solver, + + /// Force create a new snapshot, else expect one to exist onchain. + #[arg(long)] + pub force_snapshot: bool, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct EmergencySolutionConfig { + /// The block hash at which scraping happens. If none is provided, the latest head is used. + #[arg(long)] + pub at: Option, + + /// The solver algorithm to use. + #[command(subcommand)] + pub solver: Solver, + + /// The number of top backed winners to take. All are taken, if not provided. + pub take: Option, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct InfoOpts { + /// Serialize the output as json + #[arg(long, short)] + pub json: bool, +} + +/// Submission strategy to use. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum SubmissionStrategy { + /// Always submit. + Always, + /// Only submit if at the time, we are the best (or equal to it). + IfLeading, + /// Submit if we are no worse than `Perbill` worse than the best. + ClaimNoWorseThan(Perbill), + /// Submit if we are leading, or if the solution that's leading is more that the given + /// `Perbill` better than us. This helps detect obviously fake solutions and still combat them. + ClaimBetterThan(Perbill), +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) enum Solver { + SeqPhragmen { + #[arg(long, default_value_t = 10)] + iterations: usize, + }, + PhragMMS { + #[arg(long, default_value_t = 10)] + iterations: usize, + }, +} + +/// Custom `impl` to parse `SubmissionStrategy` from CLI. +/// +/// Possible options: +/// * --submission-strategy if-leading: only submit if leading +/// * --submission-strategy always: always submit +/// * --submission-strategy "percent-better percent": submit if submission is `n` percent better. +/// * --submission-strategy "no-worse-than percent": submit if submission is no more than `n` +/// percent worse. +impl FromStr for SubmissionStrategy { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + let res = if s == "if-leading" { + Self::IfLeading + } else if s == "always" { + Self::Always + } else if let Some(percent) = s.strip_prefix("no-worse-than ") { + let percent: u32 = percent.parse().map_err(|e| format!("{:?}", e))?; + Self::ClaimNoWorseThan(Perbill::from_percent(percent)) + } else if let Some(percent) = s.strip_prefix("percent-better ") { + let percent: u32 = percent.parse().map_err(|e| format!("{:?}", e))?; + Self::ClaimBetterThan(Perbill::from_percent(percent)) + } else { + return Err(s.into()) + }; + Ok(res) + } +} + +#[cfg(test)] +mod test_super { + use super::*; + + #[test] + fn cli_monitor_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "monitor", + "--seed-or-path", + "//Alice", + "--listen", + "head", + "--delay", + "12", + "seq-phragmen", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + connection_timeout: 60, + request_timeout: 10 * 60, + command: Command::Monitor(MonitorConfig { + seed_or_path: "//Alice".to_string(), + listen: "head".to_string(), + solver: Solver::SeqPhragmen { iterations: 10 }, + submission_strategy: SubmissionStrategy::IfLeading, + delay: 12, + }), + } + ); + } + + #[test] + fn cli_dry_run_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "dry-run", + "--seed-or-path", + "//Alice", + "phrag-mms", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + connection_timeout: 60, + request_timeout: 10 * 60, + command: Command::DryRun(DryRunConfig { + seed_or_path: "//Alice".to_string(), + at: None, + solver: Solver::PhragMMS { iterations: 10 }, + force_snapshot: false, + }), + } + ); + } + + #[test] + fn cli_emergency_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "emergency-solution", + "99", + "phrag-mms", + "--iterations", + "1337", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + connection_timeout: 60, + request_timeout: 10 * 60, + command: Command::EmergencySolution(EmergencySolutionConfig { + take: Some(99), + at: None, + solver: Solver::PhragMMS { iterations: 1337 } + }), + } + ); + } + + #[test] + fn cli_info_works() { + let opt = Opt::try_parse_from([env!("CARGO_PKG_NAME"), "--uri", "hi", "info"]).unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + connection_timeout: 60, + request_timeout: 10 * 60, + command: Command::Info(InfoOpts { json: false }) + } + ); + } + + #[test] + fn cli_request_conn_timeout_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "--request-timeout", + "10", + "--connection-timeout", + "9", + "info", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + connection_timeout: 9, + request_timeout: 10, + command: Command::Info(InfoOpts { json: false }) + } + ); + } + + #[test] + fn submission_strategy_from_str_works() { + use std::str::FromStr; + + assert_eq!(SubmissionStrategy::from_str("if-leading"), Ok(SubmissionStrategy::IfLeading)); + assert_eq!(SubmissionStrategy::from_str("always"), Ok(SubmissionStrategy::Always)); + assert_eq!( + SubmissionStrategy::from_str(" percent-better 99 "), + Ok(SubmissionStrategy::ClaimBetterThan(Perbill::from_percent(99))) + ); + } +} diff --git a/polkadot/utils/staking-miner/src/prelude.rs b/polkadot/utils/staking-miner/src/prelude.rs new file mode 100644 index 000000000000..fb701ece2384 --- /dev/null +++ b/polkadot/utils/staking-miner/src/prelude.rs @@ -0,0 +1,55 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Types that we don't fetch from a particular runtime and just assume that they are constant all +//! of the place. +//! +//! It is actually easy to convert the rest as well, but it'll be a lot of noise in our codebase, +//! needing to sprinkle `any_runtime` in a few extra places. + +/// The account id type. +pub type AccountId = core_primitives::AccountId; +/// The block number type. +pub type BlockNumber = core_primitives::BlockNumber; +/// The balance type. +pub type Balance = core_primitives::Balance; +/// Index of a transaction in the chain. +pub type Nonce = core_primitives::Nonce; +/// The hash type. We re-export it here, but we can easily get it from block as well. +pub type Hash = core_primitives::Hash; +/// The header type. We re-export it here, but we can easily get it from block as well. +pub type Header = core_primitives::Header; +/// The block type. +pub type Block = core_primitives::Block; + +pub use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; + +/// Default URI to connect to. +pub const DEFAULT_URI: &str = "wss://rpc.polkadot.io:443"; +/// The logging target. +pub const LOG_TARGET: &str = "staking-miner"; + +/// The election provider pallet. +pub use pallet_election_provider_multi_phase as EPM; + +/// The externalities type. +pub type Ext = sp_state_machine::TestExternalities>; + +/// The key pair type being used. We "strongly" assume sr25519 for simplicity. +pub type Pair = sp_core::sr25519::Pair; + +/// A dynamic token type used to represent account balances. +pub type Token = sub_tokens::dynamic::DynamicToken; diff --git a/polkadot/utils/staking-miner/src/rpc.rs b/polkadot/utils/staking-miner/src/rpc.rs new file mode 100644 index 000000000000..2d25616e2a17 --- /dev/null +++ b/polkadot/utils/staking-miner/src/rpc.rs @@ -0,0 +1,182 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! JSON-RPC related types and helpers. + +use super::*; +use jsonrpsee::{ + core::{Error as RpcError, RpcResult}, + proc_macros::rpc, +}; +use pallet_transaction_payment::RuntimeDispatchInfo; +use sc_transaction_pool_api::TransactionStatus; +use sp_core::{storage::StorageKey, Bytes}; +use sp_version::RuntimeVersion; +use std::{future::Future, time::Duration}; + +#[derive(frame_support::DebugNoBound, thiserror::Error)] +pub(crate) enum RpcHelperError { + JsonRpsee(#[from] jsonrpsee::core::Error), + Codec(#[from] codec::Error), +} + +impl std::fmt::Display for RpcHelperError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ::fmt(self, f) + } +} + +#[rpc(client)] +pub trait RpcApi { + /// Fetch system name. + #[method(name = "system_chain")] + async fn system_chain(&self) -> RpcResult; + + /// Fetch a storage key. + #[method(name = "state_getStorage")] + async fn storage(&self, key: &StorageKey, hash: Option) -> RpcResult>; + + /// Fetch the runtime version. + #[method(name = "state_getRuntimeVersion")] + async fn runtime_version(&self, at: Option) -> RpcResult; + + /// Fetch the payment query info. + #[method(name = "payment_queryInfo")] + async fn payment_query_info( + &self, + encoded_xt: &Bytes, + at: Option<&Hash>, + ) -> RpcResult>; + + /// Dry run an extrinsic at a given block. Return SCALE encoded + /// [`sp_runtime::ApplyExtrinsicResult`]. + #[method(name = "system_dryRun")] + async fn dry_run(&self, extrinsic: &Bytes, at: Option) -> RpcResult; + + /// Get hash of the n-th block in the canon chain. + /// + /// By default returns latest block hash. + #[method(name = "chain_getBlockHash", aliases = ["chain_getHead"], blocking)] + fn block_hash(&self, hash: Option) -> RpcResult>; + + /// Get hash of the last finalized block in the canon chain. + #[method(name = "chain_getFinalizedHead", aliases = ["chain_getFinalisedHead"], blocking)] + fn finalized_head(&self) -> RpcResult; + + /// Submit an extrinsic to watch. + /// + /// See [`TransactionStatus`](sc_transaction_pool_api::TransactionStatus) for details on + /// transaction life cycle. + #[subscription( + name = "author_submitAndWatchExtrinsic" => "author_extrinsicUpdate", + unsubscribe = "author_unwatchExtrinsic", + item = TransactionStatus + )] + fn watch_extrinsic(&self, bytes: &Bytes); + + /// New head subscription. + #[subscription( + name = "chain_subscribeNewHeads" => "newHead", + unsubscribe = "chain_unsubscribeNewHeads", + item = Header + )] + fn subscribe_new_heads(&self); + + /// Finalized head subscription. + #[subscription( + name = "chain_subscribeFinalizedHeads" => "chain_finalizedHead", + unsubscribe = "chain_unsubscribeFinalizedHeads", + item = Header + )] + fn subscribe_finalized_heads(&self); +} + +type Uri = String; + +/// Wraps a shared web-socket JSON-RPC client that can be cloned. +#[derive(Clone, Debug)] +pub(crate) struct SharedRpcClient(Arc, Uri); + +impl Deref for SharedRpcClient { + type Target = WsClient; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl SharedRpcClient { + /// Get the URI of the client. + pub fn uri(&self) -> &str { + &self.1 + } + + /// Create a new shared JSON-RPC web-socket client. + pub(crate) async fn new( + uri: &str, + connection_timeout: Duration, + request_timeout: Duration, + ) -> Result { + let client = WsClientBuilder::default() + .connection_timeout(connection_timeout) + .max_request_body_size(u32::MAX) + .request_timeout(request_timeout) + .max_concurrent_requests(u32::MAX as usize) + .build(uri) + .await?; + Ok(Self(Arc::new(client), uri.to_owned())) + } + + /// Get a storage item and decode it as `T`. + /// + /// # Return value: + /// + /// The function returns: + /// + /// * `Ok(Some(val))` if successful. + /// * `Ok(None)` if the storage item was not found. + /// * `Err(e)` if the JSON-RPC call failed. + pub(crate) async fn get_storage_and_decode<'a, T: codec::Decode>( + &self, + key: &StorageKey, + hash: Option, + ) -> Result, RpcHelperError> { + if let Some(bytes) = self.storage(key, hash).await? { + let decoded = ::decode(&mut &*bytes.0) + .map_err::(Into::into)?; + Ok(Some(decoded)) + } else { + Ok(None) + } + } +} + +/// Takes a future that returns `Bytes` and tries to decode those bytes into the type `Dec`. +/// Warning: don't use for storage, it will fail for non-existent storage items. +/// +/// # Return value: +/// +/// The function returns: +/// +/// * `Ok(val)` if successful. +/// * `Err(RpcHelperError::JsonRpsee)` if the JSON-RPC call failed. +/// * `Err(RpcHelperError::Codec)` if `Bytes` could not be decoded. +pub(crate) async fn await_request_and_decode<'a, Dec: codec::Decode>( + req: impl Future>, +) -> Result { + let bytes = req.await?; + Dec::decode(&mut &*bytes.0).map_err::(Into::into) +} diff --git a/polkadot/utils/staking-miner/src/runtime_versions.rs b/polkadot/utils/staking-miner/src/runtime_versions.rs new file mode 100644 index 000000000000..38af05ead241 --- /dev/null +++ b/polkadot/utils/staking-miner/src/runtime_versions.rs @@ -0,0 +1,90 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +use sp_version::RuntimeVersion; +use std::fmt; + +#[derive(Debug, serde::Serialize)] +pub(crate) struct RuntimeWrapper<'a>(pub &'a RuntimeVersion); + +impl<'a> fmt::Display for RuntimeWrapper<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let width = 16; + + writeln!( + f, + r#" impl_name : {impl_name:>width$} + spec_name : {spec_name:>width$} + spec_version : {spec_version:>width$} + transaction_version : {transaction_version:>width$} + impl_version : {impl_version:>width$} + authoringVersion : {authoring_version:>width$} + state_version : {state_version:>width$}"#, + spec_name = self.0.spec_name.to_string(), + impl_name = self.0.impl_name.to_string(), + spec_version = self.0.spec_version, + impl_version = self.0.impl_version, + authoring_version = self.0.authoring_version, + transaction_version = self.0.transaction_version, + state_version = self.0.state_version, + ) + } +} + +impl<'a> From<&'a RuntimeVersion> for RuntimeWrapper<'a> { + fn from(r: &'a RuntimeVersion) -> Self { + RuntimeWrapper(r) + } +} + +#[derive(Debug, serde::Serialize)] +pub(crate) struct RuntimeVersions<'a> { + /// The `RuntimeVersion` linked in the staking-miner + pub linked: RuntimeWrapper<'a>, + + /// The `RuntimeVersion` reported by the node we connect to via RPC + pub remote: RuntimeWrapper<'a>, + + /// This `bool` reports whether both remote and linked `RuntimeVersion` are compatible + /// and if the staking-miner is expected to work properly against the remote runtime + compatible: bool, +} + +impl<'a> RuntimeVersions<'a> { + pub fn new( + remote_runtime_version: &'a RuntimeVersion, + linked_runtime_version: &'a RuntimeVersion, + ) -> Self { + Self { + remote: remote_runtime_version.into(), + linked: linked_runtime_version.into(), + compatible: are_runtimes_compatible(remote_runtime_version, linked_runtime_version), + } + } +} + +/// Check whether runtimes are compatible. Currently we only support equality. +fn are_runtimes_compatible(r1: &RuntimeVersion, r2: &RuntimeVersion) -> bool { + r1 == r2 +} + +impl<'a> fmt::Display for RuntimeVersions<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let _ = write!(f, "- linked:\n{}", self.linked); + let _ = write!(f, "- remote :\n{}", self.remote); + write!(f, "Compatible: {}", if self.compatible { "YES" } else { "NO" }) + } +} diff --git a/polkadot/utils/staking-miner/src/signer.rs b/polkadot/utils/staking-miner/src/signer.rs new file mode 100644 index 000000000000..e6677ccd3a66 --- /dev/null +++ b/polkadot/utils/staking-miner/src/signer.rs @@ -0,0 +1,84 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Wrappers around creating a signer account. + +use crate::{prelude::*, rpc::SharedRpcClient, AccountId, Error, Nonce, Pair, LOG_TARGET}; +use frame_system::AccountInfo; +use sp_core::{crypto::Pair as _, storage::StorageKey}; + +pub(crate) const SIGNER_ACCOUNT_WILL_EXIST: &str = + "signer account is checked to exist upon startup; it can only die if it transfers funds out \ + of it, or get slashed. If it does not exist at this point, it is likely due to a bug, or the \ + signer got slashed. Terminating."; + +/// Some information about the signer. Redundant at this point, but makes life easier. +#[derive(Clone)] +pub(crate) struct Signer { + /// The account id. + pub(crate) account: AccountId, + + /// The full crypto key-pair. + pub(crate) pair: Pair, +} + +pub(crate) async fn get_account_info + EPM::Config>( + rpc: &SharedRpcClient, + who: &T::AccountId, + maybe_at: Option, +) -> Result>, Error> { + rpc.get_storage_and_decode::>( + &StorageKey(>::hashed_key_for(&who)), + maybe_at, + ) + .await + .map_err(Into::into) +} + +/// Read the signer account's URI +pub(crate) async fn signer_uri_from_string< + T: frame_system::Config< + AccountId = AccountId, + Nonce = Nonce, + AccountData = pallet_balances::AccountData, + Hash = Hash, + > + EPM::Config, +>( + mut seed_or_path: &str, + client: &SharedRpcClient, +) -> Result> { + seed_or_path = seed_or_path.trim(); + + let seed = match std::fs::read(seed_or_path) { + Ok(s) => String::from_utf8(s).map_err(|_| Error::::AccountDoesNotExists)?, + Err(_) => seed_or_path.to_string(), + }; + let seed = seed.trim(); + + let pair = Pair::from_string(seed, None)?; + let account = T::AccountId::from(pair.public()); + let _info = get_account_info::(client, &account, None) + .await? + .ok_or(Error::::AccountDoesNotExists)?; + log::info!( + target: LOG_TARGET, + "loaded account {:?}, free: {:?}, info: {:?}", + &account, + Token::from(_info.data.free), + _info + ); + Ok(Signer { account, pair }) +} diff --git a/polkadot/utils/staking-miner/tests/cli.rs b/polkadot/utils/staking-miner/tests/cli.rs new file mode 100644 index 000000000000..1ced1239e553 --- /dev/null +++ b/polkadot/utils/staking-miner/tests/cli.rs @@ -0,0 +1,49 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +use assert_cmd::{cargo::cargo_bin, Command}; +use serde_json::{Result, Value}; + +#[test] +fn cli_version_works() { + let crate_name = env!("CARGO_PKG_NAME"); + let output = Command::new(cargo_bin(crate_name)).arg("--version").output().unwrap(); + + assert!(output.status.success(), "command returned with non-success exit code"); + let version = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + + assert_eq!(version, format!("{} {}", crate_name, env!("CARGO_PKG_VERSION"))); +} + +#[test] +fn cli_info_works() { + let crate_name = env!("CARGO_PKG_NAME"); + let output = Command::new(cargo_bin(crate_name)) + .arg("info") + .arg("--json") + .env("RUST_LOG", "none") + .output() + .unwrap(); + + assert!(output.status.success(), "command returned with non-success exit code"); + let info = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + let v: Result = serde_json::from_str(&info); + let v = v.unwrap(); + assert!(!v["builtin"].to_string().is_empty()); + assert!(!v["builtin"]["spec_name"].to_string().is_empty()); + assert!(!v["builtin"]["spec_version"].to_string().is_empty()); + assert!(!v["remote"].to_string().is_empty()); +} diff --git a/substrate/frame/election-provider-multi-phase/src/lib.rs b/substrate/frame/election-provider-multi-phase/src/lib.rs index 05f9b24f8f9c..cf52b6b52898 100644 --- a/substrate/frame/election-provider-multi-phase/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/src/lib.rs @@ -149,8 +149,7 @@ //! while this binary lives in the Polkadot repository, this particular subcommand of it can work //! against any substrate-based chain. //! -//! See the [`staking-miner`](https://github.com/paritytech/staking-miner-v2) docs for more -//! information. +//! See the `staking-miner` documentation in the Polkadot repository for more information. //! //! ## Feasible Solution (correct solution) //!