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

Apply peer action when there are gossip validation errors #3781

Merged
merged 4 commits into from
Feb 25, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 6 additions & 1 deletion packages/lodestar/src/chain/errors/gossipValidation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import {LodestarError} from "@chainsafe/lodestar-utils";
import {PeerAction} from "../../network";

export enum GossipAction {
IGNORE = "IGNORE",
REJECT = "REJECT",
}

export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
/** The action at gossipsub side */
action: GossipAction;
/** The action at node side */
lodestarAction: PeerAction | null;

constructor(action: GossipAction, type: T) {
constructor(action: GossipAction, lodestarAction: PeerAction | null, type: T) {
super(type);
this.action = action;
this.lodestarAction = lodestarAction;
}
}
27 changes: 19 additions & 8 deletions packages/lodestar/src/chain/validation/aggregateAndProof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from ".
import {AttestationError, AttestationErrorCode, GossipAction} from "../errors";
import {getCommitteeIndices, verifyHeadBlockAndTargetRoot, verifyPropagationSlotRange} from "./attestation";
import {RegenCaller} from "../regen";
import {PeerAction} from "../../network/peers";

export async function validateGossipAggregateAndProof(
chain: IBeaconChain,
Expand All @@ -34,7 +35,9 @@ export async function validateGossipAggregateAndProof(

// [REJECT] The attestation's epoch matches its target -- i.e. attestation.data.target.epoch == compute_epoch_at_slot(attestation.data.slot)
if (targetEpoch !== attEpoch) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.BAD_TARGET_EPOCH});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.BAD_TARGET_EPOCH,
});
}

// [IGNORE] aggregate.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
Expand All @@ -46,7 +49,7 @@ export async function validateGossipAggregateAndProof(
// index aggregate_and_proof.aggregator_index for the epoch aggregate.data.target.epoch.
const aggregatorIndex = aggregateAndProof.aggregatorIndex;
if (chain.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, null, {
code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN,
targetEpoch,
aggregatorIndex,
Expand All @@ -64,7 +67,7 @@ export async function validateGossipAggregateAndProof(
const attHeadState = await chain.regen
.getState(attHeadBlock.stateRoot, RegenCaller.validateGossipAggregateAndProof)
.catch((e: Error) => {
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, null, {
code: AttestationErrorCode.MISSING_ATTESTATION_HEAD_STATE,
error: e as Error,
});
Expand All @@ -83,19 +86,25 @@ export async function validateGossipAggregateAndProof(
// len(get_attesting_indices(state, aggregate.data, aggregate.aggregation_bits)) >= 1.
if (attestingIndices.length < 1) {
// missing attestation participants
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.EMPTY_AGGREGATION_BITFIELD});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.EMPTY_AGGREGATION_BITFIELD,
});
}

// [REJECT] aggregate_and_proof.selection_proof selects the validator as an aggregator for the slot
// -- i.e. is_aggregator(state, aggregate.data.slot, aggregate.data.index, aggregate_and_proof.selection_proof) returns True.
if (!isAggregatorFromCommitteeLength(committeeIndices.length, aggregateAndProof.selectionProof)) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_AGGREGATOR});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.INVALID_AGGREGATOR,
});
}

// [REJECT] The aggregator's validator index is within the committee
// -- i.e. aggregate_and_proof.aggregator_index in get_beacon_committee(state, aggregate.data.slot, aggregate.data.index).
if (!committeeIndices.includes(aggregateAndProof.aggregatorIndex)) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.AGGREGATOR_NOT_IN_COMMITTEE});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.AGGREGATOR_NOT_IN_COMMITTEE,
});
}

// [REJECT] The aggregate_and_proof.selection_proof is a valid signature of the aggregate.data.slot
Expand All @@ -109,14 +118,16 @@ export async function validateGossipAggregateAndProof(
allForks.getIndexedAttestationSignatureSet(attHeadState, indexedAttestation),
];
if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true}))) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SIGNATURE});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.INVALID_SIGNATURE,
});
}

// It's important to double check that the attestation still hasn't been observed, since
// there can be a race-condition if we receive two attestations at the same time and
// process them in different threads.
if (chain.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, null, {
code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN,
targetEpoch,
aggregatorIndex,
Expand Down
35 changes: 21 additions & 14 deletions packages/lodestar/src/chain/validation/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {IBeaconChain} from "..";
import {AttestationError, AttestationErrorCode, GossipAction} from "../errors";
import {MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC} from "../../constants";
import {RegenCaller} from "../regen";
import {PeerAction} from "../../network";

const {getIndexedAttestationSignatureSet} = allForks;

Expand All @@ -41,7 +42,7 @@ export async function validateGossipAttestation(

// [REJECT] The attestation's epoch matches its target -- i.e. attestation.data.target.epoch == compute_epoch_at_slot(attestation.data.slot)
if (targetEpoch !== attEpoch) {
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.BAD_TARGET_EPOCH,
});
}
Expand All @@ -60,7 +61,9 @@ export async function validateGossipAttestation(
bitIndex = getSingleBitIndex(aggregationBits);
} catch (e) {
if (e instanceof AggregationBitsError && e.type.code === AggregationBitsErrorCode.NOT_EXACTLY_ONE_BIT_SET) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET,
});
} else {
throw e;
}
Expand Down Expand Up @@ -89,7 +92,7 @@ export async function validateGossipAttestation(
const attHeadState = await chain.regen
.getState(attHeadBlock.stateRoot, RegenCaller.validateGossipAttestation)
.catch((e: Error) => {
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, null, {
code: AttestationErrorCode.MISSING_ATTESTATION_HEAD_STATE,
error: e as Error,
});
Expand All @@ -105,7 +108,9 @@ export async function validateGossipAttestation(
// -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)).
// > TODO: Is this necessary? Lighthouse does not do this check
if (aggregationBits.length !== committeeIndices.length) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS,
});
}

