Skip to content

Commit

Permalink
Feat/execution (#4)
Browse files Browse the repository at this point in the history
* chore: dependencies and etc.

* feat: execution process

* fix: workflow

* fix: add ethers

* fix: remarks
  • Loading branch information
vgorkavenko authored Apr 8, 2024
1 parent 1a7cc4f commit 37191a4
Show file tree
Hide file tree
Showing 24 changed files with 1,078 additions and 62 deletions.
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
ETH_NETWORK=1
EL_RPC_URLS=https://mainnet.infura.io/v3/...
CL_API_URLS=https://quiknode.pro/...
LIDO_STAKING_MODULE_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320
CSM_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320
VERIFIER_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6321
TX_SIGNER_PRIVATE_KEY=0x...

# Daemon working mode
ETH_NETWORK=1
EL_RPC_URLS=https://mainnet.infura.io/v3/...
CL_API_URLS=https://quiknode.pro/...
KEYSAPI_API_URLS=https://keys-api.lido.fi/
LIDO_STAKING_MODULE_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320
CSM_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320
VERIFIER_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6321
TX_SIGNER_PRIVATE_KEY=0x...
3 changes: 3 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,8 @@ jobs:
- name: Install Node dependencies
run: yarn install --frozen-lockfile --non-interactive

- name: Generate types
run: yarn typechain

- name: Linters check
run: yarn lint
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/dist
/node_modules

# generated files
/src/common/contracts/types

# ENV
/.env

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ If any of these time thresholds are breached, we can't be sure that if there was

```bash
$ yarn install
$ yarn run typechain
```

## Running the app
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"private": true,
"license": "GPL-3.0",
"scripts": {
"typechain": "typechain --target=ethers-v5 --out-dir src/common/contracts/types src/common/contracts/abi/*.json",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
Expand All @@ -23,23 +24,28 @@
},
"dependencies": {
"@huanshiwushuang/lowdb": "^6.0.2",
"@lido-nestjs/constants": "^5.2.1",
"@lido-nestjs/execution": "^1.12.0",
"@lido-nestjs/logger": "^1.3.2",
"@lodestar/params": "^1.16.0",
"@lodestar/types": "^1.15.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@typechain/ethers-v5": "^11.1.2",
"@types/cli-progress": "^3.11.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cli-progress": "^3.12.0",
"ethers": "^5.7.2",
"nest-commander": "^3.12.5",
"nest-winston": "^1.9.4",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"stream-chain": "^2.2.5",
"stream-json": "^1.8.0",
"typechain": "^8.3.2",
"undici": "^6.4.0",
"winston": "^3.11.0"
},
Expand Down
67 changes: 66 additions & 1 deletion src/common/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IsString,
Max,
Min,
ValidateIf,
validateSync,
} from 'class-validator';

