Skip to content

Commit

Permalink
feat: add system contracts on the first run (#372)
Browse files Browse the repository at this point in the history
# 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

<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

This PR fixes: #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 <[email protected]>
  • Loading branch information
kiriyaga-txfusion and vasyl-ivanchuk authored Jan 21, 2025
1 parent 1bd495d commit 5576811
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/app/src/components/contract/InfoTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<CopyContent :value="contract.address" />
</table-body-column>
</tr>
<tr>
<tr v-if="contract.creatorAddress && contract.creatorTxHash">
<table-body-column class="contract-info-field-label">
{{ t("contract.table.creator") }}
</table-body-column>
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -130,6 +131,7 @@ import { DataFetcherService } from "./dataFetcher/dataFetcher.service";
Logger,
RetryDelayProvider,
DbMetricsService,
SystemContractService,
],
})
export class AppModule {}
16 changes: 16 additions & 0 deletions packages/worker/src/app.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand All @@ -39,6 +40,7 @@ describe("AppService", () => {
let tokenOffChainDataSaverService: TokenOffChainDataSaverService;
let dataSourceMock: DataSource;
let configServiceMock: ConfigService;
let systemContractService: SystemContractService;

beforeEach(async () => {
balancesCleanerService = mock<BalancesCleanerService>({
Expand Down Expand Up @@ -68,6 +70,9 @@ describe("AppService", () => {
configServiceMock = mock<ConfigService>({
get: jest.fn().mockReturnValue(false),
});
systemContractService = mock<SystemContractService>({
addSystemContracts: jest.fn().mockResolvedValue(null),
});

const module = await Test.createTestingModule({
imports: [EventEmitterModule.forRoot()],
Expand Down Expand Up @@ -106,6 +111,10 @@ describe("AppService", () => {
provide: ConfigService,
useValue: configServiceMock,
},
{
provide: SystemContractService,
useValue: systemContractService,
},
],
}).compile();

Expand Down Expand Up @@ -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", () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/worker/src/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
});
}
Expand Down
107 changes: 107 additions & 0 deletions packages/worker/src/contract/systemContract.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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<BlockchainService>({
getCode: jest.fn().mockImplementation((address: string) => Promise.resolve(`${address}-code`)),
});

addressRepositoryMock = mock<AddressRepository>({
find: jest.fn(),
});

const app: TestingModule = await Test.createTestingModule({
providers: [
SystemContractService,
{
provide: BlockchainService,
useValue: blockchainServiceMock,
},
{
provide: AddressRepository,
useValue: addressRepositoryMock,
},
],
}).compile();

app.useLogger(mock<Logger>());
systemContractService = app.get<SystemContractService>(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>({ 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>({ 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`,
});
}
}
});
});
});
143 changes: 143 additions & 0 deletions packages/worker/src/contract/systemContract.service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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",
},
];
}
}

0 comments on commit 5576811

Please sign in to comment.