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