// LH > verify_middle_checks
Expand All @@ -119,7 +124,7 @@ export async function validateGossipAttestation(
// which may be pre-computed along with the committee information for the signature check.
const expectedSubnet = computeSubnetForSlot(attHeadState, attSlot, attIndex);
if (subnet !== null && subnet !== expectedSubnet) {
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.INVALID_SUBNET_ID,
received: subnet,
expected: expectedSubnet,
Expand All @@ -129,7 +134,7 @@ export async function validateGossipAttestation(
// [IGNORE] There has been no other valid attestation seen on an attestation subnet that has an
// identical attestation.data.target.epoch and participating validator index.
if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, null, {
code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN,
targetEpoch,
validatorIndex,
Expand All @@ -144,7 +149,9 @@ export async function validateGossipAttestation(
};
const signatureSet = getIndexedAttestationSignatureSet(attHeadState, indexedAttestation);
if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true}))) {
throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SIGNATURE});
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.INVALID_SIGNATURE,
});
}

// Now that the attestation has been fully verified, store that we have received a valid attestation from this validator.
Expand All @@ -153,7 +160,7 @@ export async function validateGossipAttestation(
// there can be a race-condition if we receive two attestations at the same time and
// process them in different threads.
if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, null, {
code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN,
targetEpoch,
validatorIndex,
Expand Down Expand Up @@ -182,14 +189,14 @@ export function verifyPropagationSlotRange(chain: IBeaconChain, attestationSlot:
0
);
if (attestationSlot < earliestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, PeerAction.LowToleranceError, {
code: AttestationErrorCode.PAST_SLOT,
earliestPermissibleSlot,
attestationSlot,
});
}
if (attestationSlot > latestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, PeerAction.LowToleranceError, {
code: AttestationErrorCode.FUTURE_SLOT,
latestPermissibleSlot,
attestationSlot,
Expand Down Expand Up @@ -230,7 +237,7 @@ function verifyHeadBlockIsKnown(chain: IBeaconChain, beaconBlockRoot: Root): IPr

const headBlock = chain.forkChoice.getBlock(beaconBlockRoot);
if (headBlock === null) {
throw new AttestationError(GossipAction.IGNORE, {
throw new AttestationError(GossipAction.IGNORE, null, {
code: AttestationErrorCode.UNKNOWN_BEACON_BLOCK_ROOT,
root: toHexString(beaconBlockRoot.valueOf() as typeof beaconBlockRoot),
});
Expand Down Expand Up @@ -258,7 +265,7 @@ function verifyAttestationTargetRoot(headBlock: IProtoBlock, targetRoot: Root, a
//
// Reference:
// https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.INVALID_TARGET_ROOT,
targetRoot: toHexString(targetRoot),
expected: null,
Expand All @@ -278,7 +285,7 @@ function verifyAttestationTargetRoot(headBlock: IProtoBlock, targetRoot: Root, a
// TODO: Do a fast comparision to convert and compare byte by byte
if (expectedTargetRoot !== toHexString(targetRoot)) {
// Reject any attestation with an invalid target root.
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.INVALID_TARGET_ROOT,
targetRoot: toHexString(targetRoot),
expected: expectedTargetRoot,
Expand All @@ -296,7 +303,7 @@ export function getCommitteeIndices(
const slotCommittees = committees[attestationSlot % SLOTS_PER_EPOCH];

if (attestationIndex >= slotCommittees.length) {
throw new AttestationError(GossipAction.REJECT, {
throw new AttestationError(GossipAction.REJECT, PeerAction.LowToleranceError, {
code: AttestationErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
index: attestationIndex,
});
Expand Down
7 changes: 4 additions & 3 deletions packages/lodestar/src/chain/validation/attesterSlashing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {phase0, allForks, getAttesterSlashableIndices} from "@chainsafe/lodestar-beacon-state-transition";
import {IBeaconChain} from "..";
import {PeerAction} from "../../network/peers";
import {AttesterSlashingError, AttesterSlashingErrorCode, GossipAction} from "../errors";

export async function validateGossipAttesterSlashing(
Expand All @@ -12,7 +13,7 @@ export async function validateGossipAttesterSlashing(
// ), verify if any(attester_slashed_indices.difference(prior_seen_attester_slashed_indices))).
const intersectingIndices = getAttesterSlashableIndices(attesterSlashing);
if (chain.opPool.hasSeenAttesterSlashing(intersectingIndices)) {
throw new AttesterSlashingError(GossipAction.IGNORE, {
throw new AttesterSlashingError(GossipAction.IGNORE, null, {
code: AttesterSlashingErrorCode.ALREADY_EXISTS,
});
}
Expand All @@ -24,15 +25,15 @@ export async function validateGossipAttesterSlashing(
// verifySignature = false, verified in batch below
allForks.assertValidAttesterSlashing(state, attesterSlashing, false);
} catch (e) {
throw new AttesterSlashingError(GossipAction.REJECT, {
throw new AttesterSlashingError(GossipAction.REJECT, PeerAction.HighToleranceError, {
code: AttesterSlashingErrorCode.INVALID,
error: e as Error,
});
}

const signatureSets = allForks.getAttesterSlashingSignatureSets(state, attesterSlashing);
if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true}))) {
throw new AttesterSlashingError(GossipAction.REJECT, {
throw new AttesterSlashingError(GossipAction.REJECT, PeerAction.HighToleranceError, {
code: AttesterSlashingErrorCode.INVALID,
error: Error("Invalid signature"),
});
Expand Down
Loading