@@ -225,6 +225,53 @@ describe('ContractSyncService', () => {
225225 } ) ;
226226 } ) ;
227227
228+ describe ( 'class ID verification deduplication' , ( ) => {
229+ const contract2 = AztecAddress . fromBigInt ( 300n ) ;
230+
231+ it ( 'verifies class ID only once per contract across scope batches' , async ( ) => {
232+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
233+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeB ] ) ;
234+ expectVerifiedContracts ( contractAddress ) ;
235+ } ) ;
236+
237+ it ( 'verifies class ID separately for different contracts' , async ( ) => {
238+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
239+ await service . ensureContractSynced ( contract2 , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
240+ expectVerifiedContracts ( contractAddress , contract2 ) ;
241+ } ) ;
242+
243+ it ( 're-verifies class ID after wipe' , async ( ) => {
244+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
245+ service . wipe ( ) ;
246+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeB ] ) ;
247+ expectVerifiedContracts ( contractAddress , contractAddress ) ;
248+ } ) ;
249+
250+ it ( 're-verifies class ID after discardStaged' , async ( ) => {
251+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
252+ await service . discardStaged ( jobId ) ;
253+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
254+ expectVerifiedContracts ( contractAddress , contractAddress ) ;
255+ } ) ;
256+
257+ it ( 're-verifies class ID after verification failure' , async ( ) => {
258+ contractStore . getContractInstance . mockRejectedValueOnce ( new Error ( 'node unavailable' ) ) ;
259+ await expect (
260+ service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ,
261+ ) . rejects . toThrow ( 'node unavailable' ) ;
262+
263+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
264+ expectVerifiedContracts ( contractAddress , contractAddress ) ;
265+ } ) ;
266+
267+ it ( 'does not re-verify class ID when only scope cache is invalidated' , async ( ) => {
268+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
269+ service . invalidateContractForScopes ( contractAddress , [ scopeA ] ) ;
270+ await service . ensureContractSynced ( contractAddress , null , utilityExecutor , anchorBlockHeader , jobId , [ scopeA ] ) ;
271+ expectVerifiedContracts ( contractAddress ) ;
272+ } ) ;
273+ } ) ;
274+
228275 describe ( 'invalidateContractForScopes' , ( ) => {
229276 const contract2 = AztecAddress . fromBigInt ( 300n ) ;
230277
@@ -337,5 +384,13 @@ describe('ContractSyncService', () => {
337384 }
338385 } ;
339386
387+ /** Asserts that class ID verification was triggered for each contract address in the given sequence. */
388+ const expectVerifiedContracts = ( ...addresses : AztecAddress [ ] ) => {
389+ expect ( contractStore . getContractInstance ) . toHaveBeenCalledTimes ( addresses . length ) ;
390+ for ( let i = 0 ; i < addresses . length ; i ++ ) {
391+ expect ( contractStore . getContractInstance ) . toHaveBeenNthCalledWith ( i + 1 , addresses [ i ] ) ;
392+ }
393+ } ;
394+
340395 const expectNoSync = ( ) => expect ( utilityExecutor ) . not . toHaveBeenCalled ( ) ;
341396} ) ;
0 commit comments