Skip to content

Commit 629219e

Browse files
author
AztecBot
committed
Merge branch 'next' into merge-train/barretenberg
2 parents 79771a7 + b3c41b1 commit 629219e

15 files changed

Lines changed: 183 additions & 62 deletions

File tree

barretenberg/cpp/pil/vm2/tx.pil

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ namespace tx;
525525
note_hash_tree_check.next_root
526526
};
527527

528-
// If reverted, the next state is unconstrained. This is okay since we are going to restore the state at the end of setup or fail proving.
528+
// If reverted, the next state is unconstrained as `should_note_hash_append = 0`. This is okay since we are going to restore the state at the end of setup or fail proving.
529529
// Namely, #[RESTORE_STATE_ON_REVERT] restores the tree state to its value at the end of setup.
530530
should_note_hash_append * (prev_note_hash_tree_size + 1 - next_note_hash_tree_size) = 0;
531531
should_note_hash_append * (prev_num_note_hashes_emitted + 1 - next_num_note_hashes_emitted) = 0;
@@ -556,12 +556,12 @@ namespace tx;
556556
pol commit nullifier_tree_height;
557557
should_nullifier_append * (nullifier_tree_height - constants.NULLIFIER_TREE_HEIGHT) = 0;
558558

559-
// We revert if the nullifier already exists.
559+
// Collisions are disallowed, if the nullifier already exists the transaction is unprovable
560560
#[NULLIFIER_APPEND]
561561
should_nullifier_append {
562562
leaf_value,
563563
prev_nullifier_tree_root,
564-
reverted,
564+
precomputed.zero, // Nullifiers emitted in tx trace cannot have existed
565565
next_nullifier_tree_root,
566566
prev_nullifier_tree_size,
567567
discard, // from tx_discard.pil virtual trace
@@ -580,10 +580,10 @@ namespace tx;
580580
indexed_tree_check.sel_silo
581581
};
582582

583-
// If reverted, the next state is unconstrained. This is okay since we are going to restore the state at the end of setup.
583+
// If reverted, the next state is unconstrained since `should_nullifier_append = 0`. This is okay since we are going to restore the state at the end of setup.
584584
// Namely, #[RESTORE_STATE_ON_REVERT] restores the tree state to its value at the end of setup.
585-
should_nullifier_append * (1 - reverted) * (prev_nullifier_tree_size + 1 - next_nullifier_tree_size) = 0;
586-
should_nullifier_append * (1 - reverted) * (prev_num_nullifiers_emitted + 1 - next_num_nullifiers_emitted) = 0;
585+
should_nullifier_append * (prev_nullifier_tree_size + 1 - next_nullifier_tree_size) = 0;
586+
should_nullifier_append * (prev_num_nullifiers_emitted + 1 - next_num_nullifiers_emitted) = 0;
587587

588588
// ===== L2 - L1 Messages =====
589589
pol commit should_try_l2_l1_msg_append; // @boolean (follows from definition)

