Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 36 additions & 24 deletions barretenberg/cpp/src/barretenberg/honk/transcript/transcript.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,35 @@ template <typename FF> class BaseTranscript {
TranscriptManifest manifest;

/**
* @brief Compute c_next = H( Compress(c_prev || round_buffer) )
*
* @brief Compute next challenge c_next = H( Compress(c_prev || round_buffer) )
* @details This function computes a new challenge for the current round using the previous challenge
* and the current round data, if they are exist.
Comment thread
lucasxia01 marked this conversation as resolved.
Outdated
* It clears the current_round_data if nonempty after computing the challenge to minimize how much we
* compress. It also sets previous_challenge_buffer to the current challenge buffer to set up next function
* call.
* @return std::array<uint8_t, HASH_OUTPUT_SIZE>
*/
[[nodiscard]] std::array<uint8_t, HASH_OUTPUT_SIZE> get_next_challenge_buffer() const
[[nodiscard]] std::array<uint8_t, HASH_OUTPUT_SIZE> get_next_challenge_buffer()
{
// Prevent challenge generation if nothing was sent by the prover.
ASSERT(!current_round_data.empty());

// concatenate the hash of the previous round (if not the first round) with the current round data.
// Empty buffer to compare against
std::array<uint8_t, HASH_OUTPUT_SIZE> empty_challenge_buffer{};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this only needed to check if there was a previous challenge or not? It seems clearer to just use Adrians original if (round_number > 0) check. Maybe I'm missing something? if (!no_previous_challenge) is kind of icky :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

can't use round_number because in the 0th round, we might want to call this function multiple times. I could change the bool to first_challenge instead to avoid double negative?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see. Maybe just a first_challenge bool that's initialized to true then gets set to false. I just find it a bit hard to understand what the intent is as you have it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good idea, just did this instead

bool no_previous_challenge = (previous_challenge_buffer == empty_challenge_buffer);
// Prevent challenge generation if nothing was sent by the prover AND this is the first challenge we're
// generating.
ASSERT(!(current_round_data.empty() && no_previous_challenge));

// concatenate the previous challenge (if this is not the first challenge) with the current round data.
// TODO(Adrian): Do we want to use a domain separator as the initial challenge buffer?
// We could be cheeky and use the hash of the manifest as domain separator, which would prevent us from having
// to domain separate all the data. (See https://safe-hash.dev)
std::vector<uint8_t> full_buffer;
if (round_number > 0) {
if (!no_previous_challenge) {
full_buffer.insert(full_buffer.end(), previous_challenge_buffer.begin(), previous_challenge_buffer.end());
}
full_buffer.insert(full_buffer.end(), current_round_data.begin(), current_round_data.end());
if (!current_round_data.empty()) {
full_buffer.insert(full_buffer.end(), current_round_data.begin(), current_round_data.end());
current_round_data.clear(); // clear the round data buffer since it has been used
}

// Pre-hash the full buffer to minimize the amount of data passed to the cryptographic hash function.
// Only a collision-resistant hash-function like Pedersen is required for this step.
Expand All @@ -109,7 +120,8 @@ template <typename FF> class BaseTranscript {

std::array<uint8_t, HASH_OUTPUT_SIZE> new_challenge_buffer;
std::copy_n(base_hash.begin(), HASH_OUTPUT_SIZE, new_challenge_buffer.begin());

// update previous challenge buffer for next time we call this function
previous_challenge_buffer = new_challenge_buffer;
return new_challenge_buffer;
};

Expand All @@ -131,8 +143,11 @@ template <typename FF> class BaseTranscript {
public:
/**
* @brief After all the prover messages have been sent, finalize the round by hashing all the data and then create
* the number of requested challenges which will be increasing powers of the first challenge. Finally, reset the
* state in preparation for the next round.
* the number of requested challenges.
* @details Challenges are generated by iteratively hashing over the previous challenge, using
* get_next_challenge_buffer().
* TODO(#741): Optimizations for this function include generalizing type of hash, splitting hashes into
Comment thread
lucasxia01 marked this conversation as resolved.
Outdated
* multiple challenges.
*
* @param labels human-readable names for the challenges for the manifest
* @return std::array<FF, num_challenges> challenges for this round.
Expand All @@ -145,26 +160,23 @@ template <typename FF> class BaseTranscript {
manifest.add_challenge(round_number, labels...);

// Compute the new challenge buffer from which we derive the challenges.
auto next_challenge_buffer = get_next_challenge_buffer();

// Create challenges from bytes.
std::array<FF, num_challenges> challenges{};

std::array<uint8_t, sizeof(FF)> field_element_buffer{};
std::copy_n(next_challenge_buffer.begin(), HASH_OUTPUT_SIZE, field_element_buffer.begin());

challenges[0] = from_buffer<FF>(field_element_buffer);

// TODO(#583): rework the transcript to have a better structure and be able to produce a variable amount of
// challenges that are not powers of each other
for (size_t i = 1; i < num_challenges; i++) {
challenges[i] = challenges[i - 1] * challenges[0];
// Generate the challenges by iteratively hashing over the previous challenge.
for (size_t i = 0; i < num_challenges; i++) {
auto next_challenge_buffer = get_next_challenge_buffer(); // get next challenge buffer
std::array<uint8_t, sizeof(FF)> field_element_buffer{};
// copy half of the hash to lower 128 bits of challenge
std::copy_n(next_challenge_buffer.begin(),
HASH_OUTPUT_SIZE / 2,
field_element_buffer.begin() + HASH_OUTPUT_SIZE / 2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure how this serialization works but I would've assumed the byte at index 0 would correspond to the lowest byte of the deserialized result. If thats the case then you'd be writing to the highest 16 bytes of the field element. I assume you checked this and I'm wrong?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah actually good point. I did check this, but I'm not sure why this happens. I believe the Adrian's old code actually made this mistake and put it in the upper 16 bytes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bizarre. Maybe there was good reason for doing this but its not intuitive. Worth a comment I would say

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

so it actually reads the buffer like an integer; not entirely sure if some of the bytes get reversed or anything like that, but the resulting challenge is indeed 128 bits.

challenges[i] = from_buffer<FF>(field_element_buffer);
}

// Prepare for next round.
++round_number;
current_round_data.clear();
previous_challenge_buffer = next_challenge_buffer;

return challenges;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,34 @@ TEST_F(UltraTranscriptTests, VerifierManifestConsistency)
}
}

/**
* @brief Check that multiple challenges can be generated and sanity check
* @details We generate 6 challenges that are each 128 bits, and check that they are not 0.
*
*/
TEST_F(UltraTranscriptTests, ChallengeGenerationTest)
Comment thread
lucasxia01 marked this conversation as resolved.
{
// initialized with random value sent to verifier
auto transcript = ProverTranscript<FF>::init_empty();
// test a bunch of challenges
auto challenges = transcript.get_challenges("a", "b", "c", "d", "e", "f");
// print challenges and check they are not 0
for (size_t i = 0; i < challenges.size(); ++i) {
info("Challenge ", i, ": ", challenges[i]);
ASSERT_NE(challenges[i], 0) << "Challenge " << i << " is 0";
}
constexpr uint32_t random_val{ 17 }; // arbitrary
transcript.send_to_verifier("random val", random_val);
// test more challenges
auto [a, b, c] = transcript.get_challenges("a", "b", "c");
info("Challenge a: ", a);
info("Challenge b: ", b);
info("Challenge c: ", c);
ASSERT_NE(a, 0) << "Challenge a is 0";
ASSERT_NE(b, 0) << "Challenge a is 0";
ASSERT_NE(b, 0) << "Challenge a is 0";
}

TEST_F(UltraTranscriptTests, FoldingManifestTest)
{
using Flavor = flavor::Ultra;
Expand Down