mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-08 23:28:10 -05:00
**Motivation** Use latest `module` and `moduleResolution` for TS. **Description** - To use [subpath imports](https://nodejs.org/api/packages.html#subpath-imports) in the PR #8320 we need to update the module solution strategy for TS. - That requires to change the `module` for the TS as well. - Earlier tried to stay with `node18` or `node20`, but the `ts-node` does not work with that. - Maintaining different tsconfig for ts-node is more of hassle on wrong run. - So decided to stick with `nodenext` strategy for `moduleResolution` **Steps to test or reproduce** Run all tests --------- Co-authored-by: Cayman <caymannava@gmail.com>
348 lines
13 KiB
TypeScript
348 lines
13 KiB
TypeScript
import {fromHexString} from "@chainsafe/ssz";
|
|
import {routes} from "@lodestar/api/beacon";
|
|
import {ChainConfig} from "@lodestar/config";
|
|
import {SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
import {BLSPubkey, Epoch, Slot, phase0, ssz} from "@lodestar/types";
|
|
import {Validator} from "@lodestar/validator";
|
|
import {afterEach, describe, expect, it} from "vitest";
|
|
import {BeaconNode} from "../../../src/node/index.js";
|
|
import {ClockEvent} from "../../../src/util/clock.js";
|
|
import {waitForEvent} from "../../utils/events/resolver.js";
|
|
import {LogLevel, TestLoggerOpts, testLogger} from "../../utils/logger.js";
|
|
import {connect} from "../../utils/network.js";
|
|
import {getDevBeaconNode} from "../../utils/node/beacon.js";
|
|
import {getAndInitDevValidators} from "../../utils/node/validator.js";
|
|
|
|
type PubkeyHex = string;
|
|
|
|
// TODO: Reconsider this tests latter.
|
|
// Doppelganger testing can be split in two items:
|
|
// 1. Can a running beacon node detect liveness of the validator?
|
|
// 2. Does the validator use the liveness data to identify doppelganger correctly?
|
|
//
|
|
// Attempting to do both 1. and 2. in this e2e test more expensive than necessary.
|
|
// Unit tests in the validator cover 2., so some test in lodestar package should cover 1.
|
|
// https://github.com/ChainSafe/lodestar/issues/5967
|
|
describe.skip("doppelganger / doppelganger test", () => {
|
|
const afterEachCallbacks: (() => Promise<unknown> | void)[] = [];
|
|
afterEach(async () => {
|
|
while (afterEachCallbacks.length > 0) {
|
|
const callback = afterEachCallbacks.pop();
|
|
if (callback) await callback();
|
|
}
|
|
});
|
|
|
|
const validatorCount = 1;
|
|
const genesisSlotsDelay = 5;
|
|
const beaconParams: Pick<ChainConfig, "SECONDS_PER_SLOT"> = {
|
|
SECONDS_PER_SLOT: 2,
|
|
};
|
|
|
|
const timeout = (SLOTS_PER_EPOCH + genesisSlotsDelay) * beaconParams.SECONDS_PER_SLOT * 1000;
|
|
|
|
type TestConfig = {
|
|
genesisTime?: number;
|
|
doppelgangerProtection?: boolean;
|
|
};
|
|
|
|
async function createBNAndVC(config?: TestConfig): Promise<{beaconNode: BeaconNode; validators: Validator[]}> {
|
|
const testLoggerOpts: TestLoggerOpts = {level: LogLevel.info};
|
|
const loggerNodeA = testLogger("doppelganger", testLoggerOpts);
|
|
|
|
const bn = await getDevBeaconNode({
|
|
params: beaconParams,
|
|
options: {
|
|
sync: {isSingleNode: true},
|
|
api: {rest: {enabled: false}},
|
|
chain: {blsVerifyAllMainThread: true},
|
|
},
|
|
validatorCount,
|
|
logger: loggerNodeA,
|
|
genesisTime: config?.genesisTime,
|
|
});
|
|
|
|
afterEachCallbacks.push(() => bn.close());
|
|
|
|
const {validators: validatorsWithDoppelganger} = await getAndInitDevValidators({
|
|
node: bn,
|
|
logPrefix: "doppelganger",
|
|
validatorsPerClient: validatorCount,
|
|
validatorClientCount: 1,
|
|
startIndex: 0,
|
|
useRestApi: false,
|
|
testLoggerOpts,
|
|
doppelgangerProtection: config?.doppelgangerProtection,
|
|
});
|
|
afterEachCallbacks.push(() => Promise.all(validatorsWithDoppelganger.map((v) => v.close())));
|
|
|
|
return {beaconNode: bn, validators: validatorsWithDoppelganger};
|
|
}
|
|
|
|
it("should not have doppelganger protection if started before genesis", async () => {
|
|
const committeeIndex = 0;
|
|
const validatorIndex = 0;
|
|
|
|
const {beaconNode: bn, validators: validatorsWithDoppelganger} = await createBNAndVC({
|
|
doppelgangerProtection: true,
|
|
});
|
|
|
|
const validatorUnderTest = validatorsWithDoppelganger[0];
|
|
const pubKey = validatorUnderTest.validatorStore.votingPubkeys()[0];
|
|
const beaconBlock = ssz.phase0.BeaconBlock.defaultValue();
|
|
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signBlock(fromHexString(pubKey), beaconBlock, bn.chain.clock.currentSlot)
|
|
).resolves.toBeWithMessage(
|
|
undefined,
|
|
"Signing should be possible if starting at genesis since doppelganger should be off"
|
|
);
|
|
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signAttestation(
|
|
createAttesterDuty(fromHexString(pubKey), bn.chain.clock.currentSlot, committeeIndex, validatorIndex),
|
|
generateAttestationData(
|
|
bn.chain.clock.currentEpoch - 1,
|
|
bn.chain.clock.currentEpoch,
|
|
committeeIndex,
|
|
bn.chain.clock.currentSlot
|
|
),
|
|
bn.chain.clock.currentEpoch
|
|
)
|
|
).resolves.toBeWithMessage(
|
|
undefined,
|
|
"Signing should be possible if starting at genesis since doppelganger should be off"
|
|
);
|
|
});
|
|
|
|
it("should shut down validator if same key is active and started after genesis", async () => {
|
|
// set genesis time to allow at least an epoch
|
|
const genesisTime = Math.floor(Date.now() / 1000) - SLOTS_PER_EPOCH * beaconParams.SECONDS_PER_SLOT;
|
|
|
|
const {beaconNode: bn, validators: validatorsWithDoppelganger} = await createBNAndVC({
|
|
genesisTime,
|
|
doppelgangerProtection: true,
|
|
});
|
|
|
|
const {beaconNode: bn2, validators} = await createBNAndVC({
|
|
genesisTime: bn.chain.getHeadState().genesisTime,
|
|
});
|
|
|
|
await connect(bn2.network, bn.network);
|
|
|
|
expect(validators[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator without doppelganger protection should be running"
|
|
);
|
|
expect(validatorsWithDoppelganger[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator with doppelganger protection should be running before first epoch"
|
|
);
|
|
await waitForEvent<phase0.Checkpoint>(bn2.chain.clock, ClockEvent.epoch, timeout);
|
|
// After first epoch doppelganger protection should have stopped the validatorsWithDoppelganger
|
|
expect(validators[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator without doppelganger protection should still be running after first epoch"
|
|
);
|
|
const pubkeyOfIndex: PubkeyHex = validatorsWithDoppelganger[0].validatorStore.getPubkeyOfIndex(0) as PubkeyHex;
|
|
expect(validatorsWithDoppelganger[0].validatorStore.isDoppelgangerSafe(pubkeyOfIndex)).toBeWithMessage(
|
|
false,
|
|
"validator with doppelganger protection should be stopped after first epoch"
|
|
);
|
|
});
|
|
|
|
it("should shut down validator if same key is active with same BN and started after genesis", async () => {
|
|
const doppelgangerProtection = true;
|
|
const testLoggerOpts: TestLoggerOpts = {level: LogLevel.info};
|
|
|
|
// set genesis time to allow at least an epoch
|
|
const genesisTime = Math.floor(Date.now() / 1000) - SLOTS_PER_EPOCH * beaconParams.SECONDS_PER_SLOT;
|
|
|
|
const {beaconNode: bn, validators: validator0WithDoppelganger} = await createBNAndVC({
|
|
genesisTime,
|
|
doppelgangerProtection,
|
|
});
|
|
|
|
const {validators: validator0WithoutDoppelganger} = await getAndInitDevValidators({
|
|
logPrefix: "doppelganger2",
|
|
node: bn,
|
|
validatorsPerClient: validatorCount,
|
|
validatorClientCount: 1,
|
|
startIndex: 0,
|
|
useRestApi: false,
|
|
testLoggerOpts,
|
|
doppelgangerProtection: false,
|
|
});
|
|
afterEachCallbacks.push(() => Promise.all(validator0WithoutDoppelganger.map((v) => v.close())));
|
|
|
|
expect(validator0WithDoppelganger[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator with doppelganger protection should be running"
|
|
);
|
|
expect(validator0WithoutDoppelganger[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator without doppelganger protection should be running before first epoch"
|
|
);
|
|
await waitForEvent<phase0.Checkpoint>(bn.chain.clock, ClockEvent.epoch, timeout);
|
|
//After first epoch doppelganger protection should have stopped the validator0WithDoppelganger
|
|
expect(validator0WithoutDoppelganger[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator without doppelganger protection should still be running after first epoch"
|
|
);
|
|
const pubkeyOfIndex: PubkeyHex = validator0WithDoppelganger[0].validatorStore.getPubkeyOfIndex(0) as PubkeyHex;
|
|
expect(validator0WithDoppelganger[0].validatorStore.isDoppelgangerSafe(pubkeyOfIndex)).toBeWithMessage(
|
|
false,
|
|
"validator with doppelganger protection should be stopped after first epoch"
|
|
);
|
|
});
|
|
|
|
it("should not shut down validator if key is different", async () => {
|
|
const doppelgangerProtection = true;
|
|
|
|
const {beaconNode: bn, validators: validatorsWithDoppelganger} = await createBNAndVC({
|
|
doppelgangerProtection,
|
|
});
|
|
|
|
const {beaconNode: bn2, validators} = await createBNAndVC({
|
|
genesisTime: bn.chain.getHeadState().genesisTime,
|
|
doppelgangerProtection: false,
|
|
});
|
|
|
|
await connect(bn2.network, bn.network);
|
|
|
|
expect(validators[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator without doppelganger protection should be running"
|
|
);
|
|
expect(validatorsWithDoppelganger[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator with doppelganger protection should be running before first epoch"
|
|
);
|
|
await waitForEvent<phase0.Checkpoint>(bn2.chain.clock, ClockEvent.epoch, timeout);
|
|
expect(validators[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator without doppelganger protection should still be running after first epoch"
|
|
);
|
|
expect(validatorsWithDoppelganger[0].isRunning).toBeWithMessage(
|
|
true,
|
|
"validator with doppelganger protection should still be active after first epoch"
|
|
);
|
|
});
|
|
|
|
it("should not sign block if doppelganger period has not passed and not started at genesis", async () => {
|
|
const doppelgangerProtection = true;
|
|
|
|
// set genesis time to allow at least an epoch
|
|
const genesisTime = Math.floor(Date.now() / 1000) - SLOTS_PER_EPOCH * beaconParams.SECONDS_PER_SLOT;
|
|
|
|
const {beaconNode: bn, validators: validatorsWithDoppelganger} = await createBNAndVC({
|
|
genesisTime,
|
|
doppelgangerProtection,
|
|
});
|
|
|
|
const validatorUnderTest = validatorsWithDoppelganger[0];
|
|
const pubKey = validatorUnderTest.validatorStore.votingPubkeys()[0];
|
|
const beaconBlock = ssz.phase0.BeaconBlock.defaultValue();
|
|
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signBlock(fromHexString(pubKey), beaconBlock, bn.chain.clock.currentSlot)
|
|
).rejects.toThrow(`Doppelganger state for key ${pubKey} is not safe`);
|
|
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signBlock(fromHexString(pubKey), beaconBlock, bn.chain.clock.currentSlot)
|
|
).rejects.toThrow(`Doppelganger state for key ${pubKey} is not safe`);
|
|
|
|
await waitForEvent<phase0.Checkpoint>(bn.chain.clock, ClockEvent.epoch, timeout);
|
|
|
|
// Signing should be possible after doppelganger check has elapsed
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signBlock(fromHexString(pubKey), beaconBlock, bn.chain.clock.currentSlot)
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("should not sign attestations if doppelganger period has not passed and started after genesis", async () => {
|
|
const doppelgangerProtection = true;
|
|
|
|
// set genesis time to allow at least an epoch
|
|
const genesisTime = Math.floor(Date.now() / 1000) - SLOTS_PER_EPOCH * beaconParams.SECONDS_PER_SLOT;
|
|
|
|
const {beaconNode: bn, validators: validatorsWithDoppelganger} = await createBNAndVC({
|
|
genesisTime,
|
|
doppelgangerProtection,
|
|
});
|
|
|
|
const validatorUnderTest = validatorsWithDoppelganger[0];
|
|
const pubKey = validatorUnderTest.validatorStore.votingPubkeys()[0];
|
|
const committeeIndex = 0;
|
|
const validatorIndex = 0;
|
|
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signAttestation(
|
|
createAttesterDuty(fromHexString(pubKey), bn.chain.clock.currentSlot, committeeIndex, validatorIndex),
|
|
generateAttestationData(
|
|
bn.chain.clock.currentEpoch,
|
|
bn.chain.clock.currentEpoch,
|
|
committeeIndex,
|
|
bn.chain.clock.currentSlot
|
|
),
|
|
bn.chain.clock.currentEpoch
|
|
)
|
|
).rejects.toThrow(`Doppelganger state for key ${pubKey} is not safe`);
|
|
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signAttestation(
|
|
createAttesterDuty(fromHexString(pubKey), bn.chain.clock.currentSlot, committeeIndex, validatorIndex),
|
|
generateAttestationData(bn.chain.clock.currentSlot, bn.chain.clock.currentEpoch),
|
|
bn.chain.clock.currentEpoch
|
|
)
|
|
).rejects.toThrow(`Doppelganger state for key ${pubKey} is not safe`);
|
|
|
|
await waitForEvent<phase0.Checkpoint>(bn.chain.clock, ClockEvent.epoch, timeout);
|
|
|
|
// Signing should be possible after doppelganger check has elapsed
|
|
await expect(
|
|
validatorUnderTest.validatorStore.signAttestation(
|
|
createAttesterDuty(fromHexString(pubKey), bn.chain.clock.currentSlot, committeeIndex, validatorIndex),
|
|
generateAttestationData(
|
|
bn.chain.clock.currentEpoch - 1,
|
|
bn.chain.clock.currentEpoch,
|
|
committeeIndex,
|
|
bn.chain.clock.currentSlot
|
|
),
|
|
bn.chain.clock.currentEpoch
|
|
)
|
|
).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
function createAttesterDuty(
|
|
pubkey: BLSPubkey,
|
|
currentSlot: Slot,
|
|
committeeIndex: number,
|
|
validatorIndex: number
|
|
): routes.validator.AttesterDuty {
|
|
return {
|
|
pubkey,
|
|
validatorIndex,
|
|
committeeIndex: committeeIndex,
|
|
committeeLength: 1,
|
|
committeesAtSlot: 1,
|
|
validatorCommitteeIndex: 0,
|
|
slot: currentSlot,
|
|
};
|
|
}
|
|
|
|
function generateAttestationData(
|
|
sourceEpoch: Epoch,
|
|
targetEpoch: Epoch,
|
|
index = 1,
|
|
slot: Slot = 1
|
|
): phase0.AttestationData {
|
|
return {
|
|
slot: slot,
|
|
index: index,
|
|
beaconBlockRoot: Buffer.alloc(32),
|
|
source: {epoch: sourceEpoch, root: Buffer.alloc(32)},
|
|
target: {epoch: targetEpoch, root: Buffer.alloc(32)},
|
|
};
|
|
}
|