barretenberg/cpp/src/barretenberg/vm2/constraining/relations/tx.test.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,4 +849,38 @@ TEST_F(TxExecutionConstrainingWithCalldataTest, SimpleHandleCalldata)
849849
check_relation<bb::avm2::calldata_hashing<FF>>(trace);
850850
check_all_interactions<tracegen::CalldataTraceBuilder>(trace);
851851
}
852+
853+
// Verify that the nullifier state increment is unconditional when should_nullifier_append = 1.
854+
// A malicious prover cannot set reverted = 1 to skip the state increment.
855+
TEST(TxExecutionConstrainingTest, NegativeNullifierStateIncrementIsUnconditional)
856+
{
857+
// Subrelation 42: should_nullifier_append * (prev_nullifier_tree_size + 1 - next_nullifier_tree_size) = 0
858+
// Subrelation 43: should_nullifier_append * (prev_num_nullifiers_emitted + 1 - next_num_nullifiers_emitted) = 0
859+
TestTraceContainer trace({
860+
{
861+
// Row 0
862+
{ C::precomputed_first_row, 1 },
863+
},
864+
{
865+
// Row 1: Nullifier append with should_nullifier_append = 1 but state not incremented.
866+
{ C::tx_sel, 1 },
867+
{ C::tx_should_nullifier_append, 1 },
868+
{ C::tx_reverted, 1 }, // Prover tries to cheat by setting reverted = 1.
869+
{ C::tx_prev_nullifier_tree_size, 5 },
870+
{ C::tx_next_nullifier_tree_size, 5 }, // Should be 6.
871+
{ C::tx_prev_num_nullifiers_emitted, 3 },
872+
{ C::tx_next_num_nullifiers_emitted, 3 }, // Should be 4.
873+
},
874+
});
875+
876+
// Subrelation 42: tree size must increment.
877+
EXPECT_THROW_WITH_MESSAGE(check_relation<tx>(trace, static_cast<size_t>(42)), "subrelation 42");
878+
// Fix tree size, break emitted count.
879+
trace.set(C::tx_next_nullifier_tree_size, 1, 6);
880+
EXPECT_THROW_WITH_MESSAGE(check_relation<tx>(trace, static_cast<size_t>(43)), "subrelation 43");
881+
// Fix emitted count — both should pass now.
882+
trace.set(C::tx_next_num_nullifiers_emitted, 1, 4);
883+
check_relation<tx>(trace, static_cast<size_t>(42), static_cast<size_t>(43));
884+
}
885+
852886
} // namespace bb::avm2::constraining

