@@ -2,8 +2,10 @@ import { FunctionSelector } from '@aztec/aztec.js/abi';
22import type { AztecAddress } from '@aztec/aztec.js/addresses' ;
33import { EthAddress } from '@aztec/aztec.js/addresses' ;
44import { SetPublicAuthwitContractInteraction } from '@aztec/aztec.js/authorization' ;
5+ import { waitForProven } from '@aztec/aztec.js/contracts' ;
56import { PrivateFeePaymentMethod , PublicFeePaymentMethod } from '@aztec/aztec.js/fee' ;
67import { Fr } from '@aztec/aztec.js/fields' ;
8+ import type { AztecNode } from '@aztec/aztec.js/node' ;
79import { TxExecutionResult } from '@aztec/aztec.js/tx' ;
810import type { Wallet } from '@aztec/aztec.js/wallet' ;
911import type { FPCContract } from '@aztec/noir-contracts.js/FPC' ;
@@ -23,6 +25,7 @@ describe('e2e_fees failures', () => {
2325 let bananaCoin : BananaCoin ;
2426 let bananaFPC : FPCContract ;
2527 let gasSettings : GasSettings ;
28+ let aztecNode : AztecNode ;
2629 const coinbase = EthAddress . random ( ) ;
2730
2831 const t = new FeesTest ( 'failures' , 3 , { coinbase } ) ;
@@ -31,6 +34,7 @@ describe('e2e_fees failures', () => {
3134 await t . setup ( ) ;
3235 await t . applyFPCSetup ( ) ;
3336 ( { wallet, aliceAddress, sequencerAddress, bananaCoin, bananaFPC, gasSettings } = t ) ;
37+ aztecNode = t . aztecNode ;
3438
3539 // Prove up until the current state by just marking it as proven.
3640 // Then turn off the watcher to prevent it from keep proving
@@ -317,9 +321,113 @@ describe('e2e_fees failures', () => {
317321 [ aliceAddress , bananaFPC . address , sequencerAddress ] ,
318322 [ initialAliceGas , initialFPCGas - receipt . transactionFee ! , initialSequencerGas ] ,
319323 ) ;
324+
325+ // Prove the block containing the teardown-reverted tx (revert_code = 2).
326+ await t . context . watcher . trigger ( ) ;
327+ await t . cheatCodes . rollup . advanceToNextEpoch ( ) ;
328+ const provenTimeout =
329+ ( t . context . config . aztecProofSubmissionEpochs + 1 ) *
330+ t . context . config . aztecEpochDuration *
331+ t . context . config . aztecSlotDuration ;
332+ await waitForProven ( aztecNode , receipt , { provenTimeout } ) ;
333+ } ) ;
334+
335+ it ( 'proves transaction where both app logic and teardown revert' , async ( ) => {
336+ /**
337+ * Regression test for a bug where the circuit encodes revert_code as 0 or 1 (boolean),
338+ * but the TS side preserves the full RevertCode enum (BOTH_REVERTED = 3).
339+ * This causes the tx start marker in the blob data to differ, which cascades into
340+ * a spongeBlobHash mismatch in the block header.
341+ *
342+ * We trigger BOTH_REVERTED by:
343+ * - App logic: transfer more tokens than Alice has (reverts in public app logic)
344+ * - Teardown: use a bugged fee payment method whose teardown transfers an impossible amount
345+ *
346+ * See: noir-projects/noir-protocol-circuits/sponge-blob-revert-code-bug.md
347+ */
348+ const outrageousPublicAmountAliceDoesNotHave = t . ALICE_INITIAL_BANANAS * 5n ;
349+
350+ // Send a tx that will revert in BOTH app logic and teardown.
351+ const { receipt } = await bananaCoin . methods
352+ . transfer_in_public ( aliceAddress , sequencerAddress , outrageousPublicAmountAliceDoesNotHave , 0 )
353+ . send ( {
354+ from : aliceAddress ,
355+ fee : {
356+ paymentMethod : new BuggedTeardownFeePaymentMethod ( bananaFPC . address , aliceAddress , wallet , gasSettings ) ,
357+ } ,
358+ wait : { dontThrowOnRevert : true } ,
359+ } ) ;
360+
361+ expect ( receipt . executionResult ) . toBe ( TxExecutionResult . BOTH_REVERTED ) ;
362+ expect ( receipt . transactionFee ) . toBeGreaterThan ( 0n ) ;
363+
364+ // Now prove the block containing this tx via the real prover node.
365+ // The prover node will fail with "Block header mismatch" if the revert_code encoding
366+ // differs between the circuit (which uses 1) and the TS (which uses 3).
367+ await t . context . watcher . trigger ( ) ;
368+ await t . cheatCodes . rollup . advanceToNextEpoch ( ) ;
369+ const provenTimeout =
370+ ( t . context . config . aztecProofSubmissionEpochs + 1 ) *
371+ t . context . config . aztecEpochDuration *
372+ t . context . config . aztecSlotDuration ;
373+ await waitForProven ( aztecNode , receipt , { provenTimeout } ) ;
320374 } ) ;
321375} ) ;
322376
377+ /**
378+ * Fee payment method whose teardown always reverts because max_fee is set to 0.
379+ * The FPC's _pay_refund will assert `0 >= actual_fee` which always fails since actual_fee > 0.
380+ * The setup transfer of 0 tokens succeeds (and the authwit matches the 0 amount).
381+ */
382+ class BuggedTeardownFeePaymentMethod extends PublicFeePaymentMethod {
383+ override async getExecutionPayload ( ) : Promise < ExecutionPayload > {
384+ const zeroFee = new Fr ( 0n ) ;
385+ const authwitNonce = Fr . random ( ) ;
386+
387+ const asset = await this . getAsset ( ) ;
388+
389+ // Authorize the FPC to transfer 0 tokens (matches the 0 max_fee we'll pass).
390+ const setPublicAuthWitInteraction = await SetPublicAuthwitContractInteraction . create (
391+ this . wallet ,
392+ this . sender ,
393+ {
394+ caller : this . paymentContract ,
395+ call : FunctionCall . from ( {
396+ name : 'transfer_in_public' ,
397+ to : asset ,
398+ selector : await FunctionSelector . fromSignature ( 'transfer_in_public((Field),(Field),u128,Field)' ) ,
399+ type : FunctionType . PUBLIC ,
400+ hideMsgSender : false ,
401+ isStatic : false ,
402+ args : [ this . sender . toField ( ) , this . paymentContract . toField ( ) , zeroFee , authwitNonce ] ,
403+ returnTypes : [ ] ,
404+ } ) ,
405+ } ,
406+ true ,
407+ ) ;
408+
409+ return new ExecutionPayload (
410+ [
411+ ...( await setPublicAuthWitInteraction . request ( ) ) . calls ,
412+ FunctionCall . from ( {
413+ name : 'fee_entrypoint_public' ,
414+ to : this . paymentContract ,
415+ selector : await FunctionSelector . fromSignature ( 'fee_entrypoint_public(u128,Field)' ) ,
416+ type : FunctionType . PRIVATE ,
417+ hideMsgSender : false ,
418+ isStatic : false ,
419+ args : [ zeroFee , authwitNonce ] ,
420+ returnTypes : [ ] ,
421+ } ) ,
422+ ] ,
423+ [ ] ,
424+ [ ] ,
425+ [ ] ,
426+ this . paymentContract ,
427+ ) ;
428+ }
429+ }
430+
323431class BuggedSetupFeePaymentMethod extends PublicFeePaymentMethod {
324432 override async getExecutionPayload ( ) : Promise < ExecutionPayload > {
325433 const maxFee = this . gasSettings . getFeeLimit ( ) ;
0 commit comments