2019-11-29 14:42:17 +02:00
# include "BTC.h"
2019-12-08 02:36:46 +02:00
# include "LRUCache.h"
2019-11-29 14:42:17 +02:00
# include "Storage.h"
# include "rocksdb/db.h"
2019-12-08 01:00:40 +02:00
# include "rocksdb/iterator.h"
2019-11-29 14:42:17 +02:00
# include "rocksdb/options.h"
# include "rocksdb/slice.h"
# include <QByteArray>
# include <QDir>
# include <atomic>
2019-12-07 20:34:19 +02:00
# include <list>
2019-12-03 15:41:09 +02:00
# include <optional>
2019-11-29 14:42:17 +02:00
# include <shared_mutex>
2019-12-07 20:34:19 +02:00
# include <tuple>
2019-11-29 14:42:17 +02:00
# include <type_traits>
# include <vector>
2019-12-03 15:41:09 +02:00
DatabaseError : : ~ DatabaseError ( ) { } // weak vtable warning suppression
DatabaseSerializationError : : ~ DatabaseSerializationError ( ) { } // weak vtable warning suppression
DatabaseFormatError : : ~ DatabaseFormatError ( ) { } // weak vtable warning suppression
2019-11-29 14:42:17 +02:00
namespace {
/// Encapsulates the 'meta' db table
struct Meta {
uint32_t magic = 0xf33db33f , version = 0x1 ;
2019-11-30 01:33:27 +02:00
QString chain ; ///< "test", "main", etc
2019-11-29 14:42:17 +02:00
} ;
// 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.
2019-12-03 15:41:09 +02:00
/// NOTE: the byte array should live as long as the slice does. slice is just a weak ref into the byte array
2019-11-29 14:42:17 +02:00
inline rocksdb : : Slice ToSlice ( const QByteArray & ba ) { return rocksdb : : Slice ( ba . constData ( ) , size_t ( ba . size ( ) ) ) ; }
2019-12-03 15:41:09 +02:00
/// NOTE: The slice should live as long as the returned QByteArray does. The QByteArray is a weak pointer into the slice!
2019-11-29 14:42:17 +02:00
inline QByteArray FromSlice ( const rocksdb : : Slice & s ) { return QByteArray : : fromRawData ( s . data ( ) , int ( s . size ( ) ) ) ; }
2019-11-29 17:11:23 +02:00
// serialize/deser -- for basic types we use QDataStream, but we also have specializations at the end of this file
template < typename Type >
QByteArray Serialize ( const Type & n ) {
2019-11-29 14:42:17 +02:00
QByteArray ba ;
QDataStream ds ( & ba , QIODevice : : WriteOnly | QIODevice : : Truncate ) ;
ds < < n ;
return ba ;
}
2019-11-29 17:11:23 +02:00
template < typename Type >
Type Deserialize ( const QByteArray & ba , bool * ok = nullptr ) {
2019-11-29 14:42:17 +02:00
QDataStream ds ( ba ) ;
2019-11-29 17:11:23 +02:00
Type ret { } ;
2019-11-29 14:42:17 +02:00
ds > > ret ;
if ( ok )
* ok = ds . status ( ) = = QDataStream : : Status : : Ok ;
return ret ;
}
2019-12-03 06:38:19 +02:00
/// 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 < typename Scalar ,
std : : enable_if_t < std : : is_scalar_v < Scalar > & & ! std : : is_pointer_v < Scalar > , int > = 0 >
QByteArray SerializeScalar ( const Scalar & s ) {
return QByteArray ( reinterpret_cast < const char * > ( & 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 < typename Scalar ,
std : : enable_if_t < std : : is_scalar_v < Scalar > & & ! std : : is_pointer_v < Scalar > , 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 < const Scalar * > ( ba . data ( ) + pos ) ;
pos + = sizeof ( ret ) ;
} else {
if ( ok ) * ok = false ;
pos = ba . size ( ) ;
}
return ret ;
}
2019-11-29 14:42:17 +02:00
// specializations
template < > QByteArray Serialize ( const Meta & ) ;
template < > Meta Deserialize ( const QByteArray & , bool * ) ;
2019-12-07 23:30:07 +02:00
template < > QByteArray Serialize ( const CompactTXO & ) ;
template < > CompactTXO Deserialize ( const QByteArray & , bool * ) ;
template < > QByteArray Serialize ( const TXOInfo & ) ;
template < > TXOInfo Deserialize ( const QByteArray & , bool * ) ;
2019-12-03 13:57:02 +02:00
2019-12-03 15:41:09 +02:00
/// 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 < typename RetType , bool safeScalar = false >
std : : optional < RetType > 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
2019-12-08 02:36:46 +02:00
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)
2019-12-03 13:57:02 +02:00
{
2019-12-03 15:41:09 +02:00
rocksdb : : PinnableSlice datum ;
2019-12-03 13:57:02 +02:00
std : : optional < RetType > ret ;
2019-12-03 15:41:09 +02:00
if ( UNLIKELY ( ! db ) ) throw InternalError ( " GenericDBGet was passed a null pointer! " ) ;
2019-12-08 02:36:46 +02:00
const auto status = db - > Get ( ropts , db - > DefaultColumnFamily ( ) , key , & datum ) ;
2019-12-03 13:57:02 +02:00
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
2019-12-03 15:41:09 +02:00
if constexpr ( std : : is_base_of_v < QByteArray , std : : remove_cv_t < RetType > > ) {
2019-12-03 16:02:53 +02:00
static_assert ( ! safeScalar , " safeScalar=true mode is not supported for QByteArrays (it only is useful for scalar types) " ) ;
2019-12-03 15:41:09 +02:00
// special compile-time case for QByteArray subclasses -- return a deep copy of the data bytes directly.
2019-12-03 15:59:36 +02:00
// TODO: figure out a way to do this without the 1 extra copy! (PinnableSlice -> ret).
2019-12-03 19:09:38 +02:00
ret . emplace ( reinterpret_cast < const char * > ( datum . data ( ) ) , QByteArray : : size_type ( datum . size ( ) ) ) ;
2019-12-03 15:41:09 +02:00
} else if constexpr ( ! safeScalar & & std : : is_scalar_v < RetType > & & ! std : : is_pointer_v < RetType > ) {
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 " ) ) ;
}
2019-12-03 13:57:02 +02:00
bool ok ;
2019-12-03 19:09:38 +02:00
ret . emplace ( DeserializeScalar < RetType > ( FromSlice ( datum ) , & ok ) ) ;
2019-12-03 13:57:02 +02:00
if ( ! ok ) {
2019-12-03 15:41:09 +02:00
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 ( ) ) ) ;
2019-12-03 13:57:02 +02:00
}
} else {
2019-12-03 19:09:38 +02:00
if ( UNLIKELY ( acceptExtraBytesAtEndOfData ) )
Debug ( ) < < " Warning: Caller misuse of function ' " < < __FUNCTION__
< < " '. 'acceptExtraBytesAtEndOfData=true' is ignored when deserializing using QDataStream. " ;
2019-12-03 13:57:02 +02:00
bool ok ;
2019-12-03 19:09:38 +02:00
ret . emplace ( Deserialize < RetType > ( FromSlice ( datum ) , & ok ) ) ;
2019-12-03 13:57:02 +02:00
if ( ! ok ) {
2019-12-03 15:41:09 +02:00
throw DatabaseSerializationError (
QString ( " %1: Key was retrieved ok, but data could not be deserialized " )
. arg ( ! errorMsgPrefix . isEmpty ( ) ? errorMsgPrefix : " Error deserializing an object from db " ) ) ;
2019-12-03 13:57:02 +02:00
}
}
}
return ret ;
}
2019-12-03 15:41:09 +02:00
/// Conveneience for above with the missingOk flag set to false. Will always throw or return a real value.
template < typename RetType , bool safeScalar = false >
2019-12-08 02:36:46 +02:00
RetType GenericDBGetFailIfMissing ( rocksdb : : DB * db , const rocksdb : : Slice & k , const QString & errMsgPrefix = QString ( ) , bool extraDataOk = false ,
const rocksdb : : ReadOptions & ropts = rocksdb : : ReadOptions ( ) )
2019-12-03 15:41:09 +02:00
{
2019-12-08 02:36:46 +02:00
return GenericDBGet < RetType , safeScalar > ( db , k , false , errMsgPrefix , extraDataOk , ropts ) . value ( ) ;
2019-12-03 15:41:09 +02:00
}
2019-12-07 20:34:19 +02:00
// some helper data structs
struct BlkInfo {
TxNum txNum0 = 0 ;
unsigned nTx = 0 , nIns = 0 , nOuts = 0 ;
} ;
2019-11-29 14:42:17 +02:00
}
struct Storage : : Pvt
{
2019-11-30 01:33:27 +02:00
Meta meta ;
Lock metaLock ;
2019-11-29 14:42:17 +02:00
Headers headers ;
2019-11-30 00:42:33 +02:00
std : : pair < unsigned , QByteArray > lastHeaderSaved ; ///< remember the last header saved to disk, so subsequent saves leave off from this index
2019-11-29 14:42:17 +02:00
RWLock headersLock ;
std : : atomic < std : : underlying_type_t < SaveItem > > pendingSaves { 0 } ;
struct RocksDBs {
2019-12-07 20:34:19 +02:00
std : : unique_ptr < rocksdb : : DB > meta , headers , txnums , blkinfo , utxoset , shist ;
2019-11-29 14:42:17 +02:00
} db ;
2019-11-30 00:42:33 +02:00
BTC : : HeaderVerifier headerVerifier ;
Lock headerVerifierLock ;
2019-12-07 20:34:19 +02:00
std : : atomic < TxNum > txNumNext { 0 } ;
UTXOSet utxoSet ;
RWLock utxoSetLock ;
2019-12-07 23:30:07 +02:00
std : : unordered_set < TXO > utxoSetAdditionsUnsaved , utxoSetDeletionsUnsaved ;
2019-12-08 22:31:17 +02:00
bool hasSavedUtxoSet = false ;
2019-12-07 20:34:19 +02:00
std : : vector < BlkInfo > blkInfos ;
2019-12-07 23:30:07 +02:00
2019-12-08 22:07:01 +02:00
std : : atomic < unsigned > saveInterval = 100000 , compactInterval = 30000 ,
unsavedCt = 0 , uncompactedCt = 0 ;
2019-12-08 02:36:46 +02:00
2019-12-08 23:15:42 +02:00
static constexpr size_t nCacheMax = 2000000 , nCacheElasticity = 1000000 ;
2019-12-08 02:36:46 +02:00
LRU : : Cache < true , TxNum , TxHash > lruNum2Hash { nCacheMax , nCacheElasticity } ;
LRU : : Cache < true , TxHash , TxNum , HashHasher > lruHash2Num { nCacheMax , nCacheElasticity } ;
2019-11-29 14:42:17 +02:00
} ;
Storage : : Storage ( const std : : shared_ptr < Options > & options )
: Mgr ( nullptr ) , options ( options ) , p ( new Pvt )
{
setObjectName ( " Storage " ) ;
_thread . setObjectName ( objectName ( ) ) ;
}
Storage : : ~ Storage ( ) { Debug ( " %s " , __FUNCTION__ ) ; cleanup ( ) ; }
void Storage : : startup ( )
{
2019-12-07 20:34:19 +02:00
{ // open all db's ...
rocksdb : : Options opts ;
// Optimize RocksDB. This is the easiest way to get RocksDB to perform well
2019-12-08 02:36:46 +02:00
opts . IncreaseParallelism ( int ( Util : : getNPhysicalProcessors ( ) ) ) ;
2019-12-07 20:34:19 +02:00
opts . OptimizeLevelStyleCompaction ( ) ;
// create the DB if it's not already present
opts . create_if_missing = true ;
opts . error_if_exists = false ;
2019-12-08 11:45:57 +02:00
//opts.max_open_files = 50; ///< testing -- seems this affects memory usage see: https://github.com/facebook/rocksdb/issues/4112
2019-12-08 05:02:46 +02:00
opts . keep_log_file_num = 5 ; // ??
2019-12-07 20:34:19 +02:00
opts . compression = rocksdb : : CompressionType : : kNoCompression ; // for now we test without compression. TODO: characterize what is fastest and best..
using DBInfoTup = std : : tuple < QString , std : : unique_ptr < rocksdb : : DB > & > ;
const std : : list < DBInfoTup > 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
2019-11-29 14:42:17 +02:00
// load/check meta
{
2019-11-30 01:33:27 +02:00
Meta m_db ;
2019-12-03 15:41:09 +02:00
static const QString errMsg { " Incompatible database format -- delete the datadir and resynch. RocksDB error " } ;
if ( auto opt = GenericDBGet < Meta > ( 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 ) ;
2019-11-29 14:42:17 +02:00
}
2019-11-30 01:33:27 +02:00
p - > meta = m_db ;
2019-11-29 14:42:17 +02:00
Debug ( ) < < " Read meta from db ok " ;
2019-12-03 15:41:09 +02:00
} else {
// ok, did not exist .. write a new one to db
saveMeta_impl ( ) ;
2019-11-29 14:42:17 +02:00
}
}
Log ( ) < < " Loading database ... " ;
// load headers -- may throw
loadHeadersFromDB ( ) ;
2019-12-08 01:00:40 +02:00
loadUTXOSetFromDB ( ) ;
2019-11-29 14:42:17 +02:00
start ( ) ; // starts our thread
}
void Storage : : cleanup ( )
{
stop ( ) ; // joins our thread
2019-12-08 22:53:50 +02:00
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 ) ;
}
2019-11-29 14:42:17 +02:00
}
2019-12-08 22:07:01 +02:00
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 ; }
2019-11-29 14:42:17 +02:00
auto Storage : : stats ( ) const - > Stats
{
// TODO ...
return Stats ( ) ;
}
auto Storage : : mutableHeaders ( ) - > std : : pair < Headers & , ExclusiveLockGuard >
{
return std : : pair < Headers & , ExclusiveLockGuard > { p - > headers , p - > headersLock } ;
}
auto Storage : : headers ( ) const - > std : : pair < const Headers & , SharedLockGuard >
{
return std : : pair < const Headers & , SharedLockGuard > { p - > headers , p - > headersLock } ;
}
2019-11-30 00:42:33 +02:00
// Keep returned LockGuard in scope while you use the HeaderVerifier
auto Storage : : headerVerifier ( ) - > std : : pair < BTC : : HeaderVerifier & , LockGuard >
{
return std : : pair < BTC : : HeaderVerifier & , LockGuard > ( p - > headerVerifier , p - > headerVerifierLock ) ;
}
2019-12-07 20:34:19 +02:00
auto Storage : : mutableUtxoSet ( ) - > std : : pair < UTXOSet & , ExclusiveLockGuard >
{
return std : : pair < UTXOSet & , ExclusiveLockGuard > { p - > utxoSet , p - > utxoSetLock } ;
}
auto Storage : : utxoSet ( ) const - > std : : pair < const UTXOSet & , SharedLockGuard >
{
return std : : pair < const UTXOSet & , SharedLockGuard > { p - > utxoSet , p - > utxoSetLock } ;
}
2019-11-30 01:33:27 +02:00
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 ) ;
}
2019-12-07 20:34:19 +02:00
/// returns the "next" TxNum
TxNum Storage : : getTxNum ( ) const { return p - > txNumNext . load ( ) ; }
2019-12-07 15:54:56 +02:00
// should this come from headersVerifier() instead?
std : : pair < int , QByteArray > Storage : : latestTip ( ) const {
std : : pair < int , QByteArray > 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 ;
}
2019-11-29 14:42:17 +02:00
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 ) )
{
2019-12-07 23:30:07 +02:00
QTimer : : singleShot ( 0 , this , [ this ] { save_impl ( ) ; } ) ;
2019-11-29 14:42:17 +02:00
}
}
2019-12-07 23:30:07 +02:00
void Storage : : save_impl ( SaveSpec override )
2019-11-29 14:42:17 +02:00
{
2019-12-07 23:30:07 +02:00
if ( const auto flags = SaveSpec ( p - > pendingSaves . exchange ( 0 ) ) | override ; flags ) { // atomic clear of flags, grab prev val
2019-11-29 17:11:23 +02:00
try {
2019-12-08 17:21:39 +02:00
if ( flags & SaveItem : : Blocks ) { // utxo set
2019-12-07 23:30:07 +02:00
using Set = decltype ( p - > utxoSetAdditionsUnsaved ) ;
Set unsavedAdditions , unsavedDeletions ;
2019-12-08 17:21:39 +02:00
UTXOSet setCopy ;
Headers hdrCopy ;
2019-12-08 22:31:17 +02:00
bool firstUtxoSave = false ;
2019-12-07 23:30:07 +02:00
{
2019-12-08 17:21:39 +02:00
std : : scoped_lock guard ( p - > headersLock , p - > utxoSetLock ) ;
2019-12-07 23:30:07 +02:00
unsavedAdditions . swap ( p - > utxoSetAdditionsUnsaved ) ;
unsavedDeletions . swap ( p - > utxoSetDeletionsUnsaved ) ;
p - > unsavedCt = 0 ;
2019-12-08 22:31:17 +02:00
if ( unsavedAdditions . size ( ) | | unsavedDeletions . size ( ) | | ! p - > hasSavedUtxoSet ) {
2019-12-07 23:30:07 +02:00
const auto t0 = Util : : getTimeNS ( ) ; // DEBUG REMOVE ME
2019-12-08 17:21:39 +02:00
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)
2019-12-08 22:31:17 +02:00
if ( ! p - > hasSavedUtxoSet ) {
p - > hasSavedUtxoSet = true ;
firstUtxoSave = true ;
}
2019-12-07 23:30:07 +02:00
// DEBUG REMOVE ME
const auto elapsed = Util : : getTimeNS ( ) - t0 ;
2019-12-08 17:21:39 +02:00
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 ;
2019-12-07 23:30:07 +02:00
}
}
2019-12-08 17:21:39 +02:00
if ( ! setCopy . empty ( ) ) {
2019-12-08 22:31:17 +02:00
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 ) {
2019-12-08 22:48:48 +02:00
unsavedAdditions . emplace ( it - > first ) ;
2019-12-08 22:31:17 +02:00
}
}
2019-12-08 17:21:39 +02:00
saveUtxoUnsaved_impl ( setCopy , unsavedAdditions , unsavedDeletions ) ;
}
if ( ! hdrCopy . empty ( ) ) {
saveHeaders_impl ( hdrCopy ) ;
}
}
2019-11-30 01:33:27 +02:00
if ( flags & SaveItem : : Meta ) { // Meta
LockGuard l ( p - > metaLock ) ;
saveMeta_impl ( ) ;
}
2019-11-29 17:11:23 +02:00
} catch ( const std : : exception & e ) {
Fatal ( ) < < e . what ( ) ; // will abort app...
2019-11-29 14:42:17 +02:00
}
}
}
2019-11-30 01:33:27 +02:00
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 " ;
}
2019-11-29 14:42:17 +02:00
void Storage : : saveHeaders_impl ( const Headers & h )
{
if ( ! p - > db . headers ) return ;
2019-12-07 23:30:07 +02:00
//Debug() << "Saving headers ...";
2019-11-29 17:11:23 +02:00
size_t start = 0 ;
2019-11-29 14:42:17 +02:00
const auto t0 = Util : : getTimeNS ( ) ;
{
2019-11-29 17:11:23 +02:00
// 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 ) {
2019-12-03 06:38:19 +02:00
// 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 ( ) )
2019-11-29 17:11:23 +02:00
throw DatabaseError ( QString ( " Error writing header %1: %2 " ) . arg ( i ) . arg ( stat . ToString ( ) . c_str ( ) ) ) ;
}
2019-11-30 00:42:33 +02:00
// 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 ( ) )
2019-11-29 17:11:23 +02:00
throw DatabaseError ( QString ( " Error writing header size key: %1 " ) . arg ( stat . ToString ( ) . c_str ( ) ) ) ;
auto opts = rocksdb : : WriteOptions ( ) ;
2019-12-07 20:34:19 +02:00
// uncomment all this stuff if we want to do fsync and other paranoia. turned off for now.
//opts.sync = true;
//opts.low_pri = true;
2019-11-29 17:11:23 +02:00
if ( auto stat = p - > db . headers - > Write ( opts , & batch ) ; ! stat . ok ( ) )
throw DatabaseError ( QString ( " Error writing headers: %1 " ) . arg ( stat . ToString ( ) . c_str ( ) ) ) ;
2019-12-07 20:34:19 +02:00
//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()));
2019-11-29 17:11:23 +02:00
p - > lastHeaderSaved = { h . size ( ) - 1 , h . back ( ) } ; // remember last
2019-11-29 14:42:17 +02:00
}
}
2019-12-07 23:30:07 +02:00
const auto elapsed = Util : : getTimeNS ( ) - t0 ;
2019-11-29 17:11:23 +02:00
const auto ct = ( h . size ( ) - start ) ;
2019-12-07 23:30:07 +02:00
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 < TXO > & adds , const std : : unordered_set < TXO > & 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 " ;
2019-11-29 14:42:17 +02:00
}
void Storage : : loadHeadersFromDB ( )
{
2019-11-29 17:15:06 +02:00
FatalAssert ( ! ! p - > db . headers ) < < __FUNCTION__ < < " : Headers db is not open " ;
2019-11-29 14:42:17 +02:00
Debug ( ) < < " Loading headers ... " ;
2019-11-30 00:42:33 +02:00
uint32_t num = 0 ;
2019-11-29 14:42:17 +02:00
const auto t0 = Util : : getTimeNS ( ) ;
{
2019-12-03 15:41:09 +02:00
auto * const db = p - > db . headers . get ( ) ;
2019-12-08 02:36:46 +02:00
2019-12-03 15:41:09 +02:00
num = GenericDBGet < uint32_t , true > ( 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 ) ) ;
2019-11-29 14:42:17 +02:00
Headers h ;
h . reserve ( num ) ;
2019-12-03 19:09:38 +02:00
const QString errMsg ( " Error retrieving header from db " ) ;
2019-12-03 22:47:03 +02:00
const int hsz = BTC : : GetBlockHeaderSize ( ) ;
2019-11-29 17:11:23 +02:00
// read db
2019-12-03 15:41:09 +02:00
for ( uint32_t i = 0 ; i < num ; + + i ) {
2019-12-03 22:47:03 +02:00
// guaranteed to return a value or throw
2019-12-03 21:48:10 +02:00
auto bytes = GenericDBGetFailIfMissing < QByteArray > ( db , ToSlice ( SerializeScalar ( uint32_t ( i ) ) ) , errMsg ) ;
2019-12-03 22:47:03 +02:00
if ( UNLIKELY ( bytes . size ( ) ! = hsz ) )
2019-12-03 15:41:09 +02:00
throw DatabaseFormatError ( QString ( " Error reading header %1, wrong size: %2 " ) . arg ( i ) . arg ( bytes . size ( ) ) ) ;
2019-12-03 21:48:10 +02:00
h . emplace_back ( std : : move ( bytes ) ) ;
2019-11-29 17:11:23 +02:00
}
// verify headers: hashPrevBlock must match what we actually read from db
if ( num ) {
Debug ( ) < < " Verifying " < < num < < " " < < Util : : Pluralize ( " header " , num ) < < " ... " ;
2019-11-30 00:42:33 +02:00
auto [ verif , lock ] = headerVerifier ( ) ;
QString err ;
2019-11-29 17:11:23 +02:00
for ( unsigned i = 0 ; i < num ; + + i ) {
// verify headers hash chains match by checking hashPrevBlock versus actual previous hash.
2019-11-30 00:42:33 +02:00
// 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 ) ) ;
2019-11-29 17:11:23 +02:00
}
2019-11-30 00:42:33 +02:00
p - > lastHeaderSaved = verif . lastHeaderProcessed ( ) ; // remember last
2019-11-29 14:42:17 +02:00
}
// locked until scope end...
auto [ headers , lock ] = mutableHeaders ( ) ;
headers . swap ( h ) ;
}
2019-11-29 17:11:23 +02:00
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 " ;
}
2019-11-29 14:42:17 +02:00
}
2019-12-08 01:00:40 +02:00
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 < HashX , HashHasher > hashXSeen ;
2019-12-08 22:07:01 +02:00
robin_hood : : unordered_flat_map < TxNum , TxHash > num2hash ; // this also acts as a cache as well as ensuring all txhashes share the same QByteArray
2019-12-08 01:00:40 +02:00
const int currentHeight = latestTip ( ) . first ;
std : : unique_ptr < rocksdb : : Iterator > 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 ;
2019-12-08 22:07:01 +02:00
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 ) ;
2019-12-08 01:00:40 +02:00
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. " ) ;
2019-12-08 22:07:01 +02:00
num2hash . emplace ( ctxo . txNum ( ) , hash = ohash . value ( ) ) ;
2019-12-08 02:36:46 +02:00
hash = ohash . value ( ) ;
2019-12-08 22:07:01 +02:00
cost + = size_t ( hash . length ( ) ) ;
2019-12-08 01:00:40 +02:00
}
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 {
2019-12-08 22:48:48 +02:00
hashXSeen . emplace ( info . hashX ) ;
2019-12-08 01:00:40 +02:00
cost + = size_t ( info . hashX . length ( ) ) ;
}
p - > utxoSet [ TXO { hash , ctxo . N ( ) } ] = info ;
cost + = sizeof ( TXO ) + sizeof ( TXOInfo ) ;
if ( 0 = = + + ctr % 100000 ) {
2019-12-08 17:21:39 +02:00
* ( 0 = = ctr % 1000000 ? std : : make_unique < Log > ( ) : std : : make_unique < Debug > ( ) ) < < " Read " < < ctr < < " utxos ... " ;
2019-12-08 01:00:40 +02:00
}
}
Log ( ) < < " UTXO set " < < QString : : number ( cost / 1e6 , ' f ' , 3 ) < < " MiB in " < < p - > utxoSet . size ( ) < < " txos " ;
2019-12-08 22:07:01 +02:00
Debug ( ) < < " Mem savings: " < < QString : : number ( savings / 1e6 , ' f ' , 3 ) < < " MiB "
< < " ( " < < " num2hash: " < < num2hash . size ( ) < < " " < < " hashXSeen: " < < hashXSeen . size ( ) < < " ) " ;
2019-12-08 01:00:40 +02:00
}
const auto num = p - > utxoSet . size ( ) ;
if ( num ) {
const auto elapsed = Util : : getTimeNS ( ) ;
2019-12-08 22:31:17 +02:00
p - > hasSavedUtxoSet = true ; // remember that we had a utxo set in db -- this affects how/if we build the "difference set"
2019-12-08 01:00:40 +02:00
Debug ( ) < < " Read " < < num < < " " < < Util : : Pluralize ( " utxo " , num ) < < " from db in " < < QString : : number ( ( elapsed - t0 ) / 1e6 , ' f ' , 3 ) < < " msec " ;
}
}
2019-12-07 20:34:19 +02:00
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 ) {
2019-12-08 02:36:46 +02:00
const TxNum txnum = txNum0 + i ;
const TxHash & hash = ppb - > txInfos [ i ] . hash ;
const auto hashSlice = ToSlice ( hash ) ;
const auto txNumBytes = SerializeScalar ( txnum ) ;
2019-12-07 20:34:19 +02:00
// 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";
2019-12-09 10:04:30 +02:00
constexpr bool debugPrt = false ;
2019-12-07 20:34:19 +02:00
// update utxoSet
{
// add outputs
for ( const auto & [ hashX , ag ] : ppb - > hashXAggregated ) {
2019-12-09 10:04:30 +02:00
std : : unordered_set < unsigned > outputsSpentInSameBlock ;
for ( const auto iidx : ag . ins ) {
if ( const auto & opt = ppb - > inputs [ iidx ] . parentTxOutIdx ; opt . has_value ( ) )
outputsSpentInSameBlock . insert ( opt . value ( ) ) ;
}
2019-12-07 20:34:19 +02:00
for ( const auto oidx : ag . outs ) {
2019-12-09 10:04:30 +02:00
if ( outputsSpentInSameBlock . count ( oidx ) ) {
if constexpr ( debugPrt )
Debug ( ) < < " Skipping output #: " < < oidx < < " (was spent in same block) " ;
continue ;
}
2019-12-07 20:34:19 +02:00
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 ;
2019-12-07 23:30:07 +02:00
const TXO txo { hash , out . outN } ;
2019-12-07 20:34:19 +02:00
p - > utxoSet [ txo ] = info ;
2019-12-08 22:31:17 +02:00
if ( p - > hasSavedUtxoSet )
p - > utxoSetAdditionsUnsaved . insert ( txo ) ;
2019-12-07 20:34:19 +02:00
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 < HashX , HashHasher > newHashXInputsResolved ;
unsigned inum = 0 ;
2019-12-08 02:36:46 +02:00
for ( auto & in : ppb - > inputs ) {
2019-12-07 20:34:19 +02:00
if ( ! inum ) {
// coinbase.. skip
2019-12-09 10:04:30 +02:00
} 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 ( ) < < " ) " ;
2019-12-07 20:34:19 +02:00
} 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 ) ;
2019-12-08 02:36:46 +02:00
in . prevoutHash = it - > first . prevoutHash ; // ensure shallow/shared copy of QByteArray in inputs
2019-12-07 20:34:19 +02:00
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 ( ) ;
}
2019-12-08 22:31:17 +02:00
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 ) ;
}
2019-12-07 23:30:07 +02:00
}
p - > utxoSet . erase ( it ) ; // invalidate txo (spend it) by removing from map
2019-12-07 20:34:19 +02:00
} 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 ( ) ;
2019-12-07 23:30:07 +02:00
2019-12-08 22:48:48 +02:00
// enqueue a save every saveInterval blocks (note that if saveInterval is 0, we never auto-save)
if ( + + p - > unsavedCt = = p - > saveInterval & & p - > saveInterval ) {
2019-12-08 22:07:01 +02:00
save ( SaveItem : : Blocks ) ;
}
2019-12-08 22:48:48 +02:00
// enqueue compaction of utxo set every compactInterval blocks (if 0, do not auto-compact)
if ( + + p - > uncompactedCt = = p - > compactInterval & & p - > compactInterval ) {
2019-12-08 17:21:39 +02:00
// 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 ( ) ;
} ) ;
}
2019-12-07 20:34:19 +02:00
} 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
2019-12-08 22:07:01 +02:00
std : : optional < TxNum > Storage : : txNumForHash ( const TxHash & h , bool throwIfMissing , bool * wasCached , bool skipCache )
2019-12-07 20:34:19 +02:00
{
2019-12-08 22:07:01 +02:00
std : : optional < TxNum > ret ;
if ( ! skipCache ) ret = p - > lruHash2Num . tryGet ( h ) ;
2019-12-08 02:36:46 +02:00
if ( ret . has_value ( ) ) {
// save was cached flat
if ( wasCached ) * wasCached = true ;
2019-12-08 17:21:39 +02:00
// touch the other cache to refresh its lru list (if it still has the item. that is)
p - > lruNum2Hash . tryGet ( ret . value ( ) ) ;
2019-12-08 02:36:46 +02:00
return ret ; // cached, return it
} else if ( wasCached ) * wasCached = false ; // save flag if caller is interested
2019-12-07 20:34:19 +02:00
static const QString kErrMsg ( " Error reading txNumForHash from db " ) ;
2019-12-08 02:36:46 +02:00
ret = GenericDBGet < TxNum , false > ( p - > db . txnums . get ( ) , ToSlice ( h ) , ! throwIfMissing , kErrMsg ) ;
2019-12-08 22:07:01 +02:00
if ( ! skipCache & & ret . has_value ( ) ) {
2019-12-08 23:15:42 +02:00
// save in cache
2019-12-08 02:36:46 +02:00
p - > lruHash2Num . insert ( h , ret . value ( ) ) ;
2019-12-08 17:21:39 +02:00
}
2019-12-08 02:36:46 +02:00
return ret ;
2019-12-07 20:34:19 +02:00
}
2019-12-08 22:07:01 +02:00
std : : optional < TxHash > Storage : : hashForTxNum ( TxNum n , bool throwIfMissing , bool * wasCached , bool skipCache )
2019-12-07 20:34:19 +02:00
{
2019-12-08 22:07:01 +02:00
std : : optional < TxHash > ret ;
if ( ! skipCache ) ret = p - > lruNum2Hash . tryGet ( n ) ;
2019-12-08 02:36:46 +02:00
if ( ret . has_value ( ) ) {
if ( wasCached ) * wasCached = true ;
2019-12-08 17:21:39 +02:00
// touch the other cache to refresh its lru list (if it still has the item, that is)
p - > lruHash2Num . tryGet ( ret . value ( ) ) ;
2019-12-08 02:36:46 +02:00
return ret ;
} else if ( wasCached ) * wasCached = false ;
2019-12-07 20:34:19 +02:00
static const QString kErrMsg ( " Error reading hashForTxNum from db " ) ;
2019-12-08 02:36:46 +02:00
ret = GenericDBGet < TxHash , false > ( p - > db . txnums . get ( ) , ToSlice ( SerializeScalar ( n ) ) , ! throwIfMissing , kErrMsg ) ;
2019-12-08 22:07:01 +02:00
if ( ! skipCache & & ret . has_value ( ) ) {
2019-12-08 23:15:42 +02:00
// save in cache
2019-12-08 02:36:46 +02:00
p - > lruNum2Hash . insert ( n , ret . value ( ) ) ;
2019-12-08 17:21:39 +02:00
}
2019-12-08 02:36:46 +02:00
return ret ;
2019-12-07 20:34:19 +02:00
}
2019-12-09 01:11:34 +02:00
size_t Storage : : compactifyUtxoSet ( bool force )
2019-12-08 17:21:39 +02:00
{
2019-12-09 01:11:34 +02:00
if ( ! p - > uncompactedCt & & ! force )
return 0 ;
2019-12-08 17:21:39 +02:00
// the purpose of this map is to ensure that all txid's in app memory share the same implicitly shared QByteArray
2019-12-08 23:48:58 +02:00
using Empty = int8_t ;
constexpr Empty empty { } ;
robin_hood : : unordered_flat_map < HashX , Empty , HashHasher > hxSet ;
robin_hood : : unordered_flat_map < TxHash , Empty , HashHasher > txSet ;
2019-12-08 17:21:39 +02:00
size_t savings = 0 ;
2019-12-08 22:16:52 +02:00
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
2019-12-08 23:48:58 +02:00
it - > first . prevoutHash = it2 - > first ;
savings + = size_t ( it2 - > first . length ( ) ) ;
2019-12-08 22:16:52 +02:00
} else {
// first instance, store
2019-12-08 23:48:58 +02:00
txSet . emplace ( it - > first . prevoutHash , empty ) ;
2019-12-08 22:16:52 +02:00
}
if ( auto it2 = hxSet . find ( it - > second . hashX ) ; it2 ! = hxSet . end ( ) ) {
// found -- implicitly share HashX
2019-12-08 23:48:58 +02:00
it - > second . hashX = it2 - > first ;
savings + = size_t ( it2 - > first . length ( ) ) ;
2019-12-08 22:16:52 +02:00
} else {
// first instance, store
2019-12-08 23:48:58 +02:00
hxSet . emplace ( it - > second . hashX , empty ) ;
2019-12-08 22:16:52 +02:00
}
2019-12-08 17:21:39 +02:00
}
2019-12-08 22:16:52 +02:00
set . rehash ( set . size ( ) ) ;
p - > uncompactedCt = 0 ;
elapsed = Util : : getTimeNS ( ) - t0 ;
2019-12-08 17:21:39 +02:00
}
Debug ( ) < < " Compatcted utxo set: maybe saved " < < QString : : number ( savings / 1e6 , ' f ' , 3 ) < < " MiB in " < < QString : : number ( elapsed / 1e6 , ' f ' , 3 ) < < " msec " ;
return savings ;
}
2019-12-07 20:34:19 +02:00
2019-11-29 14:42:17 +02:00
namespace {
2019-11-29 17:11:23 +02:00
// specializations of Serialize/Deserialize
2019-11-29 14:42:17 +02:00
template < > QByteArray Serialize ( const Meta & m )
{
QByteArray ba ;
{
QDataStream ds ( & ba , QIODevice : : WriteOnly | QIODevice : : Truncate ) ;
2019-12-03 06:38:19 +02:00
// 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 ;
2019-11-29 14:42:17 +02:00
}
return ba ;
}
template < > Meta Deserialize ( const QByteArray & ba , bool * ok_ptr )
{
bool dummy ;
bool & ok ( ok_ptr ? * ok_ptr : dummy ) ;
ok = false ;
2019-12-03 06:38:19 +02:00
Meta m { 0 , 0 , { } } ;
2019-11-29 14:42:17 +02:00
{
QDataStream ds ( ba ) ;
2019-12-03 06:38:19 +02:00
QByteArray magicBytes ;
ds > > magicBytes ; // read magic as raw bytes.
if ( ( ok = ds . status ( ) = = QDataStream : : Status : : Ok ) ) {
m . magic = DeserializeScalar < decltype ( m . magic ) > ( magicBytes , & ok ) ;
if ( ok ) {
ds > > m . version > > m . chain ;
ok = ds . status ( ) = = QDataStream : : Status : : Ok ;
}
}
2019-11-29 14:42:17 +02:00
}
return m ;
}
2019-12-07 23:30:07 +02:00
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 ;
}
2019-11-29 14:42:17 +02:00
}