Skip to content

Commit

Permalink
Feat/cli (#8)
Browse files Browse the repository at this point in the history
* chore: misc

* fix: daemon

* chore: logs and exit

* fix: contracts

* feat: progress bar -> spinner

* chore: refactor prover service

* feat: cli
  • Loading branch information
vgorkavenko authored Apr 8, 2024
1 parent 37191a4 commit 30b8bd4
Show file tree
Hide file tree
Showing 20 changed files with 361 additions and 128 deletions.
10 changes: 0 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
# CLI working mode
ETH_NETWORK=1
EL_RPC_URLS=https://mainnet.infura.io/v3/...
CL_API_URLS=https://quiknode.pro/...
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/
CSM_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320
VERIFIER_ADDRESS=0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6321
TX_SIGNER_PRIVATE_KEY=0x...
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"private": true,
"license": "GPL-3.0",
"scripts": {
"prove": "nest build && NODE_OPTIONS=--max_old_space_size=8192 WORKING_MODE=cli node dist/main prove",
"prove:debug": "nest build && NODE_OPTIONS=--max_old_space_size=8192 WORKING_MODE=cli node --inspect dist/main prove",
"slashing": "yarn prove slashing",
"slashing:debug": "yarn prove:debug slashing",
"withdrawal": "yarn prove withdrawal",
"withdrawal:debug": "yarn prove:debug withdrawal",
"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\"",
Expand Down Expand Up @@ -37,10 +43,10 @@
"@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",
"ora-classic": "^5.4.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"stream-chain": "^2.2.5",
Expand Down
9 changes: 7 additions & 2 deletions src/cli/cli.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Module } from '@nestjs/common';

import { CliService } from './cli.service';
import { ProveCommand } from './commands/prove.command';
import { ProofInputQuestion } from './questions/proof-input.question';
import { TxExecutionQuestion } from './questions/tx-execution.question';
import { ConfigModule } from '../common/config/config.module';
import { ContractsModule } from '../common/contracts/contracts.module';
import { LoggerModule } from '../common/logger/logger.module';
import { ProverModule } from '../common/prover/prover.module';
import { ProvidersModule } from '../common/providers/providers.module';

@Module({
imports: [LoggerModule, ConfigModule, ProvidersModule, ProverModule],
providers: [CliService],
imports: [LoggerModule, ConfigModule, ContractsModule, ProvidersModule, ProverModule],
providers: [CliService, ProveCommand, ProofInputQuestion, TxExecutionQuestion],
exports: [CliService, ProveCommand, ProofInputQuestion, TxExecutionQuestion],
})
export class CliModule {}
6 changes: 3 additions & 3 deletions src/cli/cli.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { LOGGER_PROVIDER } from '@lido-nestjs/logger';
import { Inject, Injectable, LoggerService, OnApplicationBootstrap } from '@nestjs/common';
import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common';

@Injectable()
export class CliService implements OnApplicationBootstrap {
export class CliService implements OnModuleInit {
constructor(@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService) {}
async onApplicationBootstrap() {
async onModuleInit() {
this.logger.log('Working mode: CLI');
}
}
110 changes: 110 additions & 0 deletions src/cli/commands/prove.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { LOGGER_PROVIDER } from '@lido-nestjs/logger';
import { Inject, LoggerService } from '@nestjs/common';
import { Command as Commander } from 'commander';
import { Command, CommandRunner, InjectCommander, InquirerService, Option } from 'nest-commander';

import { CsmContract } from '../../common/contracts/csm-contract.service';
import { ProverService } from '../../common/prover/prover.service';
import { KeyInfoFn } from '../../common/prover/types';
import { Consensus } from '../../common/providers/consensus/consensus';
import { Execution } from '../../common/providers/execution/execution';

type ProofOptions = {
nodeOperatorId: string;
keyIndex: string;
validatorIndex: string;
block: string;
};

@Command({
name: 'prove',
description: 'Prove a withdrawal or slashing',
arguments: '<withdrawal|slashing>',
argsDescription: {
withdrawal: 'Prove a withdrawal',
slashing: 'Prove a slashing',
},
})
export class ProveCommand extends CommandRunner {
private options: ProofOptions;
private pubkey: string;

constructor(
@Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService,
@InjectCommander() private readonly commander: Commander,
protected readonly inquirerService: InquirerService,
protected readonly csm: CsmContract,
protected readonly consensus: Consensus,
protected readonly execution: Execution,
protected readonly prover: ProverService,
) {
super();
}

async run(inputs: string[], options?: ProofOptions) {
try {
this.options = await this.inquirerService.ask('proof-input', options);
this.logger.debug!(this.options);
this.pubkey = await this.csm.getNodeOperatorKey(this.options.nodeOperatorId, this.options.keyIndex);
this.logger.debug!(`Validator public key: ${this.pubkey}`);
const header = await this.consensus.getBeaconHeader('finalized');
this.logger.debug!(`Finalized slot [${header.header.message.slot}]. Root [${header.root}]`);
const { root: blockRootToProcess } = await this.consensus.getBeaconHeader(this.options.block);
const blockInfoToProcess = await this.consensus.getBlockInfo(this.options.block);
this.logger.debug!(`Block to process [${this.options.block}]`);

switch (inputs[0]) {
case 'withdrawal':
await this.prover.handleWithdrawalsInBlock(blockRootToProcess, blockInfoToProcess, header, this.keyInfoFn);
break;
case 'slashing':
await this.prover.handleSlashingsInBlock(blockInfoToProcess, header, this.keyInfoFn);
break;
}
} catch (e) {
this.commander.error(e);
}
}

@Option({
flags: '--node-operator-id <nodeOperatorId>',
description: 'Node Operator ID from the CSM',
})
parseNodeOperatorId(val: string) {
return val;
}

@Option({
flags: '--key-index <keyIndex>',
description: 'Key Index from the CSM',
})
parseKeyIndex(val: string) {
return val;
}

@Option({
flags: '--validator-index <validatorIndex>',
description: 'Validator Index from the Consensus Layer',
})
parseValidatorIndex(val: string) {
return val;
}

@Option({
flags: '--block <block>',
description: 'Block from the Consensus Layer with validator withdrawal. Might be a block root or a slot number',
})
parseBlock(val: string) {
return val;
}

keyInfoFn: KeyInfoFn = (valIndex: number) => {
if (valIndex === Number(this.options.validatorIndex)) {
return {
operatorId: Number(this.options.nodeOperatorId),
keyIndex: Number(this.options.keyIndex),
pubKey: this.pubkey,
};
}
};
}
35 changes: 35 additions & 0 deletions src/cli/questions/proof-input.question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Question, QuestionSet } from 'nest-commander';

@QuestionSet({ name: 'proof-input' })
export class ProofInputQuestion {
@Question({
message: 'Node operator ID:',
name: 'nodeOperatorId',
})
parseNodeOperatorId(val: string) {
return val;
}
@Question({
message: 'Key index:',
name: 'keyIndex',
})
parseKeyIndex(val: string) {
return val;
}

@Question({
message: 'Validator index:',
name: 'validatorIndex',
})
parseValidatorIndex(val: string) {
return val;
}

@Question({
message: 'Block (root or slot number):',
name: 'block',
})
parseBlock(val: string) {
return val;
}
}
14 changes: 14 additions & 0 deletions src/cli/questions/tx-execution.question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Question, QuestionSet } from 'nest-commander';

@QuestionSet({ name: 'tx-execution' })
export class TxExecutionQuestion {
@Question({
type: 'confirm',
askAnswered: true,
message: 'Are you sure you want to send this transaction?',
name: 'sendingConfirmed',
})
parseSendingConfirmed(val: boolean) {
return val;
}
}
5 changes: 5 additions & 0 deletions src/common/contracts/csm-contract.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ export class CsmContract {
public async isWithdrawalProved(keyInfo: KeyInfo): Promise<boolean> {
return await this.impl.isValidatorWithdrawn(keyInfo.operatorId, keyInfo.keyIndex);
}

public async getNodeOperatorKey(nodeOperatorId: string | number, keyIndex: string | number): Promise<string> {
const [key] = await this.impl.getNodeOperatorSigningKeys(nodeOperatorId, keyIndex, 1);
return key;
}
}
2 changes: 0 additions & 2 deletions src/common/contracts/verifier-contract.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export class VerifierContract {
}

public async sendSlashingProof(payload: SlashingProofPayload): Promise<void> {
this.logger.debug!(payload);
await this.execution.execute(
this.impl.callStatic.processSlashingProof,
this.impl.populateTransaction.processSlashingProof,
Expand All @@ -28,7 +27,6 @@ export class VerifierContract {
}

public async sendWithdrawalProof(payload: WithdrawalsProofPayload): Promise<void> {
this.logger.debug!(payload);
await this.execution.execute(
this.impl.callStatic.processWithdrawalProof,
this.impl.populateTransaction.processWithdrawalProof,
Expand Down
2 changes: 1 addition & 1 deletion src/common/prover/duties/slashings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export class SlashingsService {

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);
this.logger.log(`Building slashing proof payloads`);
const payloads = this.buildSlashingsProofPayloads(finalizedHeader, nextHeaderTs, stateView, slashings);
for (const payload of payloads) {
this.logger.warn(`📡 Sending slashing proof payload for validator index: ${payload.witness.validatorIndex}`);
Expand Down
7 changes: 3 additions & 4 deletions src/common/prover/duties/withdrawals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let ssz: typeof import('@lodestar/types').ssz;
let anySsz: typeof ssz.phase0 | typeof ssz.altair | typeof ssz.bellatrix | typeof ssz.capella | typeof ssz.deneb;

// according to the research https://hackmd.io/1wM8vqeNTjqt4pC3XoCUKQ?view#Proposed-solution
const FULL_WITHDRAWAL_MIN_AMOUNT = 8 * 10 ** 18; // 8 ETH
const FULL_WITHDRAWAL_MIN_AMOUNT = 8 * 10 ** 9; // 8 ETH in Gwei

type WithdrawalWithOffset = Withdrawal & { offset: number };
type InvolvedKeysWithWithdrawal = { [valIndex: string]: KeyInfo & { withdrawal: WithdrawalWithOffset } };
Expand Down Expand Up @@ -67,7 +67,6 @@ export class WithdrawalsService {
): Promise<void> {
if (!Object.keys(withdrawals).length) return;
const blockHeader = await this.consensus.getBeaconHeader(blockRoot);
this.logger.log(`Getting state for root [${blockRoot}]`);
const state = await this.consensus.getState(blockHeader.header.message.state_root);
// There is a case when the block is not historical regarding the finalized block, but it is historical
// regarding the transaction execution time. This is possible when long finalization time
Expand All @@ -89,6 +88,7 @@ export class WithdrawalsService {
// create proof against the state with withdrawals
const nextBlockHeader = (await this.consensus.getBeaconHeadersByParentRoot(blockHeader.root)).data[0];
const nextBlockTs = this.consensus.slotToTimestamp(Number(nextBlockHeader.header.message.slot));
this.logger.log(`Building withdrawal proof payloads`);
const payloads = this.buildWithdrawalsProofPayloads(
blockHeader,
nextBlockTs,
Expand All @@ -112,12 +112,11 @@ export class WithdrawalsService {
// create proof against the historical state with withdrawals
const nextBlockHeader = (await this.consensus.getBeaconHeadersByParentRoot(finalizedHeader.root)).data[0];
const nextBlockTs = this.consensus.slotToTimestamp(Number(nextBlockHeader.header.message.slot));
this.logger.log(`Getting state for root [${finalizedHeader.root}]`);
const finalizedState = await this.consensus.getState(finalizedHeader.header.message.state_root);
const summaryIndex = this.calcSummaryIndex(blockInfo);
const summarySlot = this.calcSlotOfSummary(summaryIndex);
this.logger.log(`Getting state for slot [${summarySlot}]`);
const summaryState = await this.consensus.getState(summarySlot);
this.logger.log(`Building historical withdrawal proof payloads`);
const payloads = this.buildHistoricalWithdrawalsProofPayloads(
blockHeader,
finalizedHeader,
Expand Down
38 changes: 29 additions & 9 deletions src/common/prover/prover.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SlashingsService } from './duties/slashings';
import { WithdrawalsService } from './duties/withdrawals';
import { KeyInfoFn } from './types';
import { Consensus } from '../providers/consensus/consensus';
import { BlockInfoResponse, RootHex } from '../providers/consensus/response.interface';
import { BlockHeaderResponse, BlockInfoResponse, RootHex } from '../providers/consensus/response.interface';

@Injectable()
export class ProverService {
Expand All @@ -19,19 +19,39 @@ export class ProverService {
public async handleBlock(
blockRoot: RootHex,
blockInfo: BlockInfoResponse,
finalizedBlockRoot: RootHex,
finalizedHeader: BlockHeaderResponse,
keyInfoFn: KeyInfoFn,
): Promise<void> {
await this.handleWithdrawalsInBlock(blockRoot, blockInfo, finalizedHeader, keyInfoFn);
await this.handleSlashingsInBlock(blockInfo, finalizedHeader, keyInfoFn);
}

public async handleWithdrawalsInBlock(
blockRoot: RootHex,
blockInfo: BlockInfoResponse,
finalizedHeader: BlockHeaderResponse,
keyInfoFn: KeyInfoFn,
): Promise<void> {
const slashings = await this.slashings.getUnprovenSlashings(blockInfo, keyInfoFn);
const withdrawals = await this.withdrawals.getUnprovenWithdrawals(blockInfo, keyInfoFn);
if (!Object.keys(slashings).length && !Object.keys(withdrawals).length) {
this.logger.log('Nothing to prove');
if (!Object.keys(withdrawals).length) {
this.logger.log('No withdrawals to prove');
return;
}
const finalizedHeader = await this.consensus.getBeaconHeader(finalizedBlockRoot);
// do it consistently because of the high resource usage (both the app and CL node)
await this.slashings.sendSlashingProof(finalizedHeader, slashings);
await this.withdrawals.sendWithdrawalProofs(blockRoot, blockInfo, finalizedHeader, withdrawals);
this.logger.log('🏁 Proof(s) sent');
this.logger.log('🏁 Withdrawal proof(s) sent');
}

public async handleSlashingsInBlock(
blockInfo: BlockInfoResponse,
finalizedHeader: BlockHeaderResponse,
keyInfoFn: KeyInfoFn,
): Promise<void> {
const slashings = await this.slashings.getUnprovenSlashings(blockInfo, keyInfoFn);
if (!Object.keys(slashings).length) {
this.logger.log('No slashings to prove');
return;
}
await this.slashings.sendSlashingProof(finalizedHeader, slashings);
this.logger.log('🏁 Slashing proof(s) sent');
}
}
Loading

0 comments on commit 30b8bd4

Please sign in to comment.