diff --git a/configure.ac b/configure.ac index a9e35495bca7..225f3eeb0fbe 100644 --- a/configure.ac +++ b/configure.ac @@ -149,6 +149,12 @@ AC_ARG_WITH([bdb], [use_bdb=$withval], [use_bdb=auto]) +AC_ARG_WITH([zstd], + [AS_HELP_STRING([--without-zstd], + [disable zstd compression of block and undo files (default is enabled if libzstd is found)])], + [use_zstd=$withval], + [use_zstd=auto]) + AC_ARG_ENABLE([usdt], [AS_HELP_STRING([--enable-usdt], [enable tracepoints for Userspace, Statically Defined Tracing (default is yes if sys/sdt.h is found)])], @@ -1695,7 +1701,23 @@ AC_CHECK_LIB([gmp], [__gmpz_init],GMP_LIBS=-lgmp, AC_MSG_ERROR(libgmp missing)) CPPFLAGS="$TEMP_CPPFLAGS" LDFLAGS="$TEMP_LDFLAGS" +dnl zstd check +if test "$use_zstd" != "no"; then + PKG_CHECK_MODULES([ZSTD], [libzstd >= 1.4.0], + [AC_DEFINE([ENABLE_ZSTD], [1], [Define this symbol to enable zstd compression]) + use_zstd=yes], + [if test "$use_zstd" = "yes"; then + AC_MSG_ERROR([libzstd version 1.4.0 or greater not found.]) + fi + AC_MSG_WARN([libzstd version 1.4.0 or greater not found, disabling zstd compression]) + use_zstd=no]) +fi +if test "$use_zstd" = "yes" && test "$suppress_external_warnings" != "no"; then + ZSTD_CFLAGS=SUPPRESS_WARNINGS($ZSTD_CFLAGS) +fi + AM_CONDITIONAL([ENABLE_ZMQ], [test "$use_zmq" = "yes"]) +AM_CONDITIONAL([ENABLE_ZSTD], [test "$use_zstd" = "yes"]) dnl libmultiprocess library check @@ -2104,6 +2126,7 @@ if test $bitcoin_enable_qt != "no"; then echo " with qr = $use_qr" fi echo " with zmq = $use_zmq" +echo " with zstd = $use_zstd" if test $enable_fuzz = "no"; then echo " with test = $use_tests" else diff --git a/contrib/linearize/README.md b/contrib/linearize/README.md index 9369030c331f..90d59a1b40a2 100644 --- a/contrib/linearize/README.md +++ b/contrib/linearize/README.md @@ -5,6 +5,10 @@ Construct a linear, no-fork, best version of the Dash blockchain. https://github.com/dashpay/dash_hash +Block files written by recent Dash Core versions hold zstd-compressed records; reading +them with `linearize-data.py` needs the `zstandard` module (`pip install zstandard`) +or Python 3.14+. + ## Step 1: Download hash list $ ./linearize-hashes.py linearize.cfg > hashlist.txt diff --git a/contrib/linearize/linearize-data.py b/contrib/linearize/linearize-data.py index ef06f7d00cad..ca04da2a2021 100755 --- a/contrib/linearize/linearize-data.py +++ b/contrib/linearize/linearize-data.py @@ -18,6 +18,19 @@ import glob from collections import namedtuple +try: + from compression import zstd as _zstd # Python >= 3.14 + zstd_decompress = _zstd.decompress +except ImportError: + try: + import zstandard + zstd_decompress = zstandard.ZstdDecompressor().decompress + except ImportError: + zstd_decompress = None + +# High bit of the record size field: the payload is one zstd frame of the block +BLOCK_RECORD_COMPRESSED = 0x80000000 + settings = {} def calc_hash_str(blk_hdr): @@ -77,7 +90,7 @@ def getFirstBlockFileId(block_dir_path): return blkId # Block header and extent on disk -BlockExtent = namedtuple('BlockExtent', ['fn', 'offset', 'inhdr', 'blkhdr', 'size']) +BlockExtent = namedtuple('BlockExtent', ['fn', 'offset', 'inhdr', 'blkhdr', 'size', 'compressed']) class BlockDataCopier: def __init__(self, settings, blkindex, blkmap): @@ -165,6 +178,8 @@ def fetchBlock(self, extent): '''Fetch block contents from disk given extents''' with open(self.inFileName(extent.fn), "rb") as f: f.seek(extent.offset) + if extent.compressed: + return zstd_decompress(f.read(extent.size))[80:] return f.read(extent.size) def copyOneBlock(self): @@ -206,9 +221,24 @@ def run(self): continue inLenLE = inhdr[4:] su = struct.unpack("= 3.14") + sys.exit(1) + payload_len = su[0] & ~BLOCK_RECORD_COMPRESSED + payload_offset = self.inF.tell() + rawblock = zstd_decompress(self.inF.read(payload_len)) + blk_hdr = rawblock[:80] + rawblock = rawblock[80:] + inLen = len(rawblock) + # bootstrap files always hold uncompressed records + inhdr = inMagic + struct.pack("[ \* ](#note1) | 6.3.0 | [6.2.0](https://github.com/dashpay/bls-signatures/pull/92) | No | +| [zstd](../depends/packages/zstd.mk) (block file compression) | [link](https://github.com/facebook/zstd/releases) | 1.5.7 | 1.4.0 | No | ### GUI | Dependency | Releases | Version used | Minimum required | Runtime | diff --git a/doc/release-notes-7939.md b/doc/release-notes-7939.md new file mode 100644 index 000000000000..c1c21090b5d8 --- /dev/null +++ b/doc/release-notes-7939.md @@ -0,0 +1,25 @@ +Block storage +------------- + +Block files (`blocks/blk*.dat`) can now be written zstd-compressed, one frame per record, which +saves about a fifth of the space of new block files. Block undo data (`blocks/rev*.dat`) is +compressed the same way, which saves about a quarter. +The new `-blockcompression=` option selects the zstd level. +It defaults to 0 in this release, which stores records exactly as before, so nothing changes +on disk unless the option is set. Level 3 is the recommended setting when opting in; levels above 5 +cost several times more CPU for no measurable gain and produce a startup warning. A future release +is planned to enable compression by default. + +Files written by earlier versions are read as before and are never rewritten, and this version +reads compressed records whether or not it writes them. That keeps downgrades simple: a node that +never set `-blockcompression` can be downgraded to any earlier version without a reindex, and once +a later release enables compression by default, a downgrade to this version needs no reindex +either. Only a downgrade to a version before this one after compression has been used requires +`-reindex`, since older versions cannot read the compressed records. + +zstd is an optional build dependency (`--without-zstd`, `NO_ZSTD=1` for depends). A build without +it stores everything uncompressed, accepts only `-blockcompression=0`, and refuses to start on +block files written with compression until `-reindex` re-downloads those blocks. + +`contrib/linearize/linearize-data.py` reads compressed records with the `zstandard` Python module +or Python 3.14 and later, and always writes uncompressed records to the bootstrap file. diff --git a/src/Makefile.am b/src/Makefile.am index ad095c971a9f..1025d215400d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -484,6 +484,7 @@ BITCOIN_CORE_H = \ util/vecdeque.h \ util/vector.h \ util/wpipe.h \ + util/zstd.h \ validation.h \ validationinterface.h \ versionbits.h \ @@ -532,7 +533,7 @@ obj/build.h: FORCE libbitcoin_util_a-clientversion.$(OBJEXT): obj/build.h # node # -libbitcoin_node_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(LEVELDB_CPPFLAGS) $(BOOST_CPPFLAGS) $(MINIUPNPC_CPPFLAGS) $(NATPMP_CPPFLAGS) $(EVENT_CFLAGS) $(EVENT_PTHREADS_CFLAGS) +libbitcoin_node_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(LEVELDB_CPPFLAGS) $(BOOST_CPPFLAGS) $(MINIUPNPC_CPPFLAGS) $(NATPMP_CPPFLAGS) $(EVENT_CFLAGS) $(EVENT_PTHREADS_CFLAGS) $(ZSTD_CFLAGS) libbitcoin_node_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) libbitcoin_node_a_SOURCES = \ active/context.cpp \ @@ -1038,7 +1039,7 @@ libbitcoin_common_a_SOURCES = \ # # util # -libbitcoin_util_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) +libbitcoin_util_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(ZSTD_CFLAGS) libbitcoin_util_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) libbitcoin_util_a_SOURCES = \ bls/bls_batchverifier.h \ @@ -1088,6 +1089,7 @@ libbitcoin_util_a_SOURCES = \ util/time.cpp \ util/tokenpipe.cpp \ util/wpipe.cpp \ + util/zstd.cpp \ $(BITCOIN_CORE_H) # @@ -1126,7 +1128,7 @@ bitcoin_bin_ldadd = \ $(LIBMEMENV) \ $(LIBSECP256K1) -bitcoin_bin_ldadd += $(BACKTRACE_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(ZMQ_LIBS) $(GMP_LIBS) +bitcoin_bin_ldadd += $(BACKTRACE_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(ZMQ_LIBS) $(GMP_LIBS) $(ZSTD_LIBS) dashd_SOURCES = $(bitcoin_daemon_sources) init/bitcoind.cpp dashd_CPPFLAGS = $(bitcoin_bin_cppflags) @@ -1247,8 +1249,8 @@ if BUILD_BITCOIN_KERNEL_LIB lib_LTLIBRARIES += $(LIBBITCOINKERNEL) libdashkernel_la_LDFLAGS = $(AM_LDFLAGS) -no-undefined $(RELDFLAGS) $(PTHREAD_FLAGS) -libdashkernel_la_LIBADD = $(LIBBITCOIN_CRYPTO) $(LIBUNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) $(LIBSECP256K1) $(LIBDASHBLS) $(GMP_LIBS) -libdashkernel_la_CPPFLAGS = $(AM_CPPFLAGS) -I$(builddir)/obj -I$(srcdir)/secp256k1/include -DBUILD_BITCOIN_INTERNAL -isystem$(srcdir)/dashbls/include -isystem$(srcdir)/dashbls/depends/relic/include -isystem$(srcdir)/dashbls/depends/minialloc/include -isystem$(srcdir)/immer $(BOOST_CPPFLAGS) $(LEVELDB_CPPFLAGS) -I$(srcdir)/$(UNIVALUE_INCLUDE_DIR_INT) +libdashkernel_la_LIBADD = $(LIBBITCOIN_CRYPTO) $(LIBUNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) $(LIBSECP256K1) $(LIBDASHBLS) $(GMP_LIBS) $(ZSTD_LIBS) +libdashkernel_la_CPPFLAGS = $(AM_CPPFLAGS) -I$(builddir)/obj -I$(srcdir)/secp256k1/include -DBUILD_BITCOIN_INTERNAL -isystem$(srcdir)/dashbls/include -isystem$(srcdir)/dashbls/depends/relic/include -isystem$(srcdir)/dashbls/depends/minialloc/include -isystem$(srcdir)/immer $(BOOST_CPPFLAGS) $(LEVELDB_CPPFLAGS) $(ZSTD_CFLAGS) -I$(srcdir)/$(UNIVALUE_INCLUDE_DIR_INT) # libdashkernel requires default symbol visibility, explicitly specify that # here so that things still work even when user configures with @@ -1396,6 +1398,7 @@ libdashkernel_la_SOURCES = \ util/threadnames.cpp \ util/time.cpp \ util/tokenpipe.cpp \ + util/zstd.cpp \ validation.cpp \ validationinterface.cpp \ versionbits.cpp \ diff --git a/src/Makefile.bench.include b/src/Makefile.bench.include index a9949c57b682..a872d082feb3 100644 --- a/src/Makefile.bench.include +++ b/src/Makefile.bench.include @@ -87,6 +87,7 @@ bench_bench_dash_LDADD = \ $(MINIUPNPC_LIBS) \ $(NATPMP_LIBS) \ $(GMP_LIBS) \ + $(ZSTD_LIBS) \ $(BACKTRACE_LIBS) if ENABLE_ZMQ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 9dc7d11ba75f..0b1831ae0090 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -489,7 +489,7 @@ bitcoin_qt_ldadd += $(LIBBITCOIN_ZMQ) $(ZMQ_LIBS) endif bitcoin_qt_ldadd += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) $(LIBMEMENV) \ $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ - $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) + $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) $(ZSTD_LIBS) bitcoin_qt_ldflags = $(LDFLAGS_WRAP_EXCEPTIONS) $(RELDFLAGS) $(AM_LDFLAGS) $(QT_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) $(PTHREAD_FLAGS) bitcoin_qt_libtoolflags = $(AM_LIBTOOLFLAGS) --tag CXX diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index 4690c07a5ea0..fcf7957e1590 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -90,7 +90,7 @@ endif qt_test_test_dash_qt_LDADD += $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON) $(LIBBITCOIN_UTIL) $(LIBBITCOIN_CONSENSUS) $(LIBBITCOIN_CRYPTO) $(LIBDASHBLS) $(LIBUNIVALUE) $(LIBLEVELDB) \ $(LIBMEMENV) $(BACKTRACE_LIBS) $(QT_LIBS) $(QT_DBUS_LIBS) $(QT_TEST_LIBS) \ $(QR_LIBS) $(BDB_LIBS) $(MINIUPNPC_LIBS) $(NATPMP_LIBS) $(SQLITE_LIBS) $(LIBSECP256K1) \ - $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) + $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) $(ZSTD_LIBS) qt_test_test_dash_qt_LDFLAGS = $(LDFLAGS_WRAP_EXCEPTIONS) $(RELDFLAGS) $(AM_LDFLAGS) $(QT_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) $(PTHREAD_FLAGS) qt_test_test_dash_qt_CXXFLAGS = $(AM_CXXFLAGS) $(QT_PIE_FLAGS) diff --git a/src/Makefile.test.include b/src/Makefile.test.include index d59f65985291..be5836b8d35c 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -60,6 +60,7 @@ FUZZ_SUITE_LD_COMMON = \ $(EVENT_LIBS) \ $(EVENT_PTHREADS_LIBS) \ $(GMP_LIBS) \ + $(ZSTD_LIBS) \ $(BACKTRACE_LIBS) if USE_UPNP @@ -283,7 +284,7 @@ test_test_dash_LDADD += $(LIBBITCOIN_NODE) $(LIBBITCOIN_CLI) $(LIBBITCOIN_COMMON $(LIBDASHBLS) $(LIBLEVELDB) $(LIBMEMENV) $(BACKTRACE_LIBS) $(LIBSECP256K1) $(EVENT_LIBS) $(EVENT_PTHREADS_LIBS) $(MINISKETCH_LIBS) test_test_dash_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) -test_test_dash_LDADD += $(BDB_LIBS) $(MINIUPNPC_LIBS) $(SQLITE_LIBS) $(NATPMP_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) +test_test_dash_LDADD += $(BDB_LIBS) $(MINIUPNPC_LIBS) $(SQLITE_LIBS) $(NATPMP_LIBS) $(EVENT_PTHREADS_LIBS) $(EVENT_LIBS) $(GMP_LIBS) $(ZSTD_LIBS) test_test_dash_LDFLAGS = $(LDFLAGS_WRAP_EXCEPTIONS) $(RELDFLAGS) $(AM_LDFLAGS) $(LIBTOOL_APP_LDFLAGS) $(PTHREAD_FLAGS) -static if ENABLE_ZMQ diff --git a/src/index/txindex.cpp b/src/index/txindex.cpp index 7cb5069f43e1..c7345bb9f1fa 100644 --- a/src/index/txindex.cpp +++ b/src/index/txindex.cpp @@ -7,9 +7,12 @@ #include #include #include +#include #include using node::OpenBlockFile; +using node::OpenRecordPayload; +using node::RecordHeaderPos; constexpr uint8_t DB_TXINDEX{'t'}; @@ -79,19 +82,17 @@ bool TxIndex::FindTx(const uint256& tx_hash, uint256& block_hash, CTransactionRe return false; } - AutoFile file{OpenBlockFile(postx, true)}; + AutoFile file{OpenBlockFile(RecordHeaderPos(postx), true)}; if (file.IsNull()) { LogError("%s: OpenBlockFile failed\n", __func__); return false; } CBlockHeader header; try { - file >> header; - if (fseek(file.Get(), postx.nTxOffset, SEEK_CUR)) { - LogError("%s: fseek(...) failed\n", __func__); - return false; - } - file >> tx; + util::ZstdFileReader block_in{OpenRecordPayload(file)}; + block_in >> header; + block_in.ignore(postx.nTxOffset); + block_in >> tx; } catch (const std::exception& e) { LogError("%s: Deserialize or I/O error - %s\n", __func__, e.what()); return false; diff --git a/src/init.cpp b/src/init.cpp index 008ae13fdb33..c3d6bb62e15c 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -93,6 +93,7 @@ #include #include #include +#include #include #include #include @@ -171,6 +172,7 @@ using node::CalculateCacheSizes; using node::DEFAULT_PERSIST_MEMPOOL; using node::DEFAULT_PRINTPRIORITY; using node::DEFAULT_STOPAFTERBLOCKIMPORT; +using node::MAX_RECOMMENDED_BLOCK_COMPRESSION_LEVEL; using node::g_indexes_ready_to_sync; using node::LoadChainstate; using node::MempoolPath; @@ -599,6 +601,7 @@ void SetupServerArgs(ArgsManager& argsman) argsman.AddArg("-alertnotify=", "Execute command when an alert is raised (%s in cmd is replaced by message)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #endif argsman.AddArg("-assumevalid=", 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, devnet: %s)", defaultChainParams->GetConsensus().defaultAssumeValid.GetHex(), testnetChainParams->GetConsensus().defaultAssumeValid.GetHex(), devnetChainParams->GetConsensus().defaultAssumeValid.GetHex()), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-blockcompression=", strprintf("zstd compression level for newly written block and undo files, 0 to store them uncompressed; builds without zstd support accept 0 only (default: %d)", util::DefaultZstdLevel()), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-blocksdir=", "Specify directory to hold blocks subdirectory for *.dat files (default: )", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-fastprune", "Use smaller block files and lower minimum prune height for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-tinyblk", "Use smaller block files for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); @@ -1345,6 +1348,17 @@ bool AppInitParameterInteraction(const ArgsManager& args) fPruneMode = true; } + const int64_t block_compression{args.GetIntArg("-blockcompression", util::DefaultZstdLevel())}; + if (!util::ZstdAvailable() && block_compression != 0) { + return InitError(_("-blockcompression requires a build with zstd support.")); + } + if (block_compression < 0 || block_compression > util::ZstdMaxLevel()) { + return InitError(strprintf(_("-blockcompression must be between 0 and %d."), util::ZstdMaxLevel())); + } + if (block_compression > MAX_RECOMMENDED_BLOCK_COMPRESSION_LEVEL) { + InitWarning(strprintf(_("-blockcompression=%d: levels above %d cost much more CPU per block for no measurable gain."), block_compression, MAX_RECOMMENDED_BLOCK_COMPRESSION_LEVEL)); + } + nConnectTimeout = args.GetIntArg("-timeout", DEFAULT_CONNECT_TIMEOUT); if (nConnectTimeout <= 0) { nConnectTimeout = DEFAULT_CONNECT_TIMEOUT; @@ -2104,6 +2118,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) return LoadChainstate(chainman, cache_sizes, options, *node.evodb, *node.dmnman, *node.special_tx); }); } + if (status == node::ChainstateLoadStatus::SUCCESS && !util::ZstdAvailable() && !options.reindex && chainman.m_blockman.m_have_compressed_records) { + return InitError(strprintf(_("The block files contain zstd-compressed records, but this build of %s has no zstd support. Use a build with zstd support, or restart with -reindex to re-download the blocks."), PACKAGE_NAME)); + } if (status == node::ChainstateLoadStatus::SUCCESS) { uiInterface.InitMessage(_("Verifying blocks…").translated); if (chainman.m_blockman.m_have_pruned && options.check_blocks > chainman.GetParams().MinBlocksToKeep()) { diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 822e983573cb..4b170fe1267f 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -16,11 +16,13 @@ #include #include #include +#include #include #include #include #include +#include #include namespace node { @@ -347,6 +349,9 @@ bool BlockManager::WriteBlockIndexDB() vBlocks.push_back(*it); m_dirty_blockindex.erase(it++); } + if (m_have_compressed_records && !m_block_tree_db->WriteFlag("compressedrecords", true)) { + return false; + } if (!m_block_tree_db->WriteBatchSync(vFiles, m_last_blockfile, vBlocks)) { return false; } @@ -393,6 +398,7 @@ bool BlockManager::LoadBlockIndexDB() // Check whether we have ever pruned block & undo files m_block_tree_db->ReadFlag("prunedblockfiles", m_have_pruned); + m_block_tree_db->ReadFlag("compressedrecords", m_have_compressed_records); if (m_have_pruned) { LogPrintf("LoadBlockIndexDB(): Block files have previously been pruned\n"); } @@ -508,7 +514,7 @@ CBlockFileInfo* BlockManager::GetBlockFileInfo(size_t n) return &m_blockfile_info.at(n); } -static bool UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos, const uint256& hashBlock, const CMessageHeader::MessageStartChars& messageStart) +static bool UndoWriteToDisk(Span payload, uint32_t size_field, FlatFilePos& pos, const uint256& checksum, const CMessageHeader::MessageStartChars& messageStart) { // Open history file to append AutoFile fileout{OpenUndoFile(pos)}; @@ -518,23 +524,17 @@ static bool UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos, const } // Write index header - unsigned int nSize = GetSerializeSize(blockundo, CLIENT_VERSION); - fileout << messageStart << nSize; + fileout << messageStart << size_field; - // Write undo data + // Write undo data and checksum long fileOutPos = ftell(fileout.Get()); if (fileOutPos < 0) { LogError("%s: ftell failed\n", __func__); return false; } pos.nPos = (unsigned int)fileOutPos; - fileout << blockundo; - - // calculate & write checksum - HashWriter hasher{}; - hasher << hashBlock; - hasher << blockundo; - fileout << hasher.GetHash(); + fileout.write(payload); + fileout << checksum; return true; } @@ -549,7 +549,7 @@ bool UndoReadFromDisk(CBlockUndo& blockundo, const CBlockIndex* pindex) } // Open history file to read - AutoFile filein{OpenUndoFile(pos, true)}; + AutoFile filein{OpenUndoFile(RecordHeaderPos(pos), true)}; if (filein.IsNull()) { LogError("%s: OpenUndoFile failed\n", __func__); return false; @@ -557,18 +557,21 @@ bool UndoReadFromDisk(CBlockUndo& blockundo, const CBlockIndex* pindex) // Read block uint256 hashChecksum; - HashVerifier verifier{filein}; // Use HashVerifier as reserializing may lose data, c.f. commit d342424301013ec47dc146a4beb49d5c9319d80a + uint256 computed; try { + util::ZstdFileReader undo_in{OpenRecordPayload(filein)}; + HashVerifier verifier{undo_in}; // Use HashVerifier as reserializing may lose data, c.f. commit d342424301013ec47dc146a4beb49d5c9319d80a verifier << pindex->pprev->GetBlockHash(); verifier >> blockundo; filein >> hashChecksum; + computed = verifier.GetHash(); } catch (const std::exception& e) { LogError("%s: Deserialize or I/O error - %s\n", __func__, e.what()); return false; } // Verify checksum - if (hashChecksum != verifier.GetHash()) { + if (hashChecksum != computed) { LogError("%s: Checksum mismatch\n", __func__); return false; } @@ -768,7 +771,7 @@ bool BlockManager::FindUndoPos(BlockValidationState& state, int nFile, FlatFileP return true; } -static bool WriteBlockToDisk(const CBlock& block, FlatFilePos& pos, const CMessageHeader::MessageStartChars& messageStart) +static bool WriteBlockToDisk(Span payload, uint32_t size_field, FlatFilePos& pos, const CMessageHeader::MessageStartChars& messageStart) { // Open history file to append AutoFile fileout{OpenBlockFile(pos)}; @@ -778,8 +781,7 @@ static bool WriteBlockToDisk(const CBlock& block, FlatFilePos& pos, const CMessa } // Write index header - unsigned int nSize = GetSerializeSize(block, CLIENT_VERSION); - fileout << messageStart << nSize; + fileout << messageStart << size_field; // Write block long fileOutPos = ftell(fileout.Get()); @@ -788,22 +790,53 @@ static bool WriteBlockToDisk(const CBlock& block, FlatFilePos& pos, const CMessa return false; } pos.nPos = (unsigned int)fileOutPos; - fileout << block; + fileout.write(payload); return true; } +/** Store serialized data as a zstd frame when that is smaller. Returns the record payload and its size field. */ +static std::pair BuildRecordPayload(DataStream data) +{ + const int level{util::ZstdLevel()}; + if (level > 0) { + const std::vector compressed{util::ZstdCompress(MakeByteSpan(data), level)}; + if (!compressed.empty() && compressed.size() < data.size()) { + const uint32_t size_field{static_cast(compressed.size()) | BLOCK_RECORD_COMPRESSED}; + return {DataStream{compressed}, size_field}; + } + } + const uint32_t size_field{static_cast(data.size())}; + return {std::move(data), size_field}; +} + +static std::pair BuildBlockRecordPayload(const CBlock& block) +{ + DataStream ss; + ss << block; + return BuildRecordPayload(std::move(ss)); +} + bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValidationState& state, CBlockIndex& block) { AssertLockHeld(::cs_main); // Write undo information to disk if (block.GetUndoPos().IsNull()) { + DataStream undo_data; + undo_data << blockundo; + // the checksum covers the previous block hash and the serialized undo data, compressed or not + HashWriter hasher{}; + hasher << block.pprev->GetBlockHash(); + hasher.write(MakeByteSpan(undo_data)); + const uint256 checksum{hasher.GetHash()}; + const auto [payload, size_field]{BuildRecordPayload(std::move(undo_data))}; FlatFilePos _pos; - if (!FindUndoPos(state, block.nFile, _pos, ::GetSerializeSize(blockundo, CLIENT_VERSION) + 40)) { + if (!FindUndoPos(state, block.nFile, _pos, payload.size() + BLOCK_SERIALIZATION_HEADER_SIZE + uint256::size())) { LogError("ConnectBlock(): FindUndoPos failed\n"); return false; } - if (!UndoWriteToDisk(blockundo, _pos, block.pprev->GetBlockHash(), GetParams().MessageStart())) { + if (size_field & BLOCK_RECORD_COMPRESSED) m_have_compressed_records = true; + if (!UndoWriteToDisk(MakeByteSpan(payload), size_field, _pos, checksum, GetParams().MessageStart())) { return AbortNode(state, "Failed to write undo data"); } // rev files are written in block height order, whereas blk files are written as blocks come in (often out of order) @@ -832,12 +865,33 @@ bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValid return true; } +FlatFilePos RecordHeaderPos(const FlatFilePos& pos) +{ + if (pos.nPos < BLOCK_SERIALIZATION_HEADER_SIZE) return {}; + return {pos.nFile, static_cast(pos.nPos - BLOCK_SERIALIZATION_HEADER_SIZE)}; +} + +/** Size field of the record the file is positioned at; the file is left at the payload */ +static uint32_t ReadRecordSizeField(AutoFile& filein) +{ + uint32_t size_field; + filein.ignore(CMessageHeader::MESSAGE_START_SIZE); + filein >> size_field; + return size_field; +} + +util::ZstdFileReader OpenRecordPayload(AutoFile& filein) +{ + const uint32_t size_field{ReadRecordSizeField(filein)}; + return {filein, size_field & BLOCK_RECORD_SIZE_MASK, (size_field & BLOCK_RECORD_COMPRESSED) != 0}; +} + std::optional ReadBlockFromDisk(CBlock& block, const FlatFilePos& pos, const Consensus::Params& consensusParams) { block.SetNull(); // Open history file to read - AutoFile filein{OpenBlockFile(pos, true)}; + AutoFile filein{OpenBlockFile(RecordHeaderPos(pos), true)}; if (filein.IsNull()) { LogError("ReadBlockFromDisk: OpenBlockFile failed for %s\n", pos.ToString()); return std::nullopt; @@ -845,7 +899,8 @@ std::optional ReadBlockFromDisk(CBlock& block, const FlatFilePos& pos, // Read block try { - filein >> block; + util::ZstdFileReader block_in{OpenRecordPayload(filein)}; + block_in >> block; } catch (const std::exception& e) { LogError("ReadBlockFromDisk: Deserialize or I/O error - %s at %s\n", e.what(), pos.ToString()); return std::nullopt; @@ -877,14 +932,32 @@ bool ReadBlockFromDisk(CBlock& block, const CBlockIndex* pindex, const Consensus return true; } +/** Size field of the record already stored at pos */ +static std::optional StoredBlockSizeField(const FlatFilePos& pos) +{ + AutoFile filein{OpenBlockFile(RecordHeaderPos(pos), true)}; + if (filein.IsNull()) return std::nullopt; + try { + return ReadRecordSizeField(filein); + } catch (const std::exception&) { + return std::nullopt; + } +} + FlatFilePos BlockManager::SaveBlockToDisk(const CBlock& block, int nHeight, const FlatFilePos* dbp) { unsigned int nBlockSize = ::GetSerializeSize(block, CLIENT_VERSION); FlatFilePos blockPos; const auto position_known {dbp != nullptr}; + DataStream payload; + uint32_t size_field{0}; if (position_known) { blockPos = *dbp; + // the stored record may be smaller than the serialized block + if (const auto stored{StoredBlockSizeField(blockPos)}) nBlockSize = *stored & BLOCK_RECORD_SIZE_MASK; } else { + std::tie(payload, size_field) = BuildBlockRecordPayload(block); + nBlockSize = payload.size(); // when known, blockPos.nPos points at the offset of the block data in the blk file. that already accounts for // the serialization header present in the file (the 4 magic message start bytes + the 4 length bytes = 8 bytes = BLOCK_SERIALIZATION_HEADER_SIZE). // we add BLOCK_SERIALIZATION_HEADER_SIZE only for new blocks since they will have the serialization header added when written to disk. @@ -895,7 +968,8 @@ FlatFilePos BlockManager::SaveBlockToDisk(const CBlock& block, int nHeight, cons return FlatFilePos(); } if (!position_known) { - if (!WriteBlockToDisk(block, blockPos, GetParams().MessageStart())) { + if (size_field & BLOCK_RECORD_COMPRESSED) m_have_compressed_records = true; + if (!WriteBlockToDisk(MakeByteSpan(payload), size_field, blockPos, GetParams().MessageStart())) { AbortNode("Failed to write block"); return FlatFilePos(); } diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 66eb4bfc35dd..75da634960ff 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -21,6 +21,7 @@ #include class ArgsManager; +class AutoFile; class BlockValidationState; class CBlock; class CBlockUndo; @@ -32,9 +33,14 @@ struct FlatFilePos; namespace Consensus { struct Params; } +namespace util { +class ZstdFileReader; +} namespace node { static constexpr bool DEFAULT_STOPAFTERBLOCKIMPORT{false}; +/** Higher levels cost several times more CPU per block for no measurable gain */ +static constexpr int MAX_RECOMMENDED_BLOCK_COMPRESSION_LEVEL{5}; /** The pre-allocation chunk size for blk?????.dat files (since 0.8) */ static const unsigned int BLOCKFILE_CHUNK_SIZE = 0x1000000; // 16 MiB @@ -43,8 +49,11 @@ static const unsigned int UNDOFILE_CHUNK_SIZE = 0x100000; // 1 MiB /** The maximum size of a blk?????.dat file (since 0.8) */ static const unsigned int MAX_BLOCKFILE_SIZE = 0x8000000; // 128 MiB -/** Size of header written by WriteBlockToDisk before a serialized CBlock */ +/** Size of header written before the payload of each block and undo record */ static constexpr size_t BLOCK_SERIALIZATION_HEADER_SIZE = CMessageHeader::MESSAGE_START_SIZE + sizeof(unsigned int); +/** Set in the size field of a block or undo record whose payload is one zstd frame of the serialized data */ +static constexpr uint32_t BLOCK_RECORD_COMPRESSED{0x80000000}; +static constexpr uint32_t BLOCK_RECORD_SIZE_MASK{~BLOCK_RECORD_COMPRESSED}; extern std::atomic_bool fImporting; extern std::atomic_bool fReindex; @@ -233,6 +242,9 @@ class BlockManager /** True if any block files have ever been pruned. */ bool m_have_pruned = false; + /** True if any zstd-compressed block or undo record has ever been written, which a build without zstd cannot read. */ + bool m_have_compressed_records = false; + //! Check whether the block associated with this index entry is pruned or not. bool IsBlockPruned(const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); @@ -253,6 +265,10 @@ fs::path GetBlockPosFilename(const FlatFilePos& pos); void UnlinkPrunedFiles(const std::set& setFilesToPrune); /** Functions for disk access for blocks */ +/** Position of the 8-byte header of the record whose payload starts at pos, null when pos leaves no room for one */ +FlatFilePos RecordHeaderPos(const FlatFilePos& pos); +/** Read the header of the record the file is positioned at and return a reader for its payload, decompressing it when flagged */ +util::ZstdFileReader OpenRecordPayload(AutoFile& filein); std::optional ReadBlockFromDisk(CBlock& block, const FlatFilePos& pos, const Consensus::Params& consensusParams); bool ReadBlockFromDisk(CBlock& block, const CBlockIndex* pindex, const Consensus::Params& consensusParams); diff --git a/src/test/blockmanager_tests.cpp b/src/test/blockmanager_tests.cpp index f70cbc2c2ba3..fec22104e2a9 100644 --- a/src/test/blockmanager_tests.cpp +++ b/src/test/blockmanager_tests.cpp @@ -3,18 +3,53 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include +#include +#include +#include #include #include +#include +#include