aboutsummaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rwxr-xr-xcontrib/devtools/gen-manpages.sh5
-rw-r--r--contrib/guix/README.md76
-rwxr-xr-xcontrib/guix/guix-build.sh159
-rw-r--r--contrib/guix/libexec/build.sh26
-rw-r--r--contrib/signet/README.md61
-rwxr-xr-xcontrib/signet/miner639
6 files changed, 909 insertions, 57 deletions
diff --git a/contrib/devtools/gen-manpages.sh b/contrib/devtools/gen-manpages.sh
index b933b264e7..b7bf76ce77 100755
--- a/contrib/devtools/gen-manpages.sh
+++ b/contrib/devtools/gen-manpages.sh
@@ -14,13 +14,14 @@ BITCOIND=${BITCOIND:-$BINDIR/bitcoind}
BITCOINCLI=${BITCOINCLI:-$BINDIR/bitcoin-cli}
BITCOINTX=${BITCOINTX:-$BINDIR/bitcoin-tx}
WALLET_TOOL=${WALLET_TOOL:-$BINDIR/bitcoin-wallet}
+BITCOINUTIL=${BITCOINQT:-$BINDIR/bitcoin-util}
BITCOINQT=${BITCOINQT:-$BINDIR/qt/bitcoin-qt}
[ ! -x $BITCOIND ] && echo "$BITCOIND not found or not executable." && exit 1
# Don't allow man pages to be generated for binaries built from a dirty tree
DIRTY=""
-for cmd in $BITCOIND $BITCOINCLI $BITCOINTX $WALLET_TOOL $BITCOINQT; do
+for cmd in $BITCOIND $BITCOINCLI $BITCOINTX $WALLET_TOOL $BITCOINUTIL $BITCOINQT; do
VERSION_OUTPUT=$($cmd --version)
if [[ $VERSION_OUTPUT == *"dirty"* ]]; then
DIRTY="${DIRTY}${cmd}\n"
@@ -43,7 +44,7 @@ read -r -a BTCVER <<< "$($BITCOINCLI --version | head -n1 | awk -F'[ -]' '{ prin
echo "[COPYRIGHT]" > footer.h2m
$BITCOIND --version | sed -n '1!p' >> footer.h2m
-for cmd in $BITCOIND $BITCOINCLI $BITCOINTX $WALLET_TOOL $BITCOINQT; do
+for cmd in $BITCOIND $BITCOINCLI $BITCOINTX $WALLET_TOOL $BITCOINUTIL $BITCOINQT; do
cmdname="${cmd##*/}"
help2man -N --version-string=${BTCVER[0]} --include=footer.h2m -o ${MANDIR}/${cmdname}.1 ${cmd}
sed -i "s/\\\-${BTCVER[1]}//g" ${MANDIR}/${cmdname}.1
diff --git a/contrib/guix/README.md b/contrib/guix/README.md
index dffcf99607..dbe1ea837b 100644
--- a/contrib/guix/README.md
+++ b/contrib/guix/README.md
@@ -40,25 +40,27 @@ Otherwise, follow the [Guix installation guide][guix/bin-install].
Guix allows us to achieve better binary security by using our CPU time to build
everything from scratch. However, it doesn't sacrifice user choice in pursuit of
-this: users can decide whether or not to bootstrap and to use substitutes.
+this: users can decide whether or not to bootstrap and to use substitutes
+(pre-built packages).
After installation, you may want to consider [adding substitute
-servers](#speeding-up-builds-with-substitute-servers) to speed up your build if
-that fits your security model (say, if you're just testing that this works).
-This is skippable if you're using the [Dockerfile][fanquake/guix-docker].
+servers](#speeding-up-builds-with-substitute-servers) from which to download
+pre-built packages to speed up your build if that fits your security model (say,
+if you're just testing that this works). Substitute servers are set up by
+default if you're using the [Dockerfile][fanquake/guix-docker].
-If you prefer not to use any substitutes, make sure to set
-`ADDITIONAL_GUIX_ENVIRONMENT_FLAGS` like the following snippet. The first build
-will take a while, but the resulting packages will be cached for future builds.
+If you prefer not to use any substitutes, make sure to supply `--no-substitutes`
+like in the following snippet. The first build will take a while, but the
+resulting packages will be cached for future builds.
```sh
-export ADDITIONAL_GUIX_ENVIRONMENT_FLAGS='--no-substitutes'
+export ADDITIONAL_GUIX_COMMON_FLAGS='--no-substitutes'
```
Likewise, to perform a bootstrapped build (takes even longer):
```sh
-export ADDITIONAL_GUIX_ENVIRONMENT_FLAGS='--bootstrap --no-substitutes'
+export ADDITIONAL_GUIX_COMMON_FLAGS='--no-substitutes' ADDITIONAL_GUIX_ENVIRONMENT_FLAGS='--bootstrap'
```
### Using a version of Guix with `guix time-machine` capabilities
@@ -82,17 +84,6 @@ export PATH="${HOME}/.config/guix/current/bin${PATH:+:}$PATH"
## Usage
-### As a Development Environment
-
-For a Bitcoin Core depends development environment, simply invoke
-
-```sh
-guix environment --manifest=contrib/guix/manifest.scm
-```
-
-And you'll land back in your shell with all the build dependencies required for
-a `depends` build injected into your environment.
-
### As a Tool for Deterministic Builds
From the top of a clean Bitcoin Core repository:
@@ -113,10 +104,8 @@ find output/ -type f -print0 | sort -z | xargs -r0 sha256sum
* _**HOSTS**_
Override the space-separated list of platform triples for which to perform a
- bootstrappable build. _(defaults to "x86\_64-linux-gnu
- arm-linux-gnueabihf aarch64-linux-gnu riscv64-linux-gnu")_
-
- > Windows and OS X platform triplet support are WIP.
+ bootstrappable build. _(defaults to "x86\_64-linux-gnu arm-linux-gnueabihf
+ aarch64-linux-gnu riscv64-linux-gnu x86_64-w64-mingw32")_
* _**SOURCES_PATH**_
@@ -147,13 +136,29 @@ find output/ -type f -print0 | sort -z | xargs -r0 sha256sum
string) is interpreted the same way as not setting `V` at all, and that `V=0`
has the same effect as `V=1`.
-* _**ADDITIONAL_GUIX_ENVIRONMENT_FLAGS**_
+* _**SUBSTITUTE_URLS**_
- Additional flags to be passed to `guix environment`. For a fully-bootstrapped
+ A whitespace-delimited list of URLs from which to download pre-built packages.
+ A URL is only used if its signing key is authorized (refer to the [substitute
+ servers section](#speeding-up-builds-with-substitute-servers) for more
+ details).
+
+* _**ADDITIONAL_GUIX_COMMON_FLAGS**_
+
+ Additional flags to be passed to all `guix` commands. For a fully-bootstrapped
build, set this to `--bootstrap --no-substitutes` (refer to the [security
model section](#choosing-your-security-model) for more details). Note that a
fully-bootstrapped build will take quite a long time on the first run.
+* _**ADDITIONAL_GUIX_TIMEMACHINE_FLAGS**_
+
+ Additional flags to be passed to `guix time-machine`.
+
+* _**ADDITIONAL_GUIX_ENVIRONMENT_FLAGS**_
+
+ Additional flags to be passed to the invocation of `guix environment` inside
+ `guix time-machine`.
+
## Tips and Tricks
### Speeding up builds with substitute servers
@@ -161,14 +166,15 @@ find output/ -type f -print0 | sort -z | xargs -r0 sha256sum
_This whole section is automatically done in the convenience
[Dockerfiles][fanquake/guix-docker]_
-For those who are used to life in the fast _(and trustful)_ lane, you can use
-[substitute servers][guix/substitutes] to enable binary downloads of packages.
+For those who are used to life in the fast _(and trustful)_ lane, you can
+specify [substitute servers][guix/substitutes] from which to download pre-built
+packages.
> For those who only want to use substitutes from the official Guix build farm
> and have authorized the build farm's signing key during Guix's installation,
> you don't need to do anything.
-#### Authorize the signing keys
+#### Step 1: Authorize the signing keys
For the official Guix build farm at https://ci.guix.gnu.org, run as root:
@@ -182,7 +188,7 @@ For dongcarl's substitute server at https://guix.carldong.io, run as root:
wget -qO- 'https://guix.carldong.io/signing-key.pub' | guix archive --authorize
```
-#### Use the substitute servers
+#### Step 2: Specify the substitute servers
The official Guix build farm at https://ci.guix.gnu.org is automatically used
unless the `--no-substitutes` flag is supplied.
@@ -196,7 +202,7 @@ To use dongcarl's substitute server for Bitcoin Core builds after having
[authorized his signing key](#authorize-the-signing-keys):
```
-export ADDITIONAL_GUIX_ENVIRONMENT_FLAGS='--substitute-urls="https://guix.carldong.io https://ci.guix.gnu.org"'
+export SUBSTITUTE_URLS='https://guix.carldong.io https://ci.guix.gnu.org'
```
## FAQ
@@ -212,9 +218,9 @@ As mentioned at the bottom of [this manual page][guix/bin-install]:
### When will Guix be packaged in debian?
-Vagrant Cascadian has been making good progress on this
-[here][debian/guix-package]. We have all the pieces needed to put up an APT
-repository and will likely put one up soon.
+Thanks to Vagrant Cascadian's diligent work, Guix is now [in debian
+experimental][debian/guix-experimental]! Hopefully it will make its way into a
+release soon.
[b17e]: http://bootstrappable.org/
[r12e/source-date-epoch]: https://reproducible-builds.org/docs/source-date-epoch/
@@ -226,5 +232,5 @@ repository and will likely put one up soon.
[guix/substitute-server-auth]: https://www.gnu.org/software/guix/manual/en/html_node/Substitute-Server-Authorization.html
[guix/time-machine]: https://guix.gnu.org/manual/en/html_node/Invoking-guix-time_002dmachine.html
-[debian/guix-package]: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=850644
+[debian/guix-experimental]: https://packages.debian.org/experimental/guix
[fanquake/guix-docker]: https://github.com/fanquake/core-review/tree/master/guix
diff --git a/contrib/guix/guix-build.sh b/contrib/guix/guix-build.sh
index 11d2c8b867..54cc5793f6 100755
--- a/contrib/guix/guix-build.sh
+++ b/contrib/guix/guix-build.sh
@@ -2,6 +2,116 @@
export LC_ALL=C
set -e -o pipefail
+###################
+## Sanity Checks ##
+###################
+
+################
+# Check 1: Make sure that we can invoke required tools
+################
+for cmd in git make guix cat mkdir; do
+ if ! command -v "$cmd" > /dev/null 2>&1; then
+ echo "ERR: This script requires that '$cmd' is installed and available in your \$PATH"
+ exit 1
+ fi
+done
+
+################
+# Check 2: Make sure GUIX_BUILD_OPTIONS is empty
+################
+#
+# GUIX_BUILD_OPTIONS is an environment variable recognized by guix commands that
+# can perform builds. This seems like what we want instead of
+# ADDITIONAL_GUIX_COMMON_FLAGS, but the value of GUIX_BUILD_OPTIONS is actually
+# _appended_ to normal command-line options. Meaning that they will take
+# precedence over the command-specific ADDITIONAL_GUIX_<CMD>_FLAGS.
+#
+# This seems like a poor user experience. Thus we check for GUIX_BUILD_OPTIONS's
+# existence here and direct users of this script to use our (more flexible)
+# custom environment variables.
+if [ -n "$GUIX_BUILD_OPTIONS" ]; then
+cat << EOF
+Error: Environment variable GUIX_BUILD_OPTIONS is not empty:
+ '$GUIX_BUILD_OPTIONS'
+
+Unfortunately this script is incompatible with GUIX_BUILD_OPTIONS, please unset
+GUIX_BUILD_OPTIONS and use ADDITIONAL_GUIX_COMMON_FLAGS to set build options
+across guix commands or ADDITIONAL_GUIX_<CMD>_FLAGS to set build options for a
+specific guix command.
+
+See contrib/guix/README.md for more details.
+EOF
+exit 1
+fi
+
+################
+# Check 3: Make sure that we're not in a dirty worktree
+################
+if ! git diff-index --quiet HEAD -- && [ -z "$FORCE_DIRTY_WORKTREE" ]; then
+cat << EOF
+ERR: The current git worktree is dirty, which may lead to broken builds.
+
+ Aborting...
+
+Hint: To make your git worktree clean, You may want to:
+ 1. Commit your changes,
+ 2. Stash your changes, or
+ 3. Set the 'FORCE_DIRTY_WORKTREE' environment variable if you insist on
+ using a dirty worktree
+EOF
+exit 1
+else
+ GIT_COMMIT=$(git rev-parse --short=12 HEAD)
+fi
+
+################
+# Check 4: Make sure that build directories do no exist
+################
+
+# Default to building for all supported HOSTs (overridable by environment)
+export HOSTS="${HOSTS:-x86_64-linux-gnu arm-linux-gnueabihf aarch64-linux-gnu riscv64-linux-gnu
+ x86_64-w64-mingw32}"
+
+DISTSRC_BASE="${DISTSRC_BASE:-${PWD}}"
+
+# Usage: distsrc_for_host HOST
+#
+# HOST: The current platform triple we're building for
+#
+distsrc_for_host() {
+ echo "${DISTSRC_BASE}/distsrc-${GIT_COMMIT}-${1}"
+}
+
+# Accumulate a list of build directories that already exist...
+hosts_distsrc_exists=""
+for host in $HOSTS; do
+ if [ -e "$(distsrc_for_host "$host")" ]; then
+ hosts_distsrc_exists+=" ${host}"
+ fi
+done
+
+if [ -n "$hosts_distsrc_exists" ]; then
+# ...so that we can print them out nicely in an error message
+cat << EOF
+ERR: Build directories for this commit already exist for the following platform
+ triples you're attempting to build, probably because of previous builds.
+ Please remove, or otherwise deal with them prior to starting another build.
+
+ Aborting...
+
+EOF
+for host in $hosts_distsrc_exists; do
+ echo " ${host} '$(distsrc_for_host "$host")'"
+done
+exit 1
+else
+ mkdir -p "$DISTSRC_BASE"
+fi
+
+#########
+# Setup #
+#########
+
# Determine the maximum number of jobs to run simultaneously (overridable by
# environment)
MAX_JOBS="${MAX_JOBS:-$(nproc)}"
@@ -16,11 +126,23 @@ SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log --format=%at -1)}"
# Execute "$@" in a pinned, possibly older version of Guix, for reproducibility
# across time.
time-machine() {
+ # shellcheck disable=SC2086
guix time-machine --url=https://github.com/dongcarl/guix.git \
--commit=b066c25026f21fb57677aa34692a5034338e7ee3 \
+ --max-jobs="$MAX_JOBS" \
+ ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
+ ${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_TIMEMACHINE_FLAGS} \
-- "$@"
}
+# Make sure an output directory exists for our builds
+OUTDIR="${OUTDIR:-${PWD}/output}"
+[ -e "$OUTDIR" ] || mkdir -p "$OUTDIR"
+
+#########
+# Build #
+#########
+
# Function to be called when building for host ${1} and the user interrupts the
# build
int_trap() {
@@ -38,9 +160,9 @@ and untracked files and directories will be wiped, allowing you to start anew.
EOF
}
-# Deterministically build Bitcoin Core for HOSTs (overridable by environment)
+# Deterministically build Bitcoin Core
# shellcheck disable=SC2153
-for host in ${HOSTS=x86_64-linux-gnu arm-linux-gnueabihf aarch64-linux-gnu riscv64-linux-gnu x86_64-w64-mingw32}; do
+for host in $HOSTS; do
# Display proper warning when the user interrupts the build
trap 'int_trap ${host}' INT
@@ -50,6 +172,19 @@ for host in ${HOSTS=x86_64-linux-gnu arm-linux-gnueabihf aarch64-linux-gnu riscv
# for the particular $HOST we're building for
export HOST="$host"
+ # shellcheck disable=SC2030
+cat << EOF
+INFO: Building commit ${GIT_COMMIT:?not set} for platform triple ${HOST:?not set}:
+ ...using reference timestamp: ${SOURCE_DATE_EPOCH:?not set}
+ ...running at most ${MAX_JOBS:?not set} jobs
+ ...from worktree directory: '${PWD}'
+ ...bind-mounted in container to: '/bitcoin'
+ ...in build directory: '$(distsrc_for_host "$HOST")'
+ ...bind-mounted in container to: '$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")'
+ ...outputting in: '${OUTDIR:?not set}'
+ ...bind-mounted in container to: '/outdir'
+EOF
+
# Run the build script 'contrib/guix/libexec/build.sh' in the build
# container specified by 'contrib/guix/manifest.scm'.
#
@@ -99,20 +234,36 @@ for host in ${HOSTS=x86_64-linux-gnu arm-linux-gnueabihf aarch64-linux-gnu riscv
# make the downloaded depends sources available to it. The sources
# should have been downloaded prior to this invocation.
#
- # shellcheck disable=SC2086
+ # ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"}
+ #
+ # fetch substitute from SUBSTITUTE_URLS if they are
+ # authorized
+ #
+ # Depending on the user's security model, it may be desirable to use
+ # substitutes (pre-built packages) from servers that the user trusts.
+ # Please read the README.md in the same directory as this file for
+ # more information.
+ #
+ # shellcheck disable=SC2086,SC2031
time-machine environment --manifest="${PWD}/contrib/guix/manifest.scm" \
--container \
--pure \
--no-cwd \
--share="$PWD"=/bitcoin \
+ --share="$DISTSRC_BASE"=/distsrc-base \
+ --share="$OUTDIR"=/outdir \
--expose="$(git rev-parse --git-common-dir)" \
${SOURCES_PATH:+--share="$SOURCES_PATH"} \
- ${ADDITIONAL_GUIX_ENVIRONMENT_FLAGS} \
+ --max-jobs="$MAX_JOBS" \
+ ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
+ ${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_ENVIRONMENT_FLAGS} \
-- env HOST="$host" \
MAX_JOBS="$MAX_JOBS" \
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:?unable to determine value}" \
${V:+V=1} \
${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} \
+ DISTSRC="$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")" \
+ OUTDIR=/outdir \
bash -c "cd /bitcoin && bash contrib/guix/libexec/build.sh"
)
diff --git a/contrib/guix/libexec/build.sh b/contrib/guix/libexec/build.sh
index d658c4f6a6..b00c42ce01 100644
--- a/contrib/guix/libexec/build.sh
+++ b/contrib/guix/libexec/build.sh
@@ -11,9 +11,15 @@ if [ -n "$V" ]; then
export VERBOSE="$V"
fi
-# Check that environment variables assumed to be set by the environment are set
-echo "Building for platform triple ${HOST:?not set} with reference timestamp ${SOURCE_DATE_EPOCH:?not set}..."
-echo "At most ${MAX_JOBS:?not set} jobs will run at once..."
+# Check that required environment variables are set
+cat << EOF
+Required environment variables as seen inside the container:
+ HOST: ${HOST:?not set}
+ SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH:?not set}
+ MAX_JOBS: ${MAX_JOBS:?not set}
+ DISTSRC: ${DISTSRC:?not set}
+ OUTDIR: ${OUTDIR:?not set}
+EOF
#####################
# Environment Setup #
@@ -23,19 +29,6 @@ echo "At most ${MAX_JOBS:?not set} jobs will run at once..."
# $HOSTs after successfully building.
BASEPREFIX="${PWD}/depends"
-# Setup an output directory for our build
-OUTDIR="${OUTDIR:-${PWD}/output}"
-[ -e "$OUTDIR" ] || mkdir -p "$OUTDIR"
-
-# Setup the directory where our Bitcoin Core build for HOST will occur
-DISTSRC="${DISTSRC:-${PWD}/distsrc-${HOST}}"
-if [ -e "$DISTSRC" ]; then
- echo "DISTSRC directory '${DISTSRC}' exists, probably because of previous builds... Aborting..."
- exit 1
-else
- mkdir -p "$DISTSRC"
-fi
-
# Given a package name and an output name, return the path of that output in our
# current guix environment
store_path() {
@@ -189,6 +182,7 @@ esac
# Make $HOST-specific native binaries from depends available in $PATH
export PATH="${BASEPREFIX}/${HOST}/native/bin:${PATH}"
+mkdir -p "$DISTSRC"
(
cd "$DISTSRC"
diff --git a/contrib/signet/README.md b/contrib/signet/README.md
index c4aa5ae2f7..71dc2f9638 100644
--- a/contrib/signet/README.md
+++ b/contrib/signet/README.md
@@ -17,3 +17,64 @@ Syntax: `getcoins.py [-h|--help] [-c|--cmd=<bitcoin-cli path>] [-f|--faucet=<fau
If using the default network, invoking the script with no arguments should be sufficient under normal
circumstances, but if multiple people are behind the same IP address, the faucet will by default only
accept one claim per day. See `--password` above.
+
+miner
+=====
+
+To mine the first block in your custom chain, you can run:
+
+ cd src/
+ CLI="./bitcoin-cli -conf=mysignet.conf"
+ MINER="..contrib/signet/miner"
+ GRIND="./bitcoin-util grind"
+ ADDR=$($CLI -signet getnewaddress)
+ $MINER --cli="$CLI" generate --grind-cmd="$GRIND" --address="$ADDR" --set-block-time=-1
+
+This will mine a block with the current timestamp. If you want to backdate the chain, you can give a different timestamp to --set-block-time.
+
+You will then need to pick a difficulty target. Since signet chains are primarily protected by a signature rather than proof of work, there is no need to spend as much energy as possible mining, however you may wish to choose to spend more time than the absolute minimum. The calibrate subcommand can be used to pick a target, eg:
+
+ $MINER calibrate --grind-cmd="$GRIND"
+ nbits=1e00f403 for 25s average mining time
+
+It defaults to estimating an nbits value resulting in 25s average time to find a block, but the --seconds parameter can be used to pick a different target, or the --nbits parameter can be used to estimate how long it will take for a given difficulty.
+
+Using the --ongoing parameter will then cause the signet miner to create blocks indefinitely. It will pick the time between blocks so that difficulty is adjusted to match the provided --nbits value.
+
+ $MINER --cli="$CLI" generate --grind-cmd="$GRIND" --address="$ADDR" --nbits=1e00f403 --ongoing
+
+Other options
+-------------
+
+The --debug and --quiet options are available to control how noisy the signet miner's output is. Note that the --debug, --quiet and --cli parameters must all appear before the subcommand (generate, calibrate, etc) if used.
+
+Instead of specifying --ongoing, you can specify --max-blocks=N to mine N blocks and stop.
+
+Instead of using a single address, a ranged descriptor may be provided instead (via the --descriptor parameter), with the reward for the block at height H being sent to the H'th address generated from the descriptor.
+
+Instead of calculating a specific nbits value, --min-nbits can be specified instead, in which case the mininmum signet difficulty will be targeted.
+
+By default, the signet miner mines blocks at fixed intervals with minimal variation. If you want blocks to appear more randomly, as they do in mainnet, specify the --poisson option.
+
+Using the --multiminer parameter allows mining to be distributed amongst multiple miners. For example, if you have 3 miners and want to share blocks between them, specify --multiminer=1/3 on one, --multiminer=2/3 on another, and --multiminer=3/3 on the last one. If you want one to do 10% of blocks and two others to do 45% each, --multiminer=1-10/100 on the first, and --multiminer=11-55 and --multiminer=56-100 on the others. Note that which miner mines which block is determined by the previous block hash, so occasional runs of one miner doing many blocks in a row is to be expected.
+
+When --multiminer is used, if a miner is down and does not mine a block within five minutes of when it is due, the other miners will automatically act as redundant backups ensuring the chain does not halt. The --backup-delay parameter can be used to change how long a given miner waits, allowing one to be the primary backup (after five minutes) and another to be the secondary backup (after six minutes, eg).
+
+The --standby-delay parameter can be used to make a backup miner that only mines if a block doesn't arrive on time. This can be combined with --multiminer if desired. Setting --standby-delay also prevents the first block from being mined immediately.
+
+Advanced usage
+--------------
+
+The process generate follows internally is to get a block template, convert that into a PSBT, sign the PSBT, move the signature from the signed PSBT into the block template's coinbase, grind proof of work for the block, and then submit the block to the network.
+
+These steps can instead be done explicitly:
+
+ $CLI -signet getblocktemplate '{"rules": ["signet","segwit"]}' |
+ $MINER --cli="$CLI" genpsbt --address="$ADDR" |
+ $CLI -signet -stdin walletprocesspsbt |
+ jq -r .psbt |
+ $MINER --cli="$CLI" solvepsbt --grind-cmd="$GRIND" |
+ $CLI -signet -stdin submitblock
+
+This is intended to allow you to replace part of the pipeline for further experimentation, if desired.
+
diff --git a/contrib/signet/miner b/contrib/signet/miner
new file mode 100755
index 0000000000..a3fba49d0e
--- /dev/null
+++ b/contrib/signet/miner
@@ -0,0 +1,639 @@
+#!/usr/bin/env python3
+# Copyright (c) 2020 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import argparse
+import base64
+import json
+import logging
+import math
+import os.path
+import re
+import struct
+import sys
+import time
+import subprocess
+
+from binascii import unhexlify
+from io import BytesIO
+
+PATH_BASE_CONTRIB_SIGNET = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+PATH_BASE_TEST_FUNCTIONAL = os.path.abspath(os.path.join(PATH_BASE_CONTRIB_SIGNET, "..", "..", "test", "functional"))
+sys.path.insert(0, PATH_BASE_TEST_FUNCTIONAL)
+
+from test_framework.blocktools import WITNESS_COMMITMENT_HEADER, script_BIP34_coinbase_height # noqa: E402
+from test_framework.messages import CBlock, CBlockHeader, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, FromHex, ToHex, deser_string, hash256, ser_compact_size, ser_string, ser_uint256, uint256_from_str # noqa: E402
+from test_framework.script import CScriptOp # noqa: E402
+
+logging.basicConfig(
+ format='%(asctime)s %(levelname)s %(message)s',
+ level=logging.INFO,
+ datefmt='%Y-%m-%d %H:%M:%S')
+
+SIGNET_HEADER = b"\xec\xc7\xda\xa2"
+PSBT_SIGNET_BLOCK = b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed
+RE_MULTIMINER = re.compile("^(\d+)(-(\d+))?/(\d+)$")
+
+# #### some helpers that could go into test_framework
+
+# like FromHex, but without the hex part
+def FromBinary(cls, stream):
+ """deserialize a binary stream (or bytes object) into an object"""
+ # handle bytes object by turning it into a stream
+ was_bytes = isinstance(stream, bytes)
+ if was_bytes:
+ stream = BytesIO(stream)
+ obj = cls()
+ obj.deserialize(stream)
+ if was_bytes:
+ assert len(stream.read()) == 0
+ return obj
+
+class PSBTMap:
+ """Class for serializing and deserializing PSBT maps"""
+
+ def __init__(self, map=None):
+ self.map = map if map is not None else {}
+
+ def deserialize(self, f):
+ m = {}
+ while True:
+ k = deser_string(f)
+ if len(k) == 0:
+ break
+ v = deser_string(f)
+ if len(k) == 1:
+ k = k[0]
+ assert k not in m
+ m[k] = v
+ self.map = m
+
+ def serialize(self):
+ m = b""
+ for k,v in self.map.items():
+ if isinstance(k, int) and 0 <= k and k <= 255:
+ k = bytes([k])
+ m += ser_compact_size(len(k)) + k
+ m += ser_compact_size(len(v)) + v
+ m += b"\x00"
+ return m
+
+class PSBT:
+ """Class for serializing and deserializing PSBTs"""
+
+ def __init__(self):
+ self.g = PSBTMap()
+ self.i = []
+ self.o = []
+ self.tx = None
+
+ def deserialize(self, f):
+ assert f.read(5) == b"psbt\xff"
+ self.g = FromBinary(PSBTMap, f)
+ assert 0 in self.g.map
+ self.tx = FromBinary(CTransaction, self.g.map[0])
+ self.i = [FromBinary(PSBTMap, f) for _ in self.tx.vin]
+ self.o = [FromBinary(PSBTMap, f) for _ in self.tx.vout]
+ return self
+
+ def serialize(self):
+ assert isinstance(self.g, PSBTMap)
+ assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i)
+ assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o)
+ assert 0 in self.g.map
+ tx = FromBinary(CTransaction, self.g.map[0])
+ assert len(tx.vin) == len(self.i)
+ assert len(tx.vout) == len(self.o)
+
+ psbt = [x.serialize() for x in [self.g] + self.i + self.o]
+ return b"psbt\xff" + b"".join(psbt)
+
+ def to_base64(self):
+ return base64.b64encode(self.serialize()).decode("utf8")
+
+ @classmethod
+ def from_base64(cls, b64psbt):
+ return FromBinary(cls, base64.b64decode(b64psbt))
+
+# #####
+
+def create_coinbase(height, value, spk):
+ cb = CTransaction()
+ cb.vin = [CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), 0xffffffff)]
+ cb.vout = [CTxOut(value, spk)]
+ return cb
+
+def get_witness_script(witness_root, witness_nonce):
+ commitment = uint256_from_str(hash256(ser_uint256(witness_root) + ser_uint256(witness_nonce)))
+ return b"\x6a" + CScriptOp.encode_op_pushdata(WITNESS_COMMITMENT_HEADER + ser_uint256(commitment))
+
+def signet_txs(block, challenge):
+ # assumes signet solution has not been added yet so does not need
+ # to be removed
+
+ txs = block.vtx[:]
+ txs[0] = CTransaction(txs[0])
+ txs[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER)
+ hashes = []
+ for tx in txs:
+ tx.rehash()
+ hashes.append(ser_uint256(tx.sha256))
+ mroot = block.get_merkle_root(hashes)
+
+ sd = b""
+ sd += struct.pack("<i", block.nVersion)
+ sd += ser_uint256(block.hashPrevBlock)
+ sd += ser_uint256(mroot)
+ sd += struct.pack("<I", block.nTime)
+
+ to_spend = CTransaction()
+ to_spend.nVersion = 0
+ to_spend.nLockTime = 0
+ to_spend.vin = [CTxIn(COutPoint(0, 0xFFFFFFFF), b"\x00" + CScriptOp.encode_op_pushdata(sd), 0)]
+ to_spend.vout = [CTxOut(0, challenge)]
+ to_spend.rehash()
+
+ spend = CTransaction()
+ spend.nVersion = 0
+ spend.nLockTime = 0
+ spend.vin = [CTxIn(COutPoint(to_spend.sha256, 0), b"", 0)]
+ spend.vout = [CTxOut(0, b"\x6a")]
+
+ return spend, to_spend
+
+def do_createpsbt(block, signme, spendme):
+ psbt = PSBT()
+ psbt.g = PSBTMap( {0: signme.serialize(),
+ PSBT_SIGNET_BLOCK: block.serialize()
+ } )
+ psbt.i = [ PSBTMap( {0: spendme.serialize(),
+ 3: bytes([1,0,0,0])})
+ ]
+ psbt.o = [ PSBTMap() ]
+ return psbt.to_base64()
+
+def do_decode_psbt(b64psbt):
+ psbt = PSBT.from_base64(b64psbt)
+
+ assert len(psbt.tx.vin) == 1
+ assert len(psbt.tx.vout) == 1
+ assert PSBT_SIGNET_BLOCK in psbt.g.map
+
+ scriptSig = psbt.i[0].map.get(7, b"")
+ scriptWitness = psbt.i[0].map.get(8, b"\x00")
+
+ return FromBinary(CBlock, psbt.g.map[PSBT_SIGNET_BLOCK]), ser_string(scriptSig) + scriptWitness
+
+def finish_block(block, signet_solution, grind_cmd):
+ block.vtx[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER + signet_solution)
+ block.vtx[0].rehash()
+ block.hashMerkleRoot = block.calc_merkle_root()
+ if grind_cmd is None:
+ block.solve()
+ else:
+ headhex = CBlockHeader.serialize(block).hex()
+ cmd = grind_cmd.split(" ") + [headhex]
+ newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
+ newhead = FromHex(CBlockHeader(), newheadhex.decode('utf8'))
+ block.nNonce = newhead.nNonce
+ block.rehash()
+ return block
+
+def generate_psbt(tmpl, reward_spk, *, blocktime=None):
+ signet_spk = tmpl["signet_challenge"]
+ signet_spk_bin = unhexlify(signet_spk)
+
+ cbtx = create_coinbase(height=tmpl["height"], value=tmpl["coinbasevalue"], spk=reward_spk)
+ cbtx.vin[0].nSequence = 2**32-2
+ cbtx.rehash()
+
+ block = CBlock()
+ block.nVersion = tmpl["version"]
+ block.hashPrevBlock = int(tmpl["previousblockhash"], 16)
+ block.nTime = tmpl["curtime"] if blocktime is None else blocktime
+ if block.nTime < tmpl["mintime"]:
+ block.nTime = tmpl["mintime"]
+ block.nBits = int(tmpl["bits"], 16)
+ block.nNonce = 0
+ block.vtx = [cbtx] + [FromHex(CTransaction(), t["data"]) for t in tmpl["transactions"]]
+
+ witnonce = 0
+ witroot = block.calc_witness_merkle_root()
+ cbwit = CTxInWitness()
+ cbwit.scriptWitness.stack = [ser_uint256(witnonce)]
+ block.vtx[0].wit.vtxinwit = [cbwit]
+ block.vtx[0].vout.append(CTxOut(0, get_witness_script(witroot, witnonce)))
+
+ signme, spendme = signet_txs(block, signet_spk_bin)
+
+ return do_createpsbt(block, signme, spendme)
+
+def get_reward_address(args, height):
+ if args.address is not None:
+ return args.address
+
+ if '*' not in args.descriptor:
+ addr = json.loads(args.bcli("deriveaddresses", args.descriptor))[0]
+ args.address = addr
+ return addr
+
+ remove = [k for k in args.derived_addresses.keys() if k+20 <= height]
+ for k in remove:
+ del args.derived_addresses[k]
+
+ addr = args.derived_addresses.get(height, None)
+ if addr is None:
+ addrs = json.loads(args.bcli("deriveaddresses", args.descriptor, "[%d,%d]" % (height, height+20)))
+ addr = addrs[0]
+ for k, a in enumerate(addrs):
+ args.derived_addresses[height+k] = a
+
+ return addr
+
+def get_reward_addr_spk(args, height):
+ assert args.address is not None or args.descriptor is not None
+
+ if hasattr(args, "reward_spk"):
+ return args.address, args.reward_spk
+
+ reward_addr = get_reward_address(args, height)
+ reward_spk = unhexlify(json.loads(args.bcli("getaddressinfo", reward_addr))["scriptPubKey"])
+ if args.address is not None:
+ # will always be the same, so cache
+ args.reward_spk = reward_spk
+
+ return reward_addr, reward_spk
+
+def do_genpsbt(args):
+ tmpl = json.load(sys.stdin)
+ _, reward_spk = get_reward_addr_spk(args, tmpl["height"])
+ psbt = generate_psbt(tmpl, reward_spk)
+ print(psbt)
+
+def do_solvepsbt(args):
+ block, signet_solution = do_decode_psbt(sys.stdin.read())
+ block = finish_block(block, signet_solution, args.grind_cmd)
+ print(ToHex(block))
+
+def nbits_to_target(nbits):
+ shift = (nbits >> 24) & 0xff
+ return (nbits & 0x00ffffff) * 2**(8*(shift - 3))
+
+def target_to_nbits(target):
+ tstr = "{0:x}".format(target)
+ if len(tstr) < 6:
+ tstr = ("000000"+tstr)[-6:]
+ if len(tstr) % 2 != 0:
+ tstr = "0" + tstr
+ if int(tstr[0],16) >= 0x8:
+ # avoid "negative"
+ tstr = "00" + tstr
+ fix = int(tstr[:6], 16)
+ sz = len(tstr)//2
+ if tstr[6:] != "0"*(sz*2-6):
+ fix += 1
+
+ return int("%02x%06x" % (sz,fix), 16)
+
+def seconds_to_hms(s):
+ if s == 0:
+ return "0s"
+ neg = (s < 0)
+ if neg:
+ s = -s
+ out = ""
+ if s % 60 > 0:
+ out = "%ds" % (s % 60)
+ s //= 60
+ if s % 60 > 0:
+ out = "%dm%s" % (s % 60, out)
+ s //= 60
+ if s > 0:
+ out = "%dh%s" % (s, out)
+ if neg:
+ out = "-" + out
+ return out
+
+def next_block_delta(last_nbits, last_hash, ultimate_target, do_poisson):
+ # strategy:
+ # 1) work out how far off our desired target we are
+ # 2) cap it to a factor of 4 since that's the best we can do in a single retarget period
+ # 3) use that to work out the desired average interval in this retarget period
+ # 4) if doing poisson, use the last hash to pick a uniformly random number in [0,1), and work out a random multiplier to vary the average by
+ # 5) cap the resulting interval between 1 second and 1 hour to avoid extremes
+
+ INTERVAL = 600.0*2016/2015 # 10 minutes, adjusted for the off-by-one bug
+
+ current_target = nbits_to_target(last_nbits)
+ retarget_factor = ultimate_target / current_target
+ retarget_factor = max(0.25, min(retarget_factor, 4.0))
+
+ avg_interval = INTERVAL * retarget_factor
+
+ if do_poisson:
+ det_rand = int(last_hash[-8:], 16) * 2**-32
+ this_interval_variance = -math.log1p(-det_rand)
+ else:
+ this_interval_variance = 1
+
+ this_interval = avg_interval * this_interval_variance
+ this_interval = max(1, min(this_interval, 3600))
+
+ return this_interval
+
+def next_block_is_mine(last_hash, my_blocks):
+ det_rand = int(last_hash[-16:-8], 16)
+ return my_blocks[0] <= (det_rand % my_blocks[2]) < my_blocks[1]
+
+def do_generate(args):
+ if args.max_blocks is not None:
+ if args.ongoing:
+ logging.error("Cannot specify both --ongoing and --max-blocks")
+ return 1
+ if args.max_blocks < 1:
+ logging.error("N must be a positive integer")
+ return 1
+ max_blocks = args.max_blocks
+ elif args.ongoing:
+ max_blocks = None
+ else:
+ max_blocks = 1
+
+ if args.set_block_time is not None and max_blocks != 1:
+ logging.error("Cannot specify --ongoing or --max-blocks > 1 when using --set-block-time")
+ return 1
+ if args.set_block_time is not None and args.set_block_time < 0:
+ args.set_block_time = time.time()
+ logging.info("Treating negative block time as current time (%d)" % (args.set_block_time))
+
+ if args.min_nbits:
+ if args.nbits is not None:
+ logging.error("Cannot specify --nbits and --min-nbits")
+ return 1
+ args.nbits = "1e0377ae"
+ logging.info("Using nbits=%s" % (args.nbits))
+
+ if args.set_block_time is None:
+ if args.nbits is None or len(args.nbits) != 8:
+ logging.error("Must specify --nbits (use calibrate command to determine value)")
+ return 1
+
+ if args.multiminer is None:
+ my_blocks = (0,1,1)
+ else:
+ if not args.ongoing:
+ logging.error("Cannot specify --multiminer without --ongoing")
+ return 1
+ m = RE_MULTIMINER.match(args.multiminer)
+ if m is None:
+ logging.error("--multiminer argument must be k/m or j-k/m")
+ return 1
+ start,_,stop,total = m.groups()
+ if stop is None:
+ stop = start
+ start, stop, total = map(int, (start, stop, total))
+ if stop < start or start <= 0 or total < stop or total == 0:
+ logging.error("Inconsistent values for --multiminer")
+ return 1
+ my_blocks = (start-1, stop, total)
+
+ ultimate_target = nbits_to_target(int(args.nbits,16))
+
+ mined_blocks = 0
+ bestheader = {"hash": None}
+ lastheader = None
+ while max_blocks is None or mined_blocks < max_blocks:
+
+ # current status?
+ bci = json.loads(args.bcli("getblockchaininfo"))
+
+ if bestheader["hash"] != bci["bestblockhash"]:
+ bestheader = json.loads(args.bcli("getblockheader", bci["bestblockhash"]))
+
+ if lastheader is None:
+ lastheader = bestheader["hash"]
+ elif bestheader["hash"] != lastheader:
+ next_delta = next_block_delta(int(bestheader["bits"], 16), bestheader["hash"], ultimate_target, args.poisson)
+ next_delta += bestheader["time"] - time.time()
+ next_is_mine = next_block_is_mine(bestheader["hash"], my_blocks)
+ logging.info("Received new block at height %d; next in %s (%s)", bestheader["height"], seconds_to_hms(next_delta), ("mine" if next_is_mine else "backup"))
+ lastheader = bestheader["hash"]
+
+ # when is the next block due to be mined?
+ now = time.time()
+ if args.set_block_time is not None:
+ logging.debug("Setting start time to %d", args.set_block_time)
+ mine_time = args.set_block_time
+ action_time = now
+ is_mine = True
+ elif bestheader["height"] == 0:
+ logging.error("When mining first block in a new signet, must specify --set-block-time")
+ return 1
+ else:
+
+ time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson)
+ mine_time = bestheader["time"] + time_delta
+
+ is_mine = next_block_is_mine(bci["bestblockhash"], my_blocks)
+
+ action_time = mine_time
+ if not is_mine:
+ action_time += args.backup_delay
+
+ if args.standby_delay > 0:
+ action_time += args.standby_delay
+ elif mined_blocks == 0:
+ # for non-standby, always mine immediately on startup,
+ # even if the next block shouldn't be ours
+ action_time = now
+
+ # don't want fractional times so round down
+ mine_time = int(mine_time)
+ action_time = int(action_time)
+
+ # can't mine a block 2h in the future; 1h55m for some safety
+ action_time = max(action_time, mine_time - 6900)
+
+ # ready to go? otherwise sleep and check for new block
+ if now < action_time:
+ sleep_for = min(action_time - now, 60)
+ if mine_time < now:
+ # someone else might have mined the block,
+ # so check frequently, so we don't end up late
+ # mining the next block if it's ours
+ sleep_for = min(20, sleep_for)
+ minestr = "mine" if is_mine else "backup"
+ logging.debug("Sleeping for %s, next block due in %s (%s)" % (seconds_to_hms(sleep_for), seconds_to_hms(mine_time - now), minestr))
+ time.sleep(sleep_for)
+ continue
+
+ # gbt
+ tmpl = json.loads(args.bcli("getblocktemplate", '{"rules":["signet","segwit"]}'))
+ if tmpl["previousblockhash"] != bci["bestblockhash"]:
+ logging.warning("GBT based off unexpected block (%s not %s), retrying", tmpl["previousblockhash"], bci["bestblockhash"])
+ time.sleep(1)
+ continue
+
+ logging.debug("GBT template: %s", tmpl)
+
+ if tmpl["mintime"] > mine_time:
+ logging.info("Updating block time from %d to %d", mine_time, tmpl["mintime"])
+ mine_time = tmpl["mintime"]
+ if mine_time > now:
+ logging.error("GBT mintime is in the future: %d is %d seconds later than %d", mine_time, (mine_time-now), now)
+ return 1
+
+ # address for reward
+ reward_addr, reward_spk = get_reward_addr_spk(args, tmpl["height"])
+
+ # mine block
+ logging.debug("Mining block delta=%s start=%s mine=%s", seconds_to_hms(mine_time-bestheader["time"]), mine_time, is_mine)
+ mined_blocks += 1
+ psbt = generate_psbt(tmpl, reward_spk, blocktime=mine_time)
+ psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=psbt.encode('utf8')))
+ if not psbt_signed.get("complete",False):
+ logging.debug("Generated PSBT: %s" % (psbt,))
+ sys.stderr.write("PSBT signing failed")
+ return 1
+ block, signet_solution = do_decode_psbt(psbt_signed["psbt"])
+ block = finish_block(block, signet_solution, args.grind_cmd)
+
+ # submit block
+ r = args.bcli("-stdin", "submitblock", input=ToHex(block).encode('utf8'))
+
+ # report
+ bstr = "block" if is_mine else "backup block"
+
+ next_delta = next_block_delta(block.nBits, block.hash, ultimate_target, args.poisson)
+ next_delta += block.nTime - time.time()
+ next_is_mine = next_block_is_mine(block.hash, my_blocks)
+
+ logging.debug("Block hash %s payout to %s", block.hash, reward_addr)
+ logging.info("Mined %s at height %d; next in %s (%s)", bstr, tmpl["height"], seconds_to_hms(next_delta), ("mine" if next_is_mine else "backup"))
+ if r != "":
+ logging.warning("submitblock returned %s for height %d hash %s", r, tmpl["height"], block.hash)
+ lastheader = block.hash
+
+def do_calibrate(args):
+ if args.nbits is not None and args.seconds is not None:
+ sys.stderr.write("Can only specify one of --nbits or --seconds\n")
+ return 1
+ if args.nbits is not None and len(args.nbits) != 8:
+ sys.stderr.write("Must specify 8 hex digits for --nbits")
+ return 1
+
+ TRIALS = 600 # gets variance down pretty low
+ TRIAL_BITS = 0x1e3ea75f # takes about 5m to do 600 trials
+ #TRIAL_BITS = 0x1e7ea75f # XXX
+
+ header = CBlockHeader()
+ header.nBits = TRIAL_BITS
+ targ = nbits_to_target(header.nBits)
+
+ start = time.time()
+ count = 0
+ #CHECKS=[]
+ for i in range(TRIALS):
+ header.nTime = i
+ header.nNonce = 0
+ headhex = header.serialize().hex()
+ cmd = args.grind_cmd.split(" ") + [headhex]
+ newheadhex = subprocess.run(cmd, stdout=subprocess.PIPE, input=b"", check=True).stdout.strip()
+ #newhead = FromHex(CBlockHeader(), newheadhex.decode('utf8'))
+ #count += newhead.nNonce
+ #if (i+1) % 100 == 0:
+ # CHECKS.append((i+1, count, time.time()-start))
+
+ #print("checks =", [c*1.0 / (b*targ*2**-256) for _,b,c in CHECKS])
+
+ avg = (time.time() - start) * 1.0 / TRIALS
+ #exp_count = 2**256 / targ * TRIALS
+ #print("avg =", avg, "count =", count, "exp_count =", exp_count)
+
+ if args.nbits is not None:
+ want_targ = nbits_to_target(int(args.nbits,16))
+ want_time = avg*targ/want_targ
+ else:
+ want_time = args.seconds if args.seconds is not None else 25
+ want_targ = int(targ*(avg/want_time))
+
+ print("nbits=%08x for %ds average mining time" % (target_to_nbits(want_targ), want_time))
+ return 0
+
+def bitcoin_cli(basecmd, args, **kwargs):
+ cmd = basecmd + ["-signet"] + args
+ logging.debug("Calling bitcoin-cli: %r", cmd)
+ out = subprocess.run(cmd, stdout=subprocess.PIPE, **kwargs, check=True).stdout
+ if isinstance(out, bytes):
+ out = out.decode('utf8')
+ return out.strip()
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--cli", default="bitcoin-cli", type=str, help="bitcoin-cli command")
+ parser.add_argument("--debug", action="store_true", help="Print debugging info")
+ parser.add_argument("--quiet", action="store_true", help="Only print warnings/errors")
+
+ cmds = parser.add_subparsers(help="sub-commands")
+ genpsbt = cmds.add_parser("genpsbt", help="Generate a block PSBT for signing")
+ genpsbt.set_defaults(fn=do_genpsbt)
+
+ solvepsbt = cmds.add_parser("solvepsbt", help="Solve a signed block PSBT")
+ solvepsbt.set_defaults(fn=do_solvepsbt)
+
+ generate = cmds.add_parser("generate", help="Mine blocks")
+ generate.set_defaults(fn=do_generate)
+ generate.add_argument("--ongoing", action="store_true", help="Keep mining blocks")
+ generate.add_argument("--max-blocks", default=None, type=int, help="Max blocks to mine (default=1)")
+ generate.add_argument("--set-block-time", default=None, type=int, help="Set block time (unix timestamp)")
+ generate.add_argument("--nbits", default=None, type=str, help="Target nBits (specify difficulty)")
+ generate.add_argument("--min-nbits", action="store_true", help="Target minimum nBits (use min difficulty)")
+ generate.add_argument("--poisson", action="store_true", help="Simulate randomised block times")
+ #generate.add_argument("--signcmd", default=None, type=str, help="Alternative signing command")
+ generate.add_argument("--multiminer", default=None, type=str, help="Specify which set of blocks to mine (eg: 1-40/100 for the first 40%%, 2/3 for the second 3rd)")
+ generate.add_argument("--backup-delay", default=300, type=int, help="Seconds to delay before mining blocks reserved for other miners (default=300)")
+ generate.add_argument("--standby-delay", default=0, type=int, help="Seconds to delay before mining blocks (default=0)")
+
+ calibrate = cmds.add_parser("calibrate", help="Calibrate difficulty")
+ calibrate.set_defaults(fn=do_calibrate)
+ calibrate.add_argument("--nbits", type=str, default=None)
+ calibrate.add_argument("--seconds", type=int, default=None)
+
+ for sp in [genpsbt, generate]:
+ sp.add_argument("--address", default=None, type=str, help="Address for block reward payment")
+ sp.add_argument("--descriptor", default=None, type=str, help="Descriptor for block reward payment")
+
+ for sp in [solvepsbt, generate, calibrate]:
+ sp.add_argument("--grind-cmd", default=None, type=str, help="Command to grind a block header for proof-of-work")
+
+ args = parser.parse_args(sys.argv[1:])
+
+ args.bcli = lambda *a, input=b"", **kwargs: bitcoin_cli(args.cli.split(" "), list(a), input=input, **kwargs)
+
+ if hasattr(args, "address") and hasattr(args, "descriptor"):
+ if args.address is None and args.descriptor is None:
+ sys.stderr.write("Must specify --address or --descriptor\n")
+ return 1
+ elif args.address is not None and args.descriptor is not None:
+ sys.stderr.write("Only specify one of --address or --descriptor\n")
+ return 1
+ args.derived_addresses = {}
+
+ if args.debug:
+ logging.getLogger().setLevel(logging.DEBUG)
+ elif args.quiet:
+ logging.getLogger().setLevel(logging.WARNING)
+ else:
+ logging.getLogger().setLevel(logging.INFO)
+
+ if hasattr(args, "fn"):
+ return args.fn(args)
+ else:
+ logging.error("Must specify command")
+ return 1
+
+if __name__ == "__main__":
+ main()
+
+