Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2688] Deterministic Database Shards #3363

Closed
wants to merge 2 commits into from
Closed

[#2688] Deterministic Database Shards #3363

wants to merge 2 commits into from

Conversation

ghost
Copy link

@ghost ghost commented Apr 16, 2020

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 using 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(1)
Appnum          1
KeySize         32
Salt            digest(0)
Pepper          xxhasher(Salt)
BlockSize       0x1000
LoadFactor      0.5 (numeric 0x8000)

Note: xxhasher is native hasher of NuDB, refer to NuDB code.

The digest(i) mentioned above defined as the follows:

First, SHA256 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

Then, digest(i) is defined as i-th 64-bit portion of the above hash H with swapped order of bytes, i.e.

digest(i) = H[i*8+0] << 56 | H[i*8+1] << 48 | ... | H[i*8+7] << 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: md5[nudb.key] = 59ABA6E282AB88C632CB8870BAC1B132
Iteration 0: md5[nudb.dat] = 36F6526D79DE4E01307615CDCFA279F6
Iteration 1: md5[nudb.key] = 59ABA6E282AB88C632CB8870BAC1B132
Iteration 1: md5[nudb.dat] = 36F6526D79DE4E01307615CDCFA279F6
Iteration 2: md5[nudb.key] = 59ABA6E282AB88C632CB8870BAC1B132
Iteration 2: md5[nudb.dat] = 36F6526D79DE4E01307615CDCFA279F6
Iteration 3: md5[nudb.key] = 59ABA6E282AB88C632CB8870BAC1B132
Iteration 3: md5[nudb.dat] = 36F6526D79DE4E01307615CDCFA279F6

@ghost ghost added the Dependencies Issues associated with 3rd party dependencies (RocksDB, SQLite, etc) label Apr 16, 2020
@ghost
Copy link
Author

ghost commented Apr 16, 2020

Depend on #3332

@MarkusTeufelberger
Copy link
Collaborator

Please don't use MD5 for anything. Even if its use might be OK in this instance, just keeping/using it anywhere in the code base is the wrong move in my opinon.

@ghost
Copy link
Author

ghost commented Apr 17, 2020

Please don't use MD5 for anything. Even if its use might be OK in this instance, just keeping/using it anywhere in the code base is the wrong move in my opinon.

No problem if this is official position of Ripple.

Copy link
Contributor

@nbougalis nbougalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've marked a few nits, but there's a couple of big ticket items that I want to see addressed.

```
where `H[i]` denotes `i`-th byte of hash `H`.


Copy link
Contributor

@nbougalis nbougalis Apr 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First question, and this is more theoretical: should we modify NuDB to increase the size of the headers of the key and data files so that they both are precisely 512 bytes, increase the size of some fields (specifically the UID field that should be 256 bits) and allocate a portion of the header as an area of the database owner to store his own metadata?

Now on to more practical matters: I really don't like the way that we're taking a cryptographic hash function, chopping its output up and manipulating it.

If we make the above changes to the NuDB header structure, we can keep using SHA256 and just set UID to the hash, while storing some metadata in the reserved area.

If we can't do that, then rather than chopping amount the recombining the hash, I recommend the following: replace SHA256 with RIPEMD-160 and use it to calculate H over the following data:

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

Crack the 160 bit hash H apart into 2 64-bit values A and B and 1 32-bit value C and set:

  1. UID := A;
  2. SALT := B; and
  3. APPNUM := C | 0x5348524400000000. (0x53585244 == 'S' 'H' 'R' 'D').

This keeps the full 160-bit hash.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saving hash of shard as UID/salt was not required by this ticket. It is my proposition. Ticket recommends to set UID/salt to known constant + shard_index. Your proposition of using RIPEMD-160 instead of SHA256 is better. I think that changing NuDB standard is much more difficult because current standard is used is main net if I am not mistaking.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -114,7 +114,7 @@ class NuDBBackend
{
create_directories(folder);
nudb::create<nudb::xxhasher>(dp, kp, lp,
currentType, nudb::make_salt(), keyBytes_,
currentType, (salt ? *salt : nudb::make_salt()), keyBytes_,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider salt.value_or(nudb::make_salt()) instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@param salt Deterministic salt used to create a backend.
This allows the caller to catch exceptions.
*/
virtual void open(bool createIfMissing, boost::optional<uint64_t> salt) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't ideal: it allows an implementation detail to "bleed" into the API, but I don't quite have a better idea on how to handle it.

At a minimum the default implementation of this function, here in Backend should throw an std::runtime_error exception, with an appropriate error message that explains that the specific backend does not support deterministic salts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (!keyfile || !datfile)
return false;

nudb::detail::key_file_header keyhead;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p2peer, @miguelportilla we are reaching inside NuDB internals here; we absolutely do not want to do this. We need to modify NuDB so that it cleanly supports the operations that we need to perform.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which way do you want to modify NuDB: 1) include NuDB to rippled and change the code; 2) connect to maintainers of NuDB and ask them to push a modification? I don't know if the way 2 will be effective.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Author of NuDB said that he will research this, see his comment below. While he is researching, I put code under macro NUDB_SUPPORTS_CUSTOM_UID.

if (resp["result"]["status"] != std::string("success"))
{
std::cerr << "Env::close() failed: " << resp["result"]["status"] << std::endl;
res = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't imagine any situation where ledger_accept would not return success, but I know that you encountered at least one instance; I think it's worth understanding what happened there, because other that not running in standalone mode, there's no way the operation can fail and the unit tests essentially "force" standalone mode.

I would definitely not use std::cerr to log from here; use test.log << "whatever" instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several failures of Env::close() appears on almost every local run or Travis run, you can see the Travis log for example: https://travis-ci.com/github/ripple/rippled/jobs/320158724), lines 713-721 and 773-775. I used std::cerr to see these messages on Travis log with --quiet option.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing to std::cerr replaced by writing to journal.

if (dsh)
dsh->store(nObj);

if (!valLedger(ledger, next, dsh))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're here, please rename this function verifyLedger. Rationale: valLedger is a poor name and the term validate has a very different meaning in our code; we should avoid confusing. Similarly, please reame the message below: "verification check failed"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem if the author (@miguelportilla if I am not mistaking) agree with it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifyLedger is okay with me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@vinniefalco
Copy link
Contributor

vinniefalco commented Apr 20, 2020

None of this is necessary, all you need to do is store a key/value pair with a known key constant (say, the number 1 expressed as a 256-bit little endian integer) and with the value containing all the data you want. This can all be accomplished without changing NuDB.

@ghost
Copy link
Author

ghost commented Apr 20, 2020

None of this is necessary, all you need to do is store a key/value pair with a known key constant (say, the number 1 expressed as a 256-bit little endian integer) and with the value containing all the data you want.

These data are already saved as a finalKey under key 0. But the problem is that UID and salt should be deterministic. I can pass custom salt to the NuDB constructor, but I can't pass UID. That is the problem, so the modification of NuDB constructor is required.

@vinniefalco
Copy link
Contributor

I can't pass UID

The UID is not user-modifiable. The purpose of this field is to trivially detect when a data file does not match the key file.

@vinniefalco
Copy link
Contributor

Oh, I see... you want two shards generated by different machines to have the same NuDB file contents. In this case, yes I think we can add UID as a parameter to nudb::create, and a new function make_uid which produces the necessary value. But this could open rippled up to algorithmic complexity attacks. I will research this.

@MarkusTeufelberger
Copy link
Collaborator

I guess to prevent these complexity attacks the hash of the last header in the shard is added to the salt, so there's no real way to mount such an attack until it is already too late to know how the hash will be modified for permanent storage.

I am somewhat concerned about compression of values stored in there. Which compression scheme (if any?) must be used?

@vinniefalco
Copy link
Contributor

I guess to prevent these complexity attacks the hash of the last header in the shard is added to the salt

There is no way to mount an attack, because each node builds the shard using a random salt. And when the shard is preprocessed for sharing, it is rewritten (to put the key/value pairs in order and thus, be deterministic). But since the shard at this point is read-only, no attack is possible.

@MarkusTeufelberger
Copy link
Collaborator

MarkusTeufelberger commented Apr 21, 2020

Wouldn't an algorithmic attack also affect a database file that is only being read from (and that only contains a single bucket that has tons of spill entries due to hash collisions in the extreme case)?

Edit: Also the same contents (semantically) are not enough. The 2 files generated on 2 different machines need to be bitwise identical. You probably already meant this by "deterministic", just to be sure.

@vinniefalco
Copy link
Contributor

Wouldn't an algorithmic attack also affect a database file that is only being read from (and that only contains a single bucket that has tons of spill entries due to hash collisions in the extreme case)?

An algorithmic attack on the database works by trying to get a client to build or accept a database whose keys collide. A client only accepts a database if the contents have been signed by validators. The deterministic salt cannot be predicted in advance, so the attacker has no idea what ledger entries she must create in order to cause collisions. And once the shard is built, an attacker cannot insert anything so no attack is possible.

@MarkusTeufelberger
Copy link
Collaborator

The deterministic salt cannot be predicted in advance

Yes, due to the header hash of the last header that gets mixed in, as I wrote already. I probably misunderstood your initial comment about the "just use a constant key of 1" somewhere.

As far as i understand this here, so far it will create a deterministic header and a deterministic order of keys. I'm unsure if (e.g. after an update of lz4) the actual stored values will be deterministic though. There are 2 options: Either mandate (and document) a certain compression scheme or store all data uncompressed. Both options have their advantages and disadvantages - if one assumes that during transport content will get compressed anyways there's not much to gain from compressing at insertion (and disk space is relatively cheap for these files - they get written once and then ingested elsewhere once too, where they can be compressed by the receiver). If one assumes/proves that compression will result in huge benefits or is inherently deterministic (which might be true for several runs with the same lz4 library in single threaded mode, but I'm not even 100% certain if it would be the case for different (future) versions of that library or if that's a guarantee that they give) then these benefits of course should also be taken.

As a data point, storing these shards with uncompressed values and then compressing the resulting files with zstd was about as good as storing lz4 compressed values and compressed inner nodes at least for the very early shards that I experimented with a while back. This means there's at least the option to create uncompressed deterministic files, compress them for longer term storage or distribution and recompress them upon ingestion into whatever format or database rippled in the distant future implements. Shard contents are anyways re-verified upon import these days, right?

@vinniefalco
Copy link
Contributor

Compressing the entire uncompressed data file instead of compressing each individual key/value pair makes a lot more sense (and is easily extensible too, if you put a header in front of the resulting compressed file).

@ghost
Copy link
Author

ghost commented Apr 22, 2020

Please don't use MD5 for anything. Even if its use might be OK in this instance, just keeping/using it anywhere in the code base is the wrong move in my opinon.

Changed to RIPEMD-160.

@ghost
Copy link
Author

ghost commented Apr 22, 2020

I am somewhat concerned about compression of values stored in there. Which compression scheme (if any?) must be used?

For now, compression by default is used. I can change this to no compression, if this is better.

@codecov-io
Copy link

codecov-io commented Apr 23, 2020

Codecov Report

Merging #3363 into develop will increase coverage by 1.73%.
The diff coverage is 84.57%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #3363      +/-   ##
===========================================
+ Coverage    70.23%   71.97%   +1.73%     
===========================================
  Files          683      684       +1     
  Lines        54630    54794     +164     
===========================================
+ Hits         38370    39437    +1067     
+ Misses       16260    15357     -903     
Impacted Files Coverage Δ
src/ripple/nodestore/Backend.h 25.00% <0.00%> (-75.00%) ⬇️
src/ripple/nodestore/impl/DatabaseShardImp.h 80.00% <ø> (+48.00%) ⬆️
src/ripple/nodestore/impl/Shard.h 100.00% <ø> (+100.00%) ⬆️
src/ripple/nodestore/impl/DeterministicShard.cpp 83.33% <83.33%> (ø)
src/ripple/nodestore/impl/Shard.cpp 69.07% <92.00%> (+69.07%) ⬆️
src/ripple/nodestore/backend/NuDBFactory.cpp 70.12% <100.00%> (+6.83%) ⬆️
src/ripple/nodestore/impl/DatabaseShardImp.cpp 60.33% <100.00%> (+46.84%) ⬆️
src/ripple/server/impl/BaseWSPeer.h 68.10% <0.00%> (-0.55%) ⬇️
src/ripple/beast/xor_shift_engine.h 96.00% <0.00%> (+0.34%) ⬆️
... and 18 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2bf3b19...334f9d1. Read the comment docs.

boost::optional<uint64_t> salt)
{
Throw<std::runtime_error>(std::string(
"Deterministic appType/uid/salt not supported by bachend " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: bachend -> backend

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

static constexpr std::size_t currentType = 1;
static constexpr std::uint64_t currentType = 1;
static constexpr std::uint64_t deterministicType = 0x5348524400000000ll;
static constexpr std::uint64_t deterministicMask = 0xFFFFFFFF00000000ll;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: A ull suffix would be more consistent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@HowardHinnant HowardHinnant self-requested a review April 29, 2020 20:20
HowardHinnant
HowardHinnant previously approved these changes Apr 29, 2020
Copy link
Contributor

@HowardHinnant HowardHinnant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left just a few nitpicky comments.

{
public:
using nodeptr = std::shared_ptr<NodeObject>;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding:

DeterministicShard(DeterministicShard const&) = delete;
DeterministicShard& operator=(DeterministicShard const&) = delete;

This won't change anything. DeterministicShard is neither copyable nor movable, constructible nor assignable, due to the combination of the user-declared destructor and the datamembers. But that fact is subtle. By explicitly deleting the copy members you make it more clear that this class has no copy nor move members.

Some people would go further and delete the move members too. My personal taste is that that is overly verbose because the user-declared copy members suppress the move members.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


The `digest(i)` mentioned above defined as the follows:

First, SHA256 hash `H` calculated of the following structure
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be changed to the actual RIPEMD160 implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


backend_->open(
false,
digest(2) | 0x5348524400000000ll, /* appType */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a small explanation in the comment to this magic number? ("SHRD" in ASCII)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@HowardHinnant
Copy link
Contributor

Needs rebasing to 1.6.0-b5.

@MarkusTeufelberger
Copy link
Collaborator

Having an option for storing data uncompressed would be really nice. Might be something for a future iteration though.

@carlhua carlhua assigned pwang200 and unassigned thejohnfreeman May 27, 2020
Copy link
Contributor

@pwang200 pwang200 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Some of my comments are questions to clarify the design.

The DatabaseShardImp.cpp file has a few changes, totally about 6 lines that I am not familiar with Shard code enough to review. Please make sure other reviewers who know Shard well to review them.

src/ripple/nodestore/backend/NuDBFactory.cpp Show resolved Hide resolved
src/ripple/nodestore/impl/Shard.cpp Outdated Show resolved Hide resolved
src/ripple/nodestore/impl/Shard.cpp Show resolved Hide resolved
Copy link
Contributor

@pwang200 pwang200 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Please edit the document of the flush() function in DeterministicShard.h

@ghost
Copy link
Author

ghost commented Jun 4, 2020

Please edit the document of the flush() function in DeterministicShard.h

Done

@ghost
Copy link
Author

ghost commented Jun 4, 2020

Depends on #3437

Comment on lines 61 to 72
RIPEMD160_CTX c;
uint8_t result[20];

RIPEMD160_Init(&c);
RIPEMD160_Update(&c, &lastHash, sizeof(lastHash));
RIPEMD160_Update(&c, &index_, sizeof(index_));
auto firstSeq = db_.firstLedgerSeq(index_);
RIPEMD160_Update(&c, &firstSeq, sizeof(firstSeq));
auto lastSeq = db_.lastLedgerSeq(index_);
RIPEMD160_Update(&c, &lastSeq, sizeof(lastSeq));
RIPEMD160_Update(&c, &Shard::version, sizeof(Shard::version));
RIPEMD160_Final(result, &c);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments:

  1. Lines like RIPEMD160_Update(&c, &index_, sizeof(index_)); are problematic because they assume endianness. This won't bite us today, because we only support little-endian, but it may bite us tomorrow if we try to compile on a big-endian platform.
  2. This should be using ripple::ripemd160_hash instead; doing so also fixes the endianess issue above.
ripemd160_hasher h;
hash_append(h, lastHash);
hash_append(h, index_);
hash_append(h, db_.firstLedgerSeq);
hash_append(h, db_.lastLedgerSeq);
hash_append(h, Shard::version);

// result is std::array<std::uint8_t, 20>
auto const result = static_cast<ripemd160_hasher::result_type>(h); 

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


The `digest(i)` mentioned above defined as the follows:

First, RIPEMD160 hash `H` calculated of the following structure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please comment that all 32-bit integers are hashed in network byte order.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 76 to 97
std::uint64_t
DeterministicShard::digest(int n) const
{
int ind;
std::uint64_t res = 0;
for (int i = sizeof(std::uint64_t) - 1; i >= 0; --i)
{
res <<= 8;
ind = i + n * sizeof(std::uint64_t);
if (ind < 160 / 8)
res += hash_.data()[ind] & 0xff;
}
return res;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function works but seems a little overly complicated for what it's trying to do. You don't need to change it, but I'm wondering how you feel about this:

std::uint64_t
DeterministicShard::digest(int n) const
{
    auto const data = hash_.data();

    if (n == 2)
    { // Extract 32 bits:
        return (static_cast<std::uint64_t>(data[19]) << 24) +
            (static_cast<std::uint64_t>(data[18]) << 16) +
            (static_cast<std::uint64_t>(data[17]) << 8) +
            (static_cast<std::uint64_t>(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;
}

Note that digest(2) now returns 32 bits (as a 64-bit value), while digest(0) and digest(1) return 64-bit values.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

#include <test/jtx.h>
#include <test/nodestore/TestBase.h>

namespace ripple {
namespace NodeStore {

/* std::uniform_int_distribution is platform dependent */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh... I don't know that I care for all this. I understand the need, given the platform-specific implementation details of std::uniform_int_distribution. I wonder if there's a better option here...

Copy link
Collaborator

@seelabs seelabs Jun 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for doing this? If it's to make sure we can repro unit test bugs on all platforms, then I'd just live with the platform dependence and remove this class. There is a very small possibility that we'll see failures on some platforms that we won't be able to repro on other platforms in the first place. If this happens, we'll just have to debug on the platforms where we can repro.

(if there's another reason for the class then nvm, I only took a very quick look before weighing in).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 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. I saw that on different platforms (precisely, linux and mac) hashes of the resulting shard was different. I unvestigated this case and determine that the problem is in the class std::uniform_int_distribution which generates different pseudorandom sequences on different platforms, but we need predictable sequence.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@p2peer That is a different motivation that what I was envisioning. Thanks for the clarification!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: added comment to the code

@@ -978,6 +1090,93 @@ class DatabaseShard_test : public TestBase
}
}

std::string
ripemd160File(std::string filename)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as earlier: please use the hasher.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@carlhua carlhua requested a review from nbougalis June 22, 2020 14:32
Copy link
Contributor

@nbougalis nbougalis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Please consider updating the commit message (ref). I recommend:

Add support for deterministic database shards (#2688):

This commit, if merged, adds support to allow multiple indepedent nodes to
produce a binary identical shard for a given range of ledgers. The advantage
is that servers can use content-addressable storage, and can more efficiently
retrieve shards by downloading from multiple peers at once and then verifying
the integrity of a shard by cross-checking its checksum with the checksum
other servers report.

@carlhua carlhua added the Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required. label Jun 24, 2020
This commit, if merged, adds support to allow multiple indepedent nodes to
produce a binary identical shard for a given range of ledgers. The advantage
is that servers can use content-addressable storage, and can more efficiently
retrieve shards by downloading from multiple peers at once and then verifying
the integrity of a shard by cross-checking its checksum with the checksum
other servers report.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Dependencies Issues associated with 3rd party dependencies (RocksDB, SQLite, etc) Passed Passed code review & PR owner thinks it's ready to merge. Perf sign-off may still be required.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants