chore: initial import from lecoffre_node/miner
This commit is contained in:
commit
5b504e2679
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@ -0,0 +1,49 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Installation des dépendances de base
|
||||
RUN apt-get update && apt-get upgrade -y && \
|
||||
apt-get install -y --fix-missing \
|
||||
ca-certificates curl jq git python3 python3-pip && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Création d'un utilisateur non-root
|
||||
RUN useradd -m -u 1000 appuser && \
|
||||
mkdir -p /app && chown -R appuser:appuser /app
|
||||
|
||||
# Installer bitcoin-cli (binaire officiel)
|
||||
RUN curl -L -o /tmp/bitcoin-cli.tar.gz https://bitcoincore.org/bin/bitcoin-core-26.2/bitcoin-26.2-x86_64-linux-gnu.tar.gz \
|
||||
&& mkdir -p /tmp/bitcoin-cli \
|
||||
&& tar -xzf /tmp/bitcoin-cli.tar.gz -C /tmp/bitcoin-cli --strip-components=2 bitcoin-26.2/bin/bitcoin-cli \
|
||||
&& mv /tmp/bitcoin-cli/bitcoin-cli /usr/local/bin/bitcoin-cli \
|
||||
&& chmod +x /usr/local/bin/bitcoin-cli \
|
||||
&& rm -rf /tmp/bitcoin-cli /tmp/bitcoin-cli.tar.gz
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip3 install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Vendoriser test_framework depuis Bitcoin Core (pour le script signet/miner)
|
||||
RUN curl -L -o /tmp/bitcoin-core.tar.gz https://github.com/bitcoin/bitcoin/archive/refs/tags/v26.2.tar.gz \
|
||||
&& mkdir -p /tmp/bitcoin-core \
|
||||
&& tar -xzf /tmp/bitcoin-core.tar.gz -C /tmp/bitcoin-core --strip-components=1 \
|
||||
&& mkdir -p /app/test/functional \
|
||||
&& cp -r /tmp/bitcoin-core/test/functional/test_framework /app/test/functional/test_framework \
|
||||
&& rm -rf /tmp/bitcoin-core /tmp/bitcoin-core.tar.gz
|
||||
|
||||
COPY entrypoint.sh ./
|
||||
COPY signet_miner.py ./
|
||||
COPY signet/ ./signet/
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh && \
|
||||
chown -R appuser:appuser /app
|
||||
|
||||
USER appuser
|
||||
|
||||
VOLUME ["/bitcoin"]
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
56
entrypoint.sh
Executable file
56
entrypoint.sh
Executable file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BITCOIN_DIR=${BITCOIN_DIR:-/bitcoin}
|
||||
COOKIE_FILE=${COOKIE_FILE:-$BITCOIN_DIR/signet/.cookie}
|
||||
RPC_HOST=${RPC_HOST:-bitcoin}
|
||||
RPC_PORT=${RPC_PORT:-38332}
|
||||
POLL_INTERVAL=${POLL_INTERVAL:-5}
|
||||
WATCHONLY_WALLET=${WATCHONLY_WALLET:-watchonly}
|
||||
MINING_WALLET=${MINING_WALLET:-mining_mnemonic}
|
||||
MINER_TAG=${MINER_TAG:-lecoffre}
|
||||
|
||||
# Ajouter test_framework au PYTHONPATH
|
||||
export PYTHONPATH="/app/test/functional:${PYTHONPATH:-}"
|
||||
|
||||
if [ ! -f "$COOKIE_FILE" ]; then
|
||||
echo "Cookie introuvable: $COOKIE_FILE" >&2
|
||||
ls -la "$BITCOIN_DIR" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables attendues via miner/.env
|
||||
# COINBASE_ADDRESS est optionnel - si non défini, une adresse sera générée automatiquement
|
||||
|
||||
# Adresse du relay pour partager les rewards (optionnel)
|
||||
RELAY_ADDRESS="${RELAY_ADDRESS:-}"
|
||||
REWARD_SPLIT_RATIO="${REWARD_SPLIT_RATIO:-0.5}"
|
||||
|
||||
# Lancer le miner (les options globales doivent précéder la sous-commande)
|
||||
MINER_CMD=(
|
||||
python /app/signet/miner \
|
||||
--cli "bitcoin-cli -datadir=$BITCOIN_DIR -rpcconnect=$RPC_HOST -rpcport=$RPC_PORT -rpccookiefile=$COOKIE_FILE" \
|
||||
generate \
|
||||
--ongoing \
|
||||
--min-nbits \
|
||||
--WATCHONLY_WALLET "$WATCHONLY_WALLET" \
|
||||
--MINING_WALLET "$MINING_WALLET" \
|
||||
--MINER_TAG "$MINER_TAG"
|
||||
)
|
||||
|
||||
if [ -n "${COINBASE_ADDRESS:-}" ]; then
|
||||
MINER_CMD+=( --address "$COINBASE_ADDRESS" )
|
||||
elif [ -n "${COINBASE_DESCRIPTOR:-}" ]; then
|
||||
MINER_CMD+=( --descriptor "$COINBASE_DESCRIPTOR" )
|
||||
else
|
||||
# Générer automatiquement une adresse
|
||||
MINER_CMD+=( --address "auto" )
|
||||
fi
|
||||
|
||||
if [ -n "${RELAY_ADDRESS:-}" ]; then
|
||||
MINER_CMD+=( --relay-address "$RELAY_ADDRESS" )
|
||||
fi
|
||||
|
||||
MINER_CMD+=( --reward-split-ratio "$REWARD_SPLIT_RATIO" )
|
||||
|
||||
exec "${MINER_CMD[@]}"
|
4
miner.env
Normal file
4
miner.env
Normal file
@ -0,0 +1,4 @@
|
||||
# Configuration du miner signet
|
||||
# COINBASE_ADDRESS= # Générer automatiquement
|
||||
RELAY_ADDRESS=tsp1qqd8k3twmuq3awxjmfukhma36j4la8gzsa8t0dgfms3cfglt2gkz6wqsqpd3d2q4quq59agtyfsr7gj9t07qt0nlrlrzgmhvpn5enfm76fud6sm0y
|
||||
REWARD_SPLIT_RATIO=0.5
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
requests==2.32.3
|
||||
python-bitcointx==1.1.2
|
||||
websockets==12.0
|
83
signet/README.md
Normal file
83
signet/README.md
Normal file
@ -0,0 +1,83 @@
|
||||
Contents
|
||||
========
|
||||
This directory contains tools related to Signet, both for running a Signet yourself and for using one.
|
||||
|
||||
getcoins.py
|
||||
===========
|
||||
|
||||
A script to call a faucet to get Signet coins.
|
||||
|
||||
Syntax: `getcoins.py [-h|--help] [-c|--cmd=<bitcoin-cli path>] [-f|--faucet=<faucet URL>] [-a|--addr=<signet bech32 address>] [-p|--password=<faucet password>] [--] [<bitcoin-cli args>]`
|
||||
|
||||
* `--cmd` lets you customize the bitcoin-cli path. By default it will look for it in the PATH
|
||||
* `--faucet` lets you specify which faucet to use; the faucet is assumed to be compatible with https://github.com/kallewoof/bitcoin-faucet
|
||||
* `--addr` lets you specify a Signet address; by default, the address must be a bech32 address. This and `--cmd` above complement each other (i.e. you do not need `bitcoin-cli` if you use `--addr`)
|
||||
* `--password` lets you specify a faucet password; this is handy if you are in a classroom and set up your own faucet for your students; (above faucet does not limit by IP when password is enabled)
|
||||
|
||||
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
|
||||
=====
|
||||
|
||||
You will first 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 appropriate for your hardware, eg:
|
||||
|
||||
cd src/
|
||||
MINER="../contrib/signet/miner"
|
||||
GRIND="./bitcoin-util grind"
|
||||
$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.
|
||||
|
||||
To mine the first block in your custom chain, you can run:
|
||||
|
||||
CLI="./bitcoin-cli -conf=mysignet.conf"
|
||||
ADDR=$($CLI -signet getnewaddress)
|
||||
NBITS=1e00f403
|
||||
$MINER --cli="$CLI" generate --grind-cmd="$GRIND" --address="$ADDR" --nbits=$NBITS
|
||||
|
||||
This will mine a single block with a backdated timestamp designed to allow 100 blocks to be mined as quickly as possible, so that it is possible to do transactions.
|
||||
|
||||
Adding 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=$NBITS --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.
|
||||
|
||||
The --set-block-time option is available to manually move timestamps forward or backward (subject to the rules that blocktime must be greater than mediantime, and dates can't be more than two hours in the future). It can only be used when mining a single block (ie, not when using --ongoing or --max-blocks greater than 1).
|
||||
|
||||
Instead of using a single address, a ranged descriptor may be provided 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 minimum signet difficulty will be targeted. Signet's minimum difficulty corresponds to --nbits=1e0377ae.
|
||||
|
||||
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 (eg, to sign the block with a hardware wallet).
|
||||
|
158
signet/getcoins.py
Normal file
158
signet/getcoins.py
Normal file
@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2020-2021 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 io
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree
|
||||
|
||||
DEFAULT_GLOBAL_FAUCET = 'https://signetfaucet.com/claim'
|
||||
DEFAULT_GLOBAL_CAPTCHA = 'https://signetfaucet.com/captcha'
|
||||
GLOBAL_FIRST_BLOCK_HASH = '00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53'
|
||||
|
||||
# braille unicode block
|
||||
BASE = 0x2800
|
||||
BIT_PER_PIXEL = [
|
||||
[0x01, 0x08],
|
||||
[0x02, 0x10],
|
||||
[0x04, 0x20],
|
||||
[0x40, 0x80],
|
||||
]
|
||||
BW = 2
|
||||
BH = 4
|
||||
|
||||
# imagemagick or compatible fork (used for converting SVG)
|
||||
CONVERT = 'convert'
|
||||
|
||||
class PPMImage:
|
||||
'''
|
||||
Load a PPM image (Pillow-ish API).
|
||||
'''
|
||||
def __init__(self, f):
|
||||
if f.readline() != b'P6\n':
|
||||
raise ValueError('Invalid ppm format: header')
|
||||
line = f.readline()
|
||||
(width, height) = (int(x) for x in line.rstrip().split(b' '))
|
||||
if f.readline() != b'255\n':
|
||||
raise ValueError('Invalid ppm format: color depth')
|
||||
data = f.read(width * height * 3)
|
||||
stride = width * 3
|
||||
self.size = (width, height)
|
||||
self._grid = [[tuple(data[stride * y + 3 * x:stride * y + 3 * (x + 1)]) for x in range(width)] for y in range(height)]
|
||||
|
||||
def getpixel(self, pos):
|
||||
return self._grid[pos[1]][pos[0]]
|
||||
|
||||
def print_image(img, threshold=128):
|
||||
'''Print black-and-white image to terminal in braille unicode characters.'''
|
||||
x_blocks = (img.size[0] + BW - 1) // BW
|
||||
y_blocks = (img.size[1] + BH - 1) // BH
|
||||
|
||||
for yb in range(y_blocks):
|
||||
line = []
|
||||
for xb in range(x_blocks):
|
||||
ch = BASE
|
||||
for y in range(BH):
|
||||
for x in range(BW):
|
||||
try:
|
||||
val = img.getpixel((xb * BW + x, yb * BH + y))
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
if val[0] < threshold:
|
||||
ch |= BIT_PER_PIXEL[y][x]
|
||||
line.append(chr(ch))
|
||||
print(''.join(line))
|
||||
|
||||
parser = argparse.ArgumentParser(description='Script to get coins from a faucet.', epilog='You may need to start with double-dash (--) when providing bitcoin-cli arguments.')
|
||||
parser.add_argument('-c', '--cmd', dest='cmd', default='bitcoin-cli', help='bitcoin-cli command to use')
|
||||
parser.add_argument('-f', '--faucet', dest='faucet', default=DEFAULT_GLOBAL_FAUCET, help='URL of the faucet')
|
||||
parser.add_argument('-g', '--captcha', dest='captcha', default=DEFAULT_GLOBAL_CAPTCHA, help='URL of the faucet captcha, or empty if no captcha is needed')
|
||||
parser.add_argument('-a', '--addr', dest='addr', default='', help='Bitcoin address to which the faucet should send')
|
||||
parser.add_argument('-p', '--password', dest='password', default='', help='Faucet password, if any')
|
||||
parser.add_argument('-n', '--amount', dest='amount', default='0.001', help='Amount to request (0.001-0.1, default is 0.001)')
|
||||
parser.add_argument('-i', '--imagemagick', dest='imagemagick', default=CONVERT, help='Path to imagemagick convert utility')
|
||||
parser.add_argument('bitcoin_cli_args', nargs='*', help='Arguments to pass on to bitcoin-cli (default: -signet)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.bitcoin_cli_args == []:
|
||||
args.bitcoin_cli_args = ['-signet']
|
||||
|
||||
|
||||
def bitcoin_cli(rpc_command_and_params):
|
||||
argv = [args.cmd] + args.bitcoin_cli_args + rpc_command_and_params
|
||||
try:
|
||||
return subprocess.check_output(argv).strip().decode()
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f"The binary {args.cmd} could not be found")
|
||||
except subprocess.CalledProcessError:
|
||||
cmdline = ' '.join(argv)
|
||||
raise SystemExit(f"-----\nError while calling {cmdline} (see output above).")
|
||||
|
||||
|
||||
if args.faucet.lower() == DEFAULT_GLOBAL_FAUCET:
|
||||
# Get the hash of the block at height 1 of the currently active signet chain
|
||||
curr_signet_hash = bitcoin_cli(['getblockhash', '1'])
|
||||
if curr_signet_hash != GLOBAL_FIRST_BLOCK_HASH:
|
||||
raise SystemExit('The global faucet cannot be used with a custom Signet network. Please use the global signet or setup your custom faucet to use this functionality.\n')
|
||||
else:
|
||||
# For custom faucets, don't request captcha by default.
|
||||
if args.captcha == DEFAULT_GLOBAL_CAPTCHA:
|
||||
args.captcha = ''
|
||||
|
||||
if args.addr == '':
|
||||
# get address for receiving coins
|
||||
args.addr = bitcoin_cli(['getnewaddress', 'faucet', 'bech32'])
|
||||
|
||||
data = {'address': args.addr, 'password': args.password, 'amount': args.amount}
|
||||
|
||||
# Store cookies
|
||||
# for debugging: print(session.cookies.get_dict())
|
||||
session = requests.Session()
|
||||
|
||||
if args.captcha != '': # Retrieve a captcha
|
||||
try:
|
||||
res = session.get(args.captcha)
|
||||
res.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise SystemExit(f"Unexpected error when contacting faucet: {e}")
|
||||
|
||||
# Size limitation
|
||||
svg = xml.etree.ElementTree.fromstring(res.content)
|
||||
if svg.attrib.get('width') != '150' or svg.attrib.get('height') != '50':
|
||||
raise SystemExit("Captcha size doesn't match expected dimensions 150x50")
|
||||
|
||||
# Convert SVG image to PPM, and load it
|
||||
try:
|
||||
rv = subprocess.run([args.imagemagick, 'svg:-', '-depth', '8', 'ppm:-'], input=res.content, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f"The binary {args.imagemagick} could not be found. Please make sure ImageMagick (or a compatible fork) is installed and that the correct path is specified.")
|
||||
|
||||
img = PPMImage(io.BytesIO(rv.stdout))
|
||||
|
||||
# Terminal interaction
|
||||
print_image(img)
|
||||
print(f"Captcha from URL {args.captcha}")
|
||||
data['captcha'] = input('Enter captcha: ')
|
||||
|
||||
try:
|
||||
res = session.post(args.faucet, data=data)
|
||||
except:
|
||||
raise SystemExit(f"Unexpected error when contacting faucet: {sys.exc_info()[0]}")
|
||||
|
||||
# Display the output as per the returned status code
|
||||
if res:
|
||||
# When the return code is in between 200 and 400 i.e. successful
|
||||
print(res.text)
|
||||
elif res.status_code == 404:
|
||||
print('The specified faucet URL does not exist. Please check for any server issues/typo.')
|
||||
elif res.status_code == 429:
|
||||
print('The script does not allow for repeated transactions as the global faucet is rate-limitied to 1 request/IP/day. You can access the faucet website to get more coins manually')
|
||||
else:
|
||||
print(f'Returned Error Code {res.status_code}\n{res.text}\n')
|
||||
print('Please check the provided arguments for their validity and/or any possible typo.')
|
989
signet/miner
Normal file
989
signet/miner
Normal file
@ -0,0 +1,989 @@
|
||||
#!/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
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import asyncio, websockets
|
||||
from hashlib import sha256
|
||||
|
||||
from io import BytesIO
|
||||
from random import uniform
|
||||
from decimal import Decimal
|
||||
|
||||
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, from_hex, deser_string, hash256, ser_compact_size, ser_string, ser_uint256, tx_from_hex, uint256_from_str # noqa: E402
|
||||
from test_framework.script import CScript, 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+)$")
|
||||
CC_URL = 'ws://localhost:9823/websocket'
|
||||
|
||||
|
||||
def json_dumps(obj, **k):
|
||||
def hook(dd):
|
||||
if isinstance(dd, Decimal):
|
||||
return float(dd)
|
||||
if hasattr(dd, 'strftime'):
|
||||
return str(dd) # isoformat
|
||||
logging.error("Unhandled JSON type: %r" % dd)
|
||||
raise TypeError
|
||||
|
||||
k['default'] = hook
|
||||
|
||||
return json.dumps(obj, **k)
|
||||
|
||||
def message(action, *args):
|
||||
return json_dumps(dict(action=action, args=args))
|
||||
|
||||
POLICY = {
|
||||
'never_log': False,
|
||||
'must_log': False,
|
||||
'priv_over_ux': True,
|
||||
'boot_to_hsm': "123456",
|
||||
'period': None,
|
||||
'set_sl': None,
|
||||
'allow_sl': None,
|
||||
'rules': [
|
||||
{
|
||||
"whitelist": "",
|
||||
"per_period": None,
|
||||
"max_amount": None,
|
||||
"users": "",
|
||||
"min_users": "all",
|
||||
"local_conf": False,
|
||||
"wallet": None
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# #### some helpers that could go into test_framework
|
||||
|
||||
# like from_hex, 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, miner_tag=''):
|
||||
cb = CTransaction()
|
||||
scriptsig = bytes(script_BIP34_coinbase_height(height))
|
||||
if miner_tag is not None:
|
||||
scriptsig = CScript(scriptsig + CScriptOp.encode_op_pushdata(miner_tag.encode()))
|
||||
else:
|
||||
scriptsig = CScript(scriptsig)
|
||||
cb.vin = [CTxIn(COutPoint(0, 0xffffffff), scriptsig, 0xffffffff)]
|
||||
|
||||
# Utiliser une seule sortie pour le miner (le relay recevra des fonds via une transaction normale)
|
||||
cb.vout = [CTxOut(value, spk)]
|
||||
logging.info(f"Coinbase reward: {value} sat to miner")
|
||||
|
||||
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 = from_hex(CBlockHeader(), newheadhex.decode('utf8'))
|
||||
block.nNonce = newhead.nNonce
|
||||
block.rehash()
|
||||
return block
|
||||
|
||||
def generate_psbt(tmpl, reward_spk, *, blocktime=None, miner_tag='', relay_spk=None, reward_split_ratio=0.5):
|
||||
signet_spk = tmpl["signet_challenge"]
|
||||
signet_spk_bin = bytes.fromhex(signet_spk)
|
||||
|
||||
cbtx = create_coinbase(height=tmpl["height"], value=tmpl["coinbasevalue"], spk=reward_spk, miner_tag=miner_tag)
|
||||
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] + [tx_from_hex(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:
|
||||
if args.address == "auto":
|
||||
# Générer automatiquement une adresse
|
||||
try:
|
||||
addr = json.loads(args.bcli(f"-rpcwallet={args.MINING_WALLET}", "getnewaddress"))
|
||||
return addr
|
||||
except:
|
||||
# En cas d'erreur, utiliser une adresse simple
|
||||
logging.warning("Failed to generate new address, using simple address")
|
||||
return "tb1qauto123456789012345678901234567890"
|
||||
return args.address
|
||||
|
||||
if args.descriptor is None:
|
||||
try:
|
||||
addr = json.loads(args.bcli(f"-rpcwallet={args.MINING_WALLET}", "getnewaddress"))
|
||||
return addr
|
||||
except Exception as e:
|
||||
# En cas d'erreur, réessayer avec une approche différente
|
||||
logging.error(f"Failed to generate new address: {e}")
|
||||
try:
|
||||
# Essayer de créer une adresse avec un label
|
||||
new_addr = json.loads(args.bcli(f"-rpcwallet={args.MINING_WALLET}", "getnewaddress", "miner"))
|
||||
logging.info(f"Generated new address with label: {new_addr}")
|
||||
return new_addr
|
||||
except Exception as e2:
|
||||
logging.error(f"Failed to generate address with label: {e2}")
|
||||
# En dernier recours, utiliser une adresse simple
|
||||
return "tb1qauto123456789012345678901234567890"
|
||||
|
||||
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)
|
||||
if args.signer != None:
|
||||
wallet = args.WATCHONLY_WALLET
|
||||
else:
|
||||
wallet = args.MINING_WALLET
|
||||
print("%s", reward_addr)
|
||||
|
||||
try:
|
||||
reward_spk = bytes.fromhex(json.loads(args.bcli(f"-rpcwallet={wallet}", "getaddressinfo", reward_addr))["scriptPubKey"])
|
||||
except:
|
||||
# Si l'adresse n'est pas dans le wallet, générer une nouvelle adresse
|
||||
logging.warning(f"Address {reward_addr} not in wallet, generating new address")
|
||||
try:
|
||||
new_addr = json.loads(args.bcli(f"-rpcwallet={wallet}", "getnewaddress"))
|
||||
reward_spk = bytes.fromhex(json.loads(args.bcli(f"-rpcwallet={wallet}", "getaddressinfo", new_addr))["scriptPubKey"])
|
||||
logging.info(f"Generated new address: {new_addr}")
|
||||
# Mettre à jour l'adresse pour les logs
|
||||
reward_addr = new_addr
|
||||
except Exception as e:
|
||||
# En cas d'erreur, réessayer avec une approche différente
|
||||
logging.error(f"Failed to generate new address: {e}")
|
||||
try:
|
||||
# Essayer de créer une adresse avec un label
|
||||
new_addr = json.loads(args.bcli(f"-rpcwallet={wallet}", "getnewaddress", "miner"))
|
||||
reward_spk = bytes.fromhex(json.loads(args.bcli(f"-rpcwallet={wallet}", "getaddressinfo", new_addr))["scriptPubKey"])
|
||||
logging.info(f"Generated new address with label: {new_addr}")
|
||||
reward_addr = new_addr
|
||||
except Exception as e2:
|
||||
logging.error(f"Failed to generate address with label: {e2}")
|
||||
# En dernier recours, utiliser une adresse simple
|
||||
reward_spk = bytes.fromhex("0014" + "0" * 40)
|
||||
|
||||
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"])
|
||||
|
||||
# Obtenir l'adresse et le scriptPubKey du relay
|
||||
relay_spk = None
|
||||
if hasattr(args, 'relay_address') and args.relay_address:
|
||||
try:
|
||||
relay_spk = bytes.fromhex(json.loads(args.bcli(f"-rpcwallet={args.WATCHONLY_WALLET}", "getaddressinfo", args.relay_address))["scriptPubKey"])
|
||||
except:
|
||||
# Si l'adresse n'est pas dans le wallet, utiliser la même adresse que le miner
|
||||
logging.warning(f"Relay address {args.relay_address} not in wallet, using miner address")
|
||||
relay_spk = reward_spk
|
||||
|
||||
psbt = generate_psbt(tmpl, reward_spk, None, args.MINER_TAG, relay_spk, getattr(args, 'reward_split_ratio', 0.5))
|
||||
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(block.serialize().hex())
|
||||
|
||||
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 send_to_relay(args, miner_addr, height):
|
||||
"""Envoie des fonds au relay après avoir miné un bloc"""
|
||||
if not hasattr(args, 'relay_address') or not args.relay_address:
|
||||
return
|
||||
|
||||
relay_addr = args.relay_address
|
||||
split_ratio = getattr(args, 'reward_split_ratio', 0.5)
|
||||
|
||||
# Attendre que le bloc soit confirmé
|
||||
time.sleep(5)
|
||||
|
||||
try:
|
||||
# Vérifier le solde du wallet
|
||||
balance = json.loads(args.bcli(f"-rpcwallet={args.MINING_WALLET}", "getbalance"))
|
||||
if balance < 0.001: # Minimum 0.001 BTC
|
||||
logging.warning(f"Insufficient balance to send to relay: {balance} BTC")
|
||||
return
|
||||
|
||||
# Calculer le montant à envoyer (50% du solde disponible)
|
||||
amount = balance * split_ratio
|
||||
amount = max(0.001, min(amount, 0.1)) # Entre 0.001 et 0.1 BTC
|
||||
|
||||
# Envoyer les fonds au relay
|
||||
txid = json.loads(args.bcli(f"-rpcwallet={args.MINING_WALLET}", "sendtoaddress", relay_addr, str(amount)))
|
||||
logging.info(f"Sent {amount} BTC to relay {relay_addr}, txid: {txid}")
|
||||
|
||||
# Attendre que la transaction soit confirmée
|
||||
time.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send funds to relay: {e}")
|
||||
|
||||
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 = uniform(0.95,1.05)
|
||||
|
||||
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)
|
||||
|
||||
if args.MINER_TAG is not None:
|
||||
args.MINER_TAG = args.MINER_TAG.encode('utf-8')
|
||||
else:
|
||||
args.MINER_TAG = "default".encode('utf-8')
|
||||
|
||||
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:
|
||||
time_delta = next_block_delta(int(bestheader["bits"], 16), bci["bestblockhash"], ultimate_target, args.poisson)
|
||||
time_delta *= 100 # 100 blocks
|
||||
logging.info("Backdating time for first block to %d minutes ago" % (time_delta/60))
|
||||
mine_time = now - time_delta
|
||||
action_time = now
|
||||
is_mine = True
|
||||
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"])
|
||||
|
||||
# Obtenir l'adresse et le scriptPubKey du relay
|
||||
relay_spk = None
|
||||
if hasattr(args, 'relay_address') and args.relay_address:
|
||||
try:
|
||||
relay_spk = bytes.fromhex(json.loads(args.bcli(f"-rpcwallet={args.WATCHONLY_WALLET}", "getaddressinfo", args.relay_address))["scriptPubKey"])
|
||||
except:
|
||||
# Si l'adresse n'est pas dans le wallet, créer une adresse simple
|
||||
logging.warning(f"Relay address {args.relay_address} not in wallet, using simple address")
|
||||
# Pour l'instant, on utilise la même adresse que le miner
|
||||
relay_spk = reward_spk
|
||||
|
||||
# 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, relay_spk=relay_spk, reward_split_ratio=getattr(args, 'reward_split_ratio', 0.5))
|
||||
logging.info(f"psbt pre-processing: {psbt}")
|
||||
input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8')
|
||||
if args.signer == "coldcard":
|
||||
psbt = json.loads(args.bcli("-stdin", f"-rpcwallet={args.WATCHONLY_WALLET}", "walletprocesspsbt", input=input_stream))['psbt']
|
||||
try:
|
||||
psbt_hash = asyncio.get_event_loop().run_until_complete(coldcard_upload_psbt(psbt))
|
||||
except Exception as e:
|
||||
logging.error("Waiting 5s before trying again.")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
try:
|
||||
psbt_signed = asyncio.get_event_loop().run_until_complete(coldcard_sign_psbt(psbt_hash))
|
||||
except Exception as e:
|
||||
logging.error("Please check your keys.")
|
||||
return 1
|
||||
|
||||
try:
|
||||
assert('local_download' in psbt_signed)
|
||||
except AssertionError as e:
|
||||
logging.error("Didn't receive signed psbt")
|
||||
logging.error(f"Received: {psbt_signed}")
|
||||
logging.error("Waiting 5s before trying again.")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
input_stream = os.linesep.join([psbt_signed['local_download']['data'], "false"]).encode('utf8')
|
||||
try:
|
||||
final_psbt = json.loads(args.bcli("-stdin", "finalizepsbt", input=input_stream))
|
||||
except Exception as e:
|
||||
logging.error(f"Core can't finalize psbt: {e}")
|
||||
return 1
|
||||
if not final_psbt.get("complete",False):
|
||||
logging.error("finalizepsbt return: %s" % (final_psbt,))
|
||||
sys.stderr.write("PSBT finalization failed\n")
|
||||
return 1
|
||||
|
||||
final_psbt = final_psbt["psbt"]
|
||||
else:
|
||||
psbt_signed = json.loads(args.bcli("-stdin", f"-rpcwallet={args.MINING_WALLET}", "walletprocesspsbt", input=input_stream))
|
||||
if not psbt_signed.get("complete",False):
|
||||
logging.debug("Generated PSBT: %s" % (psbt,))
|
||||
sys.stderr.write("PSBT signing failed\n")
|
||||
return 1
|
||||
final_psbt = psbt_signed["psbt"]
|
||||
block, signet_solution = do_decode_psbt(final_psbt)
|
||||
block = finish_block(block, signet_solution, args.grind_cmd)
|
||||
|
||||
# submit block
|
||||
r = args.bcli("-stdin", "submitblock", input=block.serialize().hex().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)
|
||||
|
||||
# Envoyer des fonds au relay si configuré
|
||||
if hasattr(args, 'relay_address') and args.relay_address and getattr(args, 'reward_split_ratio', 0) > 0:
|
||||
try:
|
||||
send_to_relay(args, reward_addr, tmpl["height"])
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send funds to relay: {e}")
|
||||
|
||||
lastheader = block.hash
|
||||
|
||||
async def coldcard_upload_psbt(psbt):
|
||||
async with websockets.connect(CC_URL) as ws:
|
||||
psbt_bin = base64.b64decode(psbt)
|
||||
psbt_hash = sha256(psbt_bin).digest()
|
||||
payload = message("upload_psbt", len(psbt_bin), psbt_hash.hex(), psbt)
|
||||
await ws.send(payload)
|
||||
async for msg in ws:
|
||||
try:
|
||||
r = json.loads(msg)
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"{e}: {msg} is not valid json")
|
||||
continue
|
||||
|
||||
if 'update_status' in r:
|
||||
logging.info(f"Received update: {r['update_status']}")
|
||||
elif 'success' in r:
|
||||
break
|
||||
|
||||
return psbt_hash.hex()
|
||||
|
||||
async def coldcard_sign_psbt(psbt_hash):
|
||||
async with websockets.connect(CC_URL) as ws:
|
||||
payload = message("submit_psbt", psbt_hash, False, False, True)
|
||||
await ws.send(payload)
|
||||
|
||||
async for msg in ws:
|
||||
try:
|
||||
r = json.loads(msg)
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"{e}: {msg} is not valid json")
|
||||
continue
|
||||
|
||||
if "update_status" in r:
|
||||
continue
|
||||
elif 'local_download' in r:
|
||||
break
|
||||
|
||||
return r
|
||||
|
||||
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\n")
|
||||
return 1
|
||||
|
||||
TRIALS = 600 # gets variance down pretty low
|
||||
TRIAL_BITS = 0x1e3ea75f # takes about 5m to do 600 trials
|
||||
|
||||
header = CBlockHeader()
|
||||
header.nBits = TRIAL_BITS
|
||||
targ = nbits_to_target(header.nBits)
|
||||
|
||||
start = time.time()
|
||||
count = 0
|
||||
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()
|
||||
|
||||
avg = (time.time() - start) * 1.0 / TRIALS
|
||||
|
||||
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 do_setup(args):
|
||||
if args.signer == "coldcard":
|
||||
sys.exit(asyncio.get_event_loop().run_until_complete(coldcard_setup()))
|
||||
else:
|
||||
logging.error("Unknown option")
|
||||
sys.exit(1)
|
||||
|
||||
async def coldcard_status() -> dict:
|
||||
logging.info("Checking that server is up and connected to Coldcard")
|
||||
status = {}
|
||||
while 1:
|
||||
try:
|
||||
async with websockets.connect(CC_URL) as ws:
|
||||
payload = message("get_info")
|
||||
await ws.send(payload)
|
||||
|
||||
async for msg in ws:
|
||||
try:
|
||||
r = json.loads(msg)
|
||||
except json.JSONDecodeError:
|
||||
logging.info(f"{msg} is not valid json")
|
||||
return 1
|
||||
|
||||
try:
|
||||
if 'status' in r:
|
||||
logging.info("Got status")
|
||||
logging.info(f"{r}")
|
||||
status = r['status']
|
||||
break
|
||||
elif 'update_status' in r:
|
||||
# we ignore this as there's not all the details there
|
||||
logging.info("Received update_status, ignoring")
|
||||
continue
|
||||
elif 'error' in r:
|
||||
logging.error(f"{r['error']}")
|
||||
return 1
|
||||
else:
|
||||
logging.info(f"{r}")
|
||||
logging.info("Still waiting for Coldcard connection")
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f"{e}")
|
||||
return 1
|
||||
except ConnectionError as e:
|
||||
logging.error("Server seems to be offline. Trying again.")
|
||||
await asyncio.sleep(10)
|
||||
continue
|
||||
|
||||
logging.info(f"{status}")
|
||||
|
||||
return status
|
||||
|
||||
|
||||
async def coldcard_setup():
|
||||
status = await coldcard_status()
|
||||
|
||||
logging.info(f"{status}")
|
||||
if not status['hsm']['active']:
|
||||
logging.info("Entering Coldcard HSM setup")
|
||||
logging.info("Please follow the instructions on the coldcard")
|
||||
async with websockets.connect(CC_URL) as ws:
|
||||
payload = message("submit_policy", json_dumps(POLICY), False)
|
||||
await ws.send(payload)
|
||||
|
||||
async for msg in ws:
|
||||
try:
|
||||
r = json.loads(msg)
|
||||
except json.JSONDecodeError:
|
||||
logging.info(f"{msg} is not valid json")
|
||||
return 1
|
||||
|
||||
try:
|
||||
if 'update_status' in r:
|
||||
if r['update_status']['hsm']['active']:
|
||||
logging.info("HSM is now activated. Proceeding.")
|
||||
break
|
||||
else:
|
||||
logging.info(f"Received update: {r['update_status']['hsm']}")
|
||||
except KeyError:
|
||||
logging.error(f"Received unexpected message: {r}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logging.error(f"{e}")
|
||||
return 1
|
||||
elif not status['sl_loaded']:
|
||||
while 1:
|
||||
status = await coldcard_status()
|
||||
if status['sl_loaded']:
|
||||
logging.info("Coldcard in HSM mode. Proceeding...")
|
||||
break
|
||||
await asyncio.sleep(10)
|
||||
else:
|
||||
logging.info("Coldcard already in HSM mode. Proceeding...")
|
||||
|
||||
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)
|
||||
genpsbt.add_argument('--MINER_TAG', required=True, help='Miner tag')
|
||||
|
||||
solvepsbt = cmds.add_parser("solvepsbt", help="Solve a signed block PSBT")
|
||||
solvepsbt.set_defaults(fn=do_solvepsbt)
|
||||
|
||||
setup = cmds.add_parser("setup", help="Setup HSM for coldcard")
|
||||
setup.set_defaults(fn=do_setup)
|
||||
setup.add_argument("--signer", default=None, type=str, help="Who's signing blocks (default: Bitcoin Core)")
|
||||
|
||||
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("--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)")
|
||||
generate.add_argument("--signer", default=None, type=str, help="Who's signing blocks (default: Bitcoin Core)")
|
||||
generate.add_argument('--WATCHONLY_WALLET', required=True, help='Watch-only wallet')
|
||||
generate.add_argument('--MINING_WALLET', required=True, help='Mining wallet')
|
||||
generate.add_argument('--MINER_TAG', required=True, help='Miner tag')
|
||||
|
||||
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")
|
||||
sp.add_argument("--relay-address", default=None, type=str, help="Address for relay reward payment (50% of block reward)")
|
||||
sp.add_argument("--reward-split-ratio", default=0.5, type=float, help="Ratio of block reward to send to relay (default: 0.5)")
|
||||
|
||||
for sp in [solvepsbt, generate, calibrate]:
|
||||
sp.add_argument("--grind-cmd", default=None, type=str, required=(sp==calibrate), 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()
|
78
signet_miner.py
Normal file
78
signet_miner.py
Normal file
@ -0,0 +1,78 @@
|
||||
import argparse
|
||||
import base64
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def rpc_call(host: str, port: int, cookie_path: str, method: str, params):
|
||||
with open(cookie_path, 'r', encoding='utf-8') as f:
|
||||
cookie = f.read().strip()
|
||||
auth = base64.b64encode(cookie.encode()).decode()
|
||||
|
||||
conn = http.client.HTTPConnection(host, port, timeout=30)
|
||||
payload = json.dumps({"jsonrpc": "1.0", "id": "miner", "method": method, "params": params})
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Basic {auth}"}
|
||||
conn.request("POST", "/", payload, headers)
|
||||
resp = conn.getresponse()
|
||||
body = resp.read()
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"RPC HTTP {resp.status}: {body.decode(errors='ignore')}")
|
||||
data = json.loads(body)
|
||||
if data.get("error"):
|
||||
raise RuntimeError(str(data["error"]))
|
||||
return data["result"]
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--cookie', required=True)
|
||||
p.add_argument('--rpc-host', default='bitcoin')
|
||||
p.add_argument('--rpc-port', type=int, default=38332)
|
||||
p.add_argument('--poll-interval', type=int, default=5)
|
||||
args = p.parse_args()
|
||||
|
||||
# Paramètres via env
|
||||
challenge = os.environ.get('SIGNET_CHALLENGE', '')
|
||||
xprv = os.environ.get('SIGNET_MINER_XPRV', '')
|
||||
derivation = os.environ.get('DERIVATION_PATH', "48'/1'/0'/2'/0/0")
|
||||
coinbase_addr = os.environ.get('COINBASE_ADDRESS', '')
|
||||
|
||||
if not challenge:
|
||||
raise SystemExit('SIGNET_CHALLENGE non défini')
|
||||
if not xprv:
|
||||
print('Avertissement: SIGNET_MINER_XPRV non défini (mode lecture gbt uniquement)')
|
||||
if not coinbase_addr:
|
||||
raise SystemExit('COINBASE_ADDRESS non défini')
|
||||
|
||||
print('Miner signet: host=%s port=%d' % (args.rpc_host, args.rpc_port), flush=True)
|
||||
print('Challenge:', challenge, flush=True)
|
||||
print('Derivation:', derivation, flush=True)
|
||||
print('Coinbase address:', coinbase_addr, flush=True)
|
||||
|
||||
try:
|
||||
bh = rpc_call(args.rpc_host, args.rpc_port, args.cookie, 'getblockcount', [])
|
||||
print('Hauteur actuelle:', bh, flush=True)
|
||||
except Exception as e:
|
||||
print('Erreur RPC initiale:', e, flush=True)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Inclure la règle signet comme demandé par bitcoind en signet
|
||||
tmpl = rpc_call(
|
||||
args.rpc_host,
|
||||
args.rpc_port,
|
||||
args.cookie,
|
||||
'getblocktemplate',
|
||||
[{"rules": ["segwit", "signet"]}]
|
||||
)
|
||||
print('Template: height=%s nTx=%s' % (tmpl.get('height'), len(tmpl.get('transactions', []))), flush=True)
|
||||
# TODO: construire coinbase (coinbase_addr), header, signer selon le challenge signet puis submitblock
|
||||
except Exception as e:
|
||||
print('Erreur getblocktemplate:', e, flush=True)
|
||||
time.sleep(args.poll_interval)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
78
tools/import_signet_descriptors.sh
Executable file
78
tools/import_signet_descriptors.sh
Executable file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Fichier d'env introuvable: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
|
||||
cli() {
|
||||
docker exec "$BITCOIN_CONTAINER" sh -lc "bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf -signet $*"
|
||||
}
|
||||
|
||||
cli_stdin() {
|
||||
local subcmd="$1"; shift || true
|
||||
docker exec -i "$BITCOIN_CONTAINER" sh -lc "bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf -signet -stdin $subcmd"
|
||||
}
|
||||
|
||||
extract_checksum() {
|
||||
sed -n 's/.*"checksum"[[:space:]]*:[[:space:]]*"\([a-z0-9]\{8\}\)".*/\1/p' | tr -d '\n'
|
||||
}
|
||||
|
||||
wallet_is_descriptors() {
|
||||
cli -rpcwallet="$MINING_WALLET" getwalletinfo 2>/dev/null | grep -q '"descriptors"[[:space:]]*:[[:space:]]*true'
|
||||
}
|
||||
|
||||
wallet_exists() {
|
||||
cli -rpcwallet="$MINING_WALLET" getwalletinfo >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_descriptors_wallet() {
|
||||
local wallet_path="/home/bitcoin/.bitcoin/signet/wallets/$MINING_WALLET"
|
||||
if wallet_exists && wallet_is_descriptors; then
|
||||
return 0
|
||||
fi
|
||||
cli -rpcwallet="$MINING_WALLET" unloadwallet "$MINING_WALLET" >/dev/null 2>&1 || true
|
||||
docker exec "$BITCOIN_CONTAINER" sh -lc "if [ -d '$wallet_path' ]; then rm -rf '$wallet_path'; fi"
|
||||
cli createwallet "$MINING_WALLET" false true "" true true true false >/dev/null
|
||||
}
|
||||
|
||||
ensure_descriptors_wallet
|
||||
|
||||
DESC_EXT_ORIG="wsh(multi(1,[$MINING_FINGERPRINT/$MINING_PATH_PREFIX]$MINING_XPRV/0/*))"
|
||||
DESC_INT_ORIG="wsh(multi(1,[$MINING_FINGERPRINT/$MINING_PATH_PREFIX]$MINING_XPRV/1/*))"
|
||||
|
||||
CS_EXT=$(printf '%s' "$DESC_EXT_ORIG" | cli_stdin getdescriptorinfo | extract_checksum)
|
||||
CS_INT=$(printf '%s' "$DESC_INT_ORIG" | cli_stdin getdescriptorinfo | extract_checksum)
|
||||
DESC_EXT="$DESC_EXT_ORIG#$CS_EXT"
|
||||
DESC_INT="$DESC_INT_ORIG#$CS_INT"
|
||||
|
||||
PAYLOAD_MINER=$(printf '[{"desc":"%s","timestamp":"now","active":true,"range":[0,1000]},{"desc":"%s","timestamp":"now","active":true,"internal":true,"range":[0,1000]}]' "$DESC_EXT" "$DESC_INT")
|
||||
printf '%s\n' "$PAYLOAD_MINER" | cli_stdin importdescriptors | cat
|
||||
|
||||
# Adresse coinbase: si vide, utiliser getnewaddress du wallet (garanti p2wsh multisig)
|
||||
if [ -z "${COINBASE_ADDRESS:-}" ]; then
|
||||
ADDR=$(cli -rpcwallet="$MINING_WALLET" getnewaddress coinbase bech32)
|
||||
tmpfile=$(mktemp)
|
||||
awk -v addr="$ADDR" 'BEGIN{updated=0} /^COINBASE_ADDRESS=/{print "COINBASE_ADDRESS=" addr; updated=1; next} {print} END{if(updated==0) print "COINBASE_ADDRESS=" addr}' "$ENV_FILE" > "$tmpfile"
|
||||
mv "$tmpfile" "$ENV_FILE"
|
||||
echo "COINBASE_ADDRESS=$ADDR"
|
||||
else
|
||||
echo "COINBASE_ADDRESS=$COINBASE_ADDRESS"
|
||||
fi
|
||||
|
||||
if [ -n "${CHALLENGE_ALLPUBS:-}" ]; then
|
||||
CHALL_PRIV_ORIG=$(printf '%s' "$CHALLENGE_ALLPUBS" | sed -E "s#\[$MINING_FINGERPRINT/$MINING_PATH_PREFIX\]tpub[[:alnum:]]+#[$MINING_FINGERPRINT/$MINING_PATH_PREFIX]$MINING_XPRV#g")
|
||||
CS_CHALL=$(printf '%s' "$CHALL_PRIV_ORIG" | cli_stdin getdescriptorinfo | extract_checksum)
|
||||
CHALL_PRIV="$CHALL_PRIV_ORIG#$CS_CHALL"
|
||||
PAYLOAD_CHAL=$(printf '[{"desc":"%s","timestamp":"now","active":false,"range":[0,1000]}]' "$CHALL_PRIV")
|
||||
printf '%s\n' "$PAYLOAD_CHAL" | cli_stdin importdescriptors | cat
|
||||
fi
|
||||
|
||||
echo "Import terminé."
|
75
tools/priv_key.json
Normal file
75
tools/priv_key.json
Normal file
@ -0,0 +1,75 @@
|
||||
[
|
||||
{
|
||||
"Mnemonic": "tower keen enrich problem essence east plastic lounge merge sand increase company",
|
||||
"xprv": "tprv8inwidD6qpNwMNY5ZadhYMn62d1WHvSVMRH2pPAj7RsAZGCY4YTiT1McMQSg5DAyijPBZ4HroX83vZQAevQkJSZUVH8kro9JnVbhTPBSAxL",
|
||||
"wallet descriptor": "wsh(multi(1,[86936c07/48'/1'/0'/2']tpubDFUys3FLzC4cEqZsTEJHwmSCbeXSTFdPvisp6uD2XhfZPkTJgwHJdVyUXYcfLRrikRxA2MpBaZWE5kZCtHFc15aVtktsHMrTijDjq2dKRGK/0/*))#pslna7dm",
|
||||
},
|
||||
{
|
||||
"Mnemonic": "deer trust ceiling youth brass rapid scout cradle better clap spike morning",
|
||||
"xprv": "tprv8iNgodqVZKJgEFGhmoouPYPAj7EzaqjqToGcdZGUTVcDpu8YSvvhmoppYp7vWG2LR2SrF93AVYZGgG9bCzuQs1xqwJ2QW8hRwtEVdyUofuH",
|
||||
"wallet descriptor": "wsh(multi(1,[5df7e4b0/48'/1'/0'/2']tpubDF4ix3sjhgzM7iJVfTUVnx3HJ8kvkAvk36sPv5JmsmQcfPPK5KkHxJSgixZAdcYEsGcvHacm1hW4iLksGoTZocJozuaA2BTNp3GEvW432qu/0/*))#4ma3uvl0"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "control load guard error caution hundred main adjust happy infant safe brother",
|
||||
"xprv": "tprv8hTsDtqzaXPoZtQSrfR4HKfAXo1qmh8Xb6oJt5PY5WtET5ecfZ8bUEBoofVrH6s8STU586QHhYSppmQ3n1nvsZ6p5VaKu4MHxsvzUf1gg2D",
|
||||
"wallet descriptor": "wsh(multi(1,[a3a9eb52/48'/1'/0'/2']tpubDE9uNJtEiu5UTMSEkK5egjKH6pXmw2KSAQQ6AbRqVngdHZuPHwxBeiofypHrGmG1WkvAtgjjn7gmPddzaz3ymQj9m3CDFLGEB6Ao4xqripj/0/*))#ju6z6s7v"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "venture ice crash venture tourist tail naive curtain pilot engage code celery",
|
||||
"xprv": "tprv8iTk1ZRgwq4NAysrRgY1Wbycbfvmb7cYgjAGTeR573gKFJ4BFuGtEoaotCS6wdGiUfC2BTHg79tiX7i6NuFiTfjiaM8LXfNzL77YuBGY3K7",
|
||||
"wallet descriptor": "wsh(multi(1,[46d93da5/48'/1'/0'/2']tpubDF9n9yTw6Ck34SueKLCbv1djAhShkSoTG2m3kATNXKUi5nJwtJ6URJCg4M1je81fyabsX4t6F2itrQinMuu3cYLbpLbVQwWBUwYA8pPyKdZ/0/*))#8q8j9sft"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "sheriff zone betray cactus error enable code flat coyote worry guitar equal",
|
||||
"xprv": "tprv8iajQdFf4a7eUEKhE1gukrNEcxY4MZk1FJyKWgwZj48mhqTminKn6mkxyfyn1QwJ2XUke2aiXfXNQ2XqpGBbwXSSRK8hvqHQJuHWHs68YTh",
|
||||
"wallet descriptor": "wsh(multi(1,[d3c3bc8f/48'/1'/0'/2']tpubDFGmZ3HuCwoKMhMV7fMWAG2MBz3zWtvupca6oCys9KwAYKiYMB9NHGNq9qvVgPgDgpDLSiCqnp71f7WsV9N1cLkzsjqW9gxJF9VQ9oSZcj9/0/*))#7r3f3xys"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "bottom sight mistake next reveal modify rather bulk mountain arrow useful buzz",
|
||||
"xprv": "tprv8hqVmTiP493atpsuqdt87Hx5hjU9NwHG9hrWBa97rS8TwTYZBK5Jd8BfH4Jv154oP9YRWy7kU9p7xnqTwXCAnKZuEpACt2uzTx83HrTjqen",
|
||||
"wallet descriptor": "wsh(multi(1,[7f7d263a/48'/1'/0'/2']tpubDEXXuskdCWjFnHuhjHYiWhcCGkz5YGUAj1THU6BRGhvrmwoKohttocoXTCCE9udffumcou7ZYUR5RNqwHW4kw7Jv2UXUUSKeKqJd9xGmSCs/0/*))#zc5ruh7c"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "payment ill whisper noble casual shallow clown pipe keen pencil fluid term""xprv": "tprv8hMLjbE25N5UhZJadZDHb42RNFhgfSLdcsGnhu7BYt5Wt8UzXhcF5ANLuezsgJUNyCC4TJtekes9gssUCm6UKASnqMTPwm6KcePSw4npybF""wallet descriptor": "wsh(multi(1,[154159b3/48'/1'/0'/2']tpubDE3Nt1GGDjm9b2LNXCsszTgXwHDcpmXYCAsZzR9Uy9suicjmA6RqFezD5o8EWHk1vrztkPreHbYXKqGAdupKJNcKWYViKsQNMfr4uW8vcWq/0/*))#5k6w6h6g"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "lesson trumpet royal bright three oval vague organ atom joke favorite april",
|
||||
"xprv": "tprv8ixSxhVBoDTfv846JKDo5Qu8L89wZnmXUZCwdN1sm8CRfS1RpecrWmtthiTqepjnBroRit3Zygn53z3v8QWp3bqQYjev2Mn92g9jGkzaGya",
|
||||
"wallet descriptor": "wsh(multi(1,[fca68db6/48'/1'/0'/2']tpubDFeV77XRwb9Lob5tBxtPUpZEu9fsj7xS3roiut4BBPzpVvGCT3SShGWksqUYLqKBrt7xeKmmmgSrgbRiffcoS5KPiqyDWk5Kgvxek52XnNV/0/*))#j6sm3ntm"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "lyrics undo baby chicken possible vicious capital fun order salon maple source",
|
||||
"xprv": "tprv8ixaRLep3QQB2Rn9UpXXJVt9qcrDL6QTuHJusiKjPPgzkPmDveAQNBViJBrJakLKaoc3w7JzNUXAkSaeQHJAzGsMnrJQggWNkMn1e9rihgP",
|
||||
"wallet descriptor": "wsh(multi(1,[ef9d9ce6/48'/1'/0'/2']tpubDFecZkh4Bn5qutowNUC7huYGQeN9VRbNUauhAEN2ofVPat1zZ2yzYg7aULxsdzh79AFz7rBTVQeu2BsBay88XrFLc5diENj4ibizrwPNMbM/0/*))#zyhj4kj3"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "canoe coral egg public boss stable mercy side tennis behind dance shy",
|
||||
"xprv": "tprv8j58z2XeVe29DeDqb8UABtF14mo4MCPWm5CmkL5BegHBa3prhkLz2HF4JFwU5Z6ypnA7qCVcwdyPGj5yqXPoXiaE2Rcosmx9Ntiav39vRfp",
|
||||
"wallet descriptor": "wsh(multi(1,[8e236875/48'/1'/0'/2']tpubDFmB8SZte1hp77FdUn8kbHu7doJzWXaRLNoZ2r7V4x5aQY5dL9AaCmrvUNZSPYHJKeqto8roTvUpwWFazfxHEg5DvMq8br266uuD1JKieWj/0/*))#8xxkeruq"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "expire document depend hamster spy become blossom midnight ecology salon all earth",
|
||||
"xprv": "tprv8ii6Q43XVJhTrqb9oYUgz1kXXUc763g8c3tgZK38ei9Bwkaw12wEdXzgemB6fmF4jgDBAdavNg4YXyRe1XSx3jxjZ8i2fHruuyn6bP4r7uq",
|
||||
"wallet descriptor": "wsh(multi(1,[d03aacca/48'/1'/0'/2']tpubDFQ8YU5mdgP8kJcwhC9HPRQe6W83FNs3BMVTqq5S4ywanEqhdRkpp2cYpro3XRXKJPi8d1d3m4L2JXWdNQFfs31x37S3zfPpd7pwKEwLAm7/0/*))#phcw966k"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "movie west unit carbon adapt liberty crack easily raise toward brother quality",
|
||||
"xprv": "tprv8iszPBk3CEovNt6aHBMHPqeejpNJVKCqws1jfmHobNiC2s497W2jL9nde2FmTBWKMpkuKXDuFKPrBqKcEpsjgYeLsoQVT3MgsHoTjTL64qB",
|
||||
"wallet descriptor": "wsh(multi(1,[ce3600ea/48'/1'/0'/2']tpubDFa2XbnHLcVbGM8NAq1soFJmJqtEeePkXAcWxHL71eWasMJujtrKWeQVp7NHQY5euJL2bFuBkVQHk4uoDrVRfCEELLxJhHuNouPquffbmUy/0/*))#lwv5ura2"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "tip mixture supreme govern faculty panel judge motion aim write soon arrive",
|
||||
"xprv": "tprv8hJQahhR4cqcu6hkUkpVx3ex9tUxNWrY5EFd4zLdxT5Ycn3V14Kp6V7XLEtJ2N5p5dpVeP9mhMqNwTBBeJaavMDquLh8SRFfdDejAy8yygX",
|
||||
"wallet descriptor": "wsh(multi(1,[fe898c92/48'/1'/0'/2']tpubDDzSj7jfCzXHnZjYNQV6MTK4iuztXr3SeXrQMWNwNiswTGJFdT9QGyjPWMoYcoPY9HCYbLdcMGiDokrWDWWZEhg8HpbgebenhJujvTzMeeN/0/*))#675457w4"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "identify devote dice young air turkey angle code observe innocent fragile bench",
|
||||
"xprv": "tprv8iUcGCBaM1XJqsswVqLkJjgQjvKHmGaKrvX9sqBrmRrUTroa5EKEes4x3L7AFE7tLDW4mUCWLmpAhFrjQvZ1uUzuAaziFvLwrtq253g9yzp",
|
||||
"wallet descriptor": "wsh(multi(1,[d33c583b/48'/1'/0'/2']tpubDFAeQcDpVPCyjLujPV1Li9LXJwqDvbmESE7wAMEABhesJM4Lhd8pqMgpDVSmf4cpdsfZbDWkhfyxeyG3SaWcB4MqEqhbseQ8mk41PPHb57T/0/*))#u9xx2lkz"
|
||||
},
|
||||
{
|
||||
"Mnemonic": "slide hollow decade federal pair brief furnace fit pelican heart better place",
|
||||
"xprv": "tprv8iVREMet5hjVGBfng2VLnsrTMPqPDFkVoUAqoy78zKEye1u6XaG8jKju3nsf6GN1UTMbkFWoD6TiTTvnP2ez4NvXyX9c1UMfQ932CmZjuLg",
|
||||
"wallet descriptor": "wsh(multi(1,[facf6b1f/48'/1'/0'/2']tpubDFBTNmh8E5RA9ehaZg9wCHWZvRMKNawQNmmd6V9SQb3NUW9s9y5iupMmDxAbBFFrytzotW9hu8REgqSFg26Q8mcvBjSAaVz9QcNzmCxRJdv/0/*))#3407up02"
|
||||
}
|
||||
]
|
25
tools/pubkeys.json
Normal file
25
tools/pubkeys.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"type": "wsh",
|
||||
"multi": "sortedmulti",
|
||||
"min": "1",
|
||||
"pubkeys": [
|
||||
"[fca68db6/48'/1'/0'/2']tpubDFeV77XRwb9Lob5tBxtPUpZEu9fsj7xS3roiut4BBPzpVvGCT3SShGWksqUYLqKBrt7xeKmmmgSrgbRiffcoS5KPiqyDWk5Kgvxek52XnNV/0/*",
|
||||
"[5df7e4b0/48'/1'/0'/2']tpubDF4ix3sjhgzM7iJVfTUVnx3HJ8kvkAvk36sPv5JmsmQcfPPK5KkHxJSgixZAdcYEsGcvHacm1hW4iLksGoTZocJozuaA2BTNp3GEvW432qu/0/*",
|
||||
"[ef9d9ce6/48'/1'/0'/2']tpubDFecZkh4Bn5qutowNUC7huYGQeN9VRbNUauhAEN2ofVPat1zZ2yzYg7aULxsdzh79AFz7rBTVQeu2BsBay88XrFLc5diENj4ibizrwPNMbM/0/*",
|
||||
"[86936c07/48'/1'/0'/2']tpubDFUys3FLzC4cEqZsTEJHwmSCbeXSTFdPvisp6uD2XhfZPkTJgwHJdVyUXYcfLRrikRxA2MpBaZWE5kZCtHFc15aVtktsHMrTijDjq2dKRGK/0/*",
|
||||
"[7f7d263a/48'/1'/0'/2']tpubDEXXuskdCWjFnHuhjHYiWhcCGkz5YGUAj1THU6BRGhvrmwoKohttocoXTCCE9udffumcou7ZYUR5RNqwHW4kw7Jv2UXUUSKeKqJd9xGmSCs/0/*",
|
||||
"[154159b3/48'/1'/0'/2']tpubDE3Nt1GGDjm9b2LNXCsszTgXwHDcpmXYCAsZzR9Uy9suicjmA6RqFezD5o8EWHk1vrztkPreHbYXKqGAdupKJNcKWYViKsQNMfr4uW8vcWq/0/*",
|
||||
"[46d93da5/48'/1'/0'/2']tpubDF9n9yTw6Ck34SueKLCbv1djAhShkSoTG2m3kATNXKUi5nJwtJ6URJCg4M1je81fyabsX4t6F2itrQinMuu3cYLbpLbVQwWBUwYA8pPyKdZ/0/*",
|
||||
"[d3c3bc8f/48'/1'/0'/2']tpubDFGmZ3HuCwoKMhMV7fMWAG2MBz3zWtvupca6oCys9KwAYKiYMB9NHGNq9qvVgPgDgpDLSiCqnp71f7WsV9N1cLkzsjqW9gxJF9VQ9oSZcj9/0/*",
|
||||
"[8e236875/48'/1'/0'/2']tpubDFmB8SZte1hp77FdUn8kbHu7doJzWXaRLNoZ2r7V4x5aQY5dL9AaCmrvUNZSPYHJKeqto8roTvUpwWFazfxHEg5DvMq8br266uuD1JKieWj/0/*",
|
||||
"[a3a9eb52/48'/1'/0'/2']tpubDE9uNJtEiu5UTMSEkK5egjKH6pXmw2KSAQQ6AbRqVngdHZuPHwxBeiofypHrGmG1WkvAtgjjn7gmPddzaz3ymQj9m3CDFLGEB6Ao4xqripj/0/*",
|
||||
"[d03aacca/48'/1'/0'/2']tpubDFQ8YU5mdgP8kJcwhC9HPRQe6W83FNs3BMVTqq5S4ywanEqhdRkpp2cYpro3XRXKJPi8d1d3m4L2JXWdNQFfs31x37S3zfPpd7pwKEwLAm7/0/*",
|
||||
"[ce3600ea/48'/1'/0'/2']tpubDFa2XbnHLcVbGM8NAq1soFJmJqtEeePkXAcWxHL71eWasMJujtrKWeQVp7NHQY5euJL2bFuBkVQHk4uoDrVRfCEELLxJhHuNouPquffbmUy/0/*",
|
||||
"[fe898c92/48'/1'/0'/2']tpubDDzSj7jfCzXHnZjYNQV6MTK4iuztXr3SeXrQMWNwNiswTGJFdT9QGyjPWMoYcoPY9HCYbLdcMGiDokrWDWWZEhg8HpbgebenhJujvTzMeeN/0/*",
|
||||
"[d33c583b/48'/1'/0'/2']tpubDFAeQcDpVPCyjLujPV1Li9LXJwqDvbmESE7wAMEABhesJM4Lhd8pqMgpDVSmf4cpdsfZbDWkhfyxeyG3SaWcB4MqEqhbseQ8mk41PPHb57T/0/*",
|
||||
"[facf6b1f/48'/1'/0'/2']tpubDFBTNmh8E5RA9ehaZg9wCHWZvRMKNawQNmmd6V9SQb3NUW9s9y5iupMmDxAbBFFrytzotW9hu8REgqSFg26Q8mcvBjSAaVz9QcNzmCxRJdv/0/*"
|
||||
],
|
||||
"checksum": "#jmqku76u",
|
||||
"signet_challenge": "0020341c43803863c252df326e73574a27d7e19322992061017b0dc893e2eab90821",
|
||||
"magic": "b066463d"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user