Merge branch 'unstable' into nc/epbs-p2p

This commit is contained in:
NC
2026-01-05 14:01:59 -10:00
23 changed files with 131 additions and 83 deletions

View File

@@ -28,6 +28,7 @@ export async function verifyBlocksSignatures(
): Promise<{verifySignaturesTime: number}> {
const isValidPromises: Promise<boolean>[] = [];
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const currentSyncCommitteeIndexed = preState0.epochCtx.currentSyncCommitteeIndexed;
// 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().
@@ -41,9 +42,16 @@ export async function verifyBlocksSignatures(
: //
// Verify signatures per block to track which block is invalid
bls.verifySignatureSets(
getBlockSignatureSets(config, index2pubkey, preState0, block, indexedAttestationsByBlock[i], {
skipProposerSignature: opts.validProposerSignature,
})
getBlockSignatureSets(
config,
index2pubkey,
currentSyncCommitteeIndexed,
block,
indexedAttestationsByBlock[i],
{
skipProposerSignature: opts.validProposerSignature,
}
)
);
// getBlockSignatureSets() takes 45ms in benchmarks for 2022Q2 mainnet blocks (100 sigs). When syncing a 32 blocks

View File

@@ -265,6 +265,18 @@ async function validateAggregateAndProof(
});
}
// Same race-condition check as above for seen aggregators
if (
!skipValidationKnownAttesters &&
chain.seenAggregatedAttestations.isKnown(targetEpoch, attIndex, attDataRootHex, aggregationBits)
) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN,
targetEpoch,
aggregateRoot: attDataRootHex,
});
}
chain.seenAggregators.add(targetEpoch, aggregatorIndex);
chain.seenAggregatedAttestations.add(
targetEpoch,

View File

@@ -51,7 +51,12 @@ export async function validateAttesterSlashing(
});
}
const signatureSets = getAttesterSlashingSignatureSets(chain.config, chain.index2pubkey, state, attesterSlashing);
const signatureSets = getAttesterSlashingSignatureSets(
chain.config,
chain.index2pubkey,
state.slot,
attesterSlashing
);
if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) {
throw new AttesterSlashingError(GossipAction.REJECT, {
code: AttesterSlashingErrorCode.INVALID,

View File

@@ -155,7 +155,7 @@ export async function validateGossipBlock(
// [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey.
if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) {
const signatureSet = getBlockProposerSignatureSet(chain.config, chain.index2pubkey, blockState, signedBlock);
const signatureSet = getBlockProposerSignatureSet(chain.config, chain.index2pubkey, signedBlock);
// Don't batch so verification is not delayed
if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) {
throw new BlockGossipError(GossipAction.REJECT, {

View File

@@ -44,7 +44,12 @@ async function validateProposerSlashing(
});
}
const signatureSets = getProposerSlashingSignatureSets(chain.config, chain.index2pubkey, state, proposerSlashing);
const signatureSets = getProposerSlashingSignatureSets(
chain.config,
chain.index2pubkey,
state.slot,
proposerSlashing
);
if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) {
throw new ProposerSlashingError(GossipAction.REJECT, {
code: ProposerSlashingErrorCode.INVALID,

View File

@@ -59,7 +59,7 @@ async function validateVoluntaryExit(
});
}
const signatureSet = getVoluntaryExitSignatureSet(chain.config, chain.index2pubkey, state, voluntaryExit);
const signatureSet = getVoluntaryExitSignatureSet(chain.config, chain.index2pubkey, state.slot, voluntaryExit);
if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) {
throw new VoluntaryExitError(GossipAction.REJECT, {
code: VoluntaryExitErrorCode.INVALID_SIGNATURE,

View File

@@ -55,8 +55,7 @@ export async function verifyBlockProposerSignature(
if (blocks.length === 1 && blocks[0].message.slot === GENESIS_SLOT) return;
const signatures = blocks.reduce((sigs: ISignatureSet[], block) => {
// genesis block doesn't have valid signature
if (block.message.slot !== GENESIS_SLOT)
sigs.push(getBlockProposerSignatureSet(config, index2pubkey, state, block));
if (block.message.slot !== GENESIS_SLOT) sigs.push(getBlockProposerSignatureSet(config, index2pubkey, block));
return sigs;
}, []);

View File

@@ -21,7 +21,7 @@ export function isValidIndexedAttestation(
}
if (verifySignature) {
return verifySignatureSet(getIndexedAttestationSignatureSet(config, index2pubkey, state, indexedAttestation));
return verifySignatureSet(getIndexedAttestationSignatureSet(config, index2pubkey, state.slot, indexedAttestation));
}
return true;
}
@@ -38,7 +38,9 @@ export function isValidIndexedAttestationBigint(
}
if (verifySignature) {
return verifySignatureSet(getIndexedAttestationBigintSignatureSet(config, index2pubkey, state, indexedAttestation));
return verifySignatureSet(
getIndexedAttestationBigintSignatureSet(config, index2pubkey, state.slot, indexedAttestation)
);
}
return true;
}

View File

@@ -67,7 +67,7 @@ export function processAttestationsAltair(
const sigSet = getAttestationWithIndicesSignatureSet(
state.config,
epochCtx.index2pubkey,
state,
state.slot,
attestation,
attestingIndices
);

View File

@@ -80,7 +80,7 @@ export function assertValidProposerSlashing(
const signatureSets = getProposerSlashingSignatureSets(
state.config,
state.epochCtx.index2pubkey,
state,
state.slot,
proposerSlashing
);
for (let i = 0; i < signatureSets.length; i++) {

View File

@@ -12,12 +12,12 @@ import {getRandaoMix} from "../util/index.js";
* PERF: Fixed work independent of block contents.
*/
export function processRandao(state: CachedBeaconStateAllForks, block: BeaconBlock, verifySignature = true): void {
const {epochCtx} = state;
const {epochCtx, config} = state;
const epoch = epochCtx.epoch;
const randaoReveal = block.body.randaoReveal;
// verify RANDAO reveal
if (verifySignature && !verifyRandaoSignature(state.config, epochCtx.index2pubkey, state, block)) {
if (verifySignature && !verifyRandaoSignature(config, epochCtx.index2pubkey, block)) {
throw new Error("RANDAO reveal is an invalid signature");
}

View File

@@ -3,6 +3,7 @@ import {BeaconConfig} from "@lodestar/config";
import {DOMAIN_SYNC_COMMITTEE, SYNC_COMMITTEE_SIZE} from "@lodestar/params";
import {altair, ssz} from "@lodestar/types";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js";
import {G2_POINT_AT_INFINITY} from "../constants/index.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {
@@ -28,7 +29,7 @@ export function processSyncAggregate(
const signatureSet = getSyncCommitteeSignatureSet(
state.config,
state.epochCtx.index2pubkey,
state,
state.epochCtx.currentSyncCommitteeIndexed,
block,
participantIndices
);
@@ -73,7 +74,7 @@ export function processSyncAggregate(
export function getSyncCommitteeSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
currentSyncCommitteeIndexed: SyncCommitteeCache,
block: altair.BeaconBlock,
/** Optional parameter to prevent computing it twice */
participantIndices?: number[]
@@ -101,7 +102,7 @@ export function getSyncCommitteeSignatureSet(
const rootSigned = block.parentRoot;
if (!participantIndices) {
const committeeIndices = state.epochCtx.currentSyncCommitteeIndexed.validatorIndices;
const committeeIndices = currentSyncCommitteeIndexed.validatorIndices;
participantIndices = syncAggregate.syncCommitteeBits.intersectValues(committeeIndices);
}
@@ -115,7 +116,9 @@ export function getSyncCommitteeSignatureSet(
throw Error("Empty sync committee signature is not infinity");
}
const domain = config.getDomain(state.slot, DOMAIN_SYNC_COMMITTEE, previousSlot);
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const domain = config.getDomain(block.slot, DOMAIN_SYNC_COMMITTEE, previousSlot);
return {
type: SignatureSetType.aggregate,

View File

@@ -76,7 +76,7 @@ export function getVoluntaryExitValidity(
if (
verifySignature &&
!verifyVoluntaryExitSignature(state.config, epochCtx.index2pubkey, state, signedVoluntaryExit)
!verifyVoluntaryExitSignature(state.config, epochCtx.index2pubkey, state.slot, signedVoluntaryExit)
) {
return VoluntaryExitValidity.invalidSignature;
}

View File

@@ -1,19 +1,20 @@
import {BeaconConfig} from "@lodestar/config";
import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params";
import {AttesterSlashing, IndexedAttestationBigint, SignedBeaconBlock, ssz} from "@lodestar/types";
import {AttesterSlashing, IndexedAttestationBigint, SignedBeaconBlock, Slot, ssz} from "@lodestar/types";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {ISignatureSet, SignatureSetType, computeSigningRoot, computeStartSlotAtEpoch} from "../util/index.js";
/** Get signature sets from all AttesterSlashing objects in a block */
export function getAttesterSlashingsSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock
): ISignatureSet[] {
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const blockSlot = signedBlock.message.slot;
return signedBlock.message.body.attesterSlashings.flatMap((attesterSlashing) =>
getAttesterSlashingSignatureSets(config, index2pubkey, state, attesterSlashing)
getAttesterSlashingSignatureSets(config, index2pubkey, blockSlot, attesterSlashing)
);
}
@@ -21,22 +22,22 @@ export function getAttesterSlashingsSignatureSets(
export function getAttesterSlashingSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
attesterSlashing: AttesterSlashing
): ISignatureSet[] {
return [attesterSlashing.attestation1, attesterSlashing.attestation2].map((attestation) =>
getIndexedAttestationBigintSignatureSet(config, index2pubkey, state, attestation)
getIndexedAttestationBigintSignatureSet(config, index2pubkey, stateSlot, attestation)
);
}
export function getIndexedAttestationBigintSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
indexedAttestation: IndexedAttestationBigint
): ISignatureSet {
const slot = computeStartSlotAtEpoch(Number(indexedAttestation.data.target.epoch as bigint));
const domain = config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot);
const messageSlot = computeStartSlotAtEpoch(Number(indexedAttestation.data.target.epoch as bigint));
const domain = config.getDomain(stateSlot, DOMAIN_BEACON_ATTESTER, messageSlot);
return {
type: SignatureSetType.aggregate,

View File

@@ -3,7 +3,7 @@ import {ForkSeq} from "@lodestar/params";
import {IndexedAttestation, SignedBeaconBlock, altair, capella} from "@lodestar/types";
import {getSyncCommitteeSignatureSet} from "../block/processSyncCommittee.js";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../types.js";
import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js";
import {ISignatureSet} from "../util/index.js";
import {getAttesterSlashingsSignatureSets} from "./attesterSlashings.js";
import {getBlsToExecutionChangeSignatureSets} from "./blsToExecutionChange.js";
@@ -31,7 +31,7 @@ export * from "./voluntaryExits.js";
export function getBlockSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
currentSyncCommitteeIndexed: SyncCommitteeCache,
signedBlock: SignedBeaconBlock,
indexedAttestations: IndexedAttestation[],
opts?: {
@@ -40,18 +40,18 @@ export function getBlockSignatureSets(
}
): ISignatureSet[] {
// fork based validations
const fork = state.config.getForkSeq(signedBlock.message.slot);
const fork = config.getForkSeq(signedBlock.message.slot);
const signatureSets = [
getRandaoRevealSignatureSet(config, index2pubkey, state, signedBlock.message),
...getProposerSlashingsSignatureSets(config, index2pubkey, state, signedBlock),
...getAttesterSlashingsSignatureSets(config, index2pubkey, state, signedBlock),
...getAttestationsSignatureSets(config, index2pubkey, state, signedBlock, indexedAttestations),
...getVoluntaryExitsSignatureSets(config, index2pubkey, state, signedBlock),
getRandaoRevealSignatureSet(config, index2pubkey, signedBlock.message),
...getProposerSlashingsSignatureSets(config, index2pubkey, signedBlock),
...getAttesterSlashingsSignatureSets(config, index2pubkey, signedBlock),
...getAttestationsSignatureSets(config, index2pubkey, signedBlock, indexedAttestations),
...getVoluntaryExitsSignatureSets(config, index2pubkey, signedBlock),
];
if (!opts?.skipProposerSignature) {
signatureSets.push(getBlockProposerSignatureSet(config, index2pubkey, state, signedBlock));
signatureSets.push(getBlockProposerSignatureSet(config, index2pubkey, signedBlock));
}
// Only after altair fork, validate tSyncCommitteeSignature
@@ -59,7 +59,7 @@ export function getBlockSignatureSets(
const syncCommitteeSignatureSet = getSyncCommitteeSignatureSet(
config,
index2pubkey,
state as CachedBeaconStateAltair,
currentSyncCommitteeIndexed,
(signedBlock as altair.SignedBeaconBlock).message
);
// There may be no participants in this syncCommitteeSignature, so it must not be validated
@@ -71,7 +71,7 @@ export function getBlockSignatureSets(
// only after capella fork
if (fork >= ForkSeq.capella) {
const blsToExecutionChangeSignatureSets = getBlsToExecutionChangeSignatureSets(
state.config,
config,
signedBlock as capella.SignedBeaconBlock
);
if (blsToExecutionChangeSignatureSets.length > 0) {

View File

@@ -1,8 +1,7 @@
import {BeaconConfig} from "@lodestar/config";
import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params";
import {IndexedAttestation, SignedBeaconBlock, phase0, ssz} from "@lodestar/types";
import {IndexedAttestation, SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {
ISignatureSet,
computeSigningRoot,
@@ -12,11 +11,11 @@ import {
export function getAttestationDataSigningRoot(
config: BeaconConfig,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
data: phase0.AttestationData
): Uint8Array {
const slot = computeStartSlotAtEpoch(data.target.epoch);
const domain = config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot);
const messageSlot = computeStartSlotAtEpoch(data.target.epoch);
const domain = config.getDomain(stateSlot, DOMAIN_BEACON_ATTESTER, messageSlot);
return computeSigningRoot(ssz.phase0.AttestationData, data, domain);
}
@@ -24,13 +23,13 @@ export function getAttestationDataSigningRoot(
export function getAttestationWithIndicesSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
attestation: Pick<phase0.Attestation, "data" | "signature">,
attestingIndices: number[]
): ISignatureSet {
return createAggregateSignatureSetFromComponents(
attestingIndices.map((i) => index2pubkey[i]),
getAttestationDataSigningRoot(config, state, attestation.data),
getAttestationDataSigningRoot(config, stateSlot, attestation.data),
attestation.signature
);
}
@@ -38,13 +37,13 @@ export function getAttestationWithIndicesSignatureSet(
export function getIndexedAttestationSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
indexedAttestation: IndexedAttestation
): ISignatureSet {
return getAttestationWithIndicesSignatureSet(
config,
index2pubkey,
state,
stateSlot,
indexedAttestation,
indexedAttestation.attestingIndices
);
@@ -53,7 +52,6 @@ export function getIndexedAttestationSignatureSet(
export function getAttestationsSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock,
indexedAttestations: IndexedAttestation[]
): ISignatureSet[] {
@@ -62,7 +60,10 @@ export function getAttestationsSignatureSets(
`Indexed attestations length mismatch: got ${indexedAttestations.length}, expected ${signedBlock.message.body.attestations.length}`
);
}
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const blockSlot = signedBlock.message.slot;
return indexedAttestations.map((indexedAttestation) =>
getIndexedAttestationSignatureSet(config, index2pubkey, state, indexedAttestation)
getIndexedAttestationSignatureSet(config, index2pubkey, blockSlot, indexedAttestation)
);
}

View File

@@ -9,20 +9,21 @@ import {ISignatureSet, SignatureSetType, verifySignatureSet} from "../util/signa
export function verifyProposerSignature(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock
): boolean {
const signatureSet = getBlockProposerSignatureSet(config, index2pubkey, state, signedBlock);
const signatureSet = getBlockProposerSignatureSet(config, index2pubkey, signedBlock);
return verifySignatureSet(signatureSet);
}
export function getBlockProposerSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock
): ISignatureSet {
const domain = config.getDomain(state.slot, DOMAIN_BEACON_PROPOSER, signedBlock.message.slot);
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const blockSlot = signedBlock.message.slot;
const domain = config.getDomain(blockSlot, DOMAIN_BEACON_PROPOSER, blockSlot);
const blockType = isBlindedBeaconBlock(signedBlock.message)
? config.getPostBellatrixForkTypes(signedBlock.message.slot).BlindedBeaconBlock

View File

@@ -1,8 +1,7 @@
import {BeaconConfig} from "@lodestar/config";
import {DOMAIN_BEACON_PROPOSER} from "@lodestar/params";
import {SignedBeaconBlock, phase0, ssz} from "@lodestar/types";
import {SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {ISignatureSet, SignatureSetType, computeSigningRoot} from "../util/index.js";
/**
@@ -11,7 +10,7 @@ import {ISignatureSet, SignatureSetType, computeSigningRoot} from "../util/index
export function getProposerSlashingSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
proposerSlashing: phase0.ProposerSlashing
): ISignatureSet[] {
const pubkey = index2pubkey[proposerSlashing.signedHeader1.message.proposerIndex];
@@ -19,7 +18,7 @@ export function getProposerSlashingSignatureSets(
// In state transition, ProposerSlashing headers are only partially validated. Their slot could be higher than the
// clock and the slashing would still be valid. Must use bigint variants to hash correctly to all possible values
return [proposerSlashing.signedHeader1, proposerSlashing.signedHeader2].map((signedHeader): ISignatureSet => {
const domain = config.getDomain(state.slot, DOMAIN_BEACON_PROPOSER, Number(signedHeader.message.slot as bigint));
const domain = config.getDomain(stateSlot, DOMAIN_BEACON_PROPOSER, Number(signedHeader.message.slot as bigint));
return {
type: SignatureSetType.single,
@@ -33,10 +32,12 @@ export function getProposerSlashingSignatureSets(
export function getProposerSlashingsSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock
): ISignatureSet[] {
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const blockSlot = signedBlock.message.slot;
return signedBlock.message.body.proposerSlashings.flatMap((proposerSlashing) =>
getProposerSlashingSignatureSets(config, index2pubkey, state, proposerSlashing)
getProposerSlashingSignatureSets(config, index2pubkey, blockSlot, proposerSlashing)
);
}

View File

@@ -2,7 +2,6 @@ import {BeaconConfig} from "@lodestar/config";
import {DOMAIN_RANDAO} from "@lodestar/params";
import {BeaconBlock, ssz} from "@lodestar/types";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {
ISignatureSet,
SignatureSetType,
@@ -14,10 +13,9 @@ import {
export function verifyRandaoSignature(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
block: BeaconBlock
): boolean {
return verifySignatureSet(getRandaoRevealSignatureSet(config, index2pubkey, state, block));
return verifySignatureSet(getRandaoRevealSignatureSet(config, index2pubkey, block));
}
/**
@@ -26,12 +24,13 @@ export function verifyRandaoSignature(
export function getRandaoRevealSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
block: BeaconBlock
): ISignatureSet {
// should not get epoch from epochCtx
const epoch = computeEpochAtSlot(block.slot);
const domain = config.getDomain(state.slot, DOMAIN_RANDAO, block.slot);
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const domain = config.getDomain(block.slot, DOMAIN_RANDAO, block.slot);
return {
type: SignatureSetType.single,

View File

@@ -1,7 +1,6 @@
import {BeaconConfig} from "@lodestar/config";
import {SignedBeaconBlock, phase0, ssz} from "@lodestar/types";
import {SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types";
import {Index2PubkeyCache} from "../cache/pubkeyCache.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {
ISignatureSet,
SignatureSetType,
@@ -13,10 +12,10 @@ import {
export function verifyVoluntaryExitSignature(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
signedVoluntaryExit: phase0.SignedVoluntaryExit
): boolean {
return verifySignatureSet(getVoluntaryExitSignatureSet(config, index2pubkey, state, signedVoluntaryExit));
return verifySignatureSet(getVoluntaryExitSignatureSet(config, index2pubkey, stateSlot, signedVoluntaryExit));
}
/**
@@ -25,11 +24,11 @@ export function verifyVoluntaryExitSignature(
export function getVoluntaryExitSignatureSet(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
stateSlot: Slot,
signedVoluntaryExit: phase0.SignedVoluntaryExit
): ISignatureSet {
const slot = computeStartSlotAtEpoch(signedVoluntaryExit.message.epoch);
const domain = config.getDomainForVoluntaryExit(state.slot, slot);
const messageSlot = computeStartSlotAtEpoch(signedVoluntaryExit.message.epoch);
const domain = config.getDomainForVoluntaryExit(stateSlot, messageSlot);
return {
type: SignatureSetType.single,
@@ -42,10 +41,12 @@ export function getVoluntaryExitSignatureSet(
export function getVoluntaryExitsSignatureSets(
config: BeaconConfig,
index2pubkey: Index2PubkeyCache,
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock
): ISignatureSet[] {
// the getDomain() api requires the state slot as 1st param, however it's the same to block.slot in state-transition
// and the same epoch when we verify blocks in batch in beacon-node. So we can safely use block.slot here.
const blockSlot = signedBlock.message.slot;
return signedBlock.message.body.voluntaryExits.map((voluntaryExit) =>
getVoluntaryExitSignatureSet(config, index2pubkey, state, voluntaryExit)
getVoluntaryExitSignatureSet(config, index2pubkey, blockSlot, voluntaryExit)
);
}

View File

@@ -111,10 +111,7 @@ export function stateTransition(
postState = processSlotsWithTransientCache(postState, blockSlot, options, {metrics, validatorMonitor});
// Verify proposer signature only
if (
verifyProposer &&
!verifyProposerSignature(postState.config, postState.epochCtx.index2pubkey, postState, signedBlock)
) {
if (verifyProposer && !verifyProposerSignature(postState.config, postState.epochCtx.index2pubkey, signedBlock)) {
throw new Error("Invalid block signature");
}

View File

@@ -73,7 +73,7 @@ describe("signatureSets", () => {
const signatureSets = getBlockSignatureSets(
state.config,
state.epochCtx.index2pubkey,
state,
state.epochCtx.currentSyncCommitteeIndexed,
signedBlock,
indexedAttestations
);

View File

@@ -469,20 +469,33 @@ export class AttestationDutiesService {
const res = await this.api.validator.submitBeaconCommitteeSelections({selections: partialSelections});
const combinedSelections = res.value();
this.logger.debug("Received combined beacon committee selection proofs", {epoch, count: combinedSelections.length});
const combinedSelections = new Map<ValidatorIndex, routes.validator.BeaconCommitteeSelection>();
for (const selection of res.value()) {
combinedSelections.set(selection.validatorIndex, selection);
}
this.logger.debug("Received combined beacon committee selection proofs", {epoch, count: combinedSelections.size});
for (const dutyAndProof of duties) {
const {slot, validatorIndex, committeeIndex, committeeLength} = dutyAndProof.duty;
const logCtxValidator = {slot, index: committeeIndex, validatorIndex};
const combinedSelection = combinedSelections.find((s) => s.validatorIndex === validatorIndex && s.slot === slot);
const combinedSelection = combinedSelections.get(validatorIndex);
if (!combinedSelection) {
this.logger.debug("Did not receive combined beacon committee selection proof", logCtxValidator);
continue;
}
if (combinedSelection.slot !== slot) {
this.logger.debug("Received combined beacon committee selection proof for different slot", {
expected: slot,
actual: combinedSelection.slot,
index: committeeIndex,
validatorIndex,
});
continue;
}
const isAggregator = isAggregatorFromCommitteeLength(committeeLength, combinedSelection.selectionProof);
if (isAggregator) {