Skip to content

chore: stdlib::poseidon2 internal audit#16534

Merged
iakovenkos merged 37 commits intomerge-train/barretenbergfrom
si/poseidon-2-internal-audit
Sep 5, 2025
Merged

chore: stdlib::poseidon2 internal audit#16534
iakovenkos merged 37 commits intomerge-train/barretenbergfrom
si/poseidon-2-internal-audit

Conversation

@iakovenkos
Copy link
Copy Markdown
Contributor

@iakovenkos iakovenkos commented Aug 22, 2025

🧾stdlib::poseidon2 internal audit

  • Most of the Poseidon2-related methods and classes were unnecessarily templated on a variety of parameters, such as rate and capacity of the permutation, while at the same time our Circuit Builders would only support the case rate = 3, capacity = 1. To avoid confusion and improve the readability, I minimized the number of templates.
  • The sponge class was only invoked with a sequence of absorb calls that would lead to a single squezze call. It allowed to simplify the class logic.
  • Simplified the Poseidon2Permutation class by re-using some of the field_t methods. Improved the docs.
  • Added a detailed readme that contains links to the sources, including the padding choices in sponge and the explanation of the custom Poseidon2 gates
  • Audited relations, expanded corresponding docs
  • Added a bunch of tests - collision-resistance sanity check, test against the hash outputs from https://github.com/zemse/poseidon2-evm, several basic failure tests