Expand Down Expand Up @@ -43,7 +44,42 @@ export class EnvironmentVariables {

@IsNotEmpty()
@IsString()
public LIDO_STAKING_MODULE_ADDRESS: string;
public CSM_ADDRESS: string;

@IsNotEmpty()
@IsString()
public VERIFIER_ADDRESS: string;

@IsNotEmpty()
@IsString()
@ValidateIf((vars) => !vars.DRY_RUN)
public TX_SIGNER_PRIVATE_KEY: string;

@IsNumber()
@Transform(({ value }) => parseInt(value, 10), { toClassOnly: true })
public TX_MIN_GAS_PRIORITY_FEE = 50_000_000; // 0.05 gwei

@IsNumber()
@Transform(({ value }) => parseInt(value, 10), { toClassOnly: true })
public TX_MAX_GAS_PRIORITY_FEE = 10_000_000_000; // 10 gwei

@IsNumber()
@Max(100)
@Transform(({ value }) => parseInt(value, 10), { toClassOnly: true })
public TX_GAS_PRIORITY_FEE_PERCENTILE = 25;

@IsNumber()
@Transform(({ value }) => parseInt(value, 10), { toClassOnly: true })
public TX_GAS_FEE_HISTORY_DAYS = 1;

@IsNumber()
@Max(100)
@Transform(({ value }) => parseInt(value, 10), { toClassOnly: true })
public TX_GAS_FEE_HISTORY_PERCENTILE = 20;

@IsNumber()
@Transform(({ value }) => parseInt(value, 10), { toClassOnly: true })
public TX_GAS_LIMIT = 1_000_000;

@IsNumber()
@Min(30 * MINUTE)
Expand All @@ -68,6 +104,7 @@ export class EnvironmentVariables {
LOG_FORMAT: LogFormat = LogFormat.Simple;

@IsBoolean()
@Transform(({ value }) => toBoolean(value), { toClassOnly: true })
public DRY_RUN = false;

@IsNotEmpty()
Expand Down Expand Up @@ -145,3 +182,31 @@ export function validate(config: Record<string, unknown>) {

return validatedConfig;
}

const toBoolean = (value: any): boolean => {
if (typeof value === 'boolean') {
return value;
}

if (typeof value === 'number') {
return !!value;
}

if (!(typeof value === 'string')) {
return false;
}

switch (value.toLowerCase().trim()) {
case 'true':
case 'yes':
case '1':
return true;
case 'false':
case 'no':
case '0':
case null:
return false;
default:
return false;
}
};
1 change: 1 addition & 0 deletions src/common/contracts/abi/csm.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/common/contracts/abi/verifier.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"type":"constructor","inputs":[{"name":"slotsPerEpoch","type":"uint64","internalType":"uint64"},{"name":"gIHistoricalSummaries","type":"bytes32","internalType":"GIndex"},{"name":"gIFirstWithdrawal","type":"bytes32","internalType":"GIndex"},{"name":"gIFirstValidator","type":"bytes32","internalType":"GIndex"},{"name":"firstSupportedSlot","type":"uint64","internalType":"Slot"}],"stateMutability":"nonpayable"},{"type":"function","name":"BEACON_ROOTS","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"FIRST_SUPPORTED_SLOT","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"Slot"}],"stateMutability":"view"},{"type":"function","name":"GI_FIRST_VALIDATOR","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"GIndex"}],"stateMutability":"view"},{"type":"function","name":"GI_FIRST_WITHDRAWAL","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"GIndex"}],"stateMutability":"view"},{"type":"function","name":"GI_HISTORICAL_SUMMARIES","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"GIndex"}],"stateMutability":"view"},{"type":"function","name":"SLOTS_PER_EPOCH","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_locator","type":"address","internalType":"address"},{"name":"_module","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"locator","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"module","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"processHistoricalWithdrawalProof","inputs":[{"name":"beaconBlock","type":"tuple","internalType":"struct ICSVerifier.ProvableBeaconBlockHeader","components":[{"name":"header","type":"tuple","internalType":"struct BeaconBlockHeader","components":[{"name":"slot","type":"uint64","internalType":"uint64"},{"name":"proposerIndex","type":"uint64","internalType":"uint64"},{"name":"parentRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"},{"name":"bodyRoot","type":"bytes32","internalType":"bytes32"}]},{"name":"rootsTimestamp","type":"uint64","internalType":"uint64"}]},{"name":"oldBlock","type":"tuple","internalType":"struct ICSVerifier.HistoricalHeaderWitness","components":[{"name":"header","type":"tuple","internalType":"struct BeaconBlockHeader","components":[{"name":"slot","type":"uint64","internalType":"uint64"},{"name":"proposerIndex","type":"uint64","internalType":"uint64"},{"name":"parentRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"},{"name":"bodyRoot","type":"bytes32","internalType":"bytes32"}]},{"name":"rootGIndex","type":"bytes32","internalType":"GIndex"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"witness","type":"tuple","internalType":"struct ICSVerifier.WithdrawalWitness","components":[{"name":"withdrawalOffset","type":"uint8","internalType":"uint8"},{"name":"withdrawalIndex","type":"uint64","internalType":"uint64"},{"name":"validatorIndex","type":"uint64","internalType":"uint64"},{"name":"amount","type":"uint64","internalType":"uint64"},{"name":"withdrawalCredentials","type":"bytes32","internalType":"bytes32"},{"name":"effectiveBalance","type":"uint64","internalType":"uint64"},{"name":"slashed","type":"bool","internalType":"bool"},{"name":"activationEligibilityEpoch","type":"uint64","internalType":"uint64"},{"name":"activationEpoch","type":"uint64","internalType":"uint64"},{"name":"exitEpoch","type":"uint64","internalType":"uint64"},{"name":"withdrawableEpoch","type":"uint64","internalType":"uint64"},{"name":"withdrawalProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"validatorProof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processSlashingProof","inputs":[{"name":"beaconBlock","type":"tuple","internalType":"struct ICSVerifier.ProvableBeaconBlockHeader","components":[{"name":"header","type":"tuple","internalType":"struct BeaconBlockHeader","components":[{"name":"slot","type":"uint64","internalType":"uint64"},{"name":"proposerIndex","type":"uint64","internalType":"uint64"},{"name":"parentRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"},{"name":"bodyRoot","type":"bytes32","internalType":"bytes32"}]},{"name":"rootsTimestamp","type":"uint64","internalType":"uint64"}]},{"name":"witness","type":"tuple","internalType":"struct ICSVerifier.SlashingWitness","components":[{"name":"validatorIndex","type":"uint64","internalType":"uint64"},{"name":"withdrawalCredentials","type":"bytes32","internalType":"bytes32"},{"name":"effectiveBalance","type":"uint64","internalType":"uint64"},{"name":"activationEligibilityEpoch","type":"uint64","internalType":"uint64"},{"name":"activationEpoch","type":"uint64","internalType":"uint64"},{"name":"exitEpoch","type":"uint64","internalType":"uint64"},{"name":"withdrawableEpoch","type":"uint64","internalType":"uint64"},{"name":"validatorProof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processWithdrawalProof","inputs":[{"name":"beaconBlock","type":"tuple","internalType":"struct ICSVerifier.ProvableBeaconBlockHeader","components":[{"name":"header","type":"tuple","internalType":"struct BeaconBlockHeader","components":[{"name":"slot","type":"uint64","internalType":"uint64"},{"name":"proposerIndex","type":"uint64","internalType":"uint64"},{"name":"parentRoot","type":"bytes32","internalType":"bytes32"},{"name":"stateRoot","type":"bytes32","internalType":"bytes32"},{"name":"bodyRoot","type":"bytes32","internalType":"bytes32"}]},{"name":"rootsTimestamp","type":"uint64","internalType":"uint64"}]},{"name":"witness","type":"tuple","internalType":"struct ICSVerifier.WithdrawalWitness","components":[{"name":"withdrawalOffset","type":"uint8","internalType":"uint8"},{"name":"withdrawalIndex","type":"uint64","internalType":"uint64"},{"name":"validatorIndex","type":"uint64","internalType":"uint64"},{"name":"amount","type":"uint64","internalType":"uint64"},{"name":"withdrawalCredentials","type":"bytes32","internalType":"bytes32"},{"name":"effectiveBalance","type":"uint64","internalType":"uint64"},{"name":"slashed","type":"bool","internalType":"bool"},{"name":"activationEligibilityEpoch","type":"uint64","internalType":"uint64"},{"name":"activationEpoch","type":"uint64","internalType":"uint64"},{"name":"exitEpoch","type":"uint64","internalType":"uint64"},{"name":"withdrawableEpoch","type":"uint64","internalType":"uint64"},{"name":"withdrawalProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"validatorProof","type":"bytes32[]","internalType":"bytes32[]"}]},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"error","name":"IndexOutOfRange","inputs":[]},{"type":"error","name":"InvalidBlockHeader","inputs":[]},{"type":"error","name":"InvalidChainConfig","inputs":[]},{"type":"error","name":"InvalidGIndex","inputs":[]},{"type":"error","name":"InvalidWithdrawalAddress","inputs":[]},{"type":"error","name":"PartialWitdrawal","inputs":[]},{"type":"error","name":"RootNotFound","inputs":[]},{"type":"error","name":"UnsupportedSlot","inputs":[{"name":"slot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ValidatorNotWithdrawn","inputs":[]}]
12 changes: 12 additions & 0 deletions src/common/contracts/contracts.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';

import { CsmContract } from './csm-contract.service';
import { VerifierContract } from './verifier-contract.service';
import { ProvidersModule } from '../providers/providers.module';

@Module({
imports: [ProvidersModule],
providers: [CsmContract, VerifierContract],
exports: [CsmContract, VerifierContract],
})
export class ContractsModule {}
26 changes: 26 additions & 0 deletions src/common/contracts/csm-contract.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';

import { Csm, Csm__factory } from './types';
import { ConfigService } from '../config/config.service';
import { KeyInfo } from '../prover/types';
import { Execution } from '../providers/execution/execution';

@Injectable()
export class CsmContract {
private impl: Csm;

constructor(
protected readonly config: ConfigService,
protected readonly execution: Execution,
) {
this.impl = Csm__factory.connect(this.config.get('CSM_ADDRESS'), this.execution.provider);
}

public async isSlashingProved(keyInfo: KeyInfo): Promise<boolean> {
return await this.impl.isValidatorSlashed(keyInfo.operatorId, keyInfo.keyIndex);
}

public async isWithdrawalProved(keyInfo: KeyInfo): Promise<boolean> {
return await this.impl.isValidatorWithdrawn(keyInfo.operatorId, keyInfo.keyIndex);
}
}
46 changes: 46 additions & 0 deletions src/common/contracts/verifier-contract.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { LOGGER_PROVIDER } from '@lido-nestjs/logger';
import { Inject, Injectable, LoggerService } from '@nestjs/common';

import { Verifier, Verifier__factory } from './types';
import { ConfigService } from '../config/config.service';
import { HistoricalWithdrawalsProofPayload, SlashingProofPayload, WithdrawalsProofPayload } from '../prover/types';
import { Execution } from '../providers/execution/execution';

@Injectable()
export class VerifierContract {
private impl: Verifier;

constructor(
@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService,
protected readonly config: ConfigService,
protected readonly execution: Execution,
) {
this.impl = Verifier__factory.connect(this.config.get('VERIFIER_ADDRESS'), this.execution.provider);
}

public async sendSlashingProof(payload: SlashingProofPayload): Promise<void> {
this.logger.debug!(payload);
await this.execution.execute(
this.impl.callStatic.processSlashingProof,
this.impl.populateTransaction.processSlashingProof,
[payload.beaconBlock, payload.witness, payload.nodeOperatorId, payload.keyIndex],
);
}

public async sendWithdrawalProof(payload: WithdrawalsProofPayload): Promise<void> {
this.logger.debug!(payload);
await this.execution.execute(
this.impl.callStatic.processWithdrawalProof,
this.impl.populateTransaction.processWithdrawalProof,
[payload.beaconBlock, payload.witness, payload.nodeOperatorId, payload.keyIndex],
);
}

public async sendHistoricalWithdrawalProof(payload: HistoricalWithdrawalsProofPayload): Promise<void> {
await this.execution.execute(
this.impl.callStatic.processHistoricalWithdrawalProof,
this.impl.populateTransaction.processHistoricalWithdrawalProof,
[payload.beaconBlock, payload.oldBlock, payload.witness, payload.nodeOperatorId, payload.keyIndex],
);
}
}
3 changes: 2 additions & 1 deletion src/common/prometheus/prometheus.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';

import { PrometheusService } from './prometheus.service';

@Global()
@Module({
providers: [PrometheusService],
exports: [PrometheusService],
Expand Down
24 changes: 12 additions & 12 deletions src/common/prover/duties/slashings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { ContainerTreeViewType } from '@chainsafe/ssz/lib/view/container';
import { LOGGER_PROVIDER } from '@lido-nestjs/logger';
import { Inject, Injectable, LoggerService } from '@nestjs/common';

import { CsmContract } from '../../contracts/csm-contract.service';
import { VerifierContract } from '../../contracts/verifier-contract.service';
import { Consensus } from '../../providers/consensus/consensus';
import { BlockHeaderResponse, BlockInfoResponse } from '../../providers/consensus/response.interface';
import { generateValidatorProof, toHex, verifyProof } from '../helpers/proofs';
import { KeyInfo, KeyInfoFn, SlashingProvePayload } from '../types';
import { KeyInfo, KeyInfoFn, SlashingProofPayload } from '../types';

let ssz: typeof import('@lodestar/types').ssz;
let anySsz: typeof ssz.phase0 | typeof ssz.altair | typeof ssz.bellatrix | typeof ssz.capella | typeof ssz.deneb;
Expand All @@ -17,6 +19,8 @@ export class SlashingsService {
constructor(
@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService,
protected readonly consensus: Consensus,
protected readonly csm: CsmContract,
protected readonly verifier: VerifierContract,
) {}

public async getUnprovenSlashings(blockInfo: BlockInfoResponse, keyInfoFn: KeyInfoFn): Promise<InvolvedKeys> {
Expand All @@ -27,9 +31,7 @@ export class SlashingsService {
if (!Object.keys(slashings).length) return {};
const unproven: InvolvedKeys = {};
for (const [valIndex, keyInfo] of Object.entries(slashings)) {
// TODO: implement
// const proved = await this.execution.isSlashingProved(slashing);
const proved = false;
const proved = await this.csm.isSlashingProved(keyInfo);
if (!proved) unproven[valIndex] = keyInfo;
}
const unprovenCount = Object.keys(unproven).length;
Expand All @@ -41,19 +43,17 @@ export class SlashingsService {
return unproven;
}

public async sendSlashingProves(finalizedHeader: BlockHeaderResponse, slashings: InvolvedKeys): Promise<void> {
public async sendSlashingProof(finalizedHeader: BlockHeaderResponse, slashings: InvolvedKeys): Promise<void> {
if (!Object.keys(slashings).length) return;
this.logger.log(`Getting state for root [${finalizedHeader.root}]`);
const finalizedState = await this.consensus.getState(finalizedHeader.header.message.state_root);
const nextHeader = (await this.consensus.getBeaconHeadersByParentRoot(finalizedHeader.root)).data[0];
const nextHeaderTs = this.consensus.slotToTimestamp(Number(nextHeader.header.message.slot));
const stateView = this.consensus.stateToView(finalizedState.bodyBytes, finalizedState.forkName);
const payloads = this.buildSlashingsProvePayloads(finalizedHeader, nextHeaderTs, stateView, slashings);
const payloads = this.buildSlashingsProofPayloads(finalizedHeader, nextHeaderTs, stateView, slashings);
for (const payload of payloads) {
this.logger.warn(`📡 Sending slashing prove payload for validator index: ${payload.witness.validatorIndex}`);
// TODO: implement
// TODO: ask before sending if CLI
this.logger.log(payload);
this.logger.warn(`📡 Sending slashing proof payload for validator index: ${payload.witness.validatorIndex}`);
await this.verifier.sendSlashingProof(payload);
}
}

Expand Down Expand Up @@ -88,12 +88,12 @@ export class SlashingsService {
return slashed;
}

private *buildSlashingsProvePayloads(
private *buildSlashingsProofPayloads(
currentHeader: BlockHeaderResponse,
nextHeaderTimestamp: number,
stateView: ContainerTreeViewType<typeof anySsz.BeaconState.fields>,
slashings: InvolvedKeys,
): Generator<SlashingProvePayload> {
): Generator<SlashingProofPayload> {
for (const [valIndex, keyInfo] of Object.entries(slashings)) {
const validator = stateView.validators.get(Number(valIndex));
const validatorProof = generateValidatorProof(stateView, Number(valIndex));
Expand Down
Loading

0 comments on commit 37191a4

Please sign in to comment.