barretenberg/cpp/src/barretenberg/vm2/generated/relations/lookups_tx.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ struct lookup_tx_nullifier_append_settings_ {
181181
static constexpr std::array<ColumnAndShifts, LOOKUP_TUPLE_SIZE> SRC_COLUMNS = {
182182
ColumnAndShifts::tx_leaf_value,
183183
ColumnAndShifts::tx_prev_nullifier_tree_root,
184-
ColumnAndShifts::tx_reverted,
184+
ColumnAndShifts::precomputed_zero,
185185
ColumnAndShifts::tx_next_nullifier_tree_root,
186186
ColumnAndShifts::tx_prev_nullifier_tree_size,
187187
ColumnAndShifts::tx_discard,

barretenberg/cpp/src/barretenberg/vm2/generated/relations/tx.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ template <typename FF_> class txImpl {
1616

1717
static constexpr std::array<size_t, 64> SUBRELATION_PARTIAL_LENGTHS = {
1818
3, 3, 3, 3, 3, 3, 2, 3, 5, 3, 3, 3, 5, 4, 3, 3, 5, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 2, 3, 5,
19-
3, 3, 3, 3, 3, 5, 4, 3, 3, 3, 4, 4, 3, 5, 4, 3, 4, 3, 2, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
19+
3, 3, 3, 3, 3, 5, 4, 3, 3, 3, 3, 3, 3, 5, 4, 3, 4, 3, 2, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
2020
};
2121

2222
template <typename AllEntities> inline static bool skip(const AllEntities& in)

barretenberg/cpp/src/barretenberg/vm2/generated/relations/tx_impl.hpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,15 +342,13 @@ void txImpl<FF_>::accumulate(ContainerOverSubrelations& evals,
342342
{
343343
using View = typename std::tuple_element_t<42, ContainerOverSubrelations>::View;
344344
auto tmp = static_cast<View>(in.get(C::tx_should_nullifier_append)) *
345-
(FF(1) - static_cast<View>(in.get(C::tx_reverted))) *
346345
((static_cast<View>(in.get(C::tx_prev_nullifier_tree_size)) + FF(1)) -
347346
static_cast<View>(in.get(C::tx_next_nullifier_tree_size)));
348347
std::get<42>(evals) += (tmp * scaling_factor);
349348
}
350349
{
351350
using View = typename std::tuple_element_t<43, ContainerOverSubrelations>::View;
352351
auto tmp = static_cast<View>(in.get(C::tx_should_nullifier_append)) *
353-
(FF(1) - static_cast<View>(in.get(C::tx_reverted))) *
354352
((static_cast<View>(in.get(C::tx_prev_num_nullifiers_emitted)) + FF(1)) -
355353
static_cast<View>(in.get(C::tx_next_num_nullifiers_emitted)));
356354
std::get<43>(evals) += (tmp * scaling_factor);

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/tx_execution.cpp

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,12 @@ std::string get_halting_information(const EnqueuedCallResult& result)
5858
* - Cleanup (11)
5959
*
6060
* If an error occurs during non-revertible insertions or a Setup phase enqueued call fails,
61-
* the transaction is considered unprovable and an unrecoverable TxExecutionException is thrown.
62-
* If an error occurs during revertible insertions or App Logic phase, all the state changes are reverted
63-
* to the post-setup state and we continue with the Teardown phase.
61+
* the transaction is considered unprovable and an unrecoverable exception is thrown.
62+
* If a side-effect limit is reached during revertible insertions or App Logic phase fails, all the state
63+
* changes are reverted to the post-setup state and we continue with the Teardown phase.
64+
* A nullifier collision during revertible insertions is ALSO unprovable (not revertible): the nullifier
65+
* originated from private, so a collision indicates the tx should never have been proposed. The
66+
* NullifierCollisionException propagates out of simulate() as-is.
6467
* If an error occurs during Teardown phase, all the state changes are reverted to the post-setup state and
6568
* we continue with the Collect Gas Fees phase.
6669
*
@@ -69,9 +72,11 @@ std::string get_halting_information(const EnqueuedCallResult& result)
6972
*
7073
* @param tx The transaction to simulate.
7174
* @return The result of the transaction simulation.
75+
* @throws NullifierCollisionException if a private nullifier collision occurs in either the non-revertible
76+
* or revertible insertion phase.
7277
* @throws TxExecutionException if
73-
* - there is a nullifier collision or the maximum number of
74-
* nullifiers, note hashes, or L2 to L1 messages is reached as part of the non-revertible insertions.
78+
* - the maximum number of nullifiers, note hashes, or L2 to L1 messages is reached as part of the
79+
* non-revertible insertions.
7580
* - a Setup phase enqueued call fails.
7681
* - the fee payer does not have enough balance to pay the fee.
7782
* Note: Other low-level exceptions of other types are not caught and will be thrown.
@@ -161,8 +166,9 @@ TxExecutionResult TxExecution::simulate(const Tx& tx)
161166
call_stack_metadata_collector.set_phase(CoarseTransactionPhase::APP_LOGIC);
162167

163168
try {
164-
// Insert revertibles. This can throw if there is a nullifier collision.
165-
// Such an exception should be handled and the tx be provable.
169+
// Insert revertibles. This can throw TxExecutionException on a side-effect limit error
170+
// (handled here, tx is provable) or NullifierCollisionException on a collision
171+
// (not handled here - propagates as unrecoverable, since the tx is unprovable).
166172
// We catch separately here to record the revert reason in call stack metadata,
167173
// since no calls have populated the metadata yet at this point.
168174
try {
@@ -366,7 +372,8 @@ void TxExecution::emit_public_call_request(const PublicCallRequestWithCalldata&
366372
*
367373
* @param revertible Whether the nullifier is revertible.
368374
* @param nullifier The nullifier to insert.
369-
* @throws TxExecutionException if the maximum number of nullifiers is reached or a nullifier collision occurs.
375+
* @throws TxExecutionException if the maximum number of nullifiers is reached.
376+
* @throws NullifierCollisionException if the nullifier collides with an existing one (unrecoverable).
370377
*/
371378
void TxExecution::emit_nullifier(bool revertible, const FF& nullifier)
372379
{
@@ -383,7 +390,13 @@ void TxExecution::emit_nullifier(bool revertible, const FF& nullifier)
383390
try {
384391
merkle_db.siloed_nullifier_write(nullifier);
385392
} catch (const NullifierCollisionException& e) {
386-
throw TxExecutionException(e.what());
393+
// Do not handle the NullifierCollisionException as any collision at the tx execution
394+
// level is unprovable (i.e. this is an unrecoverable error).
395+
// Rethrow with more information - note that this exception isn't being caught in this file
396+
throw NullifierCollisionException(format("[",
397+
revertible ? "R" : "NR",
398+
"_NULLIFIER_INSERTION] UNRECOVERABLE ERROR! Nullifier collision: ",
399+
e.what()));
387400
}
388401

389402
events.emit(TxPhaseEvent{ .phase = phase,
@@ -489,11 +502,11 @@ void TxExecution::emit_l2_to_l1_message(bool revertible, const ScopedL2ToL1Messa
489502
/**
490503
* @brief Insert the non-revertible accumulated data into the Merkle DB and emit corresponding events.
491504
* It might error if the limits for the number of allowable inserts are exceeded or a nullifier collision occurs,
492-
* but this results in an unprovable tx.
505+
* either of which results in an unprovable tx.
493506
*
494507
* @param tx The transaction to insert the non-revertible accumulated data into.
495-
* @throws TxExecutionException if the maximum number of nullifiers, note hashes, L2 to L1 messages is reached, or a
496-
* nullifier collision occurs.
508+
* @throws TxExecutionException if the maximum number of nullifiers, note hashes, or L2 to L1 messages is reached.
509+
* @throws NullifierCollisionException if a nullifier collision occurs (unrecoverable).
497510
*/
498511
void TxExecution::insert_non_revertibles(const Tx& tx)
499512
{
@@ -541,11 +554,12 @@ void TxExecution::insert_non_revertibles(const Tx& tx)
541554

542555
/**
543556
* @brief Insert the revertible accumulated data into the Merkle DB and emit corresponding events.
544-
* It might error if the limits for the number of allowable inserts are exceeded or a nullifier collision occurs.
557+
* A side-effect limit error is recoverable (the caller reverts to post-setup); a nullifier
558+
* collision is unrecoverable (the tx is unprovable).
545559
*
546560
* @param tx The transaction to insert the revertible accumulated data into.
547-
* @throws TxExecutionException if the maximum number of nullifiers, note hashes, L2 to L1 messages is reached, or a
548-
* nullifier collision occurs.
561+
* @throws TxExecutionException if the maximum number of nullifiers, note hashes, or L2 to L1 messages is reached.
562+
* @throws NullifierCollisionException if a nullifier collision occurs (unrecoverable).
549563
*/
550564
void TxExecution::insert_revertibles(const Tx& tx)
551565
{

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/tx_execution.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ class TxExecution final {
7676
TxContext tx_context;
7777
bool skip_fee_enforcement;
7878

79-
// This function can throw if there is a nullifier collision.
79+
// Throws TxExecutionException on a side-effect limit error, or NullifierCollisionException on a
80+
// nullifier collision. Both are unrecoverable — they result in an unprovable tx.
8081
void insert_non_revertibles(const Tx& tx);
81-
// This function can throw if there is a nullifier collision.
82+
// Throws TxExecutionException on a side-effect limit error (recoverable — caller reverts to
83+
// post-setup), or NullifierCollisionException on a nullifier collision (unrecoverable — unprovable tx).
8284
void insert_revertibles(const Tx& tx);
8385
void emit_public_call_request(const PublicCallRequestWithCalldata& call,
8486
TransactionPhase phase,

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/tx_execution.test.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,5 +450,72 @@ TEST_F(TxExecutionTest, L2ToL1MessageLimitReached)
450450
EXPECT_EQ(reverts, 1);
451451
}
452452

453+
TEST_F(TxExecutionTest, RevertibleNullifierCollisionIsUnrecoverable)
454+
{
455+
// A nullifier collision during revertible insertion must propagate as NullifierCollisionException
456+
// (not TxExecutionException), bypassing all recovery logic and making the tx unprovable.
457+
Tx tx = {
458+
.hash = "0x1234567890abcdef",
459+
.non_revertible_accumulated_data =
460+
AccumulatedData{
461+
.nullifiers = testing::random_fields(1),
462+
},
463+
.revertible_accumulated_data =
464+
AccumulatedData{
465+
.nullifiers = testing::random_fields(1),
466+
},
467+
.fee_payer = FF::random_element(),
468+
};
469+
470+
AppendOnlyTreeSnapshot dummy_snapshot = {
471+
.root = 0,
472+
.next_available_leaf_index = 0,
473+
};
474+
TreeStates tree_state = {
475+
.note_hash_tree = { .tree = dummy_snapshot, .counter = 0 },
476+
.nullifier_tree = { .tree = dummy_snapshot, .counter = 0 },
477+
.l1_to_l2_message_tree = { .tree = dummy_snapshot, .counter = 0 },
478+
.public_data_tree = { .tree = dummy_snapshot, .counter = 0 },
479+
};
480+
ON_CALL(merkle_db, get_tree_state()).WillByDefault([&]() { return tree_state; });
481+
482+
// First call (non-revertible) succeeds, second call (revertible) collides.
483+
EXPECT_CALL(merkle_db, siloed_nullifier_write(_))
484+
.WillOnce([&](const auto& /*nullifier*/) { tree_state.nullifier_tree.counter++; })
485+
.WillOnce([](const auto& /*nullifier*/) { throw NullifierCollisionException("Nullifier collision"); });
486+
487+
EXPECT_THROW(tx_execution.simulate(tx), NullifierCollisionException);
488+
}
489+
490+
TEST_F(TxExecutionTest, NonRevertibleNullifierCollisionIsUnrecoverable)
491+
{
492+
// A nullifier collision during non-revertible insertion must propagate as NullifierCollisionException.
493+
Tx tx = {
494+
.hash = "0x1234567890abcdef",
495+
.non_revertible_accumulated_data =
496+
AccumulatedData{
497+
.nullifiers = testing::random_fields(1),
498+
},
499+
.fee_payer = FF::random_element(),
500+
};
501+
502+
AppendOnlyTreeSnapshot dummy_snapshot = {
503+
.root = 0,
504+
.next_available_leaf_index = 0,
505+
};
506+
TreeStates tree_state = {
507+
.note_hash_tree = { .tree = dummy_snapshot, .counter = 0 },
508+
.nullifier_tree = { .tree = dummy_snapshot, .counter = 0 },
509+
.l1_to_l2_message_tree = { .tree = dummy_snapshot, .counter = 0 },
510+
.public_data_tree = { .tree = dummy_snapshot, .counter = 0 },
511+
};
512+
ON_CALL(merkle_db, get_tree_state()).WillByDefault([&]() { return tree_state; });
513+
ON_CALL(merkle_db, siloed_nullifier_write(_)).WillByDefault([](const auto& /*nullifier*/) {
514+
throw NullifierCollisionException("Nullifier collision");
515+
});
516+
517+
EXPECT_THROW(tx_execution.simulate(tx), NullifierCollisionException);
518+
}
519+
453520
} // namespace
454521
} // namespace bb::avm2::simulation

yarn-project/bb-prover/src/avm_proving_tests/avm_check_circuit2.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { MAX_NULLIFIERS_PER_TX } from '@aztec/constants';
12
import { Fr } from '@aztec/foundation/curves/bn254';
23
import { AvmTestContractArtifact } from '@aztec/noir-test-contracts.js/AvmTest';
34
import { AztecAddress } from '@aztec/stdlib/aztec-address';
@@ -77,6 +78,10 @@ describe('AVM check-circuit – unhappy paths 2', () => {
7778
});
7879

7980
it('error during revertible insertions - skips to teardown', async () => {
81+
// Exceed MAX_NULLIFIERS_PER_TX during revertible insertions so that the revertible phase
82+
// errors out and the tx skips to teardown. Note: nullifier *collisions* during revertible
83+
// insertions are unprovable (not revertible), so we use the side-effect limit path instead.
84+
const revertibleNullifiers = Array.from({ length: MAX_NULLIFIERS_PER_TX }, (_, i) => new Fr(100_000 + i));
8085
await tester.simProveVerify(
8186
sender,
8287
/*setupCalls=*/ [],
@@ -96,9 +101,9 @@ describe('AVM check-circuit – unhappy paths 2', () => {
96101
},
97102
/*expectRevert=*/ true,
98103
/*feePayer=*/ sender,
99-
// duplicate nullifiers during revertible insertions!
100104
/*privateInsertions=*/ {
101-
revertible: { nullifiers: [new Fr(100_000), /*duplicate*/ new Fr(100_000)] },
105+
// Total nullifiers = 1 non-revertible + MAX_NULLIFIERS_PER_TX revertible = over the limit.
106+
revertible: { nullifiers: revertibleNullifiers },
102107
nonRevertible: { nullifiers: [/*firstNullifier=*/ new Fr(66000)] },
103108
},
104109
);

0 commit comments

Comments
 (0)