@iakovenkos iakovenkos self-assigned this Aug 25, 2025
namespace bb::stdlib {

template <typename Params, typename Builder> class Poseidon2Permutation {
template <typename Builder> class Poseidon2Permutation {
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.

redundant template param

const bb::fr four(4);
// create the 6 gates for the initial matrix multiplication
// gate 1: Compute tmp1 = state[0] + state[1] + 2 * state[3]
field_t<Builder> tmp1 =
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.

seems more robust to re-use add_two from field_t, as we don't allow hashing constants, the result of add_two is always a new witness, which is normalized. hence, it's safe to use get_witness_index() above.

template <size_t rate, size_t capacity, size_t t, typename Permutation, typename Builder> class FieldSponge {
template <size_t rate, size_t capacity, size_t t, typename Builder> class FieldSponge {
public:
/**
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.

Not needed in our use-cases.

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.

are we opting to fix the capacity etc?

}

std::array<field_t, rate> perform_duplex()
void perform_duplex()
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.

this method used to mutate state and return the output equal to the updated state.

* @param input
* @return std::array<field_t, out_len>
*/
template <size_t out_len>
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.

the function is only used with out_len == 1

ASSERT(!data.empty());
ASSERT(data[0].get_context() != nullptr);

Builder* builder = data[0].get_context();
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.

this is done in the hash method

@iakovenkos iakovenkos marked this pull request as ready for review September 3, 2025 13:06
@fcarreiro fcarreiro removed their request for review September 3, 2025 15:38
@iakovenkos iakovenkos requested a review from kashbrti September 3, 2025 15:45
// Add ĉ₀⁽ⁱ⁾ stored in the selector and convert to Lagrange basis
auto s1 = Accumulator(w_1 + c_0_int);

// Apply S-box. Note that the multiplication is performed point-wise
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.

hmm, should we add an S-BOX method at this point instead of having the macro and use it across both internal and external relation?

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.

I don't knoow, feels a bit redundant tbh, cause it's only done once in the internal one

Copy link
Copy Markdown
Contributor

@kashbrti kashbrti left a comment

Choose a reason for hiding this comment

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

other than few nitpicks lgtm. haven't looked too much into the README and the tests.

template <size_t rate, size_t capacity, size_t t, typename Permutation, typename Builder> class FieldSponge {
template <size_t rate, size_t capacity, size_t t, typename Builder> class FieldSponge {
public:
/**
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.

are we opting to fix the capacity etc?

};
template <typename Builder> class FieldSponge {
private:
using Permutation = Poseidon2Permutation<Builder>;
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.

could you add a reference where we have gotten the sponge spec and the parameters from?

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.

it's in the readme

state = Permutation::permutation(builder, state);
// return `rate` number of field elements from the sponge state.
std::array<field_t, rate> output;
for (size_t i = 0; i < rate; ++i) {
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.

were we just copying the state and outputting it here?

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.

yep

static field_t hash_internal(std::span<const field_t> input)
{
size_t in_len = input.size();
const uint256_t iv = (static_cast<uint256_t>(in_len) << 64) + out_len - 1;
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.

just moved to the constructor. still have mixed feelings about the amount of flexibility we're removing from the sponge. I guess internally it doesn't matter. but thinking whether we want it to be a standalone primitive that other devs can play around with.

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.

there was no flexibility tbh, cause create_poseidon2_***_gate supports only the case where rate + capacity = num_wires (=4). templating those circuit builder methods on these params and storing several batches of round constants sounds painful. so before my changes we couldn't just instantiate the permutation with different params

// The final state consists of 4 elements, we only use the first element, which means that the remaining
// 3 witnesses are only used in a single gate.
for (const auto& elem : sponge.state) {
builder->update_used_witnesses(elem.witness_index);
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.

this used to be skipped for MegaCircuitBuilder was there a reason for this?

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.

Daniel's boomerang test is only instantiated with UltraCircuitBuilder but in both cases the "tail" of the final state is un-used

static void matrix_multiplication_external(Builder* builder, State& state);
static void record_current_state_into_next_row(Builder* builder, const State& state, auto& block)
{
builder->create_dummy_gate(block,
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.

create_dummy_gate just constructs a gate that is never active? I agree then that the naming is funky

template <typename Params, typename Builder>
typename Poseidon2Permutation<Params, Builder>::State Poseidon2Permutation<Params, Builder>::permutation(
Builder* builder, const typename Poseidon2Permutation<Params, Builder>::State& input)
template <typename Builder>
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.

just fixing the params to BN254 it seems.

* @param block Either `poseidon2_external` or `poseidon2_internal` block of the Execution Trace
*/
static void matrix_multiplication_external(Builder* builder, State& state);
static void record_current_state_into_next_row(Builder* builder, const State& state, auto& block)
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.

nitpick, but maybe store or write is a better name? (write might be confused by a memory write)

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.

changed to propagate_current_state_to_next_row()

// add the cache into sponge state
// Add the cache into sponge state
for (size_t i = 0; i < rate; ++i) {
state[i] += cache[i];
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.

@kashbrti the padding is happening here. when squeeze calls perform_duplex(), only first num_inputs % 3 are not const 0

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.

hmm, so I see that we're keeping a cache of size 3, whenever it fills up we're calling the perform_duplex. don't see what we do when for the last block (of length 3) we don't have more elements to fill the block. I guess in this case, we're just calling squeeze on it?

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.

squeeze will again call perform_duplex. but the cache will not have an element in index 2 anymore, am I missing something? because it seems each call of perform_duplex just empties the cache.

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.

ah cache is a std::array<field_t,3> and not a std::vector<field_t> so it'll have a 0 field_t there.
but shouldn't we have a bit of 1 before starting to pad with 0s? like now we will have a collision between a genuine input that ends with 0 and one that needs padding by 1 element it seems.

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.

Well, we don't support fixed length mode at all. So, IV prevents this. I added a test checking that there are no collisions between {x}, {x,0}, and {x,0,0}

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 guess the length used in the IV would mitigate that, but still might be better to take a standard approach.

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.

ah seems like I forgot to press comment XD. yea I guess the IV is preventing the collisions

@kashbrti
Copy link
Copy Markdown
Contributor

kashbrti commented Sep 5, 2025

also please add a PR description.

@iakovenkos iakovenkos changed the title chore: stdlib poseidon2 internal audit chore: stdlib::poseidon2 internal audit Sep 5, 2025
@iakovenkos iakovenkos merged commit 41de29a into merge-train/barretenberg Sep 5, 2025
6 checks passed
@iakovenkos iakovenkos deleted the si/poseidon-2-internal-audit branch September 5, 2025 11:45
github-merge-queue bot pushed a commit that referenced this pull request Sep 5, 2025
BEGIN_COMMIT_OVERRIDE
chore: databus internal audit - small cleanups and tests coverage
(#16703)
chore: `stdlib::poseidon2` internal audit (#16534)
END_COMMIT_OVERRIDE
mralj pushed a commit that referenced this pull request Oct 13, 2025
- Most of the `Poseidon2`-related methods and classes were unnecessarily
templated on a variety of parameters, such as `rate` and `capacity` of
the permutation, while at the same time our Circuit Builders would only
support the case `rate = 3`, `capacity = 1`. To avoid confusion and
improve the readability, I minimized the number of templates.
- The `sponge` class was only invoked with a sequence of `absorb` calls
that would lead to a single `squezze` call. It allowed to simplify the
class logic.
- Simplified the `Poseidon2Permutation` class by re-using some of the
`field_t` methods. Improved the docs.
- Added a detailed **readme** that contains links to the sources,
including the padding choices in `sponge` and the explanation of the
custom Poseidon2 gates
- Audited relations, expanded corresponding docs
- Added a bunch of tests - collision-resistance sanity check, test
against the hash outputs from https://github.com/zemse/poseidon2-evm,
several basic failure tests
ludamad pushed a commit that referenced this pull request Dec 16, 2025
### 🧾`stdlib::poseidon2` internal audit

- Most of the `Poseidon2`-related methods and classes were unnecessarily
templated on a variety of parameters, such as `rate` and `capacity` of
the permutation, while at the same time our Circuit Builders would only
support the case `rate = 3`, `capacity = 1`. To avoid confusion and
improve the readability, I minimized the number of templates.
- The `sponge` class was only invoked with a sequence of `absorb` calls
that would lead to a single `squezze` call. It allowed to simplify the
class logic.
- Simplified the `Poseidon2Permutation` class by re-using some of the
`field_t` methods. Improved the docs.
- Added a detailed **readme** that contains links to the sources,
including the padding choices in `sponge` and the explanation of the
custom Poseidon2 gates
- Audited relations, expanded corresponding docs
- Added a bunch of tests - collision-resistance sanity check, test
against the hash outputs from https://github.com/zemse/poseidon2-evm,
several basic failure tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants