Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/index/base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ CBlockLocator GetLocator(interfaces::Chain& chain, const uint256& block_hash)
return locator;
}

BaseIndex::DB::DB(const fs::path& path, size_t n_cache_size, bool f_memory, bool f_wipe, bool f_obfuscate) :
BaseIndex::DB::DB(const fs::path& path, size_t n_cache_size, bool f_memory, bool f_wipe) :
CDBWrapper{DBParams{
.path = path,
.cache_bytes = n_cache_size,
.memory_only = f_memory,
.wipe_data = f_wipe,
.obfuscate = f_obfuscate,
.obfuscate = false,
.options = [] { DBOptions options; node::ReadDatabaseArgs(gArgs, options); return options; }()}}
{}

Expand Down
3 changes: 1 addition & 2 deletions src/index/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ class BaseIndex : public CValidationInterface
class DB : public CDBWrapper
{
public:
DB(const fs::path& path, size_t n_cache_size,
bool f_memory = false, bool f_wipe = false, bool f_obfuscate = false);
DB(const fs::path& path, size_t n_cache_size, bool f_memory = false, bool f_wipe = false);

/// Read block locator of the chain that the index is in sync with.
bool ReadBestBlock(CBlockLocator& locator) const;
Expand Down
152 changes: 152 additions & 0 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
#include <policy/policy.h>
#include <policy/settings.h>
#include <protocol.h>
#include <regex>
#include <rpc/blockchain.h>
#include <rpc/register.h>
#include <rpc/server.h>
Expand Down Expand Up @@ -100,6 +101,7 @@
#include <cstdio>
#include <fstream>
#include <functional>
#include <ranges>
#include <set>
#include <string>
#include <thread>
Expand Down Expand Up @@ -520,6 +522,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
"(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >=%u = automatically prune block files to stay under the specified target size in MiB)", MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reindex", "If enabled, wipe chain state and block index, and rebuild them from blk*.dat files on disk. Also wipe and rebuild other optional indexes that are active. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reindex-chainstate", "If enabled, wipe chain state, and rebuild it from blk*.dat files on disk. If an assumeutxo snapshot was loaded, its chainstate will be wiped as well. The snapshot can then be reloaded via RPC.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-reobfuscate-blocks", "Reobfuscate existing blk*/rev* files. If a 16 character hexadecimal value is provided, it's used as the new XOR key; otherwise, the value is treated as a boolean and a random key is generated. This operation is resumable and the number of worker threads is controlled by -par.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-settings=<file>", strprintf("Specify path to dynamic settings data file. Can be disabled with -nosettings. File is written at runtime and not meant to be edited by users (use %s instead for custom settings). Relative paths will be prefixed by datadir location. (default: %s)", BITCOIN_CONF_FILENAME, BITCOIN_SETTINGS_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
#if HAVE_SYSTEM
argsman.AddArg("-startupnotify=<cmd>", "Execute command on startup.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
Expand Down Expand Up @@ -1267,6 +1270,131 @@ static std::optional<CService> CheckBindingConflicts(const CConnman::Options& co
return std::nullopt;
}

static bool ObfuscateBlocks(
const util::SignalInterrupt& interrupt,
std::string_view suffix,
size_t thread_count,
const fs::path& blocks_dir,
const fs::path& xor_dat,
const fs::path& xor_new,
const std::span<const std::byte> requested_key
) {
// Read all block and undo file names
auto collect_block_files{[&blocks_dir] {
std::vector<std::pair<std::string, fs::path>> files;
const std::regex dat_filename_pattern{R"(^(?:blk|rev)(\d+)\.dat$)", std::regex::optimize};
for (const auto& entry : fs::directory_iterator(blocks_dir)) {
const auto name{fs::PathToString(entry.path().filename())};
if (std::smatch index; entry.is_regular_file() && std::regex_match(name, index, dat_filename_pattern)) {
files.emplace_back(index[1], entry.path());
}
}
std::ranges::sort(files); // sort by index ascending, grouping blk/rev pairs together
return files;
}};

auto write_missing_key{[&](const fs::path& file, const std::array<std::byte, Obfuscation::KEY_SIZE>& default_bytes) -> bool {
if (!fs::exists(file)) {
AutoFile autofile{fsbridge::fopen(file, "wb")};
autofile << default_bytes;
if (!autofile.Commit() || autofile.fclose()) return false;
}
return true;
}};
auto read_key{[&](const fs::path& file) -> std::optional<Obfuscation> {
std::array<std::byte, Obfuscation::KEY_SIZE> obfuscation{};
AutoFile{fsbridge::fopen(file, "rb")} >> obfuscation;
return Obfuscation{obfuscation};
}};

// Create delta obfuscation key by combining the old ^ new so that we only have to iterate once
auto create_delta_obfuscation{[&]() -> std::optional<Obfuscation> {
// Ensure old key exists
if (!write_missing_key(xor_dat, {})) return std::nullopt;
const auto old_obfuscation{read_key(xor_dat)};
LogInfo("[obfuscate] old key: %s", old_obfuscation->HexKey());

// Prepare new key
std::array<std::byte, Obfuscation::KEY_SIZE> new_bytes{};
if (requested_key.size() == Obfuscation::KEY_SIZE) {
std::copy_n(requested_key.begin(), Obfuscation::KEY_SIZE, new_bytes.begin());
} else {
FastRandomContext{}.fillrand(new_bytes);
}
if (!write_missing_key(xor_new, new_bytes)) return std::nullopt;
const auto new_obfuscation{read_key(xor_new)};
LogInfo("[obfuscate] new key: %s", new_obfuscation->HexKey());

// Combine keys
std::array<std::byte, Obfuscation::KEY_SIZE> delta_bytes{};
(*old_obfuscation)(delta_bytes);
(*new_obfuscation)(delta_bytes);
return Obfuscation{delta_bytes};
}};

auto migrate_single_blockfile{[](const fs::path& file, std::string_view suffix, const Obfuscation& delta_obfuscation) -> bool {
AutoFile old_blocks{fsbridge::fopen(file, "rb"), delta_obfuscation}; // deobfuscate & reobfuscate with a single combined key
AutoFile new_blocks{fsbridge::fopen(file + suffix, "wb")};

std::vector<std::byte> chunk{2 * MAX_BLOCK_SERIALIZED_SIZE};
for (size_t n{0}; (n = old_blocks.detail_fread(chunk)); ) {
new_blocks.write_buffer(std::span{chunk}.first(n));
}
if (old_blocks.fclose() || !new_blocks.Commit() || new_blocks.fclose()) return false;

fs::last_write_time(file + suffix, fs::last_write_time(file)); // preserve timestamp
Assert(RemoveOver(file));
return true;
}};

// Start reobfuscation
const auto start{SteadyClock::now()};

const auto delta_obfuscation{create_delta_obfuscation()};
if (!delta_obfuscation) return false;

const auto& files{collect_block_files()};
LogInfo("[obfuscate] Reobfuscating %zu block and undo files", files.size());
const Obfuscation delta{*delta_obfuscation};

// Migrate undo and block files atomically
std::atomic_size_t migrated{0};
{
std::vector<std::thread> threads;
threads.reserve(thread_count);
if (thread_count > 1) LogInfo("[obfuscate] Using %zu threads", thread_count);
for (size_t t{0}; t < thread_count; ++t) {
threads.emplace_back([&, t, delta] {
for (size_t i{t}; i < files.size(); i += thread_count) {
if (!interrupt && migrate_single_blockfile(files[i].second, suffix, delta)) {
if (const auto done{++migrated}; done % 100 == 0) {
LogInfo("[obfuscate] %d%% done", (done * 100) / files.size());
}
}
}
});
}
for (auto& th : threads) th.join();
}
if (migrated != files.size()) return false;

// After migration rename new files to old names and use the new obfuscation key
for (const auto& entry : fs::directory_iterator(blocks_dir)) {
const auto filename{fs::PathToString(entry.path().filename())};
if (entry.path() != xor_new && entry.is_regular_file() && filename.ends_with(suffix)) {
const auto destination{entry.path().parent_path() / util::RemoveSuffixView(filename, suffix)};
Assert(RenameOver(entry.path(), destination));
}
}
Assert(RenameOver(xor_new, xor_dat)); // last step, signaling completion
DirectoryCommit(blocks_dir);

LogInfo("[obfuscate] Block and Undo file migration finished in %is", Ticks<std::chrono::seconds>(SteadyClock::now() - start));

return true;
}


// A GUI user may opt to retry once with do_reindex set if there is a failure during chainstate initialization.
// The function therefore has to support re-entry.
static ChainstateLoadResult InitAndLoadChainstate(
Expand Down Expand Up @@ -1321,6 +1449,30 @@ static ChainstateLoadResult InitAndLoadChainstate(
};
Assert(ApplyArgsManOptions(args, blockman_opts)); // no error can happen, already checked in AppInitParameterInteraction

{
// Has to be run before chainman creation
constexpr auto block_obfuscation_suffix{".reobfuscated"};
const auto blocks_dir{blockman_opts.blocks_dir};
const auto xor_dat{blocks_dir / "xor.dat"};
const auto xor_new{xor_dat + block_obfuscation_suffix};

std::vector<std::byte> requested_key{};
if (const auto arg{args.GetArg("-reobfuscate-blocks")}) {
if (arg->size() == 2 * Obfuscation::KEY_SIZE) {
requested_key = ParseHex<std::byte>(arg.value());
} else if (*arg != "0") {
requested_key = FastRandomContext{}.randbytes<std::byte>(Obfuscation::KEY_SIZE);
}
}
// reobfuscate if requested or if resuming a previous run
if (requested_key.size() || fs::exists(xor_new)) {
const auto thread_count{std::clamp<size_t>(args.GetIntArg("-par", GetNumCores()), 1, 50)};
if (!ObfuscateBlocks(*g_shutdown, block_obfuscation_suffix, thread_count, blocks_dir, xor_dat, xor_new, requested_key)) {
return {ChainstateLoadStatus::FAILURE, _("Block obfuscation failed")};
}
}
}

// Creating the chainstate manager internally creates a BlockManager, opens
// the blocks tree db, and wipes existing block files in case of a reindex.
// The coinsdb is opened at a later point on LoadChainstate.
Expand Down
11 changes: 9 additions & 2 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1173,8 +1173,15 @@ static auto InitBlocksdirXorKey(const BlockManager::Options& opts)
HexStr(obfuscation), fs::PathToString(xor_key_path)),
};
}
LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(obfuscation));
return Obfuscation{obfuscation};
const Obfuscation result{obfuscation};
if (result) {
LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(obfuscation));
} else {
LogWarning("The obfuscation of the blocksdir *.dat files isn't active, restart with `-reobfuscate-blocks` option to start the obfuscation process. "
"Note that this operation can take more than an hour on slow systems.");
}

return result;
}

BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts)
Expand Down
5 changes: 5 additions & 0 deletions src/util/fs.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ static inline path operator+(path p1, const char* p2)
p1 += p2;
return p1;
}
static inline path operator+(path p1, std::string_view p2)
{
p1 += p2;
return p1;
}
static inline path operator+(path p1, path::value_type p2)
{
p1 += p2;
Expand Down
9 changes: 8 additions & 1 deletion src/util/fs_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,20 @@ fs::path GetSpecialFolderPath(int nFolder, bool fCreate)
}
#endif

bool RenameOver(fs::path src, fs::path dest)
bool RenameOver(const fs::path& src, const fs::path& dest)
{
std::error_code error;
fs::rename(src, dest, error);
return !error;
}

bool RemoveOver(const fs::path& path)
{
std::error_code error;
fs::remove(path, error);
return !error;
}

/**
* Ignores exceptions thrown by create_directories if the requested directory exists.
* Specifically handles case where path p exists, but it wasn't possible for the user to
Expand Down
8 changes: 3 additions & 5 deletions src/util/fs_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@ bool TruncateFile(FILE* file, unsigned int length);
int RaiseFileDescriptorLimit(int nMinFD);
void AllocateFileRange(FILE* file, unsigned int offset, unsigned int length);

/**
* Rename src to dest.
* @return true if the rename was successful.
*/
[[nodiscard]] bool RenameOver(fs::path src, fs::path dest);
[[nodiscard]] bool RenameOver(const fs::path& src, const fs::path& dest);

[[nodiscard]] bool RemoveOver(const fs::path& path);

namespace util {
enum class LockResult {
Expand Down
1 change: 1 addition & 0 deletions src/util/obfuscation.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class Obfuscation
void Serialize(Stream& s) const
{
// Use vector serialization for convenient compact size prefix.
// Note that the `xor.dat` file uses array serialization instead.
std::vector<std::byte> bytes{KEY_SIZE};
std::memcpy(bytes.data(), &m_rotations[0], KEY_SIZE);
s << bytes;
Expand Down
82 changes: 82 additions & 0 deletions test/functional/feature_reobfuscation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
# Copyright (c) 2025-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Happy-path reobfuscation test:

- Node starts once (framework), which creates blk00000.dat / rev00000.dat.
- Add many tiny blk*/rev* files so progress logging has milestones.
- Restart with -reobfuscate-blocks=1 and check logs + xor.dat.
- Restart without the flag; verify the active obfuscation key in logs matches xor.dat.
"""

from pathlib import Path

from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal


class ReobfuscateBlocksSmokeTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
self.uses_wallet = None
self.extra_args = [[
"-checkblocks=0",
"-checklevel=0",
"-txindex=0",
"-coinstatsindex=0",
"-blockfilterindex=0",
]]

def _paths(self):
blocks_dir = Path(self.nodes[0].blocks_path)
return {
"blocks_dir": blocks_dir,
"blk0": blocks_dir / "blk00000.dat",
"rev0": blocks_dir / "rev00000.dat",
"xor_dat": blocks_dir / "xor.dat",
}

def run_test(self):
paths = self._paths()
blocks_dir, blk0, rev0, xor_dat = paths["blocks_dir"], paths["blk0"], paths["rev0"], paths["xor_dat"]

assert blk0.exists() and rev0.exists(), "Sanity: blk00000.dat and rev00000.dat should exist"

self.log.info("Snapshot initial contents")
before_blk = blk0.read_bytes()
before_rev = rev0.read_bytes()

self.log.info("Add many dummy block/undo files for progress logging (200 pairs in total)")
for i in range(1, 200):
(blocks_dir / f"blk{i:05d}.dat").write_bytes(b"\0")
(blocks_dir / f"rev{i:05d}.dat").write_bytes(b"\0")

self.log.info("Restarting with reobfuscation enabled")
with self.nodes[0].assert_debug_log(expected_msgs=[
"[obfuscate] Reobfuscating 400 block and undo files",
"% done",
]):
self.restart_node(0, extra_args=self.extra_args[0] + ["-reobfuscate-blocks", "-par=2"])
self.stop_node(0)

assert xor_dat.exists(), "xor.dat not created"
assert_equal(xor_dat.stat().st_size, 8)

self.log.info("Files should have been rewritten")
assert before_blk != blk0.read_bytes(), "blk00000.dat content did not change"
assert before_rev != rev0.read_bytes(), "rev00000.dat content did not change"

self.log.info("Start again without the flag; node should log the active obfuscation key matching xor.dat")
with self.nodes[0].assert_debug_log(expected_msgs=[
"Using obfuscation key for blocksdir *.dat files",
f"'{xor_dat.read_bytes().hex()}'",
]):
self.start_node(0, extra_args=self.extra_args[0])
self.generate(self.nodes[0], 1)
self.stop_node(0)


if __name__ == "__main__":
ReobfuscateBlocksSmokeTest(__file__).main()
1 change: 1 addition & 0 deletions test/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
'wallet_anchor.py',
'feature_reindex.py',
'feature_reindex_readonly.py',
'feature_reobfuscation.py',
'wallet_labels.py',
'p2p_compactblocks.py',
'p2p_compactblocks_blocksonly.py',
Expand Down
Loading