Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/execution #4

Merged
merged 5 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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...
vgorkavenko marked this conversation as resolved.
Show resolved Hide resolved
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
Loading