Skip to content

Commit b05b281

Browse files
committed
Merge #16899: UTXO snapshot creation (dumptxoutset)
92b2f53 test: add dumptxoutset RPC test (James O'Beirne) c1ccbc3 devtools: add utxo_snapshot.sh (James O'Beirne) 57cf74c rpc: add dumptxoutset (James O'Beirne) 92fafb3 coinstats: add coins_count (James O'Beirne) 707fde7 add unused SnapshotMetadata class (James O'Beirne) Pull request description: This is part of the [assumeutxo project](https://github.com/bitcoin/bitcoin/projects/11): Parent PR: #15606 Issue: #15605 Specification: https://github.com/jamesob/assumeutxo-docs/tree/master/proposal --- This changeset defines the serialization format for UTXO snapshots and adds an RPC command for creating them, `dumptxoutset`. It also adds a convenience script for generating and verifying snapshots at a certain height, since that requires doing a hacky rewind of the chain via `invalidateblock`. All of this is unused at the moment. ACKs for top commit: laanwj: ACK 92b2f53 Tree-SHA512: 200dff87767f157d627e99506ec543465d9329860a6cd49363081619c437163a640a46d008faa92b1f44fd403bfc7a7c9e851c658b5a4849efa9a34ca976bf31
2 parents d35b121 + 92b2f53 commit b05b281

File tree

9 files changed

+270
-10
lines changed

9 files changed

+270
-10
lines changed

contrib/devtools/utxo_snapshot.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Copyright (c) 2019 The Bitcoin Core developers
4+
# Distributed under the MIT software license, see the accompanying
5+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
6+
#
7+
export LC_ALL=C
8+
9+
set -ueo pipefail
10+
11+
if (( $# < 3 )); then
12+
echo 'Usage: utxo_snapshot.sh <generate-at-height> <snapshot-out-path> <bitcoin-cli-call ...>'
13+
echo
14+
echo " if <snapshot-out-path> is '-', don't produce a snapshot file but instead print the "
15+
echo " expected assumeutxo hash"
16+
echo
17+
echo 'Examples:'
18+
echo
19+
echo " ./contrib/devtools/utxo_snapshot.sh 570000 utxo.dat ./src/bitcoin-cli -datadir=\$(pwd)/testdata"
20+
echo ' ./contrib/devtools/utxo_snapshot.sh 570000 - ./src/bitcoin-cli'
21+
exit 1
22+
fi
23+
24+
GENERATE_AT_HEIGHT="${1}"; shift;
25+
OUTPUT_PATH="${1}"; shift;
26+
# Most of the calls we make take a while to run, so pad with a lengthy timeout.
27+
BITCOIN_CLI_CALL="${*} -rpcclienttimeout=9999999"
28+
29+
# Block we'll invalidate/reconsider to rewind/fast-forward the chain.
30+
PIVOT_BLOCKHASH=$($BITCOIN_CLI_CALL getblockhash $(( GENERATE_AT_HEIGHT + 1 )) )
31+
32+
(>&2 echo "Rewinding chain back to height ${GENERATE_AT_HEIGHT} (by invalidating ${PIVOT_BLOCKHASH}); this may take a while")
33+
${BITCOIN_CLI_CALL} invalidateblock "${PIVOT_BLOCKHASH}"
34+
35+
if [[ "${OUTPUT_PATH}" = "-" ]]; then
36+
(>&2 echo "Generating txoutset info...")
37+
${BITCOIN_CLI_CALL} gettxoutsetinfo | grep hash_serialized_2 | sed 's/^.*: "\(.\+\)\+",/\1/g'
38+
else
39+
(>&2 echo "Generating UTXO snapshot...")
40+
${BITCOIN_CLI_CALL} dumptxoutset "${OUTPUT_PATH}"
41+
fi
42+
43+
(>&2 echo "Restoring chain to original height; this may take a while")
44+
${BITCOIN_CLI_CALL} reconsiderblock "${PIVOT_BLOCKHASH}"

src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ BITCOIN_CORE_H = \
161161
node/context.h \
162162
node/psbt.h \
163163
node/transaction.h \
164+
node/utxo_snapshot.h \
164165
noui.h \
165166
optional.h \
166167
outputtype.h \

src/node/coinstats.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ static void ApplyStats(CCoinsStats &stats, CHashWriter& ss, const uint256& hash,
3838
//! Calculate statistics about the unspent transaction output set
3939
bool GetUTXOStats(CCoinsView *view, CCoinsStats &stats)
4040
{
41+
stats = CCoinsStats();
4142
std::unique_ptr<CCoinsViewCursor> pcursor(view->Cursor());
4243
assert(pcursor);
4344

@@ -61,6 +62,7 @@ bool GetUTXOStats(CCoinsView *view, CCoinsStats &stats)
6162
}
6263
prevkey = key.hash;
6364
outputs[key.n] = std::move(coin);
65+
stats.coins_count++;
6466
} else {
6567
return error("%s: unable to read value", __func__);
6668
}

src/node/coinstats.h

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ class CCoinsView;
1515

1616
struct CCoinsStats
1717
{
18-
int nHeight;
19-
uint256 hashBlock;
20-
uint64_t nTransactions;
21-
uint64_t nTransactionOutputs;
22-
uint64_t nBogoSize;
23-
uint256 hashSerialized;
24-
uint64_t nDiskSize;
25-
CAmount nTotalAmount;
26-
27-
CCoinsStats() : nHeight(0), nTransactions(0), nTransactionOutputs(0), nBogoSize(0), nDiskSize(0), nTotalAmount(0) {}
18+
int nHeight{0};
19+
uint256 hashBlock{};
20+
uint64_t nTransactions{0};
21+
uint64_t nTransactionOutputs{0};
22+
uint64_t nBogoSize{0};
23+
uint256 hashSerialized{};
24+
uint64_t nDiskSize{0};
25+
CAmount nTotalAmount{0};
26+
27+
//! The number of coins contained.
28+
uint64_t coins_count{0};
2829
};
2930

3031
//! Calculate statistics about the unspent transaction output set

src/node/utxo_snapshot.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2009-2010 Satoshi Nakamoto
2+
// Copyright (c) 2009-2019 The Bitcoin Core developers
3+
// Distributed under the MIT software license, see the accompanying
4+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
#ifndef BITCOIN_NODE_UTXO_SNAPSHOT_H
7+
#define BITCOIN_NODE_UTXO_SNAPSHOT_H
8+
9+
#include <uint256.h>
10+
#include <serialize.h>
11+
12+
//! Metadata describing a serialized version of a UTXO set from which an
13+
//! assumeutxo CChainState can be constructed.
14+
class SnapshotMetadata
15+
{
16+
public:
17+
//! The hash of the block that reflects the tip of the chain for the
18+
//! UTXO set contained in this snapshot.
19+
uint256 m_base_blockhash;
20+
21+
//! The number of coins in the UTXO set contained in this snapshot. Used
22+
//! during snapshot load to estimate progress of UTXO set reconstruction.
23+
uint64_t m_coins_count = 0;
24+
25+
//! Necessary to "fake" the base nChainTx so that we can estimate progress during
26+
//! initial block download for the assumeutxo chainstate.
27+
unsigned int m_nchaintx = 0;
28+
29+
SnapshotMetadata() { }
30+
SnapshotMetadata(
31+
const uint256& base_blockhash,
32+
uint64_t coins_count,
33+
unsigned int nchaintx) :
34+
m_base_blockhash(base_blockhash),
35+
m_coins_count(coins_count),
36+
m_nchaintx(nchaintx) { }
37+
38+
ADD_SERIALIZE_METHODS;
39+
40+
template <typename Stream, typename Operation>
41+
inline void SerializationOp(Stream& s, Operation ser_action)
42+
{
43+
READWRITE(m_base_blockhash);
44+
READWRITE(m_coins_count);
45+
READWRITE(m_nchaintx);
46+
}
47+
48+
};
49+
50+
#endif // BITCOIN_NODE_UTXO_SNAPSHOT_H

src/rpc/blockchain.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <hash.h>
1616
#include <index/blockfilterindex.h>
1717
#include <node/coinstats.h>
18+
#include <node/utxo_snapshot.h>
1819
#include <policy/feerate.h>
1920
#include <policy/policy.h>
2021
#include <policy/rbf.h>
@@ -2245,6 +2246,113 @@ static UniValue getblockfilter(const JSONRPCRequest& request)
22452246
return ret;
22462247
}
22472248

2249+
/**
2250+
* Serialize the UTXO set to a file for loading elsewhere.
2251+
*
2252+
* @see SnapshotMetadata
2253+
*/
2254+
UniValue dumptxoutset(const JSONRPCRequest& request)
2255+
{
2256+
RPCHelpMan{
2257+
"dumptxoutset",
2258+
"\nWrite the serialized UTXO set to disk.\n"
2259+
"Incidentally flushes the latest coinsdb (leveldb) to disk.\n",
2260+
{
2261+
{"path",
2262+
RPCArg::Type::STR,
2263+
RPCArg::Optional::NO,
2264+
/* default_val */ "",
2265+
"path to the output file. If relative, will be prefixed by datadir."},
2266+
},
2267+
RPCResult{
2268+
"{\n"
2269+
" \"coins_written\": n, (numeric) the number of coins written in the snapshot\n"
2270+
" \"base_hash\": \"...\", (string) the hash of the base of the snapshot\n"
2271+
" \"base_height\": n, (string) the height of the base of the snapshot\n"
2272+
" \"path\": \"...\" (string) the absolute path that the snapshot was written to\n"
2273+
"]\n"
2274+
},
2275+
RPCExamples{
2276+
HelpExampleCli("dumptxoutset", "utxo.dat")
2277+
}
2278+
}.Check(request);
2279+
2280+
fs::path path = fs::absolute(request.params[0].get_str(), GetDataDir());
2281+
// Write to a temporary path and then move into `path` on completion
2282+
// to avoid confusion due to an interruption.
2283+
fs::path temppath = fs::absolute(request.params[0].get_str() + ".incomplete", GetDataDir());
2284+
2285+
if (fs::exists(path)) {
2286+
throw JSONRPCError(
2287+
RPC_INVALID_PARAMETER,
2288+
path.string() + " already exists. If you are sure this is what you want, "
2289+
"move it out of the way first");
2290+
}
2291+
2292+
FILE* file{fsbridge::fopen(temppath, "wb")};
2293+
CAutoFile afile{file, SER_DISK, CLIENT_VERSION};
2294+
std::unique_ptr<CCoinsViewCursor> pcursor;
2295+
CCoinsStats stats;
2296+
CBlockIndex* tip;
2297+
2298+
{
2299+
// We need to lock cs_main to ensure that the coinsdb isn't written to
2300+
// between (i) flushing coins cache to disk (coinsdb), (ii) getting stats
2301+
// based upon the coinsdb, and (iii) constructing a cursor to the
2302+
// coinsdb for use below this block.
2303+
//
2304+
// Cursors returned by leveldb iterate over snapshots, so the contents
2305+
// of the pcursor will not be affected by simultaneous writes during
2306+
// use below this block.
2307+
//
2308+
// See discussion here:
2309+
// https://github.com/bitcoin/bitcoin/pull/15606#discussion_r274479369
2310+
//
2311+
LOCK(::cs_main);
2312+
2313+
::ChainstateActive().ForceFlushStateToDisk();
2314+
2315+
if (!GetUTXOStats(&::ChainstateActive().CoinsDB(), stats)) {
2316+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
2317+
}
2318+
2319+
pcursor = std::unique_ptr<CCoinsViewCursor>(::ChainstateActive().CoinsDB().Cursor());
2320+
tip = LookupBlockIndex(stats.hashBlock);
2321+
CHECK_NONFATAL(tip);
2322+
}
2323+
2324+
SnapshotMetadata metadata{tip->GetBlockHash(), stats.coins_count, tip->nChainTx};
2325+
2326+
afile << metadata;
2327+
2328+
COutPoint key;
2329+
Coin coin;
2330+
unsigned int iter{0};
2331+
2332+
while (pcursor->Valid()) {
2333+
if (iter % 5000 == 0 && !IsRPCRunning()) {
2334+
throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Shutting down");
2335+
}
2336+
++iter;
2337+
if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
2338+
afile << key;
2339+
afile << coin;
2340+
}
2341+
2342+
pcursor->Next();
2343+
}
2344+
2345+
afile.fclose();
2346+
fs::rename(temppath, path);
2347+
2348+
UniValue result(UniValue::VOBJ);
2349+
result.pushKV("coins_written", stats.coins_count);
2350+
result.pushKV("base_hash", tip->GetBlockHash().ToString());
2351+
result.pushKV("base_height", tip->nHeight);
2352+
result.pushKV("path", path.string());
2353+
return result;
2354+
}
2355+
22482356
// clang-format off
22492357
static const CRPCCommand commands[] =
22502358
{ // category name actor (function) argNames
@@ -2281,6 +2389,7 @@ static const CRPCCommand commands[] =
22812389
{ "hidden", "waitforblock", &waitforblock, {"blockhash","timeout"} },
22822390
{ "hidden", "waitforblockheight", &waitforblockheight, {"height","timeout"} },
22832391
{ "hidden", "syncwithvalidationinterfacequeue", &syncwithvalidationinterfacequeue, {} },
2392+
{ "hidden", "dumptxoutset", &dumptxoutset, {"path"} },
22842393
};
22852394
// clang-format on
22862395

src/validation.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <txmempool.h> // For CTxMemPool::cs
2222
#include <txdb.h>
2323
#include <versionbits.h>
24+
#include <serialize.h>
2425

2526
#include <algorithm>
2627
#include <atomic>

test/functional/rpc_dumptxoutset.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2019 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test the generation of UTXO snapshots using `dumptxoutset`.
6+
"""
7+
from test_framework.test_framework import BitcoinTestFramework
8+
from test_framework.util import assert_equal, assert_raises_rpc_error
9+
10+
import hashlib
11+
from pathlib import Path
12+
13+
14+
class DumptxoutsetTest(BitcoinTestFramework):
15+
def set_test_params(self):
16+
self.setup_clean_chain = True
17+
self.num_nodes = 1
18+
19+
def run_test(self):
20+
"""Test a trivial usage of the dumptxoutset RPC command."""
21+
node = self.nodes[0]
22+
mocktime = node.getblockheader(node.getblockhash(0))['time'] + 1
23+
node.setmocktime(mocktime)
24+
node.generate(100)
25+
26+
FILENAME = 'txoutset.dat'
27+
out = node.dumptxoutset(FILENAME)
28+
expected_path = Path(node.datadir) / 'regtest' / FILENAME
29+
30+
assert expected_path.is_file()
31+
32+
assert_equal(out['coins_written'], 100)
33+
assert_equal(out['base_height'], 100)
34+
assert_equal(out['path'], str(expected_path))
35+
# Blockhash should be deterministic based on mocked time.
36+
assert_equal(
37+
out['base_hash'],
38+
'6fd417acba2a8738b06fee43330c50d58e6a725046c3d843c8dd7e51d46d1ed6')
39+
40+
with open(str(expected_path), 'rb') as f:
41+
digest = hashlib.sha256(f.read()).hexdigest()
42+
# UTXO snapshot hash should be deterministic based on mocked time.
43+
assert_equal(
44+
digest, 'be032e5f248264ba08e11099ac09dbd001f6f87ffc68bf0f87043d8146d50664')
45+
46+
# Specifying a path to an existing file will fail.
47+
assert_raises_rpc_error(
48+
-8, '{} already exists'.format(FILENAME), node.dumptxoutset, FILENAME)
49+
50+
if __name__ == '__main__':
51+
DumptxoutsetTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@
191191
'rpc_uptime.py',
192192
'wallet_resendwallettransactions.py',
193193
'wallet_fallbackfee.py',
194+
'rpc_dumptxoutset.py',
194195
'feature_minchainwork.py',
195196
'rpc_getblockstats.py',
196197
'wallet_create_tx.py',

0 commit comments

Comments
 (0)