Skip to content

Commit 72f3476

Browse files
committed
Create createhintfile RPC
1 parent ce4d1aa commit 72f3476

4 files changed

Lines changed: 291 additions & 0 deletions

File tree

contrib/ibd-booster-hints-gen/booster-gen.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def main():
9595
# TODO: make node_datadir arg optional and set it to this if none is set:
9696
#datadir = Path.home() / ".bitcoin"
9797
datadir = Path(args.node_datadir)
98+
print(f"Using node datadir: {datadir}")
9899
print("Loading chain manager... ", end='', flush=True)
99100
chainman = pbk.load_chainman(datadir, chaintype)
100101
print("done.")
@@ -108,6 +109,7 @@ def main():
108109

109110
for block_height in range(0, snapshot_height+1):
110111
start_time = time.time()
112+
print(f"Processing block {block_height}... ", end='', flush=True)
111113
block_index = chainman.get_block_index_from_height(block_height)
112114
block_data = chainman.read_block_from_disk(block_index).data
113115
block = from_binary(CBlock, block_data)
@@ -142,6 +144,8 @@ def main():
142144
outputs_bitmap.extend(outputs_bitmap_extended)
143145
#print(f"bitmap creation took {(time.time()-t3):.3f}s")
144146

147+
print(f"outputs_bitmap: {outputs_bitmap}")
148+
145149
hints_writer.write_block_bits(outputs_bitmap)
146150
took_time = time.time() - start_time
147151
if args.verbose:

src/rpc/blockchain.cpp

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
#include <stdint.h>
5656

5757
#include <condition_variable>
58+
#include <fstream>
5859
#include <iterator>
5960
#include <memory>
6061
#include <mutex>
@@ -2945,6 +2946,288 @@ class TemporaryRollback
29452946
};
29462947
};
29472948

