Skip to content

Commit

Permalink
feat: exclude eligible intersections
Browse files Browse the repository at this point in the history
  • Loading branch information
avsetsin committed Jul 14, 2022
1 parent 3a5bd04 commit 5cb5b9c
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 23 deletions.
1 change: 1 addition & 0 deletions src/bls/bls.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class BlsService implements OnModuleInit {

return blst.verify(signingRoot, blsPublicKey, blsSignature);
} catch (error) {
this.logger.warn('Failed to verify deposit data', depositData);
this.logger.error(error);

return false;
Expand Down
5 changes: 5 additions & 0 deletions src/contracts/deposit/deposit.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PrometheusModule } from 'common/prometheus';
import { LoggerModule } from 'common/logger';
import { ConfigModule } from 'common/config';
import { APP_VERSION } from 'app.constants';
import { BlsService } from 'bls';

const mockSleep = sleep as jest.MockedFunction<typeof sleep>;

Expand All @@ -31,6 +32,7 @@ describe('DepositService', () => {
let depositService: DepositService;
let loggerService: LoggerService;
let repositoryService: RepositoryService;
let blsService: BlsService;

const depositAddress = '0x' + '1'.repeat(40);

Expand All @@ -50,6 +52,7 @@ describe('DepositService', () => {
cacheService = moduleRef.get(CacheService);
depositService = moduleRef.get(DepositService);
repositoryService = moduleRef.get(RepositoryService);
blsService = moduleRef.get(BlsService);
loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER);

jest.spyOn(loggerService, 'log').mockImplementation(() => undefined);
Expand Down Expand Up @@ -251,6 +254,8 @@ describe('DepositService', () => {
.spyOn(providerService.provider, 'getBlockNumber')
.mockImplementation(async () => endBlock);

jest.spyOn(blsService, 'verify').mockImplementation(() => true);

const mockProviderCall = jest
.spyOn(providerService.provider, 'getLogs')
.mockImplementation(async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/contracts/lido/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lido.module';
export * from './lido.service';
8 changes: 8 additions & 0 deletions src/contracts/lido/lido.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { LidoService } from './lido.service';

@Module({
providers: [LidoService],
exports: [LidoService],
})
export class LidoModule {}
47 changes: 47 additions & 0 deletions src/contracts/lido/lido.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Test } from '@nestjs/testing';
import { ConfigModule } from 'common/config';
import { LoggerModule } from 'common/logger';
import { MockProviderModule, ProviderService } from 'provider';
import { LidoAbi__factory } from 'generated';
import { RepositoryModule } from 'contracts/repository';
import { Interface } from '@ethersproject/abi';
import { LidoService } from './lido.service';
import { LidoModule } from './lido.module';

describe('SecurityService', () => {
let lidoService: LidoService;
let providerService: ProviderService;

beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ConfigModule.forRoot(),
MockProviderModule.forRoot(),
LoggerModule,
LidoModule,
RepositoryModule,
],
}).compile();

lidoService = moduleRef.get(LidoService);
providerService = moduleRef.get(ProviderService);
});

describe('getWithdrawalCredentials', () => {
it('should return withdrawal credentials', async () => {
const expected = '0x' + '1'.repeat(64);

const mockProviderCall = jest
.spyOn(providerService.provider, 'call')
.mockImplementation(async () => {
const iface = new Interface(LidoAbi__factory.abi);
const result = [expected];
return iface.encodeFunctionResult('getWithdrawalCredentials', result);
});

const wc = await lidoService.getWithdrawalCredentials();
expect(wc).toBe(expected);
expect(mockProviderCall).toBeCalledTimes(1);
});
});
});
19 changes: 19 additions & 0 deletions src/contracts/lido/lido.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { RepositoryService } from 'contracts/repository';
import { BlockTag } from 'provider';

@Injectable()
export class LidoService {
constructor(private repositoryService: RepositoryService) {}

/**
* Returns withdrawal credentials from the contract
*/
public async getWithdrawalCredentials(blockTag?: BlockTag): Promise<string> {
const contract = await this.repositoryService.getCachedLidoContract();

return await contract.getWithdrawalCredentials({
blockTag: blockTag as any,
});
}
}
9 changes: 8 additions & 1 deletion src/guardian/guardian.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { Module } from '@nestjs/common';
import { DepositModule } from 'contracts/deposit';
import { RegistryModule } from 'contracts/registry';
import { SecurityModule } from 'contracts/security';
import { LidoModule } from 'contracts/lido';
import { MessagesModule } from 'messages';
import { GuardianService } from './guardian.service';

@Module({
imports: [RegistryModule, DepositModule, SecurityModule, MessagesModule],
imports: [
RegistryModule,
DepositModule,
SecurityModule,
LidoModule,
MessagesModule,
],
providers: [GuardianService],
exports: [GuardianService],
})
Expand Down
78 changes: 74 additions & 4 deletions src/guardian/guardian.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { GuardianModule } from 'guardian';
import { DepositService } from 'contracts/deposit';
import { RegistryService } from 'contracts/registry';
import { SecurityService } from 'contracts/security';
import { LidoService } from 'contracts/lido';
import { RepositoryModule, RepositoryService } from 'contracts/repository';
import { MessagesService, MessageType } from 'messages';

