11import type { EpochCacheInterface } from '@aztec/epoch-cache' ;
2+ import { NoCommitteeError } from '@aztec/ethereum/contracts' ;
23import type { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer' ;
34import type { EthAddress } from '@aztec/foundation/eth-address' ;
45import {
@@ -9,12 +10,13 @@ import {
910} from '@aztec/stdlib/p2p' ;
1011import type { TxHash } from '@aztec/stdlib/tx' ;
1112
13+ import { jest } from '@jest/globals' ;
1214import type { MockProxy } from 'jest-mock-extended' ;
1315
1416export interface ProposalValidatorTestParams < TProposal extends BlockProposal | CheckpointProposal > {
1517 validatorFactory : (
1618 epochCache : EpochCacheInterface ,
17- opts : { txsPermitted : boolean } ,
19+ opts : { txsPermitted : boolean ; maxTxsPerBlock ?: number } ,
1820 ) => { validate : ( proposal : TProposal ) => Promise < ValidationResult > } ;
1921 makeProposal : ( options ?: any ) => Promise < TProposal > ;
2022 makeHeader : ( epochNumber : number | bigint , slotNumber : number | bigint , blockNumber : number | bigint ) => any ;
@@ -105,6 +107,26 @@ export function sharedProposalValidatorTests<TProposal extends BlockProposal | C
105107 expect ( result ) . toEqual ( { result : 'ignore' } ) ;
106108 } ) ;
107109
110+ it ( 'returns mid tolerance error if proposal has invalid signature' , async ( ) => {
111+ const currentProposer = getSigner ( ) ;
112+ const header = makeHeader ( 1 , 100 , 100 ) ;
113+ const mockProposal = await makeProposal ( {
114+ blockHeader : header ,
115+ lastBlockHeader : header ,
116+ signer : currentProposer ,
117+ } ) ;
118+
119+ // Override getSender to return undefined (invalid signature)
120+ jest . spyOn ( mockProposal as any , 'getSender' ) . mockReturnValue ( undefined ) ;
121+
122+ mockGetProposer ( getAddress ( currentProposer ) , getAddress ( ) ) ;
123+ const result = await validator . validate ( mockProposal ) ;
124+ expect ( result ) . toEqual ( { result : 'reject' , severity : PeerErrorSeverity . MidToleranceError } ) ;
125+
126+ // Should not try to resolve proposer if signature is invalid
127+ expect ( epochCache . getProposerAttesterAddressInSlot ) . not . toHaveBeenCalled ( ) ;
128+ } ) ;
129+
108130 it ( 'returns mid tolerance error if proposer is not current proposer for current slot' , async ( ) => {
109131 const currentProposer = getSigner ( ) ;
110132 const nextProposer = getSigner ( ) ;
@@ -152,6 +174,34 @@ export function sharedProposalValidatorTests<TProposal extends BlockProposal | C
152174 expect ( result ) . toEqual ( { result : 'reject' , severity : PeerErrorSeverity . MidToleranceError } ) ;
153175 } ) ;
154176
177+ it ( 'accepts proposal when proposer is undefined (open committee)' , async ( ) => {
178+ const currentProposer = getSigner ( ) ;
179+ const header = makeHeader ( 1 , 100 , 100 ) ;
180+ const mockProposal = await makeProposal ( {
181+ blockHeader : header ,
182+ lastBlockHeader : header ,
183+ signer : currentProposer ,
184+ } ) ;
185+
186+ epochCache . getProposerAttesterAddressInSlot . mockResolvedValue ( undefined ) ;
187+ const result = await validator . validate ( mockProposal ) ;
188+ expect ( result ) . toEqual ( { result : 'accept' } ) ;
189+ } ) ;
190+
191+ it ( 'returns low tolerance error when getProposerAttesterAddressInSlot throws NoCommitteeError' , async ( ) => {
192+ const currentProposer = getSigner ( ) ;
193+ const header = makeHeader ( 1 , 100 , 100 ) ;
194+ const mockProposal = await makeProposal ( {
195+ blockHeader : header ,
196+ lastBlockHeader : header ,
197+ signer : currentProposer ,
198+ } ) ;
199+
200+ epochCache . getProposerAttesterAddressInSlot . mockRejectedValue ( new NoCommitteeError ( ) ) ;
201+ const result = await validator . validate ( mockProposal ) ;
202+ expect ( result ) . toEqual ( { result : 'reject' , severity : PeerErrorSeverity . LowToleranceError } ) ;
203+ } ) ;
204+
155205 it ( 'returns undefined if proposal is valid for current slot and proposer' , async ( ) => {
156206 const currentProposer = getSigner ( ) ;
157207 const nextProposer = getSigner ( ) ;
@@ -226,5 +276,98 @@ export function sharedProposalValidatorTests<TProposal extends BlockProposal | C
226276 expect ( result ) . toEqual ( { result : 'accept' } ) ;
227277 } ) ;
228278 } ) ;
279+
280+ describe ( 'embedded tx validation' , ( ) => {
281+ it ( 'returns mid tolerance error if embedded txs are not listed in txHashes' , async ( ) => {
282+ const currentProposer = getSigner ( ) ;
283+ const txHashes = getTxHashes ( 2 ) ;
284+ const header = makeHeader ( 1 , 100 , 100 ) ;
285+ const mockProposal = await makeProposal ( {
286+ blockHeader : header ,
287+ lastBlockHeader : header ,
288+ signer : currentProposer ,
289+ txHashes,
290+ } ) ;
291+
292+ // Create a fake tx whose hash is NOT in txHashes
293+ const fakeTxHash = getTxHashes ( 1 ) [ 0 ] ;
294+ const fakeTx = { getTxHash : ( ) => fakeTxHash , validateTxHash : ( ) => Promise . resolve ( true ) } ;
295+ Object . defineProperty ( mockProposal , 'txs' , { get : ( ) => [ fakeTx ] , configurable : true } ) ;
296+
297+ mockGetProposer ( getAddress ( currentProposer ) , getAddress ( ) ) ;
298+ const result = await validator . validate ( mockProposal ) ;
299+ expect ( result ) . toEqual ( { result : 'reject' , severity : PeerErrorSeverity . MidToleranceError } ) ;
300+ } ) ;
301+
302+ it ( 'returns low tolerance error if embedded tx has invalid tx hash' , async ( ) => {
303+ const currentProposer = getSigner ( ) ;
304+ const txHashes = getTxHashes ( 2 ) ;
305+ const header = makeHeader ( 1 , 100 , 100 ) ;
306+ const mockProposal = await makeProposal ( {
307+ blockHeader : header ,
308+ lastBlockHeader : header ,
309+ signer : currentProposer ,
310+ txHashes,
311+ } ) ;
312+
313+ // Create a fake tx whose hash IS in txHashes but validateTxHash returns false
314+ const fakeTx = { getTxHash : ( ) => txHashes [ 0 ] , validateTxHash : ( ) => Promise . resolve ( false ) } ;
315+ Object . defineProperty ( mockProposal , 'txs' , { get : ( ) => [ fakeTx ] , configurable : true } ) ;
316+
317+ mockGetProposer ( getAddress ( currentProposer ) , getAddress ( ) ) ;
318+ const result = await validator . validate ( mockProposal ) ;
319+ expect ( result ) . toEqual ( { result : 'reject' , severity : PeerErrorSeverity . LowToleranceError } ) ;
320+ } ) ;
321+ } ) ;
322+
323+ describe ( 'maxTxsPerBlock validation' , ( ) => {
324+ it ( 'rejects proposal when txHashes exceed maxTxsPerBlock' , async ( ) => {
325+ const validatorWithMaxTxs = validatorFactory ( epochCache , { txsPermitted : true , maxTxsPerBlock : 2 } ) ;
326+ const currentProposer = getSigner ( ) ;
327+ const header = makeHeader ( 1 , 100 , 100 ) ;
328+ const mockProposal = await makeProposal ( {
329+ blockHeader : header ,
330+ lastBlockHeader : header ,
331+ signer : currentProposer ,
332+ txHashes : getTxHashes ( 3 ) ,
333+ } ) ;
334+
335+ mockGetProposer ( getAddress ( currentProposer ) , getAddress ( ) ) ;
336+ const result = await validatorWithMaxTxs . validate ( mockProposal ) ;
337+ expect ( result ) . toEqual ( { result : 'reject' , severity : PeerErrorSeverity . MidToleranceError } ) ;
338+ } ) ;
339+
340+ it ( 'accepts proposal when txHashes count equals maxTxsPerBlock' , async ( ) => {
341+ const validatorWithMaxTxs = validatorFactory ( epochCache , { txsPermitted : true , maxTxsPerBlock : 2 } ) ;
342+ const currentProposer = getSigner ( ) ;
343+ const header = makeHeader ( 1 , 100 , 100 ) ;
344+ const mockProposal = await makeProposal ( {
345+ blockHeader : header ,
346+ lastBlockHeader : header ,
347+ signer : currentProposer ,
348+ txHashes : getTxHashes ( 2 ) ,
349+ } ) ;
350+
351+ mockGetProposer ( getAddress ( currentProposer ) , getAddress ( ) ) ;
352+ const result = await validatorWithMaxTxs . validate ( mockProposal ) ;
353+ expect ( result ) . toEqual ( { result : 'accept' } ) ;
354+ } ) ;
355+
356+ it ( 'accepts proposal when maxTxsPerBlock is not set (unlimited)' , async ( ) => {
357+ // Default validator has no maxTxsPerBlock
358+ const currentProposer = getSigner ( ) ;
359+ const header = makeHeader ( 1 , 100 , 100 ) ;
360+ const mockProposal = await makeProposal ( {
361+ blockHeader : header ,
362+ lastBlockHeader : header ,
363+ signer : currentProposer ,
364+ txHashes : getTxHashes ( 10 ) ,
365+ } ) ;
366+
367+ mockGetProposer ( getAddress ( currentProposer ) , getAddress ( ) ) ;
368+ const result = await validator . validate ( mockProposal ) ;
369+ expect ( result ) . toEqual ( { result : 'accept' } ) ;
370+ } ) ;
371+ } ) ;
229372 } ) ;
230373}
0 commit comments