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/cli #8

Merged
merged 7 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
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
Loading