Expand All @@ -20,6 +21,7 @@ describe('GuardianService', () => {
let loggerService: LoggerService;
let depositService: DepositService;
let registryService: RegistryService;
let lidoService: LidoService;
let messagesService: MessagesService;
let securityService: SecurityService;
let repositoryService: RepositoryService;
Expand All @@ -40,6 +42,7 @@ describe('GuardianService', () => {
guardianService = moduleRef.get(GuardianService);
depositService = moduleRef.get(DepositService);
registryService = moduleRef.get(RegistryService);
lidoService = moduleRef.get(LidoService);
messagesService = moduleRef.get(MessagesService);
securityService = moduleRef.get(SecurityService);
repositoryService = moduleRef.get(RepositoryService);
Expand Down Expand Up @@ -79,7 +82,7 @@ describe('GuardianService', () => {

expect(matched).toBeInstanceOf(Array);
expect(matched).toHaveLength(1);
expect(matched).toContain('0x1');
expect(matched).toContainEqual({ pubkey: '0x1' });
});

it('should not find the keys when they don’t match', () => {
Expand Down Expand Up @@ -131,7 +134,7 @@ describe('GuardianService', () => {

expect(matched).toBeInstanceOf(Array);
expect(matched).toHaveLength(1);
expect(matched).toContain(pubkey);
expect(matched).toContainEqual({ pubkey });
});

it('should not find the keys when they don’t match', () => {
Expand Down Expand Up @@ -231,11 +234,15 @@ describe('GuardianService', () => {
});

describe('checkKeysIntersections', () => {
const lidoWC = '0x12';
const attackerWC = '0x23';
const depositedPubKeys = ['0x1234', '0x5678'];
const depositedEvents = {
startBlock: 1,
endBlock: 5,
events: depositedPubKeys.map((pubkey) => ({ pubkey } as any)),
events: depositedPubKeys.map(
(pubkey) => ({ pubkey, valid: true } as any),
),
};
const nodeOperatorsCache = {
depositRoot: '0x2345',
Expand All @@ -260,7 +267,15 @@ describe('GuardianService', () => {
it('should call handleKeysIntersections if next keys are found in the deposit contract', async () => {
const depositedKey = depositedPubKeys[0];
const nextSigningKeys = [depositedKey];
const blockData = { ...currentBlockData, nextSigningKeys };
const events = currentBlockData.depositedEvents.events.map(
({ ...data }) => ({ ...data, wc: attackerWC } as any),
);

const blockData = {
...currentBlockData,
depositedEvents: { ...currentBlockData.depositedEvents, events },
nextSigningKeys,
};

const mockHandleCorrectKeys = jest
.spyOn(guardianService, 'handleCorrectKeys')
Expand All @@ -270,11 +285,16 @@ describe('GuardianService', () => {
.spyOn(guardianService, 'handleKeysIntersections')
.mockImplementation(async () => undefined);

const mockGetWithdrawalCredentials = jest
.spyOn(lidoService, 'getWithdrawalCredentials')
.mockImplementation(async () => lidoWC);

await guardianService.checkKeysIntersections(blockData);

expect(mockHandleCorrectKeys).not.toBeCalled();
expect(mockHandleKeysIntersections).toBeCalledTimes(1);
expect(mockHandleKeysIntersections).toBeCalledWith(blockData);
expect(mockGetWithdrawalCredentials).toBeCalledTimes(1);
});

it('should call handleCorrectKeys if Lido next keys are not found in the deposit contract', async () => {
Expand Down Expand Up @@ -349,6 +369,56 @@ describe('GuardianService', () => {
});
});

describe('excludeEligibleIntersections', () => {
const pubkey = '0x1234';
const lidoWC = '0x12';
const attackerWC = '0x23';
const blockData = { blockHash: '0x1234' } as any;

beforeEach(async () => {
jest
.spyOn(lidoService, 'getWithdrawalCredentials')
.mockImplementation(async () => lidoWC);
});

it('should exclude invalid intersections', async () => {
const intersections = [{ valid: false, pubkey, wc: lidoWC } as any];

const filteredIntersections =
await guardianService.excludeEligibleIntersections(
intersections,
blockData,
);

expect(filteredIntersections).toHaveLength(0);
});

it('should exclude intersections with lido WC', async () => {
const intersections = [{ valid: true, pubkey, wc: lidoWC } as any];

const filteredIntersections =
await guardianService.excludeEligibleIntersections(
intersections,
blockData,
);

expect(filteredIntersections).toHaveLength(0);
});

it('should not exclude intersections with attacker WC', async () => {
const intersections = [{ valid: true, pubkey, wc: attackerWC } as any];

const filteredIntersections =
await guardianService.excludeEligibleIntersections(
intersections,
blockData,
);

expect(filteredIntersections).toHaveLength(1);
expect(filteredIntersections).toEqual(intersections);
});
});

describe('handleKeysIntersections', () => {
const signature = {} as any;
const blockData = { blockNumber: 1 } as any;
Expand Down
Loading

0 comments on commit 5cb5b9c

Please sign in to comment.