diff --git a/contrib/signet/README.md b/contrib/signet/README.md new file mode 100644 index 000000000000..e39327b6e5bc --- /dev/null +++ b/contrib/signet/README.md @@ -0,0 +1,60 @@ +Contents +======== +This directory contains tools related to Signet, both for running a Signet yourself and for using one. + +The `args.sh` script is a helper script used by the other scripts and should not be invoked directly. + +getcoins.sh +=========== + +A script to call a faucet to get Signet coins. + +Syntax: `getcoins.sh [--help] [--cmd=] [--faucet=] [--addr=] [--password=] [--] []` + +* `--cmd` lets you customize the bitcoin-cli path. By default it will look for it in the PATH, then in `../../src/` +* `--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. + +issuer.sh +========= + +A script to regularly issue Signet blocks. + +Syntax: `issuer.sh [--help] [--cmd=] [--] []` + +* `` is a time in seconds to wait between each block generation +* `--cmd` lets you customize the bitcoin-cli path. By default it will look for it in the PATH, then in `../../src/` + +Signet, just like other bitcoin networks, uses proof of work alongside the block signature; this +includes the difficulty adjustment every 2016 blocks. +The `` exists to allow you to maintain a relatively low difficulty over an extended period +of time. E.g. an idle time of 540 means your node will end up spending roughly 1 minute grinding +hashes for each block, and wait 9 minutes after every time. + +mkblock.sh +========== + +A script to generate one Signet block. + +Syntax: `mkblock.sh []` + +This script is called by the other block issuing scripts, but can be invoked independently to generate +1 block immediately. + +secondary.sh +============ + +A script to act as backup generator in case the primary issuer goes offline. + +Syntax: `secondary.sh [--cmd=] []` + +* `` is the time in seconds that must have passed since the last block was seen for the secondary issuer to kick into motion +* `` is the time in seconds to wait after generating a block, and should preferably be the same as the idle time of the main issuer + +Running a Signet network, it is recommended to have at least one secondary running in a different +place, so it doesn't go down together with the main issuer. diff --git a/contrib/signet/addtxtoblock.py b/contrib/signet/addtxtoblock.py new file mode 100755 index 000000000000..55476d89af69 --- /dev/null +++ b/contrib/signet/addtxtoblock.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 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 sys +import os +import argparse + +# dirty hack but makes CBlock etc available +sys.path.append('/'.join(os.getcwd().split('/')[:-2]) + '/test/functional') + +from test_framework.messages import ( + CBlock, + CTransaction, + FromHex, + ToHex, +) + +from test_framework.blocktools import add_witness_commitment + +def main(): + + # Parse arguments and pass through unrecognised args + parser = argparse.ArgumentParser(add_help=False, + usage='%(prog)s [addtxtoblock options] [bitcoin block file] [tx file] [fee]', + description=__doc__, + epilog='''Help text and arguments:''', + formatter_class=argparse.RawTextHelpFormatter) + # parser.add_argument('--filter', help='filter scripts to run by regular expression') + args, unknown_args = parser.parse_known_args() + + if len(unknown_args) != 3: + print("Need three arguments (block file, tx file, and fee)") + sys.exit(1) + + [blockfile, txfile, feestr] = unknown_args + + with open(blockfile, "r", encoding="utf8") as f: + blockhex = f.read().strip() + with open(txfile, "r", encoding="utf8") as f: + txhex = f.read().strip() + + fee = int(feestr) + + block = CBlock() + FromHex(block, blockhex) + + tx = CTransaction() + FromHex(tx, txhex) + + block.vtx[0].vout[0].nValue += fee + block.vtx.append(tx) + add_witness_commitment(block) + print(ToHex(block)) + +if __name__ == '__main__': + main() diff --git a/contrib/signet/args.sh b/contrib/signet/args.sh new file mode 100755 index 000000000000..cce81a78a72d --- /dev/null +++ b/contrib/signet/args.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C + +args="-signet" +eoc= + +command -v bitcoin-cli > /dev/null \ +&& bcli="bitcoin-cli" \ +|| bcli="$(dirname $0)/../../src/bitcoin-cli" + +if [ "$VARCHECKS" = "" ]; then + VARCHECKS='if [ "$varname" = "cmd" ]; then bcli=$value;' +fi + +# compatibility; previously the bitcoin-cli path was given as first argument +if [[ "$1" != "" && "${1:$((${#1}-11))}" = "bitcoin-cli" ]]; then + echo "using $1 as bcli" + bcli=$1 + shift +fi + +for i in "$@"; do + if [ $eoc ]; then + args="$args $i" + elif [ "$i" = "--" ]; then + # end of commands; rest into args for bitcoin-cli + eoc=1 + continue + elif [ "${i:0:2}" = "--" ]; then + # command + j=${i:2} + if [ "$j" = "help" ]; then + >&2 echo -e $HELPSTRING # "syntax: $0 [--help] [--cmd=] [--faucet==https://signet.bc-2.jp/claim] []" + exit 1 + fi + export varname=${j%=*} + export value=${j#*=} + eval $VARCHECKS ' + else + >&2 echo "unknown parameter $varname (from \"$i\"); for help, type: $0 --help" + exit 1 + fi' + # if [ "$varname" = "cmd" ]; then + # bcli=$value + # elif [ "$varname" = "faucet" ]; then + # faucet=$value + else + # arg + args="$args $i" + fi +done + +if ! [ -e "$bcli" ]; then + command -v "$bcli" >/dev/null 2>&1 || { echo >&2 "error: unable to find bitcoin binary: $bcli"; exit 1; } +fi diff --git a/contrib/signet/forker.sh b/contrib/signet/forker.sh new file mode 100755 index 000000000000..72c5c5584fc0 --- /dev/null +++ b/contrib/signet/forker.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C + +# +# Take the hex formatted line separated transactions in reorg-list.txt where every even transaction is put into the even set, and every odd transaction is put into the odd set. +# That is, transactions on line 1, 3, 5, 7, ... go into the odd set, and transactions on line 2, 4, 6, 8, ... go into the even set. +# +# - Generate two blocks A and B, where A contains all or some of the odd set, and B contains the corresponding even set. +# - Sign, grind, and broadcast block A +# - Wait a random amount of time (1-10 mins) +# - Invalidate block A +# - Sign, grind, and broadcast block B +# + +bcli=$1 +shift + +function log() +{ + echo "- $(date +%H:%M:%S): $*" +} + +if [ ! -e "reorg-list.txt" ]; then + echo "reorg-list.txt not found" + exit 1 +fi + +# get address for coinbase output +addr=$($bcli "$@" getnewaddress) + +# create blocks A and B +$bcli "$@" getnewblockhex $addr > $PWD/block-a +cp block-a block-b + +odd=1 +while read -r line; do + if [ "$line" = "" ]; then continue; fi + echo $line > tx + if [ $odd -eq 1 ]; then blk="block-a"; else blk="block-b"; fi + ./addtxtoblock.py $blk tx 100 > t # note: we are throwing away all fees above 100 satoshis for now; should figure out a way to determine fees + mv t $blk + (( odd=1-odd )) +done < reorg-list.txt + +rm reorg-list.txt + +log "mining block A (to-orphan block)" +while true; do + $bcli "$@" signblock $PWD/block-a > signed-a + blockhash_a=$($bcli "$@" grindblock $PWD/signed-a 1000000000) + if [ "$blockhash_a" != "false" ]; then break; fi +done +log "mined block with hash $blockhash_a" +(( waittime=RANDOM%570 )) +(( waittime=30+waittime )) +log "waiting for $waittime s" +sleep $waittime +log "invalidating $blockhash_a" +$bcli "$@" invalidateblock $blockhash_a + +log "mining block B (replace block)" +while true; do + $bcli "$@" signblock $PWD/block-b > signed-b + blockhash_b=$($bcli "$@" grindblock $PWD/signed-b 1000000000) + if [ "$blockhash_b" != "false" ]; then break; fi +done + +echo "mined $blockhash_b" +echo "cleaning up" +rm block-b signed-b block-a signed-a diff --git a/contrib/signet/getcoins.sh b/contrib/signet/getcoins.sh new file mode 100755 index 000000000000..8a2fedf8ae49 --- /dev/null +++ b/contrib/signet/getcoins.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C + +# +# Get coins from Signet Faucet +# + +export VARCHECKS=' + if [ "$varname" = "cmd" ]; then + bcli=$value; + elif [ "$varname" = "faucet" ]; then + faucet=$value; + elif [ "$varname" = "addr" ]; then + addr=$value; + elif [ "$varname" = "password" ]; then + password=$value; + ' +export HELPSTRING="syntax: $0 [--help] [--cmd=] [--faucet=] [--addr=] [--password=] [--] []" + +bcli= +args= +password= +addr= +faucet="https://signet.bc-2.jp/claim" + +# shellcheck source=contrib/signet/args.sh +source $(dirname $0)/args.sh "$@" + +if [ "$addr" = "" ]; then + # get address for receiving coins + addr=$($bcli $args getnewaddress faucet bech32) || { echo >&2 "for help, type: $0 --help"; exit 1; } +fi + +# shellcheck disable=SC2015 +command -v "curl" > /dev/null \ +&& curl -X POST -d "address=$addr&password=$password" $faucet \ +|| wget -qO - --post-data "address=$addr&password=$password" $faucet + +echo diff --git a/contrib/signet/issuer.sh b/contrib/signet/issuer.sh new file mode 100755 index 000000000000..a83e82641e14 --- /dev/null +++ b/contrib/signet/issuer.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C + +# +# Issue blocks using a local node at a given interval. +# + +export HELPSTRING="syntax: $0 [--help] [--cmd=] [--] []" + +if [ $# -lt 1 ]; then + echo $HELPSTRING + exit 1 +fi + +function log() +{ + echo "- $(date +%H:%M:%S): $*" +} + +idletime=$1 +shift + +bcli= +args= + +# shellcheck source=contrib/signet/args.sh +source $(dirname $0)/args.sh "$@" + +MKBLOCK=$(dirname $0)/mkblock.sh + +if [ ! -e "$MKBLOCK" ]; then + >&2 echo "error: cannot locate mkblock.sh (expected to find in $MKBLOCK" + exit 1 +fi + +echo "- checking node status" +conns=$($bcli $args getconnectioncount) || { echo >&2 "node error"; exit 1; } + +if [ $conns -lt 1 ]; then + echo "warning: node is not connected to any other node" +fi + +log "node OK with $conns connection(s)" +log "mining at maximum capacity with $idletime second delay between each block" +log "hit ^C to stop" + +while true; do + if [ -e "reorg-list.txt" ]; then + ./forker.sh $bcli $args + else + log "generating next block" + blockhash=$("$MKBLOCK" "$bcli" $args) || { echo "node error; aborting" ; exit 1; } + log "mined block $($bcli $args getblockcount) $blockhash to $($bcli $args getconnectioncount) peer(s); idling for $idletime seconds" + fi + sleep $idletime +done diff --git a/contrib/signet/mkblock.sh b/contrib/signet/mkblock.sh new file mode 100755 index 000000000000..e63bdec575b2 --- /dev/null +++ b/contrib/signet/mkblock.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C + +# +# Generate a block +# + +if [ $# -lt 1 ]; then + >&2 echo "syntax: $0 []" ; exit 1 +fi + +bcli=$1 +shift + +if ! [ -e "$bcli" ]; then + command -v "$bcli" >/dev/null 2>&1 || { echo >&2 "error: unable to find bitcoin binary: $bcli"; exit 1; } +fi + +# get address for coinbase output +addr=$($bcli "$@" getnewaddress) +# start looping; we re-create the block every time we fail to grind as that resets the nonce and gives us an updated +# version of the block +while true; do + # create an unsigned, un-PoW'd block + $bcli "$@" getnewblockhex $addr > $PWD/unsigned + # sign it + $bcli "$@" signblock $PWD/unsigned > $PWD/signed + # grind proof of work; this ends up broadcasting the block, if successful (akin to "generatetoaddress") + blockhash=$($bcli "$@" grindblock $PWD/signed 10000000) + if [ "$blockhash" != "false" ]; then break; fi +done + +echo $blockhash diff --git a/contrib/signet/secondary.sh b/contrib/signet/secondary.sh new file mode 100755 index 000000000000..b3d4e33f7bed --- /dev/null +++ b/contrib/signet/secondary.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Copyright (c) 2019 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +export LC_ALL=C + +# +# Backup Issuer which will begin to issue blocks if it sees no blocks within +# a specified period of time. It will continue to make blocks until a block +# is generated from an external source (i.e. the primary issuer is back online) +# + +export HELPSTRING="syntax: $0 [--cmd=] []" + +if [ $# -lt 3 ]; then + echo $HELPSTRING + exit 1 +fi + +function log() +{ + echo "- $(date +%H:%M:%S): $*" +} + +triggertime=$1 +shift + +idletime=$1 +shift + +bcli= +args= + +# shellcheck source=contrib/signet/args.sh +source $(dirname $0)/args.sh "$@" + +echo "- checking node status" +conns=$($bcli $args getconnectioncount) || { echo >&2 "node error"; exit 1; } + +if [ $conns -lt 1 ]; then + echo "warning: node is not connected to any other node" +fi + +MKBLOCK=$(dirname $0)/mkblock.sh + +if [ ! -e "$MKBLOCK" ]; then + >&2 echo "error: cannot locate mkblock.sh (expected to find in $MKBLOCK" + exit 1 +fi + +log "node OK with $conns connection(s)" +log "hit ^C to stop" + +# outer loop alternates between watching and mining +while true; do + # inner watchdog loop + blocks=$($bcli $args getblockcount) + log "last block #$blocks; waiting up to $triggertime seconds for a new block" + remtime=$triggertime + while true; do + waittime=1800 + if [ $waittime -gt $remtime ]; then waittime=$remtime; fi + conns=$($bcli $args getconnectioncount) + if [ $conns -eq 1 ]; then s=""; else s="s"; fi + log "waiting $waittime/$remtime seconds with $conns peer$s" + sleep $waittime + new_blocks=$($bcli $args getblockcount) + if [ $new_blocks -gt $blocks ]; then + log "detected block count increase $blocks -> $new_blocks; resetting timer" + remtime=$triggertime + blocks=$new_blocks + else + (( remtime=remtime-waittime )) + if [ $remtime -lt 1 ]; then break; fi + fi + done + log "*** no blocks in $triggertime seconds; initiating issuance ***" + # inner issuer loop + while true; do + log "generating next block" + blockhash=$("$MKBLOCK" "$bcli" "$2") || { echo "node error; aborting" ; exit 1; } + blocks=$($bcli $args getblockcount) + log "mined block $new_blocks $blockhash to $($bcli $args getconnectioncount) peer(s); idling for $idletime seconds" + sleep $idletime + new_blocks=$($bcli $args getblockcount) + if [ $blocks -lt $new_blocks ]; then + log "primary issuer appears to be back online ($blocks -> $new_blocks blocks during downtime); going back to watching" + break + fi + done +done diff --git a/src/Makefile.am b/src/Makefile.am index 8fc7f61d4b78..0175b93e0362 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -189,6 +189,7 @@ BITCOIN_CORE_H = \ script/signingprovider.h \ script/standard.h \ shutdown.h \ + signet.h \ streams.h \ support/allocators/secure.h \ support/allocators/zeroafterfree.h \ @@ -471,6 +472,7 @@ libbitcoin_common_a_SOURCES = \ script/sign.cpp \ script/signingprovider.cpp \ script/standard.cpp \ + signet.cpp \ versionbitsinfo.cpp \ warnings.cpp \ $(BITCOIN_CORE_H) diff --git a/src/chainparams.cpp b/src/chainparams.cpp index ad766471dc13..65e7435a4045 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -17,13 +19,13 @@ #include #include -static CBlock CreateGenesisBlock(const char* pszTimestamp, const CScript& genesisOutputScript, uint32_t nTime, uint32_t nNonce, uint32_t nBits, int32_t nVersion, const CAmount& genesisReward) +static CBlock CreateGenesisBlock(const CScript& coinbase_sig, const CScript& genesisOutputScript, uint32_t nTime, uint32_t nNonce, uint32_t nBits, int32_t nVersion, const CAmount& genesisReward) { CMutableTransaction txNew; txNew.nVersion = 1; txNew.vin.resize(1); txNew.vout.resize(1); - txNew.vin[0].scriptSig = CScript() << 486604799 << CScriptNum(4) << std::vector((const unsigned char*)pszTimestamp, (const unsigned char*)pszTimestamp + strlen(pszTimestamp)); + txNew.vin[0].scriptSig = coinbase_sig; txNew.vout[0].nValue = genesisReward; txNew.vout[0].scriptPubKey = genesisOutputScript; @@ -52,8 +54,16 @@ static CBlock CreateGenesisBlock(const char* pszTimestamp, const CScript& genesi static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits, int32_t nVersion, const CAmount& genesisReward) { const char* pszTimestamp = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"; + const CScript coinbase_sig = CScript() << 486604799 << CScriptNum(4) << std::vector((const unsigned char*)pszTimestamp, (const unsigned char*)pszTimestamp + strlen(pszTimestamp)); + const CScript genesisOutputScript = CScript() << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f") << OP_CHECKSIG; - return CreateGenesisBlock(pszTimestamp, genesisOutputScript, nTime, nNonce, nBits, nVersion, genesisReward); + return CreateGenesisBlock(coinbase_sig, genesisOutputScript, nTime, nNonce, nBits, nVersion, genesisReward); +} + +static CBlock CreateGenesisBlockWithHash(const uint256& hash) +{ + CScript coinbase_sig = CScript() << std::vector(hash.begin(), hash.end()); + return CreateGenesisBlock(coinbase_sig, CScript(OP_RETURN), 1296688602, 2, 0x207fffff, 1, 50 * COIN); } /** @@ -76,6 +86,7 @@ class CMainParams : public CChainParams { consensus.nPowTargetSpacing = 10 * 60; consensus.fPowAllowMinDifficultyBlocks = false; consensus.fPowNoRetargeting = false; + consensus.signet_blocks = false; consensus.nRuleChangeActivationThreshold = 1916; // 95% of 2016 consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28; @@ -182,6 +193,7 @@ class CTestNetParams : public CChainParams { consensus.nPowTargetSpacing = 10 * 60; consensus.fPowAllowMinDifficultyBlocks = true; consensus.fPowNoRetargeting = false; + consensus.signet_blocks = false; consensus.nRuleChangeActivationThreshold = 1512; // 75% for testchains consensus.nMinerConfirmationWindow = 2016; // nPowTargetTimespan / nPowTargetSpacing consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28; @@ -266,6 +278,7 @@ class CRegTestParams : public CChainParams { consensus.nPowTargetSpacing = 10 * 60; consensus.fPowAllowMinDifficultyBlocks = true; consensus.fPowNoRetargeting = true; + consensus.signet_blocks = false; consensus.nRuleChangeActivationThreshold = 108; // 75% for testchains consensus.nMinerConfirmationWindow = 144; // Faster than normal for regtest (144 instead of 2016) consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].bit = 28; @@ -376,6 +389,99 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) } } +/** + * Custom params for testing. + */ +class CCustomParams : public CRegTestParams { + void UpdateFromArgs(ArgsManager& args) + { + UpdateActivationParametersFromArgs(args); + + consensus.signet_blocks = args.IsArgSet("-signet_blockscript"); + const std::string signet_script_str = gArgs.GetArg("-signet_blockscript", ""); + LogPrintf("SigNet=%s with block script %s\n", consensus.signet_blocks, signet_script_str); + std::vector signet_script_bytes = ParseHex(signet_script_str); + g_signet_blockscript = CScript(signet_script_bytes.begin(), signet_script_bytes.end()); + + consensus.nSubsidyHalvingInterval = args.GetArg("-con_nsubsidyhalvinginterval", consensus.nSubsidyHalvingInterval); + consensus.BIP16Exception = uint256S(args.GetArg("-con_bip16exception", "0x0")); + consensus.BIP34Height = args.GetArg("-con_bip34height", consensus.BIP34Height); + consensus.BIP34Hash = uint256S(args.GetArg("-con_bip34hash", "0x0")); + consensus.BIP65Height = args.GetArg("-con_bip65height", consensus.BIP65Height); + consensus.BIP66Height = args.GetArg("-con_bip66height", consensus.BIP66Height); + consensus.powLimit = uint256S(args.GetArg("-con_powlimit", "7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); + consensus.nPowTargetTimespan = args.GetArg("-con_npowtargettimespan", consensus.nPowTargetTimespan); + consensus.nPowTargetSpacing = args.GetArg("-con_npowtargetspacing", consensus.nPowTargetSpacing); + consensus.fPowAllowMinDifficultyBlocks = args.GetBoolArg("-con_fpowallowmindifficultyblocks", consensus.fPowAllowMinDifficultyBlocks); + consensus.fPowNoRetargeting = args.GetBoolArg("-con_fpownoretargeting", consensus.fPowNoRetargeting); + consensus.nRuleChangeActivationThreshold = (uint32_t)args.GetArg("-con_nrulechangeactivationthreshold", consensus.nRuleChangeActivationThreshold); + consensus.nMinerConfirmationWindow = (uint32_t)args.GetArg("-con_nminerconfirmationwindow", consensus.nMinerConfirmationWindow); + + consensus.nMinimumChainWork = uint256S(args.GetArg("-con_nminimumchainwork", "0x0")); + consensus.defaultAssumeValid = uint256S(args.GetArg("-con_defaultassumevalid", "0x00")); + + nDefaultPort = (uint64_t)args.GetArg("-ndefaultport", nDefaultPort); + nPruneAfterHeight = (uint64_t)args.GetArg("-npruneafterheight", nPruneAfterHeight); + m_assumed_blockchain_size = (uint64_t)args.GetArg("-assumed_blockchain_size", m_assumed_blockchain_size); + m_assumed_chain_state_size = (uint64_t)args.GetArg("-assumed_chain_state_size", m_assumed_chain_state_size); + fDefaultConsistencyChecks = args.GetBoolArg("-fdefaultconsistencychecks", fDefaultConsistencyChecks); + fRequireStandard = args.GetBoolArg("-frequirestandard", fRequireStandard); + m_is_test_chain = args.GetBoolArg("-is_test_chain", m_is_test_chain); + + bech32_hrp = args.GetArg("-bech32_hrp", bech32_hrp); + base58Prefixes[PUBKEY_ADDRESS] = std::vector(1, args.GetArg("-pubkeyprefix", 111)); + base58Prefixes[SCRIPT_ADDRESS] = std::vector(1, args.GetArg("-scriptprefix", 196)); + base58Prefixes[SECRET_KEY] = std::vector(1, args.GetArg("-secretprefix", 239)); + + const std::string extpubprefix = args.GetArg("-extpubkeyprefix", "043587CF"); + assert(IsHex(extpubprefix) && extpubprefix.size() == 8 && "-extpubkeyprefix must be hex string of length 8"); + base58Prefixes[EXT_PUBLIC_KEY] = ParseHex(extpubprefix); + + const std::string extprvprefix = args.GetArg("-extprvkeyprefix", "04358394"); + assert(IsHex(extprvprefix) && extprvprefix.size() == 8 && "-extprvkeyprefix must be hex string of length 8"); + base58Prefixes[EXT_SECRET_KEY] = ParseHex(extprvprefix); + + const std::string magic_str = args.GetArg("-pchmessagestart", "FABFB5DA"); + assert(IsHex(magic_str) && magic_str.size() == 8 && "-pchmessagestart must be hex string of length 8"); + const std::vector magic_byte = ParseHex(magic_str); + std::copy(begin(magic_byte), end(magic_byte), pchMessageStart); + + vSeeds.clear(); + if (gArgs.IsArgSet("-seednode")) { + const auto seednodes = gArgs.GetArgs("-seednode"); + if (seednodes.size() != 1 || seednodes[0] != "0") { + vSeeds = seednodes; + } + } + } + +public: + CCustomParams(const std::string& chain, ArgsManager& args) : CRegTestParams(args) + { + strNetworkID = chain; + UpdateFromArgs(args); + + CHashWriter h(SER_DISK, 0); + h << strNetworkID; + if (consensus.signet_blocks) { + h << g_signet_blockscript; + } + genesis = CreateGenesisBlockWithHash(h.GetHash()); + consensus.hashGenesisBlock = genesis.GetHash(); + + // Now that genesis block has been generated, we check if there is an enforcescript, and switch + // to that one, as we will not be using the real block script anymore + if (args.IsArgSet("-signet_enforcescript")) { + if (args.GetArgs("-signet_enforcescript").size() != 1) { + throw std::runtime_error(strprintf("%s: -signet_enforcescript cannot be multiple values.", __func__)); + } + const std::vector bin = ParseHex(args.GetArgs("-signet_enforcescript")[0]); + g_signet_blockscript = CScript(bin.begin(), bin.end()); + LogPrintf("SigNet enforce script %s\n", gArgs.GetArgs("-signet_enforcescript")[0]); + } + } +}; + static std::unique_ptr globalChainParams; const CChainParams &Params() { @@ -385,13 +491,15 @@ const CChainParams &Params() { std::unique_ptr CreateChainParams(const std::string& chain) { + // Reserved names for non-custom chains if (chain == CBaseChainParams::MAIN) return std::unique_ptr(new CMainParams()); else if (chain == CBaseChainParams::TESTNET) return std::unique_ptr(new CTestNetParams()); else if (chain == CBaseChainParams::REGTEST) return std::unique_ptr(new CRegTestParams(gArgs)); - throw std::runtime_error(strprintf("%s: Unknown chain %s.", __func__, chain)); + + return std::unique_ptr(new CCustomParams(chain, gArgs)); } void SelectParams(const std::string& network) diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index 4bb66c8d8b51..e8253a9465ca 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -17,12 +17,44 @@ const std::string CBaseChainParams::REGTEST = "regtest"; void SetupChainParamsBaseOptions() { - gArgs.AddArg("-chain=", "Use the chain (default: main). Allowed values: main, test, regtest", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-chain=", "Use the chain (default: main). Reserved values: main, test, regtest. With any other value, a custom chain is used.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); gArgs.AddArg("-regtest", "Enter regression test mode, which uses a special chain in which blocks can be solved instantly. " "This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); - gArgs.AddArg("-segwitheight=", "Set the activation height of segwit. -1 to disable. (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); gArgs.AddArg("-testnet", "Use the test chain. Equivalent to -chain=test.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); - gArgs.AddArg("-vbparams=deployment:start:end", "Use given start/end times for specified version bits deployment (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-signet_blockscript", "Blocks must satisfy the given script to be considered valid. If not set, the chain is not considered a signet chain. (custom only)", ArgsManager::ALLOW_STRING, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-signet_enforcescript", "Blocks must satisfy the given script to be considered valid (this replaces -signet_blockscript, and is used for opt-in-reorg mode, custom or signet only)", ArgsManager::ALLOW_STRING, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-vbparams=deployment:start:end", "Use given start/end times for specified version bits deployment (regtest or custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-segwitheight=", "Set the activation height of segwit. -1 to disable. (regtest or custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + gArgs.AddArg("-con_nsubsidyhalvinginterval", "Number of blocks between one subsidy adjustment and the next one. Default: 150 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_bip16exception", "A block hash not to validate BIP16 on. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_bip34height", "Height from which BIP34 is enforced. Default: 500 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_bip34hash", "Hardcoded hash for BIP34 activation corresponding to the bip34height so that bip30 checks can be saved. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_bip65height", "Height from which BIP65 is enforced. Default: 1351 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_bip66height", "Height from which BIP66 is enforced. Default: 1251 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_powlimit", "Maximum proof of work target. Default 7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_npowtargettimespan", "Proof of work retargetting interval in seconds. Default: 2 weeks (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_npowtargetspacing", "Proof of work target for interval between blocks in seconds. Default: 600 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_fpowallowmindifficultyblocks", "Whether the chain allows minimum difficulty blocks or not. Default: 1 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_fpownoretargeting", "Whether the chain skips proof of work retargetting or not. Default: 1 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_nminerconfirmationwindow", "Interval for BIP9 deployment activation. Default: 144 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_nrulechangeactivationthreshold", "Minimum blocks to signal readiness for a chain for BIP9 activation. Default 108 (ie 75%). (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_nminimumchainwork", "The best chain should have at least this much work. Default: 0 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-con_defaultassumevalid", "By default assume that the signatures in ancestors of this block are valid. Consider using -assumevalid instead. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-ndefaultport", "The port to listen for connections on by default. Consider using -port instead of changing the default. Default: 18444 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-npruneafterheight", "Only start prunning after this height. Default: 1000 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-assumed_blockchain_size", "Estimated current blockchain size (in GB) for UI purposes. Default 0 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-assumed_chain_state_size", "Estimated current chain state size (in GB) for UI purposes. Default 0 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-fdefaultconsistencychecks", "Whether -checkblockindex and -checkmempool are active by default or not. Consider using those options instead. Default: 1 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-frequirestandard", "Whether standard policy rules are applied in the local mempool by default. Consider using -acceptnonstdtxn=0 instead of changing the default. Default: 0 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-is_test_chain", "Whether it's allowed to set -acceptnonstdtxn=0 for this chain or not. It also affects the default for -fallbackfee=0. Consider using -fallbackfee=0 instead of changing the default. Default: 1 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-bech32_hrp", "Human readable part for bech32 addresses. See BIP173 for more info. Default: bcrt (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-pubkeyprefix", "Magic for base58 pubkeys. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-scriptprefix", "Magic for base58 scripts. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-secretprefix", "Magic for base58 secret keys. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-extpubkeyprefix", "Magic for base58 external pubkeys. Default: 043587CF (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-extprvkeyprefix", "Magic for base58 external secret keys. Default: 04358394 (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-pchmessagestart", "Magic for p2p protocol. Default: FABFB5DA (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + gArgs.AddArg("-seednode=", "Use specified node as seed node. This option can be specified multiple times to connect to multiple nodes. (custom only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); } static std::unique_ptr globalChainBaseParams; @@ -41,8 +73,8 @@ std::unique_ptr CreateBaseChainParams(const std::string& chain return MakeUnique("testnet3", 18332); else if (chain == CBaseChainParams::REGTEST) return MakeUnique("regtest", 18443); - else - throw std::runtime_error(strprintf("%s: Unknown chain %s.", __func__, chain)); + + return MakeUnique(chain, 18553); } void SelectBaseParams(const std::string& chain) diff --git a/src/chainparamsbase.h b/src/chainparamsbase.h index f34646f7acf1..0ba8bdda2b35 100644 --- a/src/chainparamsbase.h +++ b/src/chainparamsbase.h @@ -15,7 +15,7 @@ class CBaseChainParams { public: - /** BIP70 chain name strings (main, test or regtest) */ + /** BIP70 chain name strings (main, test, signet or regtest) */ static const std::string MAIN; static const std::string TESTNET; static const std::string REGTEST; diff --git a/src/consensus/params.h b/src/consensus/params.h index 8263b0fef4a8..a9da0bba133b 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -79,6 +79,12 @@ struct Params { int64_t DifficultyAdjustmentInterval() const { return nPowTargetTimespan / nPowTargetSpacing; } uint256 nMinimumChainWork; uint256 defaultAssumeValid; + + /** + * If true, witness commitments contain a payload equal to a Bitcoin Script solution + * to a signet challenge as defined in the chain params. + */ + bool signet_blocks{false}; }; } // namespace Consensus diff --git a/src/core_read.cpp b/src/core_read.cpp index a3c9cf0159b3..de6c751789f7 100644 --- a/src/core_read.cpp +++ b/src/core_read.cpp @@ -158,10 +158,27 @@ bool DecodeHexBlockHeader(CBlockHeader& header, const std::string& hex_header) return true; } +inline bool ReadFromDisk(const std::string& path, std::string& output) +{ + FILE* fp = fopen(path.c_str(), "rb"); + if (!fp) return false; + char buf[1025]; + size_t r; + buf[1024] = 0; + output = ""; + while (0 < (r = fread(buf, 1, 1024, fp))) { buf[r] = 0; output += buf; } + while (output.size() > 0 && output[output.size() - 1] == '\n') output = output.substr(0, output.size() - 1); + fclose(fp); + return true; +} + bool DecodeHexBlk(CBlock& block, const std::string& strHexBlk) { - if (!IsHex(strHexBlk)) + if (!IsHex(strHexBlk)) { + std::string actual; + if (ReadFromDisk(strHexBlk, actual)) return DecodeHexBlk(block, actual); return false; + } std::vector blockData(ParseHex(strHexBlk)); CDataStream ssBlock(blockData, SER_NETWORK, PROTOCOL_VERSION); diff --git a/src/init.cpp b/src/init.cpp index bb82130542de..41183a13d739 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -938,11 +938,6 @@ bool AppInitParameterInteraction() return InitError(strprintf(_("Config setting for %s only applied on %s network when in [%s] section.").translated, arg, network, network)); } - // Warn if unrecognized section name are present in the config file. - for (const auto& section : gArgs.GetUnrecognizedSections()) { - InitWarning(strprintf("%s:%i " + _("Section [%s] is not recognized.").translated, section.m_file, section.m_line, section.m_name)); - } - if (!fs::is_directory(GetBlocksDir())) { return InitError(strprintf(_("Specified blocks directory \"%s\" does not exist.").translated, gArgs.GetArg("-blocksdir", "").c_str())); } diff --git a/src/primitives/block.cpp b/src/primitives/block.cpp index 60c7c2d16083..b41b90612d9d 100644 --- a/src/primitives/block.cpp +++ b/src/primitives/block.cpp @@ -29,3 +29,65 @@ std::string CBlock::ToString() const } return s.str(); } + +bool CBlock::GetWitnessCommitmentSection(const uint8_t header[4], std::vector& result) const +{ + int cidx = GetWitnessCommitmentIndex(); + if (cidx == -1) return false; + auto script = vtx.at(0)->vout.at(cidx).scriptPubKey; + opcodetype opcode; + CScript::const_iterator pc = script.begin(); + ++pc; // move beyond initial OP_RETURN + while (script.GetOp(pc, opcode, result)) { + if (result.size() > 3 && !memcmp(result.data(), header, 4)) { + result.erase(result.begin(), result.begin() + 4); + return true; + } + } + result.clear(); + return false; +} + +bool CBlock::SetWitnessCommitmentSection(CMutableTransaction& mtx, const uint8_t header[4], const std::vector data) +{ + int cidx = GetWitnessCommitmentIndex(mtx); + if (cidx == -1) return false; + + CScript result; + std::vector pushdata; + auto script = mtx.vout[cidx].scriptPubKey; + opcodetype opcode; + CScript::const_iterator pc = script.begin(); + result.push_back(*pc++); + bool found = false; + while (script.GetOp(pc, opcode, pushdata)) { + if (pushdata.size() > 0) { + if (pushdata.size() > 3 && !memcmp(pushdata.data(), header, 4)) { + // replace pushdata + found = true; + pushdata.erase(pushdata.begin() + 4, pushdata.end()); + pushdata.insert(pushdata.end(), data.begin(), data.end()); + } + result << pushdata; + } else { + result << opcode; + } + } + if (!found) { + // append section as it did not exist + pushdata.clear(); + pushdata.insert(pushdata.end(), header, header + 4); + pushdata.insert(pushdata.end(), data.begin(), data.end()); + result << pushdata; + } + mtx.vout[cidx].scriptPubKey = result; + return true; +} + +bool CBlock::SetWitnessCommitmentSection(const uint8_t header[4], const std::vector data) +{ + auto mtx = CMutableTransaction(*vtx[0]); + if (!SetWitnessCommitmentSection(mtx, header, data)) return false; + vtx[0] = std::make_shared(mtx); + return true; +} diff --git a/src/primitives/block.h b/src/primitives/block.h index 750d42efbc23..db6c9be4185c 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -116,6 +116,50 @@ class CBlock : public CBlockHeader return block; } + /** + * Get the vout index of the segwit commitment in the coinbase transaction of this block. + * + * Returns -1 if no witness commitment was found. + */ + inline int GetWitnessCommitmentIndex() const + { + return vtx.empty() ? -1 : GetWitnessCommitmentIndex(*vtx.at(0)); + } + + /** + * Get the vout index of the segwit commitment in the given coinbase transaction. + * + * Returns -1 if no witness commitment was found. + */ + template static inline int GetWitnessCommitmentIndex(const T& coinbase) { + for (int64_t o = coinbase.vout.size() - 1; o > -1; --o) { + auto vospk = coinbase.vout[o].scriptPubKey; + if (vospk.size() >= 38 && vospk[0] == OP_RETURN && vospk[1] == 0x24 && vospk[2] == 0xaa && vospk[3] == 0x21 && vospk[4] == 0xa9 && vospk[5] == 0xed) { + return o; + } + } + return -1; + } + + /** + * Attempt to get the data for the section with the given header in the witness commitment of this block. + * + * Returns false if header was not found. The data (excluding the 4 byte header) is written into result if found. + */ + bool GetWitnessCommitmentSection(const uint8_t header[4], std::vector& result) const; + + /** + * Attempt to add or update the data for the section with the given header in the witness commitment of this block. + * + * This operation may fail and return false, if no witness commitment exists upon call time. Returns true on success. + */ + bool SetWitnessCommitmentSection(const uint8_t header[4], const std::vector data); + + /** + * The tx based equivalent of the above. + */ + static bool SetWitnessCommitmentSection(CMutableTransaction& tx, const uint8_t header[4], const std::vector data); + std::string ToString() const; }; diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index dcdb2479770f..a0066e26f7bd 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -47,6 +47,8 @@ static const int TOOLTIP_WRAP_THRESHOLD = 80; #define QAPP_APP_NAME_DEFAULT "Bitcoin-Qt" #define QAPP_APP_NAME_TESTNET "Bitcoin-Qt-testnet" #define QAPP_APP_NAME_REGTEST "Bitcoin-Qt-regtest" +#define QAPP_APP_NAME_SIGNET "Bitcoin-Qt-signet" +#define QAPP_APP_NAME_CUSTOM "Bitcoin-Qt-custom-chain" /* One gigabyte (GB) in bytes */ static constexpr uint64_t GB_BYTES{1000000000}; diff --git a/src/qt/networkstyle.cpp b/src/qt/networkstyle.cpp index 5c039a939eee..d48c4271b814 100644 --- a/src/qt/networkstyle.cpp +++ b/src/qt/networkstyle.cpp @@ -19,7 +19,8 @@ static const struct { } network_styles[] = { {"main", QAPP_APP_NAME_DEFAULT, 0, 0}, {"test", QAPP_APP_NAME_TESTNET, 70, 30}, - {"regtest", QAPP_APP_NAME_REGTEST, 160, 30} + {"regtest", QAPP_APP_NAME_REGTEST, 160, 30}, + {"signet", QAPP_APP_NAME_SIGNET, 35, 15}, }; static const unsigned network_styles_count = sizeof(network_styles)/sizeof(*network_styles); @@ -91,5 +92,5 @@ const NetworkStyle* NetworkStyle::instantiate(const std::string& networkId) titleAddText.c_str()); } } - return nullptr; + return new NetworkStyle(strprintf("%s-%s", QAPP_APP_NAME_CUSTOM, networkId).c_str(), 250, 30, titleAddText.c_str()); } diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 9513c2b9ac29..4aab56b6cf3f 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -22,6 +22,7 @@ #include #include #include