@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
117 lines (105 loc) • 4.72 kB
text/typescript
import {BeaconConfig} from "@lodestar/config";
import {
IBeaconStateView,
SyncCommitteeCacheEmpty,
getBlockSignatureSets,
isStatePostAltair,
} from "@lodestar/state-transition";
import {IndexedAttestation, SignedBeaconBlock} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {Metrics} from "../../metrics/metrics.js";
import {nextEventLoop} from "../../util/eventLoop.js";
import {IBlsVerifier} from "../bls/index.js";
import {BlockError, BlockErrorCode} from "../errors/blockError.js";
import {ImportBlockOpts} from "./types.js";
/**
* Verifies 1 or more block's signatures from a group of blocks in the same epoch.
* getBlockSignatureSets() guarantees to return the correct signingRoots as long as all blocks belong in the same
* epoch as `preState0`. Otherwise the shufflings won't be correct.
*
* Since all data is known in advance all signatures are verified at once in parallel.
*/
export async function verifyBlocksSignatures(
config: BeaconConfig,
bls: IBlsVerifier,
logger: Logger,
metrics: Metrics | null,
preState0: IBeaconStateView,
blocks: SignedBeaconBlock[],
indexedAttestationsByBlock: IndexedAttestation[][],
opts: ImportBlockOpts
): Promise<{verifySignaturesTime: number}> {
const isValidPromises: Promise<boolean>[] = [];
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const currentSyncCommitteeIndexed = isStatePostAltair(preState0)
? preState0.currentSyncCommitteeIndexed
: new SyncCommitteeCacheEmpty();
// Verifies signatures after running state transition, so all SyncCommittee signed roots are known at this point.
// We must ensure block.slot <= state.slot before running getAllBlockSignatureSets().
// NOTE: If in the future multiple blocks signatures are verified at once, all blocks must be in the same epoch
// so the attester and proposer shufflings are correct.
for (const [i, block] of blocks.entries()) {
// Use [i] to make clear that the index has to be correct to blame the right block below on BlockError()
isValidPromises[i] = opts.validSignatures
? // Skip all signature verification
Promise.resolve(true)
: //
// Verify signatures per block to track which block is invalid
bls.verifySignatureSets(
getBlockSignatureSets(config, currentSyncCommitteeIndexed, preState0, block, indexedAttestationsByBlock[i], {
skipProposerSignature: opts.validProposerSignature,
})
);
// getBlockSignatureSets() takes 45ms in benchmarks for 2022Q2 mainnet blocks (100 sigs). When syncing a 32 blocks
// segments it will block the event loop for 1400 ms, which is too much. This call will allow the event loop to
// yield, which will cause one block's state transition to run. However, the tradeoff is okay and doesn't slow sync
if ((i + 1) % 8 === 0) {
await nextEventLoop();
}
}
// `rejectFirstInvalidResolveAllValid()` returns on isValid result with its index
const res = await rejectFirstInvalidResolveAllValid(isValidPromises);
if (!res.allValid) {
throw new BlockError(blocks[res.index], {code: BlockErrorCode.INVALID_SIGNATURE, state: preState0});
}
const verifySignaturesTime = Date.now();
if (blocks.length === 1 && opts.seenTimestampSec !== undefined) {
const recvToValidation = verifySignaturesTime / 1000 - opts.seenTimestampSec;
const validationTime = recvToValidation - recvToValLatency;
metrics?.gossipBlock.signatureVerification.recvToValidation.observe(recvToValidation);
metrics?.gossipBlock.signatureVerification.validationTime.observe(validationTime);
logger.debug("Verified block signatures", {
slot: blocks[0].message.slot,
recvToValLatency,
recvToValidation,
validationTime,
});
}
return {verifySignaturesTime};
}
type AllValidRes = {allValid: true} | {allValid: false; index: number};
/**
* From an array of promises that resolve a boolean isValid
* - if all valid, await all and return
* - if one invalid, abort immediately and return index of invalid
*/
export function rejectFirstInvalidResolveAllValid(isValidPromises: Promise<boolean>[]): Promise<AllValidRes> {
return new Promise<AllValidRes>((resolve, reject) => {
let validCount = 0;
for (let i = 0; i < isValidPromises.length; i++) {
isValidPromises[i]
.then((isValid) => {
if (isValid) {
if (++validCount >= isValidPromises.length) {
resolve({allValid: true});
}
} else {
resolve({allValid: false, index: i});
}
})
.catch((e) => {
reject(e);
});
}
});
}