From 55768112bd6647fcba0f8e4e992534fe1a389a3a Mon Sep 17 00:00:00 2001 From: Marko Arambasic <131957563+kiriyaga-txfusion@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:26:32 +0100 Subject: [PATCH] feat: add system contracts on the first run (#372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ This PR addresses an issue where system contracts were not displayed as contracts in the UI until they were updated. The implemented solution ensures that system contracts are loaded and saved in the database immediately after work begins. This guarantees their proper display in the UI, providing a consistent and accurate reflection of the system's state. ## Why ❔ System contracts are not displayed as contracts in the UI until they have been updated. ## Checklist This PR fixes: https://github.com/matter-labs/block-explorer/issues/358 - [+] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [+] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. Co-authored-by: Vasyl Ivanchuk --- .../app/src/components/contract/InfoTable.vue | 2 +- packages/worker/src/app.module.ts | 2 + packages/worker/src/app.service.spec.ts | 16 ++ packages/worker/src/app.service.ts | 5 +- .../contract/systemContract.service.spec.ts | 107 +++++++++++++ .../src/contract/systemContract.service.ts | 143 ++++++++++++++++++ 6 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 packages/worker/src/contract/systemContract.service.spec.ts create mode 100644 packages/worker/src/contract/systemContract.service.ts diff --git a/packages/app/src/components/contract/InfoTable.vue b/packages/app/src/components/contract/InfoTable.vue index 27405d72f1..a9cab847f0 100644 --- a/packages/app/src/components/contract/InfoTable.vue +++ b/packages/app/src/components/contract/InfoTable.vue @@ -9,7 +9,7 @@ - + {{ t("contract.table.creator") }} diff --git a/packages/worker/src/app.module.ts b/packages/worker/src/app.module.ts index bec09a377a..da46beea4e 100644 --- a/packages/worker/src/app.module.ts +++ b/packages/worker/src/app.module.ts @@ -52,6 +52,7 @@ import { MetricsModule } from "./metrics"; import { DbMetricsService } from "./dbMetrics.service"; import { UnitOfWorkModule } from "./unitOfWork"; import { DataFetcherService } from "./dataFetcher/dataFetcher.service"; +import { SystemContractService } from "./contract/systemContract.service"; @Module({ imports: [ @@ -130,6 +131,7 @@ import { DataFetcherService } from "./dataFetcher/dataFetcher.service"; Logger, RetryDelayProvider, DbMetricsService, + SystemContractService, ], }) export class AppModule {} diff --git a/packages/worker/src/app.service.spec.ts b/packages/worker/src/app.service.spec.ts index 3d4afb8363..60d5a8b295 100644 --- a/packages/worker/src/app.service.spec.ts +++ b/packages/worker/src/app.service.spec.ts @@ -13,6 +13,7 @@ import { BlocksRevertService } from "./blocksRevert"; import { TokenOffChainDataSaverService } from "./token/tokenOffChainData/tokenOffChainDataSaver.service"; import runMigrations from "./utils/runMigrations"; import { BLOCKS_REVERT_DETECTED_EVENT } from "./constants"; +import { SystemContractService } from "./contract/systemContract.service"; jest.mock("./utils/runMigrations"); @@ -39,6 +40,7 @@ describe("AppService", () => { let tokenOffChainDataSaverService: TokenOffChainDataSaverService; let dataSourceMock: DataSource; let configServiceMock: ConfigService; + let systemContractService: SystemContractService; beforeEach(async () => { balancesCleanerService = mock({ @@ -68,6 +70,9 @@ describe("AppService", () => { configServiceMock = mock({ get: jest.fn().mockReturnValue(false), }); + systemContractService = mock({ + addSystemContracts: jest.fn().mockResolvedValue(null), + }); const module = await Test.createTestingModule({ imports: [EventEmitterModule.forRoot()], @@ -106,6 +111,10 @@ describe("AppService", () => { provide: ConfigService, useValue: configServiceMock, }, + { + provide: SystemContractService, + useValue: systemContractService, + }, ], }).compile(); @@ -205,6 +214,13 @@ describe("AppService", () => { appService.onModuleDestroy(); expect(tokenOffChainDataSaverService.stop).toBeCalledTimes(1); }); + + it("adds system contracts", async () => { + appService.onModuleInit(); + await migrationsRunFinished; + expect(systemContractService.addSystemContracts).toBeCalledTimes(1); + appService.onModuleDestroy(); + }); }); describe("onModuleDestroy", () => { diff --git a/packages/worker/src/app.service.ts b/packages/worker/src/app.service.ts index d6ec852c73..71d82079eb 100644 --- a/packages/worker/src/app.service.ts +++ b/packages/worker/src/app.service.ts @@ -10,6 +10,7 @@ import { CounterService } from "./counter"; import { BalancesCleanerService } from "./balance"; import { TokenOffChainDataSaverService } from "./token/tokenOffChainData/tokenOffChainDataSaver.service"; import runMigrations from "./utils/runMigrations"; +import { SystemContractService } from "./contract/systemContract.service"; @Injectable() export class AppService implements OnModuleInit, OnModuleDestroy { @@ -23,13 +24,15 @@ export class AppService implements OnModuleInit, OnModuleDestroy { private readonly balancesCleanerService: BalancesCleanerService, private readonly tokenOffChainDataSaverService: TokenOffChainDataSaverService, private readonly dataSource: DataSource, - private readonly configService: ConfigService + private readonly configService: ConfigService, + private readonly systemContractService: SystemContractService ) { this.logger = new Logger(AppService.name); } public onModuleInit() { runMigrations(this.dataSource, this.logger).then(() => { + this.systemContractService.addSystemContracts(); this.startWorkers(); }); } diff --git a/packages/worker/src/contract/systemContract.service.spec.ts b/packages/worker/src/contract/systemContract.service.spec.ts new file mode 100644 index 0000000000..8e07ebaa9f --- /dev/null +++ b/packages/worker/src/contract/systemContract.service.spec.ts @@ -0,0 +1,107 @@ +import { mock } from "jest-mock-extended"; +import { Test, TestingModule } from "@nestjs/testing"; +import { Logger } from "@nestjs/common"; +import { BlockchainService } from "../blockchain/blockchain.service"; +import { AddressRepository } from "../repositories/address.repository"; +import { SystemContractService } from "./systemContract.service"; +import { Address } from "../entities"; + +describe("SystemContractService", () => { + let systemContractService: SystemContractService; + let blockchainServiceMock: BlockchainService; + let addressRepositoryMock: AddressRepository; + const systemContracts = SystemContractService.getSystemContracts(); + + beforeEach(async () => { + blockchainServiceMock = mock({ + getCode: jest.fn().mockImplementation((address: string) => Promise.resolve(`${address}-code`)), + }); + + addressRepositoryMock = mock({ + find: jest.fn(), + }); + + const app: TestingModule = await Test.createTestingModule({ + providers: [ + SystemContractService, + { + provide: BlockchainService, + useValue: blockchainServiceMock, + }, + { + provide: AddressRepository, + useValue: addressRepositoryMock, + }, + ], + }).compile(); + + app.useLogger(mock()); + systemContractService = app.get(SystemContractService); + }); + + describe("addSystemContracts", () => { + it("doesn't add any system contracts if they already exist in DB", async () => { + (addressRepositoryMock.find as jest.Mock).mockResolvedValue( + SystemContractService.getSystemContracts().map((contract) => mock
({ address: contract.address })) + ); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(0); + }); + + it("adds all system contracts if none of them exist in the DB", async () => { + (addressRepositoryMock.find as jest.Mock).mockResolvedValue([]); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(systemContracts.length); + for (const systemContract of systemContracts) { + expect(addressRepositoryMock.add).toBeCalledWith({ + address: systemContract.address, + bytecode: `${systemContract.address}-code`, + }); + } + }); + + it("adds only missing system contracts", async () => { + const existingContractAddresses = [ + "0x000000000000000000000000000000000000800d", + "0x0000000000000000000000000000000000008006", + ]; + (addressRepositoryMock.find as jest.Mock).mockResolvedValue( + existingContractAddresses.map((existingContractAddress) => mock
({ address: existingContractAddress })) + ); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(systemContracts.length - existingContractAddresses.length); + for (const systemContract of systemContracts) { + if (!existingContractAddresses.includes(systemContract.address)) { + expect(addressRepositoryMock.add).toBeCalledWith({ + address: systemContract.address, + bytecode: `${systemContract.address}-code`, + }); + } + } + }); + + it("adds contracts only if they are deployed to the network", async () => { + const notDeployedSystemContracts = [ + "0x000000000000000000000000000000000000800d", + "0x0000000000000000000000000000000000008006", + ]; + (addressRepositoryMock.find as jest.Mock).mockResolvedValue([]); + (blockchainServiceMock.getCode as jest.Mock).mockImplementation(async (address: string) => { + if (notDeployedSystemContracts.includes(address)) { + return "0x"; + } + return `${address}-code`; + }); + await systemContractService.addSystemContracts(); + expect(addressRepositoryMock.add).toBeCalledTimes(systemContracts.length - notDeployedSystemContracts.length); + for (const systemContract of systemContracts) { + if (!notDeployedSystemContracts.includes(systemContract.address)) { + expect(addressRepositoryMock.add).toBeCalledWith({ + address: systemContract.address, + bytecode: `${systemContract.address}-code`, + }); + } + } + }); + }); +}); diff --git a/packages/worker/src/contract/systemContract.service.ts b/packages/worker/src/contract/systemContract.service.ts new file mode 100644 index 0000000000..09a8b93041 --- /dev/null +++ b/packages/worker/src/contract/systemContract.service.ts @@ -0,0 +1,143 @@ +import { Injectable } from "@nestjs/common"; +import { BlockchainService } from "../blockchain/blockchain.service"; +import { AddressRepository } from "../repositories"; +import { In } from "typeorm"; + +@Injectable() +export class SystemContractService { + constructor( + private readonly addressRepository: AddressRepository, + private readonly blockchainService: BlockchainService + ) {} + + public async addSystemContracts(): Promise { + const systemContracts = SystemContractService.getSystemContracts(); + const existingContracts = await this.addressRepository.find({ + where: { + address: In(systemContracts.map((contract) => contract.address)), + }, + select: { + address: true, + }, + }); + + for (const contract of systemContracts) { + if (!existingContracts.find((existingContract) => existingContract.address === contract.address)) { + const bytecode = await this.blockchainService.getCode(contract.address); + // some contract might not exist on the environment yet + if (bytecode !== "0x") { + await this.addressRepository.add({ + address: contract.address, + bytecode, + }); + } + } + } + } + + public static getSystemContracts() { + // name field is never used, it's just for better readability & understanding + return [ + { + address: "0x0000000000000000000000000000000000000000", + name: "EmptyContract", + }, + { + address: "0x0000000000000000000000000000000000000001", + name: "Ecrecover", + }, + { + address: "0x0000000000000000000000000000000000000002", + name: "SHA256", + }, + { + address: "0x0000000000000000000000000000000000000006", + name: "EcAdd", + }, + { + address: "0x0000000000000000000000000000000000000007", + name: "EcMul", + }, + { + address: "0x0000000000000000000000000000000000000008", + name: "EcPairing", + }, + { + address: "0x0000000000000000000000000000000000008001", + name: "EmptyContract", + }, + { + address: "0x0000000000000000000000000000000000008002", + name: "AccountCodeStorage", + }, + { + address: "0x0000000000000000000000000000000000008003", + name: "NonceHolder", + }, + { + address: "0x0000000000000000000000000000000000008004", + name: "KnownCodesStorage", + }, + { + address: "0x0000000000000000000000000000000000008005", + name: "ImmutableSimulator", + }, + { + address: "0x0000000000000000000000000000000000008006", + name: "ContractDeployer", + }, + { + address: "0x0000000000000000000000000000000000008008", + name: "L1Messenger", + }, + { + address: "0x0000000000000000000000000000000000008009", + name: "MsgValueSimulator", + }, + { + address: "0x000000000000000000000000000000000000800a", + name: "L2BaseToken", + }, + { + address: "0x000000000000000000000000000000000000800b", + name: "SystemContext", + }, + { + address: "0x000000000000000000000000000000000000800c", + name: "BootloaderUtilities", + }, + { + address: "0x000000000000000000000000000000000000800d", + name: "EventWriter", + }, + { + address: "0x000000000000000000000000000000000000800e", + name: "Compressor", + }, + { + address: "0x000000000000000000000000000000000000800f", + name: "ComplexUpgrader", + }, + { + address: "0x0000000000000000000000000000000000008010", + name: "Keccak256", + }, + { + address: "0x0000000000000000000000000000000000008012", + name: "CodeOracle", + }, + { + address: "0x0000000000000000000000000000000000000100", + name: "P256Verify", + }, + { + address: "0x0000000000000000000000000000000000008011", + name: "PubdataChunkPublisher", + }, + { + address: "0x0000000000000000000000000000000000010000", + name: "Create2Factory", + }, + ]; + } +}