Skip to content

Commit

Permalink
feat: sequencer checks fee balance
Browse files Browse the repository at this point in the history
  • Loading branch information
alexghr committed Mar 19, 2024
1 parent ba9bc4c commit 56af753
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 23 deletions.
28 changes: 28 additions & 0 deletions yarn-project/end-to-end/src/e2e_fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,34 @@ describe('e2e_fees', () => {
addPendingShieldNoteToPXE(0, RefundAmount, computeMessageSecretHash(RefundSecret), tx.txHash),
).resolves.toBeUndefined();
});

it("rejects txs that don't have enough balance to cover gas costs", async () => {
// deploy a copy of bananaFPC but don't fund it!
const bankruptFPC = await FPCContract.deploy(aliceWallet, bananaCoin.address, gasTokenContract.address)
.send()
.deployed();

await expectMapping(gasBalances, [bankruptFPC.address], [0n]);

await expect(
bananaCoin.methods
.privately_mint_private_note(10)
.send({
// we need to skip public simulation otherwise the PXE refuses to accept the TX
skipPublicSimulation: true,
fee: {
maxFee: MaxFee,
paymentMethod: new PrivateFeePaymentMethod(
bananaCoin.address,
bankruptFPC.address,
aliceWallet,
RefundSecret,
),
},
})
.wait(),
).rejects.toThrow('Tx dropped by P2P node.');
});
});

it('fails transaction that error in setup', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export class SequencerClient {
l1ToL2MessageSource,
publicProcessorFactory,
config,
config.l1Contracts.gasPortalAddress,
);

await sequencer.start();
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ describe('sequencer', () => {
it('builds a block out of a single tx', async () => {
const tx = mockTx();
tx.data.constants.txContext.chainId = chainId;
tx.data.needsSetup = false;
tx.data.needsTeardown = false;
const block = L2Block.random(lastBlockNumber + 1);
const proof = makeEmptyProof();

Expand Down Expand Up @@ -119,6 +121,8 @@ describe('sequencer', () => {
const txs = [mockTx(0x10000), mockTx(0x20000), mockTx(0x30000)];
txs.forEach(tx => {
tx.data.constants.txContext.chainId = chainId;
tx.data.needsSetup = false;
tx.data.needsTeardown = false;
});
const doubleSpendTx = txs[1];
const block = L2Block.random(lastBlockNumber + 1);
Expand Down Expand Up @@ -157,6 +161,8 @@ describe('sequencer', () => {
const txs = [mockTx(0x10000), mockTx(0x20000), mockTx(0x30000)];
txs.forEach(tx => {
tx.data.constants.txContext.chainId = chainId;
tx.data.needsSetup = false;
tx.data.needsTeardown = false;
});
const invalidChainTx = txs[1];
const block = L2Block.random(lastBlockNumber + 1);
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { WorldStateStatus, WorldStateSynchronizer } from '@aztec/world-state';
import { BlockBuilder } from '../block_builder/index.js';
import { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
import { L1Publisher } from '../publisher/l1-publisher.js';
import { WorldStatePublicDB } from '../simulator/public_executor.js';
import { ceilPowerOfTwo } from '../utils.js';
import { SequencerConfig } from './config.js';
import { ProcessedTx } from './processed_tx.js';
Expand Down Expand Up @@ -48,6 +49,7 @@ export class Sequencer {
private l1ToL2MessageSource: L1ToL2MessageSource,
private publicProcessorFactory: PublicProcessorFactory,
config: SequencerConfig = {},
private gasPortalAddress = EthAddress.ZERO,
private log = createDebugLogger('aztec:sequencer'),
) {
this.updateConfig(config);
Expand Down Expand Up @@ -179,6 +181,8 @@ export class Sequencer {
return trees.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer());
},
},
new WorldStatePublicDB(trees),
this.gasPortalAddress,
newGlobalVariables,
);

Expand Down
189 changes: 176 additions & 13 deletions yarn-project/sequencer-client/src/sequencer/tx_validator.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,59 @@
import { mockTx as baseMockTx } from '@aztec/circuit-types';
import { Fr, GlobalVariables } from '@aztec/circuits.js';
import { makeGlobalVariables } from '@aztec/circuits.js/testing';
import {
AztecAddress,
CallContext,
CallRequest,
EthAddress,
Fr,
FunctionData,
FunctionSelector,
GlobalVariables,
MAX_NON_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
MAX_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
PublicCallRequest,
} from '@aztec/circuits.js';
import { makeAztecAddress, makeGlobalVariables } from '@aztec/circuits.js/testing';
import { makeTuple } from '@aztec/foundation/array';
import { pedersenHash } from '@aztec/foundation/crypto';
import { getCanonicalGasTokenAddress } from '@aztec/protocol-contracts/gas-token';

import { MockProxy, mock, mockFn } from 'jest-mock-extended';

import { NullifierSource, TxValidator } from './tx_validator.js';
import { NullifierSource, PublicStateSource, TxValidator } from './tx_validator.js';

describe('TxValidator', () => {
let validator: TxValidator;
let globalVariables: GlobalVariables;
let nullifierSource: MockProxy<NullifierSource>;
let publicStateSource: MockProxy<PublicStateSource>;
let gasPortalAddress: EthAddress;
let gasTokenAddress: AztecAddress;

beforeEach(() => {
gasPortalAddress = EthAddress.random();
gasTokenAddress = getCanonicalGasTokenAddress(gasPortalAddress);
nullifierSource = mock<NullifierSource>({
getNullifierIndex: mockFn().mockImplementation(() => {
return Promise.resolve(undefined);
}),
});
publicStateSource = mock<PublicStateSource>({
storageRead: mockFn().mockImplementation((contractAddress: AztecAddress, _slot: Fr) => {
if (contractAddress.equals(gasTokenAddress)) {
return Promise.resolve(new Fr(1));
} else {
return Promise.reject(Fr.ZERO);
}
}),
});
globalVariables = makeGlobalVariables();
validator = new TxValidator(nullifierSource, globalVariables);
validator = new TxValidator(nullifierSource, publicStateSource, gasPortalAddress, globalVariables);
});

describe('inspects tx metadata', () => {
it('allows only transactions for the right chain', async () => {
const goodTx = mockTx();
const badTx = mockTx();
const goodTx = nonFeePayingTx();
const badTx = nonFeePayingTx();
badTx.data.constants.txContext.chainId = Fr.random();

await expect(validator.validateTxs([goodTx, badTx])).resolves.toEqual([[goodTx], [badTx]]);
Expand All @@ -33,45 +62,179 @@ describe('TxValidator', () => {

describe('inspects tx nullifiers', () => {
it('rejects duplicates in non revertible data', async () => {
const badTx = mockTx();
const badTx = nonFeePayingTx();
badTx.data.endNonRevertibleData.newNullifiers[1] = badTx.data.endNonRevertibleData.newNullifiers[0];
await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]);
});

it('rejects duplicates in revertible data', async () => {
const badTx = mockTx();
const badTx = nonFeePayingTx();
badTx.data.end.newNullifiers[1] = badTx.data.end.newNullifiers[0];
await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]);
});

it('rejects duplicates across phases', async () => {
const badTx = mockTx();
const badTx = nonFeePayingTx();
badTx.data.end.newNullifiers[0] = badTx.data.endNonRevertibleData.newNullifiers[0];
await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]);
});

it('rejects duplicates across txs', async () => {
const firstTx = mockTx();
const secondTx = mockTx();
const firstTx = nonFeePayingTx();
const secondTx = nonFeePayingTx();
secondTx.data.end.newNullifiers[0] = firstTx.data.end.newNullifiers[0];
await expect(validator.validateTxs([firstTx, secondTx])).resolves.toEqual([[firstTx], [secondTx]]);
});

it('rejects duplicates against history', async () => {
const badTx = mockTx();
const badTx = nonFeePayingTx();
nullifierSource.getNullifierIndex.mockReturnValueOnce(Promise.resolve(1n));
await expect(validator.validateTxs([badTx])).resolves.toEqual([[], [badTx]]);
});
});

describe('inspects tx gas', () => {
it('allows native fee paying txs', async () => {
const sender = makeAztecAddress();
const expectedBalanceSlot = pedersenHash([new Fr(1).toBuffer(), sender.toBuffer()]);
const tx = nativeFeePayingTx(sender);

publicStateSource.storageRead.mockImplementation((address, slot) => {
if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) {
return Promise.resolve(new Fr(1));
} else {
return Promise.resolve(Fr.ZERO);
}
});

await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]);
});

it('rejects native fee paying txs if out of balance', async () => {
const sender = makeAztecAddress();
const expectedBalanceSlot = pedersenHash([new Fr(1).toBuffer(), sender.toBuffer()]);
const tx = nativeFeePayingTx(sender);

publicStateSource.storageRead.mockImplementation((address, slot) => {
if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) {
return Promise.resolve(Fr.ZERO);
} else {
return Promise.resolve(new Fr(1));
}
});

await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]);
});

it('allows txs paying through a fee payment contract', async () => {
const fpcAddress = makeAztecAddress();
const expectedBalanceSlot = pedersenHash([new Fr(1).toBuffer(), fpcAddress.toBuffer()]);
const tx = fxFeePayingTx(fpcAddress);

publicStateSource.storageRead.mockImplementation((address, slot) => {
if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) {
return Promise.resolve(new Fr(1));
} else {
return Promise.resolve(Fr.ZERO);
}
});

await expect(validator.validateTxs([tx])).resolves.toEqual([[tx], []]);
});

it('rejects txs paying through a fee payment contract out of balance', async () => {
const fpcAddress = makeAztecAddress();
const expectedBalanceSlot = pedersenHash([new Fr(1).toBuffer(), fpcAddress.toBuffer()]);
const tx = nativeFeePayingTx(fpcAddress);

publicStateSource.storageRead.mockImplementation((address, slot) => {
if (address.equals(gasTokenAddress) && slot.equals(expectedBalanceSlot)) {
return Promise.resolve(Fr.ZERO);
} else {
return Promise.resolve(new Fr(1));
}
});

await expect(validator.validateTxs([tx])).resolves.toEqual([[], [tx]]);
});
});

// get unique txs that are also stable across test runs
let txSeed = 1;
/** Creates a mock tx for the current chain */
function mockTx() {
function nonFeePayingTx() {
const tx = baseMockTx(txSeed++, false);

tx.data.constants.txContext.chainId = globalVariables.chainId;
tx.data.constants.txContext.version = globalVariables.version;

// clear public call stacks as it's mocked data but the arrays are not correlated
tx.data.endNonRevertibleData.publicCallStack = makeTuple(
MAX_NON_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
CallRequest.empty,
);
tx.data.end.publicCallStack = makeTuple(MAX_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX, CallRequest.empty);
// use splice because it's a readonly property
tx.enqueuedPublicFunctionCalls.splice(0, tx.enqueuedPublicFunctionCalls.length);

// clear these flags because the call stack is empty now
tx.data.needsSetup = false;
tx.data.needsAppLogic = false;
tx.data.needsTeardown = false;

return tx;
}

/** Create a tx that pays for its cost natively */
function nativeFeePayingTx(feePayer: AztecAddress) {
const tx = nonFeePayingTx();
const gasTokenAddress = getCanonicalGasTokenAddress(gasPortalAddress);
const signature = FunctionSelector.random();

const feeExecutionFn = new PublicCallRequest(
gasTokenAddress,
new FunctionData(signature, false),
new CallContext(feePayer, gasTokenAddress, gasPortalAddress, signature, false, false, 1),
CallContext.empty(),
[],
);

tx.data.endNonRevertibleData.publicCallStack[0] = feeExecutionFn.toCallRequest();
tx.enqueuedPublicFunctionCalls[0] = feeExecutionFn;
tx.data.needsTeardown = true;

return tx;
}

/** Create a tx that uses fee abstraction to pay for its cost */
function fxFeePayingTx(feePaymentContract: AztecAddress) {
const tx = nonFeePayingTx();

// the contract calls itself. Both functions are internal
const feeSetupSelector = FunctionSelector.random();
const feeSetupFn = new PublicCallRequest(
feePaymentContract,
new FunctionData(feeSetupSelector, true),
new CallContext(feePaymentContract, feePaymentContract, EthAddress.ZERO, feeSetupSelector, false, false, 1),
CallContext.empty(),
[],
);
tx.data.endNonRevertibleData.publicCallStack[0] = feeSetupFn.toCallRequest();
tx.enqueuedPublicFunctionCalls[0] = feeSetupFn;
tx.data.needsSetup = true;

const feeExecutionSelector = FunctionSelector.random();
const feeExecutionFn = new PublicCallRequest(
feePaymentContract,
new FunctionData(feeExecutionSelector, true),
new CallContext(feePaymentContract, feePaymentContract, EthAddress.ZERO, feeExecutionSelector, false, false, 2),
CallContext.empty(),
[],
);
tx.data.endNonRevertibleData.publicCallStack[1] = feeExecutionFn.toCallRequest();
tx.enqueuedPublicFunctionCalls[1] = feeExecutionFn;
tx.data.needsTeardown = true;

return tx;
}
});
Loading

0 comments on commit 56af753

Please sign in to comment.