2949+
class HintsWriter {
2950+
std::ostream& file;
2951+
public:
2952+
explicit HintsWriter(std::ostream& f) : file(f) {}
2953+
2954+
// Writes a block's bits: writes 2 little-endian bytes for the size,
2955+
// then packs the bits 8 per byte.
2956+
void writeBlockBits(const std::vector<bool>& bitmap) {
2957+
std::string writebuf;
2958+
// Write the bitmap size as a 16-bit little-endian value.
2959+
uint16_t size = static_cast<uint16_t>(bitmap.size());
2960+
writebuf.push_back(static_cast<char>(size & 0xFF)); // lower byte
2961+
writebuf.push_back(static_cast<char>((size >> 8) & 0xFF)); // higher byte
2962+
2963+
uint8_t value = 0;
2964+
int bitpos = 0;
2965+
// Process each bit in the bitmap.
2966+
for (bool bit : bitmap) {
2967+
// Set the corresponding bit in 'value' if 'bit' is true.
2968+
if (bit) {
2969+
value |= (1 << bitpos);
2970+
}
2971+
++bitpos;
2972+
// Once we've accumulated 8 bits, append them to writebuf.
2973+
if (bitpos == 8) {
2974+
writebuf.push_back(static_cast<char>(value));
2975+
bitpos = 0;
2976+
value = 0;
2977+
}
2978+
}
2979+
// If there are remaining bits (less than 8), write them too.
2980+
if (bitpos != 0) {
2981+
writebuf.push_back(static_cast<char>(value));
2982+
}
2983+
2984+
// Write the entire buffer to the file.
2985+
file.write(writebuf.c_str(), writebuf.size());
2986+
}
2987+
2988+
// Writes a two-byte end marker (two 0 bytes).
2989+
void writeEndMarker() {
2990+
char marker[2] = {0, 0};
2991+
file.write(marker, 2);
2992+
}
2993+
};
2994+
2995+
2996+
static bool isOutputInUTXOSet(/*const CCoinsViewCache& coins, */NodeContext& node, const COutPoint& outpoint)
2997+
{
2998+
LOCK(::cs_main);
2999+
CCoinsViewCache& chain_view = node.chainman->ActiveChainstate().CoinsTip();
3000+
// std::optional<Coin> coin = coins.GetCoin(outpoint);
3001+
// auto coin{chain_view.GetCoin(outpoint)};
3002+
// if (!coin) {
3003+
// return false;
3004+
// } // check if the coin exists
3005+
// return !coin->IsSpent();
3006+
return chain_view.HaveCoin(outpoint);
3007+
}
3008+
3009+
/**
3010+
* Serialize the UTXO set to a file for loading elsewhere.
3011+
*
3012+
* @see SnapshotMetadata
3013+
*/
3014+
static RPCHelpMan createhintfile()
3015+
{
3016+
return RPCHelpMan{
3017+
"createhintfile",
3018+
"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"
3019+
"Unless the \"latest\" type is requested, the node will roll back to the requested height and network activity will be suspended during this process. "
3020+
"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"
3021+
"This call may take several minutes. Make sure to use no RPC timeout (bitcoin-cli -rpcclienttimeout=0)",
3022+
{
3023+
{"path", RPCArg::Type::STR, RPCArg::Optional::NO, "Path to the output file. If relative, will be prefixed by datadir."},
3024+
{"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 can currently be loaded with loadtxoutset."},
3025+
{"options", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "",
3026+
{
3027+
{"rollback", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
3028+
"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.",
3029+
RPCArgOptions{.skip_type_check = true, .type_str = {"", "string or numeric"}}},
3030+
},
3031+
},
3032+
},
3033+
RPCResult{
3034+
RPCResult::Type::OBJ, "", "",
3035+
{
3036+
// {RPCResult::Type::NUM, "coins_written", "the number of coins written in the snapshot"},
3037+
// {RPCResult::Type::STR_HEX, "base_hash", "the hash of the base of the snapshot"},
3038+
// {RPCResult::Type::NUM, "base_height", "the height of the base of the snapshot"},
3039+
{RPCResult::Type::STR, "path", "the absolute path that the snapshot was written to"},
3040+
// {RPCResult::Type::STR_HEX, "txoutset_hash", "the hash of the UTXO set contents"},
3041+
// {RPCResult::Type::NUM, "nchaintx", "the number of transactions in the chain up to and including the base block"},
3042+
}
3043+
},
3044+
RPCExamples{
3045+
HelpExampleCli("-rpcclienttimeout=0 createhintfile", "utxo.dat latest") +
3046+
HelpExampleCli("-rpcclienttimeout=0 createhintfile", "utxo.dat rollback") +
3047+
HelpExampleCli("-rpcclienttimeout=0 -named createhintfile", R"(utxo.dat rollback=853456)")
3048+
},
3049+
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
3050+
{
3051+
NodeContext& node = EnsureAnyNodeContext(request.context);
3052+
const CBlockIndex* tip{WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Tip())};
3053+
const CBlockIndex* target_index{nullptr};
3054+
const std::string snapshot_type{self.Arg<std::string>("type")};
3055+
const UniValue options{request.params[2].isNull() ? UniValue::VOBJ : request.params[2]};
3056+
if (options.exists("rollback")) {
3057+
if (!snapshot_type.empty() && snapshot_type != "rollback") {
3058+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified with rollback option", snapshot_type));
3059+
}
3060+
target_index = ParseHashOrHeight(options["rollback"], *node.chainman);
3061+
} else if (snapshot_type == "rollback") {
3062+
auto snapshot_heights = node.chainman->GetParams().GetAvailableSnapshotHeights();
3063+
CHECK_NONFATAL(snapshot_heights.size() > 0);
3064+
auto max_height = std::max_element(snapshot_heights.begin(), snapshot_heights.end());
3065+
target_index = ParseHashOrHeight(*max_height, *node.chainman);
3066+
} else if (snapshot_type == "latest") {
3067+
target_index = tip;
3068+
} else {
3069+
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid snapshot type \"%s\" specified. Please specify \"rollback\" or \"latest\"", snapshot_type));
3070+
}
3071+
3072+
const ArgsManager& args{EnsureAnyArgsman(request.context)};
3073+
const fs::path path = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str()));
3074+
// Write to a temporary path and then move into `path` on completion
3075+
// to avoid confusion due to an interruption.
3076+
const fs::path temppath = fsbridge::AbsPathJoin(args.GetDataDirNet(), fs::u8path(request.params[0].get_str() + ".incomplete"));
3077+
3078+
if (fs::exists(path)) {
3079+
throw JSONRPCError(
3080+
RPC_INVALID_PARAMETER,
3081+
path.utf8string() + " already exists. If you are sure this is what you want, "
3082+
"move it out of the way first");
3083+
}
3084+
3085+
FILE* file{fsbridge::fopen(temppath, "wb")};
3086+
AutoFile afile{file};
3087+
if (afile.IsNull()) {
3088+
throw JSONRPCError(
3089+
RPC_INVALID_PARAMETER,
3090+
"Couldn't open file " + temppath.utf8string() + " for writing.");
3091+
}
3092+
3093+
CConnman& connman = EnsureConnman(node);
3094+
const CBlockIndex* invalidate_index{nullptr};
3095+
std::optional<NetworkDisable> disable_network;
3096+
std::optional<TemporaryRollback> temporary_rollback;
3097+
3098+
// If the user wants to dump the txoutset of the current tip, we don't have
3099+
// to roll back at all
3100+
if (target_index != tip) {
3101+
// If the node is running in pruned mode we ensure all necessary block
3102+
// data is available before starting to roll back.
3103+
if (node.chainman->m_blockman.IsPruneMode()) {
3104+
LOCK(node.chainman->GetMutex());
3105+
const CBlockIndex* current_tip{node.chainman->ActiveChain().Tip()};
3106+
const CBlockIndex* first_block{node.chainman->m_blockman.GetFirstBlock(*current_tip, /*status_mask=*/BLOCK_HAVE_MASK)};
3107+
if (first_block->nHeight > target_index->nHeight) {
3108+
throw JSONRPCError(RPC_MISC_ERROR, "Hint files cannot be generated in prune mode.");
3109+
}
3110+
}
3111+
3112+
// Suspend network activity for the duration of the process when we are
3113+
// rolling back the chain to get a utxo set from a past height. We do
3114+
// this so we don't punish peers that send us that send us data that
3115+
// seems wrong in this temporary state. For example a normal new block
3116+
// would be classified as a block connecting an invalid block.
3117+
// Skip if the network is already disabled because this
3118+
// automatically re-enables the network activity at the end of the
3119+
// process which may not be what the user wants.
3120+
if (connman.GetNetworkActive()) {
3121+
disable_network.emplace(connman);
3122+
}
3123+
3124+
invalidate_index = WITH_LOCK(::cs_main, return node.chainman->ActiveChain().Next(target_index));
3125+
temporary_rollback.emplace(*node.chainman, *invalidate_index);
3126+
}
3127+
3128+
Chainstate* chainstate;
3129+
std::unique_ptr<CCoinsViewCursor> cursor;
3130+
3131+
int block_height = 0;
3132+
3133+
int startTime = 0;
3134+
int start_height = 0;
3135+
uint256 start_block;
3136+
bool start = node.chain->findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, interfaces::FoundBlock().hash(start_block).height(start_height));
3137+
3138+
int iteration = 0;
3139+
3140+
uint256 block_hash = start_block;
3141+
3142+
bool fetch_block{true};
3143+
bool block_still_active = false;
3144+
bool next_block = false;
3145+
uint256 next_block_hash;
3146+
3147+
// FILE* file{fsbridge::fopen(temppath, "wb")};
3148+
std::ofstream outfile(temppath, std::ios::binary);
3149+
HintsWriter writer(outfile);
3150+
3151+
while (!node.chain->shutdownRequested()) {
3152+
3153+
node.chain->findBlock(block_hash, interfaces::FoundBlock().inActiveChain(block_still_active).nextBlock(interfaces::FoundBlock().inActiveChain(next_block).hash(next_block_hash)));
3154+
3155+
if (fetch_block) {
3156+
// Read block data
3157+
CBlock block;
3158+
node.chain->findBlock(block_hash, interfaces::FoundBlock().data(block));
3159+
3160+
if (!block.IsNull()) {
3161+
if (!block_still_active) {
3162+
throw JSONRPCError(RPC_MISC_ERROR, strprintf("Hint files cannot be generated because the block_hash %s is inactive", block_hash.GetHex()));
3163+
}
3164+
}
3165+
3166+
std::vector<bool> block_outputs_bitmap(block.vtx.size(), false);
3167+
3168+
// std::cout << "Processing block " << block_hash.GetHex() << " at height " << block_height << std::endl;
3169+
3170+
for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
3171+
// auto vtx = block.vtx[posInBlock];
3172+
// vtx->vout
3173+
// SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast<int>(posInBlock)}, fUpdate, /*rescanning_old_block=*/true);
3174+
3175+
3176+
auto vtx = block.vtx[posInBlock];
3177+
// for (const CTxOut& txout : vtx->vout) {
3178+
// COutPoint(txid, i)
3179+
// // isOutputInUTXOSet(txout.
3180+
// }
3181+
std::vector<bool> tx_outputs_bitmap(vtx->vout.size(), false);
3182+
3183+
const Txid& txid = vtx->GetHash();
3184+
for (size_t voutIndex = 0; voutIndex < vtx->vout.size(); ++voutIndex) {
3185+
auto availabeCoin = isOutputInUTXOSet(node, COutPoint(txid, voutIndex));
3186+
3187+
// auto xx = strprintf("txid %s voutIndex %d is available: %s", txid.GetHex(), voutIndex, availabeCoin ? "available" : "spent");
3188+
// std::cout << xx << std::endl;
3189+
tx_outputs_bitmap[voutIndex] = availabeCoin;
3190+
}
3191+
3192+
block_outputs_bitmap.insert(block_outputs_bitmap.end(), tx_outputs_bitmap.begin(), tx_outputs_bitmap.end());
3193+
}
3194+
3195+
writer.writeBlockBits(block_outputs_bitmap);
3196+
}
3197+
3198+
3199+
// std::cout << "Processing block " << block_hash.GetHex() << " at height " << block_height << std::endl;
3200+
// std::cout << "next_block_hash " << next_block_hash.GetHex() << std::endl;
3201+
3202+
3203+
if (block_height >= target_index->nHeight) {
3204+
break;
3205+
}
3206+
3207+
if (!next_block) {
3208+
// break successfully when rescan has reached the tip, or
3209+
// previous block is no longer on the chain due to a reorg
3210+
break;
3211+
}
3212+
3213+
// increment block and verification progress
3214+
block_hash = next_block_hash;
3215+
++block_height;
3216+
3217+
}
3218+
3219+
writer.writeEndMarker();
3220+
outfile.close();
3221+
3222+
fs::rename(temppath, path);
3223+
3224+
UniValue result(UniValue::VOBJ);
3225+
result.pushKV("path", path.utf8string());
3226+
return result;
3227+
},
3228+
};
3229+
}
3230+
29483231
/**
29493232
* Serialize the UTXO set to a file for loading elsewhere.
29503233
*
@@ -3403,6 +3686,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t)
34033686
{"blockchain", &scanblocks},
34043687
{"blockchain", &getdescriptoractivity},
34053688
{"blockchain", &getblockfilter},
3689+
{"blockchain", &createhintfile},
34063690
{"blockchain", &dumptxoutset},
34073691
{"blockchain", &loadtxoutset},
34083692
{"blockchain", &getchainstates},

src/rpc/client.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ static const CRPCConvertParam vRPCConvertParams[] =
190190
{ "gettxoutproof", 0, "txids" },
191191
{ "gettxoutsetinfo", 1, "hash_or_height" },
192192
{ "gettxoutsetinfo", 2, "use_index"},
193+
{ "createhintfile", 2, "options" },
194+
{ "createhintfile", 2, "rollback" },
193195
{ "dumptxoutset", 2, "options" },
194196
{ "dumptxoutset", 2, "rollback" },
195197
{ "lockunspent", 0, "unlock" },

src/test/fuzz/rpc.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const std::vector<std::string> RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{
7474
"addconnection", // avoid DNS lookups
7575
"addnode", // avoid DNS lookups
7676
"addpeeraddress", // avoid DNS lookups
77+
"createhintfile", // avoid writing to disk
7778
"dumptxoutset", // avoid writing to disk
7879
"dumpwallet", // avoid writing to disk
7980
"enumeratesigners",

0 commit comments

Comments
 (0)