diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 6ecff1fda52..d4985dedb84 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -509,6 +509,7 @@ target_sources (rippled PRIVATE src/ripple/nodestore/impl/DatabaseNodeImp.cpp src/ripple/nodestore/impl/DatabaseRotatingImp.cpp src/ripple/nodestore/impl/DatabaseShardImp.cpp + src/ripple/nodestore/impl/DeterministicShard.cpp src/ripple/nodestore/impl/DecodedBlob.cpp src/ripple/nodestore/impl/DummyScheduler.cpp src/ripple/nodestore/impl/EncodedBlob.cpp diff --git a/src/ripple/nodestore/Backend.h b/src/ripple/nodestore/Backend.h index d3abc33142f..4c3ae2cdbd2 100644 --- a/src/ripple/nodestore/Backend.h +++ b/src/ripple/nodestore/Backend.h @@ -58,6 +58,25 @@ class Backend virtual void open(bool createIfMissing = true) = 0; + /** Open the backend. + @param createIfMissing Create the database files if necessary. + @param appType Deterministic appType used to create a backend. + @param uid Deterministic uid used to create a backend. + @param salt Deterministic salt used to create a backend. + This allows the caller to catch exceptions. + */ + virtual void + open( + bool createIfMissing, + boost::optional appType, + boost::optional uid, + boost::optional salt) + { + Throw(std::string( + "Deterministic appType/uid/salt not supported by backend " + + getName())); + } + /** Close the backend. This allows the caller to catch exceptions. */ diff --git a/src/ripple/nodestore/DeterministicShard.md b/src/ripple/nodestore/DeterministicShard.md new file mode 100644 index 00000000000..10fd6465327 --- /dev/null +++ b/src/ripple/nodestore/DeterministicShard.md @@ -0,0 +1,103 @@ +# Deterministic Database Shards + +This doc describes the standard way to assemble the database shard. A shard assembled using this approach becomes deterministic i.e. if two independent sides assemble the shard consists of the same ledgers, accounts and transactions, then they will obtain the same shard files `nudb.dat` and `nudb.key`. The approach deals with the `NuDB` database format only, refer to `https://github.com/vinniefalco/NuDB`. + + +## Headers + +Due to NuDB database definition, the following headers are used for database files: + +nudb.key: +``` +char[8] Type The characters "nudb.key" +uint16 Version Holds the version number +uint64 UID Unique ID generated on creation +uint64 Appnum Application defined constant +uint16 KeySize Key size in bytes +uint64 Salt A random seed +uint64 Pepper The salt hashed +uint16 BlockSize Size of a file block in bytes +uint16 LoadFactor Target fraction in 65536ths +uint8[56] Reserved Zeroes +uint8[] Reserved Zero-pad to block size +``` + +nudb.dat: +``` +char[8] Type The characters "nudb.dat" +uint16 Version Holds the version number +uint64 UID Unique ID generated on creation +uint64 Appnum Application defined constant +uint16 KeySize Key size in bytes +uint8[64] (reserved) Zeroes +``` +there all fields are saved using network byte order (most significant byte first). + +To make the shard deterministic the following parameters are used as values of header field both for `nudb.key` and `nudb.dat` files. +``` +Version 2 +UID digest(0) +Appnum digest(2) | 0x5348524400000000 /* 'SHRD' */ +KeySize 32 +Salt digest(1) +Pepper XXH64(Salt) +BlockSize 0x1000 (4096 bytes) +LoadFactor 0.5 (numeric 0x8000) +``` +Note: XXH64() is well-known hash algorithm. + +The `digest(i)` mentioned above defined as the follows: + +First, RIPEMD160 hash `H` calculated of the following structure +``` +uint256 lastHash Hash of last ledger in shard +uint32 index Index of the shard +uint32 firstSeq Sequence number of first ledger in the shard +uint32 lastSeq Sequence number of last ledger in the shard +uint32 version Version of shard, 2 at the present +``` +there all 32-bit integers are hashed in network byte order. + +Then, `digest(i)` is defined as the following portion of the above hash `H`: +``` +digest(0) = H[0] << 56 | H[2] << 48 | ... | H[14] << 0, +digest(1) = H[1] << 56 | H[3] << 48 | ... | H[15] << 0, +digest(2) = H[19] << 24 | H[18] << 16 | ... | H[16] << 0, +``` +where `H[i]` denotes `i`-th byte of hash `H`. + + +## Contents + +After deterministic shard is created using the above mentioned headers, it filled with objects. First, all objects of the shard are collected and sorted in according to their hashes. Here the objects are: ledgers, SHAmap tree nodes including accounts and transactions, and final key object with hash 0. Objects are sorted by increasing of their hashes, precisely, by increasing of hex representations of hashes in lexicographic order. + +For example, the following is an example of sorted hashes in their hex representation: +``` +0000000000000000000000000000000000000000000000000000000000000000 +154F29A919B30F50443A241C466691B046677C923EE7905AB97A4DBE8A5C2423 +2231553FC01D37A66C61BBEEACBB8C460994493E5659D118E19A8DDBB1444273 +272DCBFD8E4D5D786CF11A5444B30FB35435933B5DE6C660AA46E68CF0F5C447 +3C062FD9F0BCDCA31ACEBCD8E530D0BDAD1F1D1257B89C435616506A3EE6CB9E +58A0E5AE427CDDC1C7C06448E8C3E4BF718DE036D827881624B20465C3E1334F +... +``` + +Finally, objects added to the shard one by one in the sorted order from low to high hashes. + + +## Tests + +To perform test to deterministic shards implementation one can enter the following command: +``` +rippled --unittest ripple.NodeStore.DatabaseShard +``` + +The following is the right output of deterministic shards test: +``` +ripple.NodeStore.DatabaseShard DatabaseShard deterministic_shard with backend nudb +Iteration 0: RIPEMD160[nudb.key] = 4CFA8985836B549EC99D2E9705707F488DC91E4E +Iteration 0: RIPEMD160[nudb.dat] = 8CC61F503C36339803F8C2FC652C1102DDB889F1 +Iteration 1: RIPEMD160[nudb.key] = 4CFA8985836B549EC99D2E9705707F488DC91E4E +Iteration 1: RIPEMD160[nudb.dat] = 8CC61F503C36339803F8C2FC652C1102DDB889F1 +``` + diff --git a/src/ripple/nodestore/backend/NuDBFactory.cpp b/src/ripple/nodestore/backend/NuDBFactory.cpp index 8147f218cc0..04dfa208551 100644 --- a/src/ripple/nodestore/backend/NuDBFactory.cpp +++ b/src/ripple/nodestore/backend/NuDBFactory.cpp @@ -38,7 +38,10 @@ namespace NodeStore { class NuDBBackend : public Backend { public: - static constexpr std::size_t currentType = 1; + static constexpr std::uint64_t currentType = 1; + static constexpr std::uint64_t deterministicType = 0x5348524400000000ull; + /* "SHRD" in ASCII */ + static constexpr std::uint64_t deterministicMask = 0xFFFFFFFF00000000ull; beast::Journal const j_; size_t const keyBytes_; @@ -93,7 +96,11 @@ class NuDBBackend : public Backend } void - open(bool createIfMissing) override + open( + bool createIfMissing, + boost::optional appType, + boost::optional uid, + boost::optional salt) override { using namespace boost::filesystem; if (db_.is_open()) @@ -114,8 +121,9 @@ class NuDBBackend : public Backend dp, kp, lp, - currentType, - nudb::make_salt(), + appType.value_or(currentType), + uid.value_or(nudb::make_uid()), + salt.value_or(nudb::make_salt()), keyBytes_, nudb::block_size(kp), 0.50, @@ -128,10 +136,27 @@ class NuDBBackend : public Backend db_.open(dp, kp, lp, ec); if (ec) Throw(ec); - if (db_.appnum() != currentType) + + /** Old value currentType is accepted for appnum in traditional + * databases, new value is used for deterministic shard databases. + * New 64-bit value is constructed from fixed and random parts. + * Fixed part is bounded by bitmask deterministicMask, + * and the value of fixed part is deterministicType. + * Random part depends on the contents of the shard and may be any. + * The contents of appnum field should match either old or new rule. + */ + if (db_.appnum() != appType.value_or(currentType) && + (appType || + (db_.appnum() & deterministicMask) != deterministicType)) Throw("nodestore: unknown appnum"); } + void + open(bool createIfMissing) override + { + open(createIfMissing, boost::none, boost::none, boost::none); + } + void close() override { diff --git a/src/ripple/nodestore/impl/DeterministicShard.cpp b/src/ripple/nodestore/impl/DeterministicShard.cpp new file mode 100644 index 00000000000..151752cfcac --- /dev/null +++ b/src/ripple/nodestore/impl/DeterministicShard.cpp @@ -0,0 +1,206 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace NodeStore { + +DeterministicShard::DeterministicShard( + Application& app, + DatabaseShard const& db, + std::uint32_t index, + uint256 const& lastHash, + beast::Journal j) + : inited_(false) + , nodeset_([](nodeptr l, nodeptr r) { return l->getHash() < r->getHash(); }) + , app_(app) + , db_(db) + , index_(index) + , hash_(hash(lastHash)) + , tempdir_(db.getRootDir() / (std::to_string(index_) + ".tmp")) + , finaldir_(db.getRootDir() / std::to_string(index_)) + , ctx_(std::make_unique()) + , j_(j) +{ +} + +DeterministicShard::~DeterministicShard() +{ + close(true); +} + +uint160 +DeterministicShard::hash(uint256 const& lastHash) const +{ + using beast::hash_append; + ripemd160_hasher h; + + hash_append(h, lastHash); + hash_append(h, index_); + hash_append(h, db_.firstLedgerSeq(index_)); + hash_append(h, db_.lastLedgerSeq(index_)); + hash_append(h, Shard::version); + + auto const result = static_cast(h); + return uint160::fromVoid(result.data()); +} + +std::uint64_t +DeterministicShard::digest(int n) const +{ + auto const data = hash_.data(); + + if (n == 2) + { // Extract 32 bits: + return (static_cast(data[19]) << 24) + + (static_cast(data[18]) << 16) + + (static_cast(data[17]) << 8) + + (static_cast(data[16])); + } + + std::uint64_t ret = 0; + + if (n == 0 || n == 1) + { // Extract 64 bits + for (int i = n; i < 16; i += 2) + ret = (ret << 8) + data[i]; + } + + return ret; +} + +bool +DeterministicShard::init() +{ + if (index_ < db_.earliestShardIndex()) + { + JLOG(j_.error()) << "shard " << index_ << " is illegal"; + return false; + } + + Config const& config{app_.config()}; + + Section section{config.section(ConfigSection::shardDatabase())}; + std::string const type{get(section, "type", "nudb")}; + + if (type != "nudb") + { + JLOG(j_.error()) << "shard " << index_ << " backend type " << type + << " not supported"; + return false; + } + + auto factory{Manager::instance().find(type)}; + if (!factory) + { + JLOG(j_.error()) << "shard " << index_ + << " failed to create factory for backend type " + << type; + return false; + } + + ctx_->start(); + + section.set("path", tempdir_.string()); + backend_ = factory->createInstance( + NodeObject::keyBytes, section, scheduler_, *ctx_, j_); + + if (!backend_) + { + JLOG(j_.error()) << "shard " << index_ + << " failed to create backend type " << type; + return false; + } + + // Open or create the NuDB key/value store + bool preexist = exists(tempdir_); + if (preexist) + { + remove_all(tempdir_); + preexist = false; + } + + backend_->open( + !preexist, + digest(2) | 0x5348524400000000ll, /* appType */ + digest(0), /* uid */ + digest(1) /* salt */ + ); + + inited_ = true; + + return true; +} + +void +DeterministicShard::close(bool cancel) +{ + if (!inited_) + return; + + backend_->close(); + if (cancel) + { + remove_all(tempdir_); + } + else + { + flush(); + remove_all(finaldir_); + rename(tempdir_, finaldir_); + } + inited_ = false; +} + +void +DeterministicShard::store(nodeptr nObj) +{ + if (!inited_) + return; + + nodeset_.insert(nObj); +} + +void +DeterministicShard::flush() +{ + if (!inited_) + return; + + for (auto nObj : nodeset_) + { + backend_->store(nObj); + } + + nodeset_.clear(); +} + +} // namespace NodeStore +} // namespace ripple diff --git a/src/ripple/nodestore/impl/DeterministicShard.h b/src/ripple/nodestore/impl/DeterministicShard.h new file mode 100644 index 00000000000..91bdf1e867a --- /dev/null +++ b/src/ripple/nodestore/impl/DeterministicShard.h @@ -0,0 +1,144 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2020 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_NODESTORE_DETERMINISTICSHARD_H_INCLUDED +#define RIPPLE_NODESTORE_DETERMINISTICSHARD_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { +namespace NodeStore { + +/** DeterministicShard class. + * + * 1. The init() method creates temporary folder tempdir_, + * and the deterministic shard is initialized in that folder. + * 2. The store() method adds object to memory pool. + * 3. The flush() method stores all objects from memory pool to the shard + * located in tempdir_ in sorted order. + * 4. The close(true) method finalizes the shard and moves it from tempdir_ + * temporary folder to filandir_ permanent folder, + * deleting old (non-deterministic) shard located in finaldir_. + */ +class DeterministicShard +{ +public: + using nodeptr = std::shared_ptr; + + DeterministicShard(DeterministicShard const&) = delete; + DeterministicShard& + operator=(DeterministicShard const&) = delete; + + /** Creates the object for shard database + * + * @param app Application object + * @param db Shard Database which deterministic shard belongs to + * @param index Index of the shard + * @param lastHash Hash of last ledger in the shard + * @param j Journal to logging + */ + DeterministicShard( + Application& app, + DatabaseShard const& db, + std::uint32_t index, + uint256 const& lastHash, + beast::Journal j); + + ~DeterministicShard(); + + /** Initializes the deterministic shard. + * + * @return true is success, false if errored + */ + bool + init(); + + /** Finalizes and closes the shard. + * + * @param cancel True if reject the shard and delete all files, + * false if finalize the shard and store them + */ + void + close(bool cancel = false); + + /** Store the object into memory pool + * + * @param nobj Object to store. + */ + void + store(nodeptr nobj); + + /** Flush all objects from memory pool to shard + */ + void + flush(); + +private: + // Count hash of shard parameters: lashHash, firstSeq, lastSeq, index + uint160 + hash(const uint256& lastHash) const; + + // Get n-th 64-bit portion of shard parameters's hash + std::uint64_t + digest(int n) const; + + // If database inited + bool inited_; + + // Sorted set of stored and not flushed objects + std::set> nodeset_; + + // Application reference + Application& app_; + + // Shard database + DatabaseShard const& db_; + + // Shard Index + std::uint32_t const index_; + + // Hash used for digests + uint160 const hash_; + + // Path to temporary database files + boost::filesystem::path const tempdir_; + + // Path to final database files + boost::filesystem::path const finaldir_; + + // Dummy scheduler for deterministic write + DummyScheduler scheduler_; + + // NuDB context + std::unique_ptr ctx_; + + // NuDB key/value store for node objects + std::shared_ptr backend_; + + // Journal + beast::Journal const j_; +}; + +} // namespace NodeStore +} // namespace ripple + +#endif diff --git a/src/ripple/nodestore/impl/Shard.cpp b/src/ripple/nodestore/impl/Shard.cpp index f8799ff3d27..6f8696997c6 100644 --- a/src/ripple/nodestore/impl/Shard.cpp +++ b/src/ripple/nodestore/impl/Shard.cpp @@ -40,6 +40,7 @@ Shard::Shard( std::uint32_t index, beast::Journal j) : app_(app) + , db_(db) , index_(index) , firstSeq_(db.firstLedgerSeq(index)) , lastSeq_(std::max(firstSeq_, db.lastLedgerSeq(index))) @@ -397,7 +398,8 @@ Shard::isLegacy() const bool Shard::finalize( bool const writeSQLite, - boost::optional const& expectedHash) + boost::optional const& expectedHash, + const bool writeDeterministicShard) { assert(backend_); @@ -511,6 +513,17 @@ Shard::finalize( std::shared_ptr next; auto const lastLedgerHash{hash}; + std::shared_ptr dsh; + if (writeDeterministicShard) + { + dsh = std::make_shared( + app_, db_, index_, lastLedgerHash, j_); + if (!dsh->init()) + { + return fail("can't create deterministic shard"); + } + } + // Start with the last ledger in the shard and walk backwards from // child to parent until we reach the first ledger seq = lastSeq_; @@ -547,8 +560,11 @@ Shard::finalize( return fail("missing root TXN node"); } - if (!valLedger(ledger, next)) - return fail("failed to validate ledger"); + if (dsh) + dsh->store(nObj); + + if (!verifyLedger(ledger, next, dsh)) + return fail("verification check failed"); if (writeSQLite) { @@ -609,6 +625,12 @@ Shard::finalize( { backend_->store(nObj); + if (dsh) + { + dsh->store(nObj); + dsh->flush(); + } + std::lock_guard lock(mutex_); final_ = true; @@ -628,6 +650,23 @@ Shard::finalize( std::string("exception ") + e.what() + " in function " + __func__); } + if (dsh) + { + /* Close non-deterministic shard database. */ + backend_->close(); + /* Replace non-deterministic shard by deterministic one. */ + dsh->close(); + /* Re-open deterministic shard database. */ + backend_->open(false); + /** The finalize() function verifies the shard and, if third parameter + * is true, then replaces the shard by deterministic copy of the shard. + * After deterministic shard is created it verifies again, + * the finalize() function called here to verify deterministic shard, + * third parameter is false. + */ + return finalize(false, expectedHash, false); + } + return true; } @@ -922,9 +961,10 @@ Shard::setFileStats(std::lock_guard const&) } bool -Shard::valLedger( +Shard::verifyLedger( std::shared_ptr const& ledger, - std::shared_ptr const& next) const + std::shared_ptr const& next, + std::shared_ptr dsh) const { auto fail = [j = j_, index = index_, &ledger](std::string const& msg) { JLOG(j.fatal()) << "shard " << index << ". " << msg @@ -943,11 +983,14 @@ Shard::valLedger( return fail("Invalid ledger account hash"); bool error{false}; - auto visit = [this, &error](SHAMapAbstractNode& node) { + auto visit = [this, &error, dsh](SHAMapAbstractNode& node) { if (stop_) return false; - if (!valFetch(node.getNodeHash().as_uint256())) + auto nObj = valFetch(node.getNodeHash().as_uint256()); + if (!nObj) error = true; + else if (dsh) + dsh->store(nObj); return !error; }; diff --git a/src/ripple/nodestore/impl/Shard.h b/src/ripple/nodestore/impl/Shard.h index d43fe22a892..bf511a6f2ef 100644 --- a/src/ripple/nodestore/impl/Shard.h +++ b/src/ripple/nodestore/impl/Shard.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -128,11 +129,14 @@ class Shard final verified backend data. @param referenceHash If present, this hash must match the hash of the last ledger in the shard. + @param writeDeterministicShard If true, shard will be rewritten + deterministically. */ bool finalize( bool const writeSQLite, - boost::optional const& referenceHash); + boost::optional const& referenceHash, + const bool writeDeterministicShard = true); void stop() @@ -170,6 +174,8 @@ class Shard final Application& app_; mutable std::recursive_mutex mutex_; + DatabaseShard const& db_; + // Shard Index std::uint32_t const index_; @@ -252,10 +258,12 @@ class Shard final setFileStats(std::lock_guard const& lock); // Validate this ledger by walking its SHAMaps and verifying Merkle trees + // If dsh != NULL then save all walking SHAMaps to deterministic shard dsh bool - valLedger( + verifyLedger( std::shared_ptr const& ledger, - std::shared_ptr const& next) const; + std::shared_ptr const& next, + std::shared_ptr dsh = {}) const; // Fetches from backend and log errors based on status codes std::shared_ptr diff --git a/src/test/nodestore/DatabaseShard_test.cpp b/src/test/nodestore/DatabaseShard_test.cpp index 7e0b746cb62..c4606ecd7b4 100644 --- a/src/test/nodestore/DatabaseShard_test.cpp +++ b/src/test/nodestore/DatabaseShard_test.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -27,13 +28,135 @@ #include #include #include +#include +#include #include +#include +#include +#include #include #include namespace ripple { namespace NodeStore { +/** std::uniform_int_distribution is platform dependent. + * Unit test for deterministic shards is the following: it generates + * predictable accounts and transactions, packs them into ledgers + * and makes the shard. The hash of this shard should be equal to the + * given value. On different platforms (precisely, Linux and Mac) + * hashes of the resulting shard was different. It was unvestigated + * that the problem is in the class std::uniform_int_distribution + * which generates different pseudorandom sequences on different + * platforms, but we need predictable sequence. + */ +template +struct uniformIntDistribution +{ + using resultType = IntType; + + const resultType A, B; + + struct paramType + { + const resultType A, B; + + paramType(resultType aa, resultType bb) : A(aa), B(bb) + { + } + }; + + explicit uniformIntDistribution( + const resultType a = 0, + const resultType b = std::numeric_limits::max()) + : A(a), B(b) + { + } + + explicit uniformIntDistribution(const paramType& params) + : A(params.A), B(params.B) + { + } + + template + resultType + operator()(Generator& g) const + { + return rnd(g, A, B); + } + + template + resultType + operator()(Generator& g, const paramType& params) const + { + return rnd(g, params.A, params.B); + } + + resultType + a() const + { + return A; + } + + resultType + b() const + { + return B; + } + + resultType + min() const + { + return A; + } + + resultType + max() const + { + return B; + } + +private: + template + resultType + rnd(Generator& g, const resultType a, const resultType b) const + { + static_assert( + std::is_convertible:: + value, + "Ups..."); + static_assert( + Generator::min() == 0, "If non-zero we have handle the offset"); + const resultType range = b - a + 1; + assert(Generator::max() >= range); // Just for safety + const resultType rejectLim = g.max() % range; + resultType n; + do + n = g(); + while (n <= rejectLim); + return (n % range) + a; + } +}; + +template +Integral +randInt(Engine& engine, Integral min, Integral max) +{ + assert(max > min); + + // This should have no state and constructing it should + // be very cheap. If that turns out not to be the case + // it could be hand-optimized. + return uniformIntDistribution(min, max)(engine); +} + +template +Integral +randInt(Engine& engine, Integral max) +{ + return randInt(engine, Integral(0), max); +} + // Tests DatabaseShard class // class DatabaseShard_test : public TestBase @@ -87,7 +210,7 @@ class DatabaseShard_test : public TestBase { int p; if (n >= 2) - p = rand_int(rng_, 2 * dataSize); + p = randInt(rng_, 2 * dataSize); else p = 0; @@ -99,27 +222,27 @@ class DatabaseShard_test : public TestBase int from, to; do { - from = rand_int(rng_, n - 1); - to = rand_int(rng_, n - 1); + from = randInt(rng_, n - 1); + to = randInt(rng_, n - 1); } while (from == to); pay.push_back(std::make_pair(from, to)); } - n += !rand_int(rng_, nLedgers / dataSize); + n += !randInt(rng_, nLedgers / dataSize); if (n > accounts_.size()) { char str[9]; for (int j = 0; j < 8; ++j) - str[j] = 'a' + rand_int(rng_, 'z' - 'a'); + str[j] = 'a' + randInt(rng_, 'z' - 'a'); str[8] = 0; accounts_.emplace_back(str); } nAccounts_.push_back(n); payAccounts_.push_back(std::move(pay)); - xrpAmount_.push_back(rand_int(rng_, 90) + 10); + xrpAmount_.push_back(randInt(rng_, 90) + 10); } } @@ -495,7 +618,7 @@ class DatabaseShard_test : public TestBase } std::optional - createShard(TestData& data, DatabaseShard& db, int maxShardNumber) + createShard(TestData& data, DatabaseShard& db, int maxShardNumber = 1) { int shardNumber = -1; @@ -669,7 +792,7 @@ class DatabaseShard_test : public TestBase for (std::uint32_t i = 0; i < nTestShards * 2; ++i) { - std::uint32_t n = rand_int(data.rng_, nTestShards - 1) + 1; + std::uint32_t n = randInt(data.rng_, nTestShards - 1) + 1; if (bitMask & (1ll << n)) { db->removePreShard(n); @@ -978,6 +1101,90 @@ class DatabaseShard_test : public TestBase } } + std::string + ripemd160File(std::string filename) + { + using beast::hash_append; + std::ifstream input(filename, std::ios::in | std::ios::binary); + char buf[4096]; + ripemd160_hasher h; + + while (input.read(buf, 4096), input.gcount() > 0) + hash_append(h, buf, input.gcount()); + + auto const binResult = static_cast(h); + const auto charDigest = binResult.data(); + std::string result; + boost::algorithm::hex( + charDigest, + charDigest + sizeof(binResult), + std::back_inserter(result)); + + return result; + } + + void + testDeterministicShard( + std::string const& backendType, + std::uint64_t const seedValue) + { + using namespace test::jtx; + + std::string ripemd160Key("4CFA8985836B549EC99D2E9705707F488DC91E4E"), + ripemd160Dat("8CC61F503C36339803F8C2FC652C1102DDB889F1"); + + for (int i = 0; i < 2; i++) + { + beast::temp_dir shardDir; + { + Env env{ + *this, + testConfig( + (i ? "" : "deterministicShard"), + backendType, + shardDir.path())}; + DatabaseShard* db = env.app().getShardStore(); + BEAST_EXPECT(db); + + TestData data(seedValue, 4); + if (!BEAST_EXPECT(data.makeLedgers(env))) + return; + + if (createShard(data, *db) < 0) + return; + } + { + Env env{*this, testConfig("", backendType, shardDir.path())}; + DatabaseShard* db = env.app().getShardStore(); + BEAST_EXPECT(db); + + TestData data(seedValue, 4); + if (!BEAST_EXPECT(data.makeLedgers(env))) + return; + + waitShard(*db, 1); + + for (std::uint32_t j = 0; j < ledgersPerShard; ++j) + checkLedger(data, *db, *data.ledgers_[j]); + } + + boost::filesystem::path path(shardDir.path()); + path /= "1"; + boost::filesystem::path keypath = path / (backendType + ".key"); + std::string key = ripemd160File(keypath.string()); + boost::filesystem::path datpath = path / (backendType + ".dat"); + std::string dat = ripemd160File(datpath.string()); + + std::cerr << "Iteration " << i << ": RIPEMD160[" << backendType + << ".key] = " << key << std::endl; + std::cerr << "Iteration " << i << ": RIPEMD160[" << backendType + << ".dat] = " << dat << std::endl; + + BEAST_EXPECT(key == ripemd160Key); + BEAST_EXPECT(dat == ripemd160Dat); + } + } + void testAll(std::string const& backendType) { @@ -991,6 +1198,7 @@ class DatabaseShard_test : public TestBase testCorruptedDatabase(backendType, seedValue + 40); testIllegalFinalKey(backendType, seedValue + 50); testImport(backendType, seedValue + 60); + testDeterministicShard(backendType, seedValue + 70); } public: