#include "BTC.h" #include "LRUCache.h" #include "Storage.h" #include "rocksdb/db.h" #include "rocksdb/iterator.h" #include "rocksdb/options.h" #include "rocksdb/slice.h" #include #include #include #include #include #include #include #include #include DatabaseError::~DatabaseError(){} // weak vtable warning suppression DatabaseSerializationError::~DatabaseSerializationError() {} // weak vtable warning suppression DatabaseFormatError::~DatabaseFormatError() {} // weak vtable warning suppression namespace { /// Encapsulates the 'meta' db table struct Meta { uint32_t magic = 0xf33db33f, version = 0x1; QString chain; ///< "test", "main", etc }; // some database keys we use -- todo: if this grows large, move it elsewhere static const rocksdb::Slice kMeta{"meta"}, kNumHeaders{"num_headers"}; static constexpr size_t MAX_HEADERS = 100000000; // 100 mln max headers for now. /// NOTE: the byte array should live as long as the slice does. slice is just a weak ref into the byte array inline rocksdb::Slice ToSlice(const QByteArray &ba) { return rocksdb::Slice(ba.constData(), size_t(ba.size())); } /// NOTE: The slice should live as long as the returned QByteArray does. The QByteArray is a weak pointer into the slice! inline QByteArray FromSlice(const rocksdb::Slice &s) { return QByteArray::fromRawData(s.data(), int(s.size())); } // serialize/deser -- for basic types we use QDataStream, but we also have specializations at the end of this file template QByteArray Serialize(const Type & n) { QByteArray ba; QDataStream ds(&ba, QIODevice::WriteOnly|QIODevice::Truncate); ds << n; return ba; } template Type Deserialize(const QByteArray &ba, bool *ok = nullptr) { QDataStream ds(ba); Type ret{}; ds >> ret; if (ok) *ok = ds.status() == QDataStream::Status::Ok; return ret; } /// Serialize a simple value such as an int directly, without using the space overhead that QDataStream imposes. /// This is less safe but is more compact since the bytes of the passed-in value are written directly to the /// returned QByteArray, without any encapsulation. Note that use of this mechanism makes all data in the database /// no longer platform-neutral, which is ok. The presumption is users can re-synch their DB if switching /// architectures. template && !std::is_pointer_v, int> = 0> QByteArray SerializeScalar(const Scalar & s) { return QByteArray(reinterpret_cast(&s), sizeof(s)); } /// Inverse of above. Pass in an optional 'pos' pointer if you wish to continue reading raw scalars from the same /// QByteArray during subsequent calls to this template function. *ok, if specified, is set to false if we ran off /// the QByteArray's bounds, and a default-constructed value of 'Scalar' is returned. No other safety checking is /// done. On successful deserialization of the scalar, *pos (if specified) is updated to point just past the /// last byte of the successuflly converted item. On failure, *pos is always set to point past the end of the /// QByteArray. template && !std::is_pointer_v, int> = 0> Scalar DeserializeScalar(const QByteArray &ba, bool *ok = nullptr, int *pos_out = nullptr) { Scalar ret{}; int dummy = 0; int & pos = pos_out ? *pos_out : dummy; if (pos >= 0 && pos + int(sizeof(ret)) <= ba.size()) { if (ok) *ok = true; ret = *reinterpret_cast(ba.data() + pos); pos += sizeof(ret); } else { if (ok) *ok = false; pos = ba.size(); } return ret; } // specializations template <> QByteArray Serialize(const Meta &); template <> Meta Deserialize(const QByteArray &, bool *); template <> QByteArray Serialize(const CompactTXO &); template <> CompactTXO Deserialize(const QByteArray &, bool *); template <> QByteArray Serialize(const TXOInfo &); template <> TXOInfo Deserialize(const QByteArray &, bool *); /// DB read/write helpers /// NOTE: these may throw DatabaseError /// If missingOk=false, then the returned optional is guaranteed to have a value if this function returns without throwing. /// If missingOk=true, then if there was no other database error and the key was not found, the returned optional !has_value() /// /// Template arg "safeScalar", if true, will deserialize even scalar int, float, etc data using the Deserialize<> /// function (uses QDataStream, is platform neutral, but is slightly slower). If false, we will use the /// DeserializeScalar<> fast function for scalars such as ints. It's important to read from the DB in the same /// 'safeScalar' mode as was written! template std::optional GenericDBGet(rocksdb::DB *db, const rocksdb::Slice & key, bool missingOk = false, const QString & errorMsgPrefix = QString(), ///< used to specify a custom error message in the thrown exception bool acceptExtraBytesAtEndOfData = false, const rocksdb::ReadOptions & ropts = rocksdb::ReadOptions()) ///< if true, we are ok with extra unparsed bytes in data. otherwise we throw. (this check is only done for !safeScalar mode on basic types) { rocksdb::PinnableSlice datum; std::optional ret; if (UNLIKELY(!db)) throw InternalError("GenericDBGet was passed a null pointer!"); const auto status = db->Get(ropts, db->DefaultColumnFamily(), key, &datum); if (missingOk && status.IsNotFound()) { return ret; // optional will not has_value() to indicate missing key } else if (!status.ok()) { throw DatabaseError(QString("%1: %2") .arg(!errorMsgPrefix.isEmpty() ? errorMsgPrefix : "Error reading a key from the db") .arg(status.ToString().c_str())); } else { // ok status if constexpr (std::is_base_of_v >) { static_assert (!safeScalar, "safeScalar=true mode is not supported for QByteArrays (it only is useful for scalar types)" ); // special compile-time case for QByteArray subclasses -- return a deep copy of the data bytes directly. // TODO: figure out a way to do this without the 1 extra copy! (PinnableSlice -> ret). ret.emplace( reinterpret_cast(datum.data()), QByteArray::size_type(datum.size()) ); } else if constexpr (!safeScalar && std::is_scalar_v && !std::is_pointer_v) { if (!acceptExtraBytesAtEndOfData && datum.size() > sizeof(RetType)) { // reject extra stuff at end of data stream throw DatabaseFormatError(QString("%1: Extra bytes at the end of data") .arg(!errorMsgPrefix.isEmpty() ? errorMsgPrefix : "Database format error")); } bool ok; ret.emplace( DeserializeScalar(FromSlice(datum), &ok) ); if (!ok) { throw DatabaseSerializationError( QString("%1: Key was retrieved ok, but data could not be deserialized as a scalar '%2'") .arg(!errorMsgPrefix.isEmpty() ? errorMsgPrefix : "Error deserializing a scalar from db") .arg(typeid (RetType).name())); } } else { if (UNLIKELY(acceptExtraBytesAtEndOfData)) Debug() << "Warning: Caller misuse of function '" << __FUNCTION__ << "'. 'acceptExtraBytesAtEndOfData=true' is ignored when deserializing using QDataStream."; bool ok; ret.emplace( Deserialize(FromSlice(datum), &ok) ); if (!ok) { throw DatabaseSerializationError( QString("%1: Key was retrieved ok, but data could not be deserialized") .arg(!errorMsgPrefix.isEmpty() ? errorMsgPrefix : "Error deserializing an object from db")); } } } return ret; } /// Conveneience for above with the missingOk flag set to false. Will always throw or return a real value. template RetType GenericDBGetFailIfMissing(rocksdb::DB * db, const rocksdb::Slice &k, const QString &errMsgPrefix = QString(), bool extraDataOk = false, const rocksdb::ReadOptions & ropts = rocksdb::ReadOptions()) { return GenericDBGet(db, k, false, errMsgPrefix, extraDataOk, ropts).value(); } // some helper data structs struct BlkInfo { TxNum txNum0 = 0; unsigned nTx = 0, nIns = 0, nOuts = 0; }; } struct Storage::Pvt { Meta meta; Lock metaLock; Headers headers; std::pair lastHeaderSaved; ///< remember the last header saved to disk, so subsequent saves leave off from this index RWLock headersLock; std::atomic> pendingSaves{0}; struct RocksDBs { std::unique_ptr meta, headers, txnums, blkinfo, utxoset, shist; } db; BTC::HeaderVerifier headerVerifier; Lock headerVerifierLock; std::atomic txNumNext{0}; UTXOSet utxoSet; RWLock utxoSetLock; std::unordered_set utxoSetAdditionsUnsaved, utxoSetDeletionsUnsaved; bool hasSavedUtxoSet = false; std::vector blkInfos; std::atomic saveInterval = 100000, compactInterval = 30000, unsavedCt = 0, uncompactedCt = 0; static constexpr size_t nCacheMax = 2000000, nCacheElasticity = 1000000; LRU::Cache lruNum2Hash{nCacheMax, nCacheElasticity}; LRU::Cache lruHash2Num{nCacheMax, nCacheElasticity}; }; Storage::Storage(const std::shared_ptr & options) : Mgr(nullptr), options(options), p(new Pvt) { setObjectName("Storage"); _thread.setObjectName(objectName()); } Storage::~Storage() { Debug("%s", __FUNCTION__); cleanup(); } void Storage::startup() { { // open all db's ... rocksdb::Options opts; // Optimize RocksDB. This is the easiest way to get RocksDB to perform well opts.IncreaseParallelism(int(Util::getNPhysicalProcessors())); opts.OptimizeLevelStyleCompaction(); // create the DB if it's not already present opts.create_if_missing = true; opts.error_if_exists = false; //opts.max_open_files = 50; ///< testing -- seems this affects memory usage see: https://github.com/facebook/rocksdb/issues/4112 opts.keep_log_file_num = 5; // ?? opts.compression = rocksdb::CompressionType::kNoCompression; // for now we test without compression. TODO: characterize what is fastest and best.. using DBInfoTup = std::tuple &>; const std::list dbs2open = { { "meta", p->db.meta }, { "headers", p->db.headers }, { "txnums" , p->db.txnums }, { "blkinfo" , p->db.blkinfo }, { "utxoset", p->db.utxoset }, { "scripthash_history", p->db.shist }, }; const auto OpenDB = [this, &opts](const DBInfoTup &tup) { auto & [name, uptr] = tup; rocksdb::DB *db = nullptr; rocksdb::Status s; // try and open database const QString path = options->datadir + QDir::separator() + name; s = rocksdb::DB::Open(opts, path.toStdString(), &db); if (!s.ok() || !db) throw DatabaseError(QString("Error opening %1 database: %2 (path: %3)") .arg(name).arg(QString::fromStdString(s.ToString())).arg(path)); uptr.reset(db); }; // open all db's defined above for (auto & tup : dbs2open) OpenDB(tup); } // /open db's // load/check meta { Meta m_db; static const QString errMsg{"Incompatible database format -- delete the datadir and resynch. RocksDB error"}; if (auto opt = GenericDBGet(p->db.meta.get(), kMeta, true, errMsg); opt.has_value()) { m_db = opt.value(); if (m_db.magic != p->meta.magic || m_db.version != p->meta.version) { throw DatabaseFormatError(errMsg); } p->meta = m_db; Debug () << "Read meta from db ok"; } else { // ok, did not exist .. write a new one to db saveMeta_impl(); } } Log() << "Loading database ..."; // load headers -- may throw loadHeadersFromDB(); loadUTXOSetFromDB(); start(); // starts our thread } void Storage::cleanup() { stop(); // joins our thread if (p->unsavedCt) { Log() << "Saving unsaved data, please wait ..."; // if there is an unsavedCt, flag these as unsaved to check and save. Won't do much if they aren't really unsaved. save_impl(SaveItem::Blocks); } } unsigned Storage::saveInterval() const { return p->saveInterval; } void Storage::setSaveInterval(unsigned blocks) { p->saveInterval = blocks; p->unsavedCt = p->unsavedCt ? 1 : 0;} unsigned Storage::compactionInterval() const { return p->compactInterval; } void Storage::setCompactionInterval(unsigned blocks) { p->compactInterval = blocks; p->uncompactedCt = p->uncompactedCt ? 1 : 0; } auto Storage::stats() const -> Stats { // TODO ... return Stats(); } auto Storage::mutableHeaders() -> std::pair { return std::pair{ p->headers, p->headersLock }; } auto Storage::headers() const -> std::pair { return std::pair{ p->headers, p->headersLock }; } // Keep returned LockGuard in scope while you use the HeaderVerifier auto Storage::headerVerifier() -> std::pair { return std::pair( p->headerVerifier, p->headerVerifierLock ); } auto Storage::mutableUtxoSet() -> std::pair { return std::pair{ p->utxoSet, p->utxoSetLock }; } auto Storage::utxoSet() const -> std::pair { return std::pair{ p->utxoSet, p->utxoSetLock }; } QString Storage::getChain() const { LockGuard l(p->metaLock); return p->meta.chain; } void Storage::setChain(const QString &chain) { { LockGuard l(p->metaLock); p->meta.chain = chain; } save(SaveItem::Meta); } /// returns the "next" TxNum TxNum Storage::getTxNum() const { return p->txNumNext.load(); } // should this come from headersVerifier() instead? std::pair Storage::latestTip() const { std::pair ret; { auto [hdrs, lock] = headers(); ret.first = int(hdrs.size())-1; // -1 if no headers if (!hdrs.empty()) ret.second = BTC::HashRev(hdrs.back()); } return ret; } void Storage::save(SaveSpec typed_spec) { using IntType = decltype(p->pendingSaves.load()); // enqueue save on event loop if not previously enqueued (we know it was previously enqueued if the p->pendingSaves // atomic variable is not 0). if (const auto spec = IntType(typed_spec); ! p->pendingSaves.fetch_or(spec)) { QTimer::singleShot(0, this, [this]{save_impl();}); } } void Storage::save_impl(SaveSpec override) { if (const auto flags = SaveSpec(p->pendingSaves.exchange(0))|override; flags) { // atomic clear of flags, grab prev val try { if (flags & SaveItem::Blocks) { // utxo set using Set = decltype (p->utxoSetAdditionsUnsaved); Set unsavedAdditions, unsavedDeletions; UTXOSet setCopy; Headers hdrCopy; bool firstUtxoSave = false; { std::scoped_lock guard(p->headersLock, p->utxoSetLock); unsavedAdditions.swap(p->utxoSetAdditionsUnsaved); unsavedDeletions.swap(p->utxoSetDeletionsUnsaved); p->unsavedCt = 0; if (unsavedAdditions.size() || unsavedDeletions.size() || !p->hasSavedUtxoSet) { const auto t0 = Util::getTimeNS(); // DEBUG REMOVE ME setCopy = p->utxoSet; // <-- this copy is inefficient (takes on the order of seconds -- but it is preferable to holding the lock for potentially many seconds) if (!p->hasSavedUtxoSet) { p->hasSavedUtxoSet = true; firstUtxoSave = true; } // DEBUG REMOVE ME const auto elapsed = Util::getTimeNS() - t0; Debug() << "utxo copy took: " << QString::number(elapsed/1e6, 'f', 3) << " msec size: " << setCopy.size(); } if (const auto idx = p->lastHeaderSaved.first; !idx || idx+1 < p->headers.size()) { hdrCopy = p->headers; } } if (!setCopy.empty()) { if (firstUtxoSave) { // if first time save, we build the additions set manually here, since client code didn't // build it for us (as a performance saving measure) unsavedDeletions.clear(); for (auto it = setCopy.cbegin(); it != setCopy.cend(); ++it) { unsavedAdditions.emplace(it->first); } } saveUtxoUnsaved_impl(setCopy, unsavedAdditions, unsavedDeletions); } if (!hdrCopy.empty()) { saveHeaders_impl(hdrCopy); } } if (flags & SaveItem::Meta) { // Meta LockGuard l(p->metaLock); saveMeta_impl(); } } catch (const std::exception & e) { Fatal() << e.what(); // will abort app... } } } void Storage::saveMeta_impl() { if (!p->db.meta) return; if (auto status = p->db.meta->Put(rocksdb::WriteOptions(), kMeta, ToSlice(Serialize(p->meta))); !status.ok()) { throw DatabaseError("Failed to write meta to db"); } Debug() << "Wrote new metadata to db"; } void Storage::saveHeaders_impl(const Headers &h) { if (!p->db.headers) return; //Debug() << "Saving headers ..."; size_t start = 0; const auto t0 = Util::getTimeNS(); { // figure out where to resume from if (size_t idx = p->lastHeaderSaved.first; idx && idx < h.size() && p->lastHeaderSaved.second == h[idx]) { start = idx + 1; } if (start < h.size()) { Debug() << "Saving from height " << start; rocksdb::WriteBatch batch; for (size_t i = start; i < h.size(); ++i) { // headers are keyed off of uint32_t index (height) -- note we save the raw int value here without any QDataStream encapsulation if (auto stat = batch.Put(ToSlice(SerializeScalar(uint32_t(i))), ToSlice(h[i])); !stat.ok()) throw DatabaseError(QString("Error writing header %1: %2").arg(i).arg(stat.ToString().c_str())); } // save height to db *after* we successfully wrote all the headers if (auto stat = batch.Put(kNumHeaders, ToSlice(Serialize(uint32_t(h.size())))); !stat.ok()) throw DatabaseError(QString("Error writing header size key: %1").arg(stat.ToString().c_str())); auto opts = rocksdb::WriteOptions(); // uncomment all this stuff if we want to do fsync and other paranoia. turned off for now. //opts.sync = true; //opts.low_pri = true; if (auto stat = p->db.headers->Write(opts, &batch); !stat.ok()) throw DatabaseError(QString("Error writing headers: %1").arg(stat.ToString().c_str())); //auto fopts = rocksdb::FlushOptions(); //fopts.wait = true; fopts.allow_write_stall = true; //if (auto stat = p->db.headers->Flush(fopts); !stat.ok()) // throw DatabaseError(QString("Flush error while writing headers: %1").arg(stat.ToString().c_str())); p->lastHeaderSaved = { h.size()-1, h.back() }; // remember last } } const auto elapsed = Util::getTimeNS() - t0; const auto ct = (h.size()-start); if (ct) Debug() << "Wrote " << ct << " " << Util::Pluralize("header", ct) << " to db in " << QString::number(elapsed/1e6, 'f', 3) << " msec"; } void Storage::saveUtxoUnsaved_impl(const UTXOSet &set, const std::unordered_set &adds, const std::unordered_set &dels) { if (!p->db.utxoset || !(adds.size()+dels.size())) return; //Debug() << "Saving utxo set ..."; const auto t0 = Util::getTimeNS(); rocksdb::DB *db = p->db.utxoset.get(); const auto wopts = rocksdb::WriteOptions(); // process deletions for (const auto & txo : dels) { const auto txNum = txNumForHash(txo.prevoutHash, true).value(); // may throw CompactTXO ctxo(txNum, txo.prevoutN); if (const auto st = db->Delete(wopts, ToSlice(ctxo.toBytesCpy())); !st.ok() /*&& !st.IsNotFound()*/) throw DatabaseError(QString("Error deleting a utxo from the db: %1").arg(QString::fromStdString(st.ToString()))); } // process additions { rocksdb::WriteBatch batch; for (const auto & txo : adds) { const auto txNum = txNumForHash(txo.prevoutHash, true).value(); // may throw CompactTXO ctxo(txNum, txo.prevoutN); const auto it = set.find(txo); if (it == set.end()) throw InternalError(QString("Missing txo %1 from utxo set .. cannot save!").arg(txo.toString())); const auto & info = it->second; if (const auto st = batch.Put(ToSlice(ctxo.toBytesCpy()), ToSlice(Serialize(info))); !st.ok()) throw DatabaseError(QString("Error in writeBatch for txo %1: %2").arg(txo.toString()).arg(QString::fromStdString(st.ToString()))); } if (const auto st = db->Write(wopts, &batch); !st.ok()) throw DatabaseError(QString("Error writing batch txo additions: %1").arg(QString::fromStdString(st.ToString()))); } const auto elapsed = Util::getTimeNS() - t0; Debug() << "Wrote utxo set " << adds.size() << " adds, " << dels.size() << " dels to db in " << QString::number(elapsed/1e6, 'f', 3) << " msec"; } void Storage::loadHeadersFromDB() { FatalAssert(!!p->db.headers) << __FUNCTION__ << ": Headers db is not open"; Debug() << "Loading headers ..."; uint32_t num = 0; const auto t0 = Util::getTimeNS(); { auto * const db = p->db.headers.get(); num = GenericDBGet(db, kNumHeaders, true, "Error reading header count from database").value_or(0); // missing ok if (num > MAX_HEADERS) throw DatabaseFormatError(QString("Header count (%1) in database exceeds MAX_HEADERS! This is likely due to" " a database format mistmatch. Delete the datadir and resynch it.") .arg(num)); Headers h; h.reserve(num); const QString errMsg("Error retrieving header from db"); const int hsz = BTC::GetBlockHeaderSize(); // read db for (uint32_t i = 0; i < num; ++i) { // guaranteed to return a value or throw auto bytes = GenericDBGetFailIfMissing(db, ToSlice(SerializeScalar(uint32_t(i))), errMsg); if (UNLIKELY(bytes.size() != hsz)) throw DatabaseFormatError(QString("Error reading header %1, wrong size: %2").arg(i).arg(bytes.size())); h.emplace_back(std::move(bytes)); } // verify headers: hashPrevBlock must match what we actually read from db if (num) { Debug() << "Verifying " << num << " " << Util::Pluralize("header", num) << " ..."; auto [verif, lock] = headerVerifier(); QString err; for (unsigned i = 0; i < num; ++i) { // verify headers hash chains match by checking hashPrevBlock versus actual previous hash. // we use the helper functor HeaderVerifier for this. if (!verif(h[i], &err)) throw DatabaseError(QString("%1. Possible databaase corruption. Delete the datadir and resynch.").arg(err)); } p->lastHeaderSaved = verif.lastHeaderProcessed(); // remember last } // locked until scope end... auto [headers, lock] = mutableHeaders(); headers.swap(h); } if (num) { const auto elapsed = Util::getTimeNS(); Debug() << "Read & verified " << num << " " << Util::Pluralize("header", num) << " from db in " << QString::number((elapsed-t0)/1e6, 'f', 3) << " msec"; } } void Storage::loadUTXOSetFromDB() { FatalAssert(!!p->db.utxoset) << __FUNCTION__ << ": Utxo set db is not open"; Debug() << "Loading utxo set ..."; const auto t0 = Util::getTimeNS(); { // the purpose of this map is to ensure that all txid's in app memory share the same implicitly shared QByteArray std::unordered_set hashXSeen; robin_hood::unordered_flat_map num2hash; // this also acts as a cache as well as ensuring all txhashes share the same QByteArray const int currentHeight = latestTip().first; std::unique_ptr iter(p->db.utxoset->NewIterator(rocksdb::ReadOptions())); if (!iter) throw DatabaseError("Unable to obtain an iterator to the utxo set db"); iter->SeekToFirst(); unsigned ctr = 0; size_t savings = 0, cost = 0; for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { const auto ctxo = CompactTXO::fromBytes(FromSlice(iter->key())); if (!ctxo.isValid()) { throw DatabaseSerializationError("Read an invalid txo from the utxo set database." " This may be due to a database format mismatch." " Delete the datadir and resynch it."); } TxHash hash; if (auto it = num2hash.find(ctxo.txNum()); it != num2hash.end()) { hash = it->second; // ensure implicitly shared savings += size_t(hash.length()); } else { // below line reads data from the db. but we specify to not store the results in our lru cache.. const auto ohash = hashForTxNum(ctxo.txNum(), false, nullptr, true); if (!ohash.has_value()) throw DatabaseFormatError("A txo is missing its txhash metadata in the db." " This may be due to a database format mismatch." " Delete the datadir and resynch it."); num2hash.emplace(ctxo.txNum(), hash = ohash.value()); hash = ohash.value(); cost += size_t(hash.length()); } if (hash.length() != HashLen) throw DatabaseFormatError("A txo is missing has corrupted txhash metadata in the db." " Delete the datadir and resynch it."); auto info = TXOInfo::fromBytes(FromSlice(iter->value())); if (!info.isValid()) throw DatabaseSerializationError(QString("Txo %1:%2 has invalid metadata in the db." " This may be due to a database format mismatch." " Delete the datadir and resynch it.") .arg(QString(hash.toHex())) .arg(ctxo.N())); if (info.confirmedHeight.has_value() && int(info.confirmedHeight.value()) > currentHeight) { // TODO: reorg? Inconsisent db? FIXME Debug() << "Ignoring txo " << hash << ":" << ctxo.N() << " at height: " << info.confirmedHeight.value() << " > current height: " << currentHeight; continue; } if (auto it = hashXSeen.find(info.hashX); it != hashXSeen.end()) { info.hashX = *it; // make sure they all implicitly share the same hashx savings += size_t(info.hashX.length()); } else { hashXSeen.emplace(info.hashX); cost += size_t(info.hashX.length()); } p->utxoSet[TXO{hash, ctxo.N()}] = info; cost += sizeof(TXO) + sizeof(TXOInfo); if (0 == ++ctr % 100000) { *(0 == ctr % 1000000 ? std::make_unique() : std::make_unique()) << "Read " << ctr << " utxos ..."; } } Log() << "UTXO set " << QString::number(cost/1e6, 'f', 3) << " MiB in " << p->utxoSet.size() << " txos"; Debug() << "Mem savings: " << QString::number(savings/1e6, 'f', 3) << " MiB" << " (" << "num2hash: " << num2hash.size() << " " << " hashXSeen: " << hashXSeen.size() << ")"; } const auto num = p->utxoSet.size(); if (num) { const auto elapsed = Util::getTimeNS(); p->hasSavedUtxoSet = true; // remember that we had a utxo set in db -- this affects how/if we build the "difference set" Debug() << "Read " << num << " " << Util::Pluralize("utxo", num) << " from db in " << QString::number((elapsed-t0)/1e6, 'f', 3) << " msec"; } } QString Storage::addBlock(PreProcessedBlockPtr ppb, unsigned nReserve) { assert(bool(ppb) && bool(p)); QString errRet; std::scoped_lock guard(p->headerVerifierLock, p->headersLock, p->utxoSetLock); // take all locks now.. todo: add more locks here const auto verifUndo = p->headerVerifier; // keep a copy for undo purposes in case this fails try { // Verify header chain makes sense (by checking hashes, using the shared header verifier) QByteArray rawHeader; { QString errMsg; if (!p->headerVerifier(ppb->header, &errMsg) ) { // XXX possible reorg point. FIXME TODO // reorg here? TODO: deal with this better. throw Exception(errMsg); } // save raw header back to our buffer rawHeader = p->headerVerifier.lastHeaderProcessed().second; } // set up batch update of txnums -- we update the p->nextTxNum after all of this succeeds.. //auto t0 = Util::getTimeNS(); { rocksdb::WriteBatch batch; const TxNum txNum0 = p->txNumNext; for (size_t i = 0; i < ppb->txInfos.size(); ++i) { const TxNum txnum = txNum0 + i; const TxHash & hash = ppb->txInfos[i].hash; const auto hashSlice = ToSlice(hash); const auto txNumBytes = SerializeScalar(txnum); // txnums are keyed off of uint64_t txNum -- note we save the raw uint64 value here without any QDataStream encapsulation if (auto stat = batch.Put(ToSlice(txNumBytes), hashSlice); !stat.ok()) throw DatabaseError(QString("Error writing txNum -> txHash for txNum %1: %2").arg(txNum0 + i).arg(QString::fromStdString(stat.ToString()))); if (auto stat = batch.Put(hashSlice, ToSlice(txNumBytes)); !stat.ok()) throw DatabaseError(QString("Error writing txHash -> txNum for txNum %1: %2").arg(txNum0 + i).arg(QString::fromStdString(stat.ToString()))); } if (auto stat = p->db.txnums->Write(rocksdb::WriteOptions(), &batch); !stat.ok()) throw DatabaseError(QString("Error writing txNums batch: %1").arg(QString::fromStdString(stat.ToString()))); } //auto elapsed = Util::getTimeNS() - t0; //Debug() << "Wrote " << ppb->txInfos.size() << " new TxNums to db in " << QString::number(elapsed/1e6, 'f', 3) << " msec"; constexpr bool debugPrt = false; // update utxoSet { // add outputs for (const auto & [hashX, ag] : ppb->hashXAggregated) { std::unordered_set outputsSpentInSameBlock; for (const auto iidx : ag.ins) { if (const auto & opt = ppb->inputs[iidx].parentTxOutIdx; opt.has_value()) outputsSpentInSameBlock.insert(opt.value()); } for (const auto oidx : ag.outs) { if (outputsSpentInSameBlock.count(oidx)) { if constexpr (debugPrt) Debug() << "Skipping output #: " << oidx << " (was spent in same block)"; continue; } const auto & out = ppb->outputs[oidx]; const TxHash & hash = ppb->txInfos[out.txIdx].hash; TXOInfo info; info.hashX = hashX; info.amount = out.amount; info.confirmedHeight = ppb->height; const TXO txo{ hash, out.outN }; p->utxoSet[txo] = info; if (p->hasSavedUtxoSet) p->utxoSetAdditionsUnsaved.insert(txo); if constexpr (debugPrt) Debug() << "Added txo: " << txo.toString() << " (txid: " << hash.toHex() << " height: " << ppb->height << ") " << " amount: " << info.amount.ToString() << " for HashX: " << info.hashX.toHex(); } } // add spends (process inputs) std::unordered_set newHashXInputsResolved; unsigned inum = 0; for (auto & in : ppb->inputs) { if (!inum) { // coinbase.. skip } else if (in.parentTxOutIdx.has_value()) { // was an input that was spent in this block so it's ok to skip.. we never added it to utxo set if constexpr (debugPrt) Debug() << "Skipping input " << in.prevoutHash.toHex() << ":" << in.prevoutN << ", spent in this block (output # " << in.parentTxOutIdx.value() << ")"; } else if (const auto it = p->utxoSet.find(TXO{in.prevoutHash, in.prevoutN}); it != p->utxoSet.end()) { const auto & info = it->second; if (info.confirmedHeight.has_value() && info.confirmedHeight.value() != ppb->height) { // was a prevout from a previos block.. so the ppb didn't have it in the touched set.. // mark the spend as having touched this hashX for this ppb now. ppb->hashXAggregated[info.hashX].ins.emplace_back(inum); in.prevoutHash = it->first.prevoutHash; // ensure shallow/shared copy of QByteArray in inputs newHashXInputsResolved.insert(info.hashX); } if constexpr (debugPrt) { const auto dbgTxIdHex = ppb->txHashForInputIdx(inum).toHex(); Debug() << "Spent " << it->first.toString() << " amount: " << it->second.amount.ToString() << " in txid: " << dbgTxIdHex << " height: " << ppb->height << " input number: " << ppb->numForInputIdx(inum).value_or(0xffff) << " HashX: " << it->second.hashX.toHex(); } if (p->hasSavedUtxoSet) { if (auto it2 = p->utxoSetAdditionsUnsaved.find(it->first); it2 != p->utxoSetAdditionsUnsaved.end()) { // was in unsaved additions, no need to add it to deletions p->utxoSetAdditionsUnsaved.erase(it2); } else { // was not in unsaved additions.. add it to deletions p->utxoSetDeletionsUnsaved.insert(it->first); } } p->utxoSet.erase(it); // invalidate txo (spend it) by removing from map } else { QString s; { const auto dbgTxIdHex = ppb->txHashForInputIdx(inum).toHex(); QTextStream ts(&s); ts << "Failed to spend: " << in.prevoutHash.toHex() << ":" << in.prevoutN << " (spending txid: " << dbgTxIdHex << ")"; } throw Exception(s); } ++inum; } // sort and shrink_to_fit new hashX inputs added for (const auto & hashX : newHashXInputsResolved) { auto & ag = ppb->hashXAggregated[hashX]; std::sort(ag.ins.begin(), ag.ins.end()); // make sure they are sorted ag.ins.shrink_to_fit(); } if constexpr (debugPrt) Debug() << "utxoset size: " << p->utxoSet.size() << " block: " << ppb->height; } if (nReserve) { if (const auto size = p->headers.size(); size + nReserve < p->headers.capacity()) p->headers.reserve(size + nReserve); // reserve space for new headers in 1 go to save on copying if (const auto size = p->blkInfos.size(); size + nReserve < p->blkInfos.capacity()) p->blkInfos.reserve(size + nReserve); // reserve space for new blkinfos in 1 go to save on copying } // save blkInfos p->blkInfos.emplace_back(BlkInfo{ p->txNumNext, // .txNum0 unsigned(ppb->txInfos.size()), unsigned(ppb->inputs.size()), unsigned(ppb->outputs.size()) }); // append header (todo: see about reserving suitable chunks) p->headers.emplace_back(rawHeader); // update txNum after everything checks out p->txNumNext += ppb->txInfos.size(); // enqueue a save every saveInterval blocks (note that if saveInterval is 0, we never auto-save) if (++p->unsavedCt == p->saveInterval && p->saveInterval) { save(SaveItem::Blocks); } // enqueue compaction of utxo set every compactInterval blocks (if 0, do not auto-compact) if (++p->uncompactedCt == p->compactInterval && p->compactInterval) { // schedule a compaction -- but be sure to do it our storage thread. // also.. we "own" the utxo set to it's probably safer to delegate the work to our thread. Util::AsyncOnObject(this, [this] { compactifyUtxoSet(); }); } } catch (const std::exception & e) { errRet = e.what(); p->headerVerifier = verifUndo; // undo header verifier state } return errRet; } // some helpers for TxNum -- these may throw DatabaseError std::optional Storage::txNumForHash(const TxHash &h, bool throwIfMissing, bool *wasCached, bool skipCache) { std::optional ret; if (!skipCache) ret = p->lruHash2Num.tryGet(h); if (ret.has_value()) { // save was cached flat if (wasCached) *wasCached = true; // touch the other cache to refresh its lru list (if it still has the item. that is) p->lruNum2Hash.tryGet(ret.value()); return ret; // cached, return it } else if (wasCached) *wasCached = false; // save flag if caller is interested static const QString kErrMsg ("Error reading txNumForHash from db"); ret = GenericDBGet(p->db.txnums.get(), ToSlice(h), !throwIfMissing, kErrMsg); if (!skipCache && ret.has_value()) { // save in cache p->lruHash2Num.insert(h, ret.value()); } return ret; } std::optional Storage::hashForTxNum(TxNum n, bool throwIfMissing, bool *wasCached, bool skipCache) { std::optional ret; if (!skipCache) ret = p->lruNum2Hash.tryGet(n); if (ret.has_value()) { if (wasCached) *wasCached = true; // touch the other cache to refresh its lru list (if it still has the item, that is) p->lruHash2Num.tryGet(ret.value()); return ret; } else if (wasCached) *wasCached = false; static const QString kErrMsg ("Error reading hashForTxNum from db"); ret = GenericDBGet(p->db.txnums.get(), ToSlice(SerializeScalar(n)), !throwIfMissing, kErrMsg); if (!skipCache && ret.has_value()) { // save in cache p->lruNum2Hash.insert(n, ret.value()); } return ret; } size_t Storage::compactifyUtxoSet(bool force) { if (!p->uncompactedCt && !force) return 0; // the purpose of this map is to ensure that all txid's in app memory share the same implicitly shared QByteArray using Empty = int8_t; constexpr Empty empty{}; robin_hood::unordered_flat_map hxSet; robin_hood::unordered_flat_map txSet; size_t savings = 0; qint64 t0 = 0, elapsed = 0; { ExclusiveLockGuard g(p->utxoSetLock); Debug() << "Compacting utxo set ..."; t0 = Util::getTimeNS(); UTXOSet & set(p->utxoSet); for (auto it = set.begin(); it != set.end(); ++it) { if (auto it2 = txSet.find(it->first.prevoutHash); it2 != txSet.end()) { // found -- implicitly share TxHash it->first.prevoutHash = it2->first; savings += size_t(it2->first.length()); } else { // first instance, store txSet.emplace(it->first.prevoutHash, empty); } if (auto it2 = hxSet.find(it->second.hashX); it2 != hxSet.end()) { // found -- implicitly share HashX it->second.hashX = it2->first; savings += size_t(it2->first.length()); } else { // first instance, store hxSet.emplace(it->second.hashX, empty); } } set.rehash(set.size()); p->uncompactedCt = 0; elapsed = Util::getTimeNS() - t0; } Debug() << "Compatcted utxo set: maybe saved " << QString::number(savings/1e6, 'f', 3) << " MiB in " << QString::number(elapsed/1e6, 'f', 3) << " msec"; return savings; } namespace { // specializations of Serialize/Deserialize template <> QByteArray Serialize(const Meta &m) { QByteArray ba; { QDataStream ds(&ba, QIODevice::WriteOnly|QIODevice::Truncate); // we serialize the 'magic' value as a simple scalar as a sort of endian check for the DB ds << SerializeScalar(m.magic) << m.version << m.chain; } return ba; } template <> Meta Deserialize(const QByteArray &ba, bool *ok_ptr) { bool dummy; bool &ok (ok_ptr ? *ok_ptr : dummy); ok = false; Meta m{0, 0, {}}; { QDataStream ds(ba); QByteArray magicBytes; ds >> magicBytes; // read magic as raw bytes. if ((ok = ds.status() == QDataStream::Status::Ok)) { m.magic = DeserializeScalar(magicBytes, &ok); if (ok) { ds >> m.version >> m.chain; ok = ds.status() == QDataStream::Status::Ok; } } } return m; } template <> QByteArray Serialize [[maybe_unused]] (const CompactTXO &ctxo) { return ctxo.toBytesCpy(); } template <> CompactTXO Deserialize [[maybe_unused]] (const QByteArray &ba, bool *ok) { CompactTXO ret = CompactTXO::fromBytes(ba); if (ok) *ok = ret.isValid(); return ret; } template <> QByteArray Serialize(const TXOInfo &inf) { return inf.toBytes(); } template <> TXOInfo Deserialize [[maybe_unused]] (const QByteArray &ba, bool *ok) { TXOInfo ret = TXOInfo::fromBytes(ba); if (ok) *ok = ret.isValid(); return ret; } }