Skip to content

Commit 949b673

Browse files
committed
Merge bitcoin#28052: blockstorage: XOR blocksdir *.dat files
fa895c7 mingw: Document mode wbx workaround (MarcoFalke) fa35925 Add -blocksxor boolean option (MarcoFalke) fa7f7ac Return XOR AutoFile from BlockManager::Open*File() (MarcoFalke) Pull request description: Currently the *.dat files in the blocksdir store the data received from remote peers as-is. This may be problematic when a program other than Bitcoin Core tries to interpret them by accident. For example, an anti-virus program or other program may scan them and move them into quarantine, or delete them, or corrupt them. This may cause Bitcoin Core to fail a reorg, or fail to reply to block requests (via P2P, RPC, REST, ...). Fix this, similar to bitcoin#6650, by rolling a random XOR pattern over the dat files when writing or reading them. Obviously this can only protect against programs that accidentally and unintentionally are trying to mess with the dat files. Any program that intentionally wants to mess with the dat files can still trivially do so. The XOR pattern is only applied when the blocksdir is freshly created, and there is an option to disable it (on creation), so that people can disable it, if needed. ACKs for top commit: achow101: ACK fa895c7 TheCharlatan: Re-ACK fa895c7 hodlinator: ACK fa895c7 Tree-SHA512: c92a6a717da83bc33a9b8671a779eeefde2c63b192362ba1d71e6535ee31d08e2802b74acc908345197de9daac6930e4771595ee25b09acd5a67f7ea34854720
2 parents 44a4a01 + fa895c7 commit 949b673

10 files changed

+97
-16
lines changed

doc/files.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ Subdirectory | File(s) | Description
4747
-------------------|-----------------------|------------
4848
`blocks/` | | Blocks directory; can be specified by `-blocksdir` option (except for `blocks/index/`)
4949
`blocks/index/` | LevelDB database | Block index; `-blocksdir` option does not affect this path
50-
`blocks/` | `blkNNNNN.dat`<sup>[\[2\]](#note2)</sup> | Actual Bitcoin blocks (in network format, dumped in raw on disk, 128 MiB per file)
50+
`blocks/` | `blkNNNNN.dat`<sup>[\[2\]](#note2)</sup> | Actual Bitcoin blocks (dumped in network format, 128 MiB per file)
5151
`blocks/` | `revNNNNN.dat`<sup>[\[2\]](#note2)</sup> | Block undo data (custom format)
52+
`blocks/` | `xor.dat` | Rolling XOR pattern for block and undo data files
5253
`chainstate/` | LevelDB database | Blockchain state (a compact representation of all currently unspent transaction outputs (UTXOs) and metadata about the transactions they are from)
5354
`indexes/txindex/` | LevelDB database | Transaction index; *optional*, used if `-txindex=1`
5455
`indexes/blockfilter/basic/db/` | LevelDB database | Blockfilter index LevelDB database for the basic filtertype; *optional*, used if `-blockfilterindex=basic`

doc/release-notes-28052.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Blockstorage
2+
============
3+
4+
Block files are now XOR'd by default with a key stored in the blocksdir.
5+
Previous releases of Bitcoin Core or previous external software will not be able to read the blocksdir with a non-zero XOR-key.
6+
Refer to the `-blocksxor` help for more details.

src/init.cpp

+12-1
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ void SetupServerArgs(ArgsManager& argsman)
469469
#endif
470470
argsman.AddArg("-assumevalid=<hex>", strprintf("If this block is in the chain assume that it and its ancestors are valid and potentially skip their script verification (0 to verify all, default: %s, testnet: %s, signet: %s)", defaultChainParams->GetConsensus().defaultAssumeValid.GetHex(), testnetChainParams->GetConsensus().defaultAssumeValid.GetHex(), signetChainParams->GetConsensus().defaultAssumeValid.GetHex()), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
471471
argsman.AddArg("-blocksdir=<dir>", "Specify directory to hold blocks subdirectory for *.dat files (default: <datadir>)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
472+
argsman.AddArg("-blocksxor",
473+
strprintf("Whether an XOR-key applies to blocksdir *.dat files. "
474+
"The created XOR-key will be zeros for an existing blocksdir or when `-blocksxor=0` is "
475+
"set, and random for a freshly initialized blocksdir. "
476+
"(default: %u)",
477+
kernel::DEFAULT_XOR_BLOCKSDIR),
478+
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
472479
argsman.AddArg("-fastprune", "Use smaller block files and lower minimum prune height for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
473480
#if HAVE_SYSTEM
474481
argsman.AddArg("-blocknotify=<cmd>", "Execute command when the best block changes (%s in cmd is replaced by block hash)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -1534,7 +1541,11 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
15341541
}
15351542
LogPrintf("* Using %.1f MiB for in-memory UTXO set (plus up to %.1f MiB of unused mempool space)\n", cache_sizes.coins * (1.0 / 1024 / 1024), mempool_opts.max_size_bytes * (1.0 / 1024 / 1024));
15361543

1537-
node.chainman = std::make_unique<ChainstateManager>(*Assert(node.shutdown), chainman_opts, blockman_opts);
1544+
try {
1545+
node.chainman = std::make_unique<ChainstateManager>(*Assert(node.shutdown), chainman_opts, blockman_opts);
1546+
} catch (std::exception& e) {
1547+
return InitError(strprintf(Untranslated("Failed to initialize ChainstateManager: %s"), e.what()));
1548+
}
15381549
ChainstateManager& chainman = *node.chainman;
15391550

15401551
// This is defined and set here instead of inline in validation.h to avoid a hard

src/kernel/blockmanager_opts.h

+3
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ class CChainParams;
1414

1515
namespace kernel {
1616

17+
static constexpr bool DEFAULT_XOR_BLOCKSDIR{true};
18+
1719
/**
1820
* An options struct for `BlockManager`, more ergonomically referred to as
1921
* `BlockManager::Options` due to the using-declaration in `BlockManager`.
2022
*/
2123
struct BlockManagerOpts {
2224
const CChainParams& chainparams;
25+
bool use_xor{DEFAULT_XOR_BLOCKSDIR};
2326
uint64_t prune_target{0};
2427
bool fast_prune{false};
2528
const fs::path blocks_dir;

src/node/blockmanager_args.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
namespace node {
1717
util::Result<void> ApplyArgsManOptions(const ArgsManager& args, BlockManager::Options& opts)
1818
{
19+
if (auto value{args.GetBoolArg("-blocksxor")}) opts.use_xor = *value;
1920
// block pruning; get the amount of disk space (in MiB) to allot for block & undo files
2021
int64_t nPruneArg{args.GetIntArg("-prune", opts.prune_target)};
2122
if (nPruneArg < 0) {

src/node/blockstorage.cpp

+51-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <pow.h>
2020
#include <primitives/block.h>
2121
#include <primitives/transaction.h>
22+
#include <random.h>
2223
#include <reverse_iterator.h>
2324
#include <serialize.h>
2425
#include <signet.h>
@@ -818,13 +819,13 @@ void BlockManager::UnlinkPrunedFiles(const std::set<int>& setFilesToPrune) const
818819

819820
AutoFile BlockManager::OpenBlockFile(const FlatFilePos& pos, bool fReadOnly) const
820821
{
821-
return AutoFile{m_block_file_seq.Open(pos, fReadOnly)};
822+
return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_xor_key};
822823
}
823824

824825
/** Open an undo file (rev?????.dat) */
825826
AutoFile BlockManager::OpenUndoFile(const FlatFilePos& pos, bool fReadOnly) const
826827
{
827-
return AutoFile{m_undo_file_seq.Open(pos, fReadOnly)};
828+
return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_xor_key};
828829
}
829830

830831
fs::path BlockManager::GetBlockPosFilename(const FlatFilePos& pos) const
@@ -1144,6 +1145,54 @@ FlatFilePos BlockManager::SaveBlockToDisk(const CBlock& block, int nHeight)
11441145
return blockPos;
11451146
}
11461147

1148+
static auto InitBlocksdirXorKey(const BlockManager::Options& opts)
1149+
{
1150+
// Bytes are serialized without length indicator, so this is also the exact
1151+
// size of the XOR-key file.
1152+
std::array<std::byte, 8> xor_key{};
1153+
1154+
if (opts.use_xor && fs::is_empty(opts.blocks_dir)) {
1155+
// Only use random fresh key when the boolean option is set and on the
1156+
// very first start of the program.
1157+
FastRandomContext{}.fillrand(xor_key);
1158+
}
1159+
1160+
const fs::path xor_key_path{opts.blocks_dir / "xor.dat"};
1161+
if (fs::exists(xor_key_path)) {
1162+
// A pre-existing xor key file has priority.
1163+
AutoFile xor_key_file{fsbridge::fopen(xor_key_path, "rb")};
1164+
xor_key_file >> xor_key;
1165+
} else {
1166+
// Create initial or missing xor key file
1167+
AutoFile xor_key_file{fsbridge::fopen(xor_key_path,
1168+
#ifdef __MINGW64__
1169+
"wb" // Temporary workaround for https://github.com/bitcoin/bitcoin/issues/30210
1170+
#else
1171+
"wbx"
1172+
#endif
1173+
)};
1174+
xor_key_file << xor_key;
1175+
}
1176+
// If the user disabled the key, it must be zero.
1177+
if (!opts.use_xor && xor_key != decltype(xor_key){}) {
1178+
throw std::runtime_error{
1179+
strprintf("The blocksdir XOR-key can not be disabled when a random key was already stored! "
1180+
"Stored key: '%s', stored path: '%s'.",
1181+
HexStr(xor_key), fs::PathToString(xor_key_path)),
1182+
};
1183+
}
1184+
LogInfo("Using obfuscation key for blocksdir *.dat files (%s): '%s'\n", fs::PathToString(opts.blocks_dir), HexStr(xor_key));
1185+
return std::vector<std::byte>{xor_key.begin(), xor_key.end()};
1186+
}
1187+
1188+
BlockManager::BlockManager(const util::SignalInterrupt& interrupt, Options opts)
1189+
: m_prune_mode{opts.prune_target > 0},
1190+
m_xor_key{InitBlocksdirXorKey(opts)},
1191+
m_opts{std::move(opts)},
1192+
m_block_file_seq{FlatFileSeq{m_opts.blocks_dir, "blk", m_opts.fast_prune ? 0x4000 /* 16kB */ : BLOCKFILE_CHUNK_SIZE}},
1193+
m_undo_file_seq{FlatFileSeq{m_opts.blocks_dir, "rev", UNDOFILE_CHUNK_SIZE}},
1194+
m_interrupt{interrupt} {}
1195+
11471196
class ImportingNow
11481197
{
11491198
std::atomic<bool>& m_importing;

src/node/blockstorage.h

+3-6
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ class BlockManager
240240

241241
const bool m_prune_mode;
242242

243+
const std::vector<std::byte> m_xor_key;
244+
243245
/** Dirty block index entries. */
244246
std::set<CBlockIndex*> m_dirty_blockindex;
245247

@@ -264,12 +266,7 @@ class BlockManager
264266
public:
265267
using Options = kernel::BlockManagerOpts;
266268

267-
explicit BlockManager(const util::SignalInterrupt& interrupt, Options opts)
268-
: m_prune_mode{opts.prune_target > 0},
269-
m_opts{std::move(opts)},
270-
m_block_file_seq{FlatFileSeq{m_opts.blocks_dir, "blk", m_opts.fast_prune ? 0x4000 /* 16kB */ : BLOCKFILE_CHUNK_SIZE}},
271-
m_undo_file_seq{FlatFileSeq{m_opts.blocks_dir, "rev", UNDOFILE_CHUNK_SIZE}},
272-
m_interrupt{interrupt} {}
269+
explicit BlockManager(const util::SignalInterrupt& interrupt, Options opts);
273270

274271
const util::SignalInterrupt& m_interrupt;
275272
std::atomic<bool> m_importing{false};

src/test/streams_tests.cpp

+1-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ BOOST_AUTO_TEST_CASE(xor_file)
3030
}
3131
{
3232
#ifdef __MINGW64__
33-
// Our usage of mingw-w64 and the msvcrt runtime does not support
34-
// the x modifier for the _wfopen().
33+
// Temporary workaround for https://github.com/bitcoin/bitcoin/issues/30210
3534
const char* mode = "wb";
3635
#else
3736
const char* mode = "wbx";

test/functional/feature_loadblock.py

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ def set_test_params(self):
2626
self.setup_clean_chain = True
2727
self.num_nodes = 2
2828
self.supports_cli = False
29+
self.extra_args = [
30+
["-blocksxor=0"], # TODO: The linearize scripts should be adjusted to apply any XOR
31+
[],
32+
]
2933

3034
def run_test(self):
3135
self.nodes[1].setnetworkactive(state=False)

test/functional/feature_reindex.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,19 @@ def out_of_order(self):
3939
# we're generating them rather than getting them from peers), so to
4040
# test out-of-order handling, swap blocks 1 and 2 on disk.
4141
blk0 = self.nodes[0].blocks_path / "blk00000.dat"
42+
with open(self.nodes[0].blocks_path / "xor.dat", "rb") as xor_f:
43+
NUM_XOR_BYTES = 8 # From InitBlocksdirXorKey::xor_key.size()
44+
xor_dat = xor_f.read(NUM_XOR_BYTES)
45+
46+
def util_xor(data, key, *, offset):
47+
data = bytearray(data)
48+
for i in range(len(data)):
49+
data[i] ^= key[(i + offset) % len(key)]
50+
return bytes(data)
51+
4252
with open(blk0, 'r+b') as bf:
4353
# Read at least the first few blocks (including genesis)
44-
b = bf.read(2000)
54+
b = util_xor(bf.read(2000), xor_dat, offset=0)
4555

4656
# Find the offsets of blocks 2, 3, and 4 (the first 3 blocks beyond genesis)
4757
# by searching for the regtest marker bytes (see pchMessageStart).
@@ -55,12 +65,12 @@ def find_block(b, start):
5565
b4_start = find_block(b, b3_start)
5666

5767
# Blocks 2 and 3 should be the same size.
58-
assert_equal(b3_start-b2_start, b4_start-b3_start)
68+
assert_equal(b3_start - b2_start, b4_start - b3_start)
5969

6070
# Swap the second and third blocks (don't disturb the genesis block).
6171
bf.seek(b2_start)
62-
bf.write(b[b3_start:b4_start])
63-
bf.write(b[b2_start:b3_start])
72+
bf.write(util_xor(b[b3_start:b4_start], xor_dat, offset=b2_start))
73+
bf.write(util_xor(b[b2_start:b3_start], xor_dat, offset=b3_start))
6474

6575
# The reindexing code should detect and accommodate out of order blocks.
6676
with self.nodes[0].assert_debug_log([

0 commit comments

Comments
 (0)