Merge bitcoin/bitcoin#29553: assumeutxo: Add dumptxoutset height param, remove shell scripts

94b0adcc37 rpc, refactor: Prevent potential race conditions in dumptxoutset (Fabian Jahr)
e868a6e070 doc: Improve assumeutxo guide and add more docs/comments (Fabian Jahr)
b29c21fc92 assumeutxo: Remove devtools/utxo_snapshot.sh (Fabian Jahr)
20a1c77aa7 contrib: Remove test_utxo_snapshots.sh (Fabian Jahr)
8426850352 test: Test for dumptxoutset at specific height (Fabian Jahr)
993cafe7e4 RPC: Add type parameter to dumptxoutset (Fabian Jahr)
fccf4f91d2 RPC: Extract ReconsiderBlock helper (Fabian Jahr)
446ce51c21 RPC: Extract InvalidateBlock helper (Fabian Jahr)

Pull request description:

  This adds a height parameter to the `dumptxoutset` RPC. This internalizes the workflow that was previously done by scripts: roll back the chain to the height we actually want the snapshot from, create the snapshot, roll forward to the real tip again.

  The nice thing about internalizing this functionality is that we can write tests for the code and it gives us more options to make the functionality robust. The shell scripts we have so far will be more cumbersome to maintain in the long run, especially since we will only notice later when we have broken them. I think it's safe to remove these `test_utxo_snapshots.sh` as well when we have this option in `dumptxoutset` because we have also added some good additional functional test coverage for this functionality.

ACKs for top commit:
  Sjors:
    re-utACK 94b0adcc37
  achow101:
    ACK 94b0adcc37
  mzumsande:
    ACK 94b0adcc37
  pablomartin4btc:
    re-ACK 94b0adcc37

Tree-SHA512: a4c9af5f687d1ca7bfb579a36f363882823386b5fa80c05de531b05a2782b5da6ff5baf3ada4bca8f32f63975d86f1948175abed9affe51fc958472b5f838dab
This commit is contained in:
Ava Chow 2024-09-03 15:30:45 -04:00
commit fa5fc71199
No known key found for this signature in database
GPG key ID: 17565732E08E5E41
11 changed files with 355 additions and 418 deletions

View file

@ -1,209 +0,0 @@
#!/usr/bin/env bash
# Demonstrate the creation and usage of UTXO snapshots.
#
# A server node starts up, IBDs up to a certain height, then generates a UTXO
# snapshot at that point.
#
# The server then downloads more blocks (to create a diff from the snapshot).
#
# We bring a client up, load the UTXO snapshot, and we show the client sync to
# the "network tip" and then start a background validation of the snapshot it
# loaded. We see the background validation chainstate removed after validation
# completes.
#
# The shellcheck rule SC2086 (quoted variables) disablements are necessary
# since this rule needs to be violated in order to get bitcoind to pick up on
# $EARLY_IBD_FLAGS for the script to work.
export LC_ALL=C
set -e
BASE_HEIGHT=${1:-30000}
INCREMENTAL_HEIGHT=20000
FINAL_HEIGHT=$((BASE_HEIGHT + INCREMENTAL_HEIGHT))
SERVER_DATADIR="$(pwd)/utxodemo-data-server-$BASE_HEIGHT"
CLIENT_DATADIR="$(pwd)/utxodemo-data-client-$BASE_HEIGHT"
UTXO_DAT_FILE="$(pwd)/utxo.$BASE_HEIGHT.dat"
# Chosen to try to not interfere with any running bitcoind processes.
SERVER_PORT=8633
SERVER_RPC_PORT=8632
CLIENT_PORT=8733
CLIENT_RPC_PORT=8732
SERVER_PORTS="-port=${SERVER_PORT} -rpcport=${SERVER_RPC_PORT}"
CLIENT_PORTS="-port=${CLIENT_PORT} -rpcport=${CLIENT_RPC_PORT}"
# Ensure the client exercises all indexes to test that snapshot use works
# properly with indexes.
ALL_INDEXES="-txindex -coinstatsindex -blockfilterindex=1"
if ! command -v jq >/dev/null ; then
echo "This script requires jq to parse JSON RPC output. Please install it."
echo "(e.g. sudo apt install jq)"
exit 1
fi
DUMP_OUTPUT="dumptxoutset-output-$BASE_HEIGHT.json"
finish() {
echo
echo "Killing server and client PIDs ($SERVER_PID, $CLIENT_PID) and cleaning up datadirs"
echo
rm -f "$UTXO_DAT_FILE" "$DUMP_OUTPUT"
rm -rf "$SERVER_DATADIR" "$CLIENT_DATADIR"
kill -9 "$SERVER_PID" "$CLIENT_PID"
}
trap finish EXIT
# Need to specify these to trick client into accepting server as a peer
# it can IBD from, otherwise the default values prevent IBD from the server node.
EARLY_IBD_FLAGS="-maxtipage=9223372036854775207 -minimumchainwork=0x00"
server_rpc() {
./src/bitcoin-cli -rpcport=$SERVER_RPC_PORT -datadir="$SERVER_DATADIR" "$@"
}
client_rpc() {
./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir="$CLIENT_DATADIR" "$@"
}
server_sleep_til_boot() {
while ! server_rpc ping >/dev/null 2>&1; do sleep 0.1; done
}
client_sleep_til_boot() {
while ! client_rpc ping >/dev/null 2>&1; do sleep 0.1; done
}
server_sleep_til_shutdown() {
while server_rpc ping >/dev/null 2>&1; do sleep 0.1; done
}
mkdir -p "$SERVER_DATADIR" "$CLIENT_DATADIR"
echo "Hi, welcome to the assumeutxo demo/test"
echo
echo "We're going to"
echo
echo " - start up a 'server' node, sync it via mainnet IBD to height ${BASE_HEIGHT}"
echo " - create a UTXO snapshot at that height"
echo " - IBD ${INCREMENTAL_HEIGHT} more blocks on top of that"
echo
echo "then we'll demonstrate assumeutxo by "
echo
echo " - starting another node (the 'client') and loading the snapshot in"
echo " * first you'll have to modify the code slightly (chainparams) and recompile"
echo " * don't worry, we'll make it easy"
echo " - observing the client sync ${INCREMENTAL_HEIGHT} blocks on top of the snapshot from the server"
echo " - observing the client validate the snapshot chain via background IBD"
echo
read -p "Press [enter] to continue" _
echo
echo "-- Starting the demo. You might want to run the two following commands in"
echo " separate terminal windows:"
echo
echo " watch -n0.1 tail -n 30 $SERVER_DATADIR/debug.log"
echo " watch -n0.1 tail -n 30 $CLIENT_DATADIR/debug.log"
echo
read -p "Press [enter] to continue" _
echo
echo "-- IBDing the blocks (height=$BASE_HEIGHT) required to the server node..."
# shellcheck disable=SC2086
./src/bitcoind -logthreadnames=1 $SERVER_PORTS \
-datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -stopatheight="$BASE_HEIGHT" >/dev/null
echo
echo "-- Creating snapshot at ~ height $BASE_HEIGHT ($UTXO_DAT_FILE)..."
server_sleep_til_shutdown # wait for stopatheight to be hit
# shellcheck disable=SC2086
./src/bitcoind -logthreadnames=1 $SERVER_PORTS \
-datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -connect=0 -listen=0 >/dev/null &
SERVER_PID="$!"
server_sleep_til_boot
server_rpc dumptxoutset "$UTXO_DAT_FILE" > "$DUMP_OUTPUT"
cat "$DUMP_OUTPUT"
kill -9 "$SERVER_PID"
RPC_BASE_HEIGHT=$(jq -r .base_height < "$DUMP_OUTPUT")
RPC_AU=$(jq -r .txoutset_hash < "$DUMP_OUTPUT")
RPC_NCHAINTX=$(jq -r .nchaintx < "$DUMP_OUTPUT")
RPC_BLOCKHASH=$(jq -r .base_hash < "$DUMP_OUTPUT")
server_sleep_til_shutdown
echo
echo "-- Now: add the following to CMainParams::m_assumeutxo_data"
echo " in src/kernel/chainparams.cpp, and recompile:"
echo
echo " {.height = ${RPC_BASE_HEIGHT}, .hash_serialized = AssumeutxoHash{uint256{\"${RPC_AU}\"}}, .m_chain_tx_count = ${RPC_NCHAINTX}, .blockhash = consteval_ctor(uint256{\"${RPC_BLOCKHASH}\"})},"
echo
echo
echo "-- IBDing more blocks to the server node (height=$FINAL_HEIGHT) so there is a diff between snapshot and tip..."
# shellcheck disable=SC2086
./src/bitcoind $SERVER_PORTS -logthreadnames=1 -datadir="$SERVER_DATADIR" \
$EARLY_IBD_FLAGS -stopatheight="$FINAL_HEIGHT" >/dev/null
echo
echo "-- Starting the server node to provide blocks to the client node..."
# shellcheck disable=SC2086
./src/bitcoind $SERVER_PORTS -logthreadnames=1 -debug=net -datadir="$SERVER_DATADIR" \
$EARLY_IBD_FLAGS -connect=0 -listen=1 >/dev/null &
SERVER_PID="$!"
server_sleep_til_boot
echo
echo "-- Okay, what you're about to see is the client starting up and activating the snapshot."
echo " I'm going to display the top 14 log lines from the client on top of an RPC called"
echo " getchainstates, which is like getblockchaininfo but for both the snapshot and "
echo " background validation chainstates."
echo
echo " You're going to first see the snapshot chainstate sync to the server's tip, then"
echo " the background IBD chain kicks in to validate up to the base of the snapshot."
echo
echo " Once validation of the snapshot is done, you should see log lines indicating"
echo " that we've deleted the background validation chainstate."
echo
echo " Once everything completes, exit the watch command with CTRL+C."
echo
read -p "When you're ready for all this, hit [enter]" _
echo
echo "-- Starting the client node to get headers from the server, then load the snapshot..."
# shellcheck disable=SC2086
./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" \
-connect=0 -addnode=127.0.0.1:$SERVER_PORT -debug=net $EARLY_IBD_FLAGS >/dev/null &
CLIENT_PID="$!"
client_sleep_til_boot
echo
echo "-- Initial state of the client:"
client_rpc getchainstates
echo
echo "-- Loading UTXO snapshot into client. Calling RPC in a loop..."
while ! client_rpc loadtxoutset "$UTXO_DAT_FILE" ; do sleep 10; done
watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat"
echo
echo "-- Okay, now I'm going to restart the client to make sure that the snapshot chain reloads "
echo " as the main chain properly..."
echo
echo " Press CTRL+C after you're satisfied to exit the demo"
echo
read -p "Press [enter] to continue"
client_sleep_til_boot
# shellcheck disable=SC2086
./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" -connect=0 \
-addnode=127.0.0.1:$SERVER_PORT "$EARLY_IBD_FLAGS" >/dev/null &
CLIENT_PID="$!"
client_sleep_til_boot
watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat"
echo
echo "-- Done!"

View file

@ -1,104 +0,0 @@
#!/usr/bin/env bash
#
# Copyright (c) 2019-2023 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
set -ueo pipefail
NETWORK_DISABLED=false
if (( $# < 3 )); then
echo 'Usage: utxo_snapshot.sh <generate-at-height> <snapshot-out-path> <bitcoin-cli-call ...>'
echo
echo " if <snapshot-out-path> is '-', don't produce a snapshot file but instead print the "
echo " expected assumeutxo hash"
echo
echo 'Examples:'
echo
echo " ./contrib/devtools/utxo_snapshot.sh 570000 utxo.dat ./src/bitcoin-cli -datadir=\$(pwd)/testdata"
echo ' ./contrib/devtools/utxo_snapshot.sh 570000 - ./src/bitcoin-cli'
exit 1
fi
GENERATE_AT_HEIGHT="${1}"; shift;
OUTPUT_PATH="${1}"; shift;
# Most of the calls we make take a while to run, so pad with a lengthy timeout.
BITCOIN_CLI_CALL="${*} -rpcclienttimeout=9999999"
# Check if the node is pruned and get the pruned block height
PRUNED=$( ${BITCOIN_CLI_CALL} getblockchaininfo | awk '/pruneheight/ {print $2}' | tr -d ',' )
if (( GENERATE_AT_HEIGHT < PRUNED )); then
echo "Error: The requested snapshot height (${GENERATE_AT_HEIGHT}) should be greater than the pruned block height (${PRUNED})."
exit 1
fi
# Check current block height to ensure the node has synchronized past the required block
CURRENT_BLOCK_HEIGHT=$(${BITCOIN_CLI_CALL} getblockcount)
PIVOT_BLOCK_HEIGHT=$(( GENERATE_AT_HEIGHT + 1 ))
if (( PIVOT_BLOCK_HEIGHT > CURRENT_BLOCK_HEIGHT )); then
(>&2 echo "Error: The node has not yet synchronized to block height ${PIVOT_BLOCK_HEIGHT}.")
(>&2 echo "Please wait until the node has synchronized past this block height and try again.")
exit 1
fi
# Early exit if file at OUTPUT_PATH already exists
if [[ -e "$OUTPUT_PATH" ]]; then
(>&2 echo "Error: $OUTPUT_PATH already exists or is not a valid path.")
exit 1
fi
# Validate that the path is correct
if [[ "${OUTPUT_PATH}" != "-" && ! -d "$(dirname "${OUTPUT_PATH}")" ]]; then
(>&2 echo "Error: The directory $(dirname "${OUTPUT_PATH}") does not exist.")
exit 1
fi
function cleanup {
(>&2 echo "Restoring chain to original height; this may take a while")
${BITCOIN_CLI_CALL} reconsiderblock "${PIVOT_BLOCKHASH}"
if $NETWORK_DISABLED; then
(>&2 echo "Restoring network activity")
${BITCOIN_CLI_CALL} setnetworkactive true
fi
}
function early_exit {
(>&2 echo "Exiting due to Ctrl-C")
cleanup
exit 1
}
# Prompt the user to disable network activity
read -p "Do you want to disable network activity (setnetworkactive false) before running invalidateblock? (Y/n): " -r
if [[ "$REPLY" =~ ^[Yy]*$ || -z "$REPLY" ]]; then
# User input is "Y", "y", or Enter key, proceed with the action
NETWORK_DISABLED=true
(>&2 echo "Disabling network activity")
${BITCOIN_CLI_CALL} setnetworkactive false
else
(>&2 echo "Network activity remains enabled")
fi
# Block we'll invalidate/reconsider to rewind/fast-forward the chain.
PIVOT_BLOCKHASH=$($BITCOIN_CLI_CALL getblockhash $(( GENERATE_AT_HEIGHT + 1 )) )
# Trap for normal exit and Ctrl-C
trap cleanup EXIT
trap early_exit INT
(>&2 echo "Rewinding chain back to height ${GENERATE_AT_HEIGHT} (by invalidating ${PIVOT_BLOCKHASH}); this may take a while")
${BITCOIN_CLI_CALL} invalidateblock "${PIVOT_BLOCKHASH}"
if [[ "${OUTPUT_PATH}" = "-" ]]; then
(>&2 echo "Generating txoutset info...")
${BITCOIN_CLI_CALL} gettxoutsetinfo | grep hash_serialized_3 | sed 's/^.*: "\(.\+\)\+",/\1/g'
else
(>&2 echo "Generating UTXO snapshot...")
${BITCOIN_CLI_CALL} dumptxoutset "${OUTPUT_PATH}"
fi

85
doc/assumeutxo.md Normal file
View file

@ -0,0 +1,85 @@
# Assumeutxo Usage
Assumeutxo is a feature that allows fast bootstrapping of a validating bitcoind
instance.
For notes on the design of Assumeutxo, please refer to [the design doc](/doc/assumeutxo.md).
## Loading a snapshot
There is currently no canonical source for snapshots, but any downloaded snapshot
will be checked against a hash that's been hardcoded in source code. If there is
no source for the snapshot you need, you can generate it yourself using
`dumptxoutset` on another node that is already synced (see
[Generating a snapshot](#generating-a-snapshot)).
Once you've obtained the snapshot, you can use the RPC command `loadtxoutset` to
load it.
```
$ bitcoin-cli loadtxoutset /path/to/input
```
After the snapshot has loaded, the syncing process of both the snapshot chain
and the background IBD chain can be monitored with the `getchainstates` RPC.
### Pruning
A pruned node can load a snapshot. To save space, it's possible to delete the
snapshot file as soon as `loadtxoutset` finishes.
The minimum `-prune` setting is 550 MiB, but this functionality ignores that
minimum and uses at least 1100 MiB.
As the background sync continues there will be temporarily two chainstate
directories, each multiple gigabytes in size (likely growing larger than the
downloaded snapshot).
### Indexes
Indexes work but don't take advantage of this feature. They always start building
from the genesis block and can only apply blocks in order. Once the background
validation reaches the snapshot block, indexes will continue to build all the
way to the tip.
For indexes that support pruning, note that these indexes only allow blocks that
were already indexed to be pruned. Blocks that are not indexed yet will also
not be pruned.
This means that, if the snapshot is old, then a lot of blocks after the snapshot
block will need to be downloaded, and these blocks can't be pruned until they
are indexed, so they could consume a lot of disk space until indexing catches up
to the snapshot block.
## Generating a snapshot
The RPC command `dumptxoutset` can be used to generate a snapshot for the current
tip (using type "latest") or a recent height (using type "rollback"). A generated
snapshot from one node can then be loaded
on any other node. However, keep in mind that the snapshot hash needs to be
listed in the chainparams to make it usable. If there is no snapshot hash for
the height you have chosen already, you will need to change the code there and
re-compile.
Using the type parameter "rollback", `dumptxoutset` can also be used to verify the
hardcoded snapshot hash in the source code by regenerating the snapshot and
comparing the hash.
Example usage:
```
$ bitcoin-cli -rpcclienttimeout=0 dumptxoutset /path/to/output rollback
```
For most of the duration of `dumptxoutset` running the node is in a temporary
state that does not actually reflect reality, i.e. blocks are marked invalid
although we know they are not invalid. Because of this it is discouraged to
interact with the node in any other way during this time to avoid inconsistent
results and race conditions, particularly RPCs that interact with blockstorage.
This inconsistent state is also why network activity is temporarily disabled,
causing us to disconnect from all peers.
`dumptxoutset` takes some time to complete, independent of hardware and
what parameter is chosen. Because of that it is recommended to increase the RPC
client timeout value (use `-rpcclienttimeout=0` for no timeout).

View file

@ -1,47 +1,6 @@
# assumeutxo
# Assumeutxo Design
Assumeutxo is a feature that allows fast bootstrapping of a validating bitcoind
instance.
## Loading a snapshot
There is currently no canonical source for snapshots, but any downloaded snapshot
will be checked against a hash that's been hardcoded in source code.
Once you've obtained the snapshot, you can use the RPC command `loadtxoutset` to
load it.
### Pruning
A pruned node can load a snapshot. To save space, it's possible to delete the
snapshot file as soon as `loadtxoutset` finishes.
The minimum `-prune` setting is 550 MiB, but this functionality ignores that
minimum and uses at least 1100 MiB.
As the background sync continues there will be temporarily two chainstate
directories, each multiple gigabytes in size (likely growing larger than the
downloaded snapshot).
### Indexes
Indexes work but don't take advantage of this feature. They always start building
from the genesis block. Once the background validation reaches the snapshot block,
indexes will continue to build all the way to the tip.
For indexes that support pruning, note that no pruning will take place between
the snapshot and the tip, until the background sync has completed - after which
everything is pruned. Depending on how old the snapshot is, this may temporarily
use a significant amount of disk space.
## Generating a snapshot
The RPC command `dumptxoutset` can be used to generate a snapshot. This can be used
to create a snapshot on one node that you wish to load on another node.
It can also be used to verify the hardcoded snapshot hash in the source code.
The utility script
`./contrib/devtools/utxo_snapshot.sh` may be of use.
For notes on the usage of Assumeutxo, please refer to [the usage doc](/doc/assumeutxo.md).
## General background
@ -79,7 +38,7 @@ data.
### "Normal" operation via initial block download
`ChainstateManager` manages a single Chainstate object, for which
`m_snapshot_blockhash` is null. This chainstate is (maybe obviously)
`m_from_snapshot_blockhash` is `std::nullopt`. This chainstate is (maybe obviously)
considered active. This is the "traditional" mode of operation for bitcoind.
| | |

View file

@ -75,6 +75,22 @@ static GlobalMutex cs_blockchange;
static std::condition_variable cond_blockchange;
static CUpdatedBlock latestblock GUARDED_BY(cs_blockchange);
std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex*>
PrepareUTXOSnapshot(
Chainstate& chainstate,
const std::function<void()>& interruption_point = {})
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
UniValue WriteUTXOSnapshot(
Chainstate& chainstate,
CCoinsViewCursor* pcursor,
CCoinsStats* maybe_stats,
const CBlockIndex* tip,
AutoFile& afile,
const fs::path& path,
const fs::path& temppath,
const std::function<void()>& interruption_point = {});
/* Calculate the difficulty for a given block index.
*/
double GetDifficulty(const CBlockIndex& blockindex)
@ -1577,6 +1593,27 @@ static RPCHelpMan preciousblock()
};
}
void InvalidateBlock(ChainstateManager& chainman, const uint256 block_hash) {
BlockValidationState state;
CBlockIndex* pblockindex;
{
LOCK(chainman.GetMutex());
pblockindex = chainman.m_blockman.LookupBlockIndex(block_hash);
if (!pblockindex) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
}
}
chainman.ActiveChainstate().InvalidateBlock(state, pblockindex);
if (state.IsValid()) {
chainman.ActiveChainstate().ActivateBestChain(state);
}
if (!state.IsValid()) {
throw JSONRPCError(RPC_DATABASE_ERROR, state.ToString());
}
}
static RPCHelpMan invalidateblock()
{
return RPCHelpMan{"invalidateblock",
@ -1591,33 +1628,35 @@ static RPCHelpMan invalidateblock()
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
uint256 hash(ParseHashV(request.params[0], "blockhash"));
BlockValidationState state;
ChainstateManager& chainman = EnsureAnyChainman(request.context);
CBlockIndex* pblockindex;
{
LOCK(cs_main);
pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
if (!pblockindex) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
}
}
chainman.ActiveChainstate().InvalidateBlock(state, pblockindex);
uint256 hash(ParseHashV(request.params[0], "blockhash"));
if (state.IsValid()) {
chainman.ActiveChainstate().ActivateBestChain(state);
}
if (!state.IsValid()) {
throw JSONRPCError(RPC_DATABASE_ERROR, state.ToString());
}
InvalidateBlock(chainman, hash);
return UniValue::VNULL;
},
};
}
void ReconsiderBlock(ChainstateManager& chainman, uint256 block_hash) {
{
LOCK(chainman.GetMutex());
CBlockIndex* pblockindex = chainman.m_blockman.LookupBlockIndex(block_hash);
if (!pblockindex) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
}
chainman.ActiveChainstate().ResetBlockFailureFlags(pblockindex);
}
BlockValidationState state;
chainman.ActiveChainstate().ActivateBestChain(state);
if (!state.IsValid()) {
throw JSONRPCError(RPC_DATABASE_ERROR, state.ToString());
}
}
static RPCHelpMan reconsiderblock()
{
return RPCHelpMan{"reconsiderblock",
@ -1636,22 +1675,7 @@ static RPCHelpMan reconsiderblock()
ChainstateManager& chainman = EnsureAnyChainman(request.context);
uint256 hash(ParseHashV(request.params[0], "blockhash"));
{
LOCK(cs_main);
CBlockIndex* pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
if (!pblockindex) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
}
chainman.ActiveChainstate().ResetBlockFailureFlags(pblockindex);
}
BlockValidationState state;
chainman.ActiveChainstate().ActivateBestChain(state);
if (!state.IsValid()) {
throw JSONRPCError(RPC_DATABASE_ERROR, state.ToString());
}
ReconsiderBlock(chainman, hash);
return UniValue::VNULL;
},
@ -2640,6 +2664,25 @@ static RPCHelpMan getblockfilter()
};
}
/**
* RAII class that disables the network in its constructor and enables it in its
* destructor.
*/
class NetworkDisable
{
CConnman& m_connman;
public:
NetworkDisable(CConnman& connman) : m_connman(connman) {
m_connman.SetNetworkActive(false);
if (m_connman.GetNetworkActive()) {
throw JSONRPCError(RPC_MISC_ERROR, "Network activity could not be suspended.");
}
};
~NetworkDisable() {
m_connman.SetNetworkActive(true);
};
};
/**
* Serialize the UTXO set to a file for loading elsewhere.
*
@ -2649,9 +2692,20 @@ static RPCHelpMan dumptxoutset()
{
return RPCHelpMan{
"dumptxoutset",
"Write the serialized UTXO set to a file.",
"Write the serialized UTXO set to a file. This can be used in loadtxoutset afterwards if this snapshot height is supported in the chainparams as well.\n\n"
"Unless the the \"latest\" type is requested, the node will roll back to the requested height and network activity will be suspended during this process. "
"Because of this it is discouraged to interact with the node in any other way during the execution of this call to avoid inconsistent results and race conditions, particularly RPCs that interact with blockstorage.\n\n"
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
{
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
{"type", RPCArg::Type::STR, RPCArg::Default(""), "The type of snapshot to create. Can be \"latest\" to create a snapshot of the current UTXO set or \"rollback\" to temporarily roll back the state of the node to a historical block before creating the snapshot of a historical UTXO set. This parameter can be omitted if a separate \"rollback\" named parameter is specified indicating the height or hash of a specific historical block. If \"rollback\" is specified and separate \"rollback\" named parameter is not specified, this will roll back to the latest valid snapshot block that currently be loaded with loadtxoutset."},
{"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "",
{
{"rollback", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
"Height or hash of the block to roll back to before creating the snapshot. Note: The further this number is from the tip, the longer this process will take. Consider setting a higher -rpcclienttimeout value in this case.",
RPCArgOptions{.skip_type_check = true, .type_str = {"", "string or numeric"}}},
},
},
},
RPCResult{
RPCResult::Type::OBJ, "", "",
@ -2665,10 +2719,33 @@ static RPCHelpMan dumptxoutset()
}
},
RPCExamples{
HelpExampleCli("dumptxoutset", "utxo.dat")
HelpExampleCli("-rpcclienttimeout=0 dumptxoutset", "utxo.dat latest") +
HelpExampleCli("-rpcclienttimeout=0 dumptxoutset", "utxo.dat rollback") +
HelpExampleCli("-rpcclienttimeout=0 -named dumptxoutset", R"(utxo.dat rollback=853456)")
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
NodeContext& node = EnsureAnyNodeContext(request.context);
const CBlockIndex* tip{WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())};
const CBlockIndex* target_index{nullptr};
const std::string snapshot_type{self.Arg<std::string>("type")};
const UniValue options{request.params[2].isNull() ? UniValue::VOBJ : request.params[2]};
if (options.exists("rollback")) {
if (!snapshot_type.empty() && snapshot_type != "rollback") {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified with rollback option", snapshot_type));
}
target_index = ParseHashOrHeight(options["rollback"], *node.chainman);
} else if (snapshot_type == "rollback") {
auto snapshot_heights = node.chainman->GetParams().GetAvailableSnapshotHeights();
CHECK_NONFATAL(snapshot_heights.size() > 0);
auto max_height = std::max_element(snapshot_heights.begin(), snapshot_heights.end());
target_index = ParseHashOrHeight(*max_height, *node.chainman);
} else if (snapshot_type == "latest") {
target_index = tip;
} else {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified. Please specify \"rollback\" or \"latest\"", snapshot_type));
}
const ArgsManager& args{EnsureAnyArgsman(request.context)};
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str()));
// Write to a temporary path and then move into `path` on completion
@ -2690,10 +2767,73 @@ static RPCHelpMan dumptxoutset()
"Couldn't open file " + temppath.utf8string() + " for writing.");
}
NodeContext& node = EnsureAnyNodeContext(request.context);
UniValue result = CreateUTXOSnapshot(
node, node.chainman->ActiveChainstate(), afile, path, temppath);
fs::rename(temppath, path);
CConnman& connman = EnsureConnman(node);
const CBlockIndex* invalidate_index{nullptr};
std::unique_ptr<NetworkDisable> disable_network;
// If the user wants to dump the txoutset of the current tip, we don't have
// to roll back at all
if (target_index != tip) {
// If the node is running in pruned mode we ensure all necessary block
// data is available before starting to roll back.
if (node.chainman->m_blockman.IsPruneMode()) {
LOCK(node.chainman->GetMutex());
const CBlockIndex* current_tip{node.chainman->ActiveChain().Tip()};
const CBlockIndex* first_block{node.chainman->m_blockman.GetFirstBlock(*current_tip, /*status_mask=*/BLOCK_HAVE_MASK)};
if (first_block->nHeight > target_index->nHeight) {
throw JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height since necessary block data is already pruned.");
}
}
// Suspend network activity for the duration of the process when we are
// rolling back the chain to get a utxo set from a past height. We do
// this so we don't punish peers that send us that send us data that
// seems wrong in this temporary state. For example a normal new block
// would be classified as a block connecting an invalid block.
disable_network = std::make_unique<NetworkDisable>(connman);
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(target_index));
InvalidateBlock(*node.chainman, invalidate_index->GetBlockHash());
}
Chainstate* chainstate;
std::unique_ptr<CCoinsViewCursor> cursor;
CCoinsStats stats;
UniValue result;
UniValue error;
{
// Lock the chainstate before calling PrepareUtxoSnapshot, to be able
// to get a UTXO database cursor while the chain is pointing at the
// target block. After that, release the lock while calling
// WriteUTXOSnapshot. The cursor will remain valid and be used by
// WriteUTXOSnapshot to write a consistent snapshot even if the
// chainstate changes.
LOCK(node.chainman->GetMutex());
chainstate = &node.chainman->ActiveChainstate();
// In case there is any issue with a block being read from disk we need
// to stop here, otherwise the dump could still be created for the wrong
// height.
// The new tip could also not be the target block if we have a stale
// sister block of invalidate_index. This block (or a descendant) would
// be activated as the new tip and we would not get to new_tip_index.
if (target_index != chainstate->m_chain.Tip()) {
LogInfo("Failed to roll back to requested height, reverting to tip.\n");
error = JSONRPCError(RPC_MISC_ERROR, "Could not roll back to requested height.");
} else {
std::tie(cursor, stats, tip) = PrepareUTXOSnapshot(*chainstate, node.rpc_interruption_point);
}
}
if (error.isNull()) {
result = WriteUTXOSnapshot(*chainstate, cursor.get(), &stats, tip, afile, path, temppath, node.rpc_interruption_point);
fs::rename(temppath, path);
}
if (invalidate_index) {
ReconsiderBlock(*node.chainman, invalidate_index->GetBlockHash());
}
if (!error.isNull()) {
throw error;
}
result.pushKV("path", path.utf8string());
return result;
@ -2701,12 +2841,10 @@ static RPCHelpMan dumptxoutset()
};
}
UniValue CreateUTXOSnapshot(
NodeContext& node,
std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex*>
PrepareUTXOSnapshot(
Chainstate& chainstate,
AutoFile& afile,
const fs::path& path,
const fs::path& temppath)
const std::function<void()>& interruption_point)
{
std::unique_ptr<CCoinsViewCursor> pcursor;
std::optional<CCoinsStats> maybe_stats;
@ -2716,7 +2854,7 @@ UniValue CreateUTXOSnapshot(
// We need to lock cs_main to ensure that the coinsdb isn't written to
// between (i) flushing coins cache to disk (coinsdb), (ii) getting stats
// based upon the coinsdb, and (iii) constructing a cursor to the
// coinsdb for use below this block.
// coinsdb for use in WriteUTXOSnapshot.
//
// Cursors returned by leveldb iterate over snapshots, so the contents
// of the pcursor will not be affected by simultaneous writes during
@ -2725,11 +2863,11 @@ UniValue CreateUTXOSnapshot(
// See discussion here:
// https://github.com/bitcoin/bitcoin/pull/15606#discussion_r274479369
//
LOCK(::cs_main);
AssertLockHeld(::cs_main);
chainstate.ForceFlushStateToDisk();
maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, CoinStatsHashType::HASH_SERIALIZED, node.rpc_interruption_point);
maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, CoinStatsHashType::HASH_SERIALIZED, interruption_point);
if (!maybe_stats) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
}
@ -2738,6 +2876,19 @@ UniValue CreateUTXOSnapshot(
tip = CHECK_NONFATAL(chainstate.m_blockman.LookupBlockIndex(maybe_stats->hashBlock));
}
return {std::move(pcursor), *CHECK_NONFATAL(maybe_stats), tip};
}
UniValue WriteUTXOSnapshot(
Chainstate& chainstate,
CCoinsViewCursor* pcursor,
CCoinsStats* maybe_stats,
const CBlockIndex* tip,
AutoFile& afile,
const fs::path& path,
const fs::path& temppath,
const std::function<void()>& interruption_point)
{
LOG_TIME_SECONDS(strprintf("writing UTXO snapshot at height %s (%s) to file %s (via %s)",
tip->nHeight, tip->GetBlockHash().ToString(),
fs::PathToString(path), fs::PathToString(temppath)));
@ -2773,7 +2924,7 @@ UniValue CreateUTXOSnapshot(
pcursor->GetKey(key);
last_hash = key.hash;
while (pcursor->Valid()) {
if (iter % 5000 == 0) node.rpc_interruption_point();
if (iter % 5000 == 0) interruption_point();
++iter;
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
if (key.hash != last_hash) {
@ -2804,6 +2955,17 @@ UniValue CreateUTXOSnapshot(
return result;
}
UniValue CreateUTXOSnapshot(
node::NodeContext& node,
Chainstate& chainstate,
AutoFile& afile,
const fs::path& path,
const fs::path& tmppath)
{
auto [cursor, stats, tip]{WITH_LOCK(::cs_main, return PrepareUTXOSnapshot(chainstate, node.rpc_interruption_point))};
return WriteUTXOSnapshot(chainstate, cursor.get(), &stats, tip, afile, path, tmppath, node.rpc_interruption_point);
}
static RPCHelpMan loadtxoutset()
{
return RPCHelpMan{

View file

@ -48,7 +48,7 @@ UniValue blockheaderToJSON(const CBlockIndex& tip, const CBlockIndex& blockindex
void CalculatePercentilesByWeight(CAmount result[NUM_GETBLOCKSTATS_PERCENTILES], std::vector<std::pair<CAmount, int64_t>>& scores, int64_t total_weight);
/**
* Helper to create UTXO snapshots given a chainstate and a file handle.
* Test-only helper to create UTXO snapshots given a chainstate and a file handle.
* @return a UniValue map containing metadata about the snapshot.
*/
UniValue CreateUTXOSnapshot(

View file

@ -187,6 +187,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "gettxoutproof", 0, "txids" },
{ "gettxoutsetinfo", 1, "hash_or_height" },
{ "gettxoutsetinfo", 2, "use_index"},
{ "dumptxoutset", 2, "options" },
{ "dumptxoutset", 2, "rollback" },
{ "lockunspent", 0, "unlock" },
{ "lockunspent", 1, "transactions" },
{ "lockunspent", 2, "persistent" },

View file

@ -914,7 +914,7 @@ private:
//! Internal helper for ActivateSnapshot().
//!
//! De-serialization of a snapshot that is created with
//! CreateUTXOSnapshot() in rpc/blockchain.cpp.
//! the dumptxoutset RPC.
//! To reduce space the serialization format of the snapshot avoids
//! duplication of tx hashes. The code takes advantage of the guarantee by
//! leveldb that keys are lexicographically sorted.

View file

@ -22,6 +22,7 @@ from test_framework.util import (
assert_approx,
assert_equal,
assert_raises_rpc_error,
sha256sum_file,
)
from test_framework.wallet import (
getnewdestination,
@ -295,7 +296,7 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(n1.getblockcount(), START_HEIGHT)
self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
dump_output = n0.dumptxoutset('utxos.dat')
dump_output = n0.dumptxoutset('utxos.dat', "latest")
self.log.info("Test loading snapshot when the node tip is on the same block as the snapshot")
assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT)
@ -320,12 +321,16 @@ class AssumeutxoTest(BitcoinTestFramework):
for n in self.nodes:
assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT)
assert_equal(
dump_output['txoutset_hash'],
"a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27")
assert_equal(dump_output["nchaintx"], blocks[SNAPSHOT_BASE_HEIGHT].chain_tx)
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
def check_dump_output(output):
assert_equal(
output['txoutset_hash'],
"a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27")
assert_equal(output["nchaintx"], blocks[SNAPSHOT_BASE_HEIGHT].chain_tx)
check_dump_output(dump_output)
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
# will allow us to test n1's sync-to-tip on top of a snapshot.
self.generate(n0, nblocks=100, sync_fun=self.no_op)
@ -335,6 +340,39 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT)
self.log.info(f"Check that dumptxoutset works for past block heights")
# rollback defaults to the snapshot base height
dump_output2 = n0.dumptxoutset('utxos2.dat', "rollback")
check_dump_output(dump_output2)
assert_equal(sha256sum_file(dump_output['path']), sha256sum_file(dump_output2['path']))
# Rollback with specific height
dump_output3 = n0.dumptxoutset('utxos3.dat', rollback=SNAPSHOT_BASE_HEIGHT)
check_dump_output(dump_output3)
assert_equal(sha256sum_file(dump_output['path']), sha256sum_file(dump_output3['path']))
# Specified height that is not a snapshot height
prev_snap_height = SNAPSHOT_BASE_HEIGHT - 1
dump_output4 = n0.dumptxoutset(path='utxos4.dat', rollback=prev_snap_height)
assert_equal(
dump_output4['txoutset_hash'],
"8a1db0d6e958ce0d7c963bc6fc91ead596c027129bacec68acc40351037b09d7")
assert sha256sum_file(dump_output['path']) != sha256sum_file(dump_output4['path'])
# Use a hash instead of a height
prev_snap_hash = n0.getblockhash(prev_snap_height)
dump_output5 = n0.dumptxoutset('utxos5.dat', rollback=prev_snap_hash)
assert_equal(sha256sum_file(dump_output4['path']), sha256sum_file(dump_output5['path']))
# TODO: This is a hack to set m_best_header to the correct value after
# dumptxoutset/reconsiderblock. Otherwise the wrong error messages are
# returned in following tests. It can be removed once this bug is
# fixed. See also https://github.com/bitcoin/bitcoin/issues/26245
self.restart_node(0, ["-reindex"])
# Ensure n0 is back at the tip
assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT)
self.test_snapshot_with_less_work(dump_output['path'])
self.test_invalid_mempool_state(dump_output['path'])
self.test_invalid_snapshot_scenarios(dump_output['path'])

View file

@ -27,7 +27,7 @@ class DumptxoutsetTest(BitcoinTestFramework):
self.generate(node, COINBASE_MATURITY)
FILENAME = 'txoutset.dat'
out = node.dumptxoutset(FILENAME)
out = node.dumptxoutset(FILENAME, "latest")
expected_path = node.datadir_path / self.chain / FILENAME
assert expected_path.is_file()
@ -51,10 +51,14 @@ class DumptxoutsetTest(BitcoinTestFramework):
# Specifying a path to an existing or invalid file will fail.
assert_raises_rpc_error(
-8, '{} already exists'.format(FILENAME), node.dumptxoutset, FILENAME)
-8, '{} already exists'.format(FILENAME), node.dumptxoutset, FILENAME, "latest")
invalid_path = node.datadir_path / "invalid" / "path"
assert_raises_rpc_error(
-8, "Couldn't open file {}.incomplete for writing".format(invalid_path), node.dumptxoutset, invalid_path)
-8, "Couldn't open file {}.incomplete for writing".format(invalid_path), node.dumptxoutset, invalid_path, "latest")
self.log.info(f"Test that dumptxoutset with unknown dump type fails")
assert_raises_rpc_error(
-8, 'Invalid snapshot type "bogus" specified. Please specify "rollback" or "latest"', node.dumptxoutset, 'utxos.dat', "bogus")
if __name__ == '__main__':

View file

@ -93,7 +93,7 @@ class AssumeutxoTest(BitcoinTestFramework):
self.log.info(
f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}")
dump_output = n0.dumptxoutset('utxos.dat')
dump_output = n0.dumptxoutset('utxos.dat', "latest")
assert_equal(
dump_output['txoutset_hash'],