Delete staking miner (#1480)

* Delete staking miner

New repo should be used instead https://github.com/paritytech/staking-miner-v2

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

* Remove staking-miner CI jobs

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>

---------

Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
This commit is contained in:
Oliver Tale-Yazdi
2023-09-11 17:54:27 +02:00
committed by GitHub
parent 6df8909d94
commit 4b8bd9060e
27 changed files with 8 additions and 2722 deletions
@@ -47,7 +47,6 @@ on:
type: choice
options:
- polkadot
- staking-miner
- polkadot-parachain
permissions:
@@ -158,18 +157,6 @@ jobs:
echo "tag=latest" >> $GITHUB_OUTPUT
echo "release=${release}" >> $GITHUB_OUTPUT
- 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 }}
OWNER: ${{ env.DOCKER_OWNER }}
TAGS: ${{ join(steps.fetch_rc_refs.outputs.*, ',') || join(steps.fetch_release_refs.outputs.*, ',') }}
run: |
ls -al
echo "Building container for $BINARY"
./docker/scripts/build-injected.sh
- name: Build Injected Container image for polkadot-parachain
if: ${{ env.BINARY == 'polkadot-parachain' }}
env:
+1 -21
View File
@@ -77,26 +77,6 @@ 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:
@@ -358,7 +338,7 @@ build-subkey-linux:
extends: .build-subkey
# DAG
needs:
- job: build-staking-miner
- job: build-malus
artifacts: false
# tbd
# build-subkey-macos:
-18
View File
@@ -328,24 +328,6 @@ 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:
Generated
+5 -115
View File
@@ -494,7 +494,7 @@ dependencies = [
"ark-ff",
"ark-std",
"tracing",
"tracing-subscriber 0.2.25",
"tracing-subscriber",
]
[[package]]
@@ -4838,12 +4838,6 @@ 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,15 +7684,6 @@ 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"
@@ -8542,16 +8527,6 @@ 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"
@@ -8771,12 +8746,6 @@ 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"
@@ -15117,7 +15086,7 @@ dependencies = [
"substrate-test-runtime",
"tempfile",
"tracing",
"tracing-subscriber 0.2.25",
"tracing-subscriber",
"wat",
]
@@ -15815,7 +15784,7 @@ dependencies = [
"thiserror",
"tracing",
"tracing-log",
"tracing-subscriber 0.2.25",
"tracing-subscriber",
]
[[package]]
@@ -16394,18 +16363,6 @@ 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"
@@ -17525,7 +17482,7 @@ dependencies = [
"sp-std",
"tracing",
"tracing-core",
"tracing-subscriber 0.2.25",
"tracing-subscriber",
]
[[package]]
@@ -17807,47 +17764,6 @@ 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"
@@ -18022,14 +17938,6 @@ 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"
@@ -19111,7 +19019,7 @@ dependencies = [
"ansi_term",
"chrono",
"lazy_static",
"matchers 0.0.1",
"matchers",
"parking_lot 0.11.2",
"regex",
"serde",
@@ -19125,24 +19033,6 @@ 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.37.0"
-1
View File
@@ -172,7 +172,6 @@ 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",
@@ -1,46 +0,0 @@
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" ]
@@ -1,43 +0,0 @@
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" ]
-37
View File
@@ -1,37 +0,0 @@
# 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
```
@@ -1,13 +0,0 @@
#!/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
-13
View File
@@ -1,13 +0,0 @@
#!/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"
@@ -1,3 +0,0 @@
# Staking-miner Docker image
## [GitHub](https://github.com/paritytech/polkadot/tree/master/utils/staking-miner)
@@ -1,43 +0,0 @@
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" ]
@@ -1,18 +0,0 @@
#!/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
-2
View File
@@ -1,2 +0,0 @@
*.key
*.bin
-54
View File
@@ -1,54 +0,0 @@
[[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"
-81
View File
@@ -1,81 +0,0 @@
# 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`
-166
View File
@@ -1,166 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! 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<T: EPM::Config>(ext: &mut Ext) -> Result<(), Error<T>> {
ext.execute_with(|| {
if <EPM::Snapshot<T>>::exists() {
log::info!(target: LOG_TARGET, "snapshot already exists.");
Ok(())
} else {
log::info!(target: LOG_TARGET, "creating a fake snapshot now.");
<EPM::Pallet<T>>::create_snapshot().map(|_| ()).map_err(Into::into)
}
})
}
/// Helper method to print the encoded size of the snapshot.
async fn print_info<T: EPM::Config>(
rpc: &SharedRpcClient,
ext: &mut Ext,
raw_solution: &EPM::RawSolution<EPM::SolutionOf<T::MinerConfig>>,
extrinsic: &Bytes,
) where
<T as EPM::Config>::Currency: Currency<T::AccountId, Balance = Balance>,
{
ext.execute_with(|| {
log::info!(
target: LOG_TARGET,
"Snapshot Metadata: {:?}",
<EPM::Pallet<T>>::snapshot_metadata()
);
log::info!(
target: LOG_TARGET,
"Snapshot Encoded Length: {:?}",
<EPM::Pallet<T>>::snapshot()
.expect("snapshot must exist before calling `measure_snapshot_size`")
.encode()
.len()
);
let snapshot_size =
<EPM::Pallet<T>>::snapshot_metadata().expect("snapshot must exist by now; qed.");
let deposit = EPM::Pallet::<T>::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<T: EPM::Config>(ext: &mut Ext, count: usize) {
ext.execute_with(|| {
let mut voters = <EPM::Pallet<T>>::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 [<dry_run_cmd_ $runtime>](
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::<Runtime>(rpc.clone(), config.at, pallets).await?;
if config.force_snapshot {
force_create_snapshot::<Runtime>(&mut ext)?;
};
log::debug!(target: LOG_TARGET, "solving with {:?}", config.solver);
let raw_solution = crate::mine_with::<Runtime>(&config.solver, &mut ext, false)?;
let nonce = crate::get_account_info::<Runtime>(&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::<Runtime>(&rpc, &mut ext, &raw_solution, &bytes).await;
let feasibility_result = ext.execute_with(|| {
EPM::Pallet::<Runtime>::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::<Runtime>::put(EPM::Phase::Signed);
EPM::Pallet::<Runtime>::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::<Error<Runtime>, _>(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);
@@ -1,65 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! 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 [<emergency_solution_cmd_ $runtime>](
client: SharedRpcClient,
config: EmergencySolutionConfig,
) -> Result<(), Error<$crate::[<$runtime _runtime_exports>]::Runtime>> {
use $crate::[<$runtime _runtime_exports>]::*;
let mut ext = crate::create_election_ext::<Runtime>(client, config.at, vec![]).await?;
let raw_solution = crate::mine_with::<Runtime>(&config.solver, &mut ext, false)?;
ext.execute_with(|| {
assert!(EPM::Pallet::<Runtime>::current_phase().is_emergency());
log::info!(target: LOG_TARGET, "mined solution with {:?}", &raw_solution.score);
let ready_solution = EPM::Pallet::<Runtime>::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);
-665
View File
@@ -1,665 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! # 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::[<monitor_cmd_ $runtime>] as monitor_cmd;
pub(crate) use crate::dry_run::[<dry_run_cmd_ $runtime>] as dry_run_cmd;
pub(crate) use crate::emergency_solution::[<emergency_solution_cmd_ $runtime>] as emergency_solution_cmd;
pub(crate) use private::{[<create_uxt_ $runtime>] as create_uxt};
mod private {
use super::*;
pub(crate) fn [<create_uxt_ $runtime>](
raw_solution: EPM::RawSolution<EPM::SolutionOf<Runtime>>,
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::<Runtime>::submit { raw_solution: Box::new(raw_solution) };
let call: RuntimeCall = <EPMCall<Runtime> as std::convert::TryInto<RuntimeCall>>::try_into(local_call)
.expect("election provider pallet must exist in the runtime, thus \
inner call can be converted, qed."
);
let extra: SignedExtra = crate::[<signed_ext_builder_ $runtime>](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 = <Runtime as frame_system::Config>::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::<Runtime>::new(),
frame_system::CheckSpecVersion::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckMortality::<Runtime>::from(era),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
runtime_common::claims::PrevalidateAttests::<Runtime>::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::<Runtime>::new(),
frame_system::CheckSpecVersion::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckMortality::<Runtime>::from(era),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::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::<Runtime>::new(),
frame_system::CheckSpecVersion::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckMortality::<Runtime>::from(era),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::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<T: EPM::Config> {
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<T>),
PalletFeasibility(EPM::FeasibilityError),
AccountDoesNotExists,
IncorrectPhase,
AlreadySubmitted,
VersionMismatch,
StrategyNotSatisfied,
Other(String),
}
impl<T: EPM::Config> From<sp_core::crypto::SecretStringError> for Error<T> {
fn from(e: sp_core::crypto::SecretStringError) -> Error<T> {
Error::Crypto(e)
}
}
impl<T: EPM::Config> From<EPM::unsigned::MinerError> for Error<T> {
fn from(e: EPM::unsigned::MinerError) -> Error<T> {
Error::PalletMiner(e)
}
}
impl<T: EPM::Config> From<EPM::ElectionError<T>> for Error<T> {
fn from(e: EPM::ElectionError<T>) -> Error<T> {
Error::PalletElection(e)
}
}
impl<T: EPM::Config> From<EPM::FeasibilityError> for Error<T> {
fn from(e: EPM::FeasibilityError) -> Error<T> {
Error::PalletFeasibility(e)
}
}
impl<T: EPM::Config> std::fmt::Display for Error<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Error<T> 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<BalancingConfig> = 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<T>(
client: SharedRpcClient,
at: Option<Hash>,
additional: Vec<String>,
) -> Result<Ext, Error<T>>
where
T: EPM::Config,
{
use frame_support::{storage::generator::StorageMap, traits::PalletInfo};
use sp_core::hashing::twox_128;
let mut pallets = vec![<T as frame_system::Config>::PalletInfo::name::<EPM::Pallet<T>>()
.expect("Pallet always has name; qed.")
.to_string()];
pallets.extend(additional);
Builder::<Block>::new()
.mode(Mode::Online(OnlineConfig {
transport: Transport::Uri(client.uri().to_owned()),
at,
pallets,
hashed_prefixes: vec![<frame_system::BlockHash<T>>::prefix_hash()],
hashed_keys: vec![[twox_128(b"System"), twox_128(b"Number")].concat()],
..Default::default()
}))
.build()
.await
.map_err(|why| Error::<T>::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<T, S>(
ext: &mut Ext,
do_feasibility: bool,
) -> Result<EPM::RawSolution<EPM::SolutionOf<T::MinerConfig>>, Error<T>>
where
T: EPM::Config,
S: NposSolver<
Error = <<T as EPM::Config>::Solver as NposSolver>::Error,
AccountId = <<T as EPM::Config>::Solver as NposSolver>::AccountId,
>,
{
ext.execute_with(|| {
let (solution, _) = <EPM::Pallet<T>>::mine_solution().map_err::<Error<T>, _>(Into::into)?;
if do_feasibility {
let _ = <EPM::Pallet<T>>::feasibility_check(
solution.clone(),
EPM::ElectionCompute::Signed,
)?;
}
Ok(solution)
})
}
/// Mine a solution with the given `solver`.
fn mine_with<T>(
solver: &Solver,
ext: &mut Ext,
do_feasibility: bool,
) -> Result<EPM::RawSolution<EPM::SolutionOf<T::MinerConfig>>, Error<T>>
where
T: EPM::Config,
T::Solver: NposSolver<Error = sp_npos_elections::Error>,
{
use frame_election_provider_support::{PhragMMS, SequentialPhragmen};
match solver {
Solver::SeqPhragmen { iterations } => {
BalanceIterations::set(*iterations);
mine_solution::<
T,
SequentialPhragmen<
<T as frame_system::Config>::AccountId,
sp_runtime::Perbill,
Balancing,
>,
>(ext, do_feasibility)
},
Solver::PhragMMS { iterations } => {
BalanceIterations::set(*iterations);
mine_solution::<
T,
PhragMMS<<T as frame_system::Config>::AccountId, sp_runtime::Perbill, Balancing>,
>(ext, do_feasibility)
},
}
}
#[allow(unused)]
fn mine_dpos<T: EPM::Config>(ext: &mut Ext) -> Result<(), Error<T>> {
ext.execute_with(|| {
use std::collections::BTreeMap;
use EPM::RoundSnapshot;
let RoundSnapshot { voters, .. } = EPM::Snapshot::<T>::get().unwrap();
let desired_targets = EPM::DesiredTargets::<T>::get().unwrap();
let mut candidates_and_backing = BTreeMap::<T::AccountId, u128>::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::<Vec<(_, _)>>();
candidates_and_backing.sort_by_key(|(_, total_stake)| *total_stake);
let winners = candidates_and_backing
.into_iter()
.rev()
.take(desired_targets as usize)
.collect::<Vec<_>>();
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<T: frame_system::Config + EPM::Config>(
rpc: &SharedRpcClient,
print: bool,
) -> Result<(), Error<T>> {
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::<Runtime>(&rpc, true).await
};
let outcome = any_runtime! {
match command {
Command::Monitor(monitor_config) =>
{
let signer_account = any_runtime! {
signer::signer_uri_from_string::<Runtime>(&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::<Runtime>(&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<T: frame_system::Config>() -> sp_version::RuntimeVersion {
T::Version::get()
}
#[test]
fn any_runtime_works() {
unsafe {
RUNTIME = AnyRuntime::Polkadot;
}
let polkadot_version = any_runtime! { get_version::<Runtime>() };
unsafe {
RUNTIME = AnyRuntime::Kusama;
}
let kusama_version = any_runtime! { get_version::<Runtime>() };
assert_eq!(polkadot_version.spec_name, "polkadot".into());
assert_eq!(kusama_version.spec_name, "kusama".into());
}
}
-478
View File
@@ -1,478 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! 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<T: EPM::Config, B: BlockT<Hash = Hash>>(
rpc: &SharedRpcClient,
at: B::Hash,
) -> Result<(), Error<T>> {
let key = StorageKey(EPM::CurrentPhase::<T>::hashed_key().to_vec());
let phase = rpc
.get_storage_and_decode::<EPM::Phase<BlockNumber>>(&key, Some(at))
.await
.map_err::<Error<T>, _>(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<T, B>(
rpc: &SharedRpcClient,
at: Hash,
us: &AccountId,
) -> Result<(), Error<T>>
where
T: EPM::Config + frame_system::Config<AccountId = AccountId, Hash = Hash>,
B: BlockT,
{
let indices_key = StorageKey(EPM::SignedSubmissionIndices::<T>::hashed_key().to_vec());
let indices: SubmissionIndicesOf<T> = rpc
.get_storage_and_decode(&indices_key, Some(at))
.await
.map_err::<Error<T>, _>(Into::into)?
.unwrap_or_default();
for (_score, _bn, idx) in indices {
let key = StorageKey(EPM::SignedSubmissionsMap::<T>::hashed_key_for(idx));
if let Some(submission) = rpc
.get_storage_and_decode::<SignedSubmissionOf<T>>(&key, Some(at))
.await
.map_err::<Error<T>, _>(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<T: EPM::Config, B: BlockT>(
rpc: &SharedRpcClient,
at: Hash,
score: sp_npos_elections::ElectionScore,
strategy: SubmissionStrategy,
max_submissions: u32,
) -> Result<(), Error<T>> {
// don't care about current scores.
if matches!(strategy, SubmissionStrategy::Always) {
return Ok(())
}
let indices_key = StorageKey(EPM::SignedSubmissionIndices::<T>::hashed_key().to_vec());
let indices: SubmissionIndicesOf<T> = rpc
.get_storage_and_decode(&indices_key, Some(at))
.await
.map_err::<Error<T>, _>(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<T: EPM::Config>(
rpc: &SharedRpcClient,
mode: &str,
) -> Result<Hash, Error<T>> {
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 [<monitor_cmd_ $runtime>](
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::<StakingMinerError>();
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<StakingMinerError>,
at: Header,
signer: Signer,
config: MonitorConfig,
submit_lock: Arc<Mutex<()>>,
) {
async fn flatten<T>(
handle: tokio::task::JoinHandle<Result<T, StakingMinerError>>
) -> Result<T, StakingMinerError> {
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::<Runtime>(&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::<Runtime, Block>(&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::<Runtime, Block>(&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::<Runtime>(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::<Runtime>(&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::<Runtime>(&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 = <Runtime as frame_system::Config>::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::<Runtime>(&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::<Runtime, Block>(
&rpc1,
latest_head,
score,
config.submission_strategy,
SignedMaxSubmissions::get()
).await
});
let ensure_signed_phase_fut = tokio::spawn(async move {
ensure_signed_phase::<Runtime, Block>(&rpc2, latest_head).await
});
let account = signer.account.clone();
let no_prev_sol_fut = tokio::spawn(async move {
ensure_no_previous_solution::<Runtime, Block>(&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<frame_system::EventRecord<RuntimeEvent, <Block as BlockT>::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)));
}
}
-366
View File
@@ -1,366 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
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<Hash>,
/// 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<Hash>,
/// 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<usize>,
}
#[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<Self, Self::Err> {
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)))
);
}
}
@@ -1,55 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! 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<sp_runtime::traits::HashingFor<Block>>;
/// 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;
-182
View File
@@ -1,182 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! 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 {
<RpcHelperError as std::fmt::Debug>::fmt(self, f)
}
}
#[rpc(client)]
pub trait RpcApi {
/// Fetch system name.
#[method(name = "system_chain")]
async fn system_chain(&self) -> RpcResult<String>;
/// Fetch a storage key.
#[method(name = "state_getStorage")]
async fn storage(&self, key: &StorageKey, hash: Option<Hash>) -> RpcResult<Option<Bytes>>;
/// Fetch the runtime version.
#[method(name = "state_getRuntimeVersion")]
async fn runtime_version(&self, at: Option<Hash>) -> RpcResult<RuntimeVersion>;
/// Fetch the payment query info.
#[method(name = "payment_queryInfo")]
async fn payment_query_info(
&self,
encoded_xt: &Bytes,
at: Option<&Hash>,
) -> RpcResult<RuntimeDispatchInfo<Balance>>;
/// 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<Hash>) -> RpcResult<Bytes>;
/// 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<Hash>) -> RpcResult<Option<Hash>>;
/// Get hash of the last finalized block in the canon chain.
#[method(name = "chain_getFinalizedHead", aliases = ["chain_getFinalisedHead"], blocking)]
fn finalized_head(&self) -> RpcResult<Hash>;
/// 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<Hash, Hash>
)]
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<WsClient>, 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<Self, RpcError> {
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<Hash>,
) -> Result<Option<T>, RpcHelperError> {
if let Some(bytes) = self.storage(key, hash).await? {
let decoded = <T as codec::Decode>::decode(&mut &*bytes.0)
.map_err::<RpcHelperError, _>(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<Output = Result<Bytes, RpcError>>,
) -> Result<Dec, RpcHelperError> {
let bytes = req.await?;
Dec::decode(&mut &*bytes.0).map_err::<RpcHelperError, _>(Into::into)
}
@@ -1,90 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
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" })
}
}
@@ -1,84 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
//! 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<T: frame_system::Config<Hash = Hash> + EPM::Config>(
rpc: &SharedRpcClient,
who: &T::AccountId,
maybe_at: Option<T::Hash>,
) -> Result<Option<AccountInfo<Nonce, T::AccountData>>, Error<T>> {
rpc.get_storage_and_decode::<AccountInfo<Nonce, T::AccountData>>(
&StorageKey(<frame_system::Account<T>>::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<Balance>,
Hash = Hash,
> + EPM::Config,
>(
mut seed_or_path: &str,
client: &SharedRpcClient,
) -> Result<Signer, Error<T>> {
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::<T>::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::<T>(client, &account, None)
.await?
.ok_or(Error::<T>::AccountDoesNotExists)?;
log::info!(
target: LOG_TARGET,
"loaded account {:?}, free: {:?}, info: {:?}",
&account,
Token::from(_info.data.free),
_info
);
Ok(Signer { account, pair })
}
-49
View File
@@ -1,49 +0,0 @@
// 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 <http://www.gnu.org/licenses/>.
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<Value> = 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());
}
@@ -149,7 +149,8 @@
//! while this binary lives in the Polkadot repository, this particular subcommand of it can work
//! against any substrate-based chain.
//!
//! See the `staking-miner` documentation in the Polkadot repository for more information.
//! See the [`staking-miner`](https://github.com/paritytech/staking-miner-v2) docs for more
//! information.
//!
//! ## Feasible Solution (correct solution)
//!