@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
201 lines (174 loc) • 5.36 kB
text/typescript
import {GoodByeReasonCode} from "../../../constants/network.js";
import {
COOL_DOWN_BEFORE_DECAY_MS,
DEFAULT_SCORE,
GOSSIPSUB_NEGATIVE_SCORE_WEIGHT,
GOSSIPSUB_POSITIVE_SCORE_WEIGHT,
HALFLIFE_DECAY_MS,
MAX_SCORE,
MIN_LODESTAR_SCORE_BEFORE_BAN,
MIN_SCORE,
NO_COOL_DOWN_APPLIED,
} from "./constants.js";
import {IPeerScore, PeerScoreStat, ScoreState} from "./interface.js";
import {scoreToState} from "./utils.js";
/**
* Manage score of a peer.
*/
export class RealScore implements IPeerScore {
private lodestarScore: number;
private gossipScore: number;
private ignoreNegativeGossipScore: boolean;
/** The final score, computed from the above */
private score: number;
private lastUpdate: number;
constructor() {
this.lodestarScore = DEFAULT_SCORE;
this.gossipScore = DEFAULT_SCORE;
this.score = DEFAULT_SCORE;
this.ignoreNegativeGossipScore = false;
this.lastUpdate = Date.now();
}
isCoolingDown(): boolean {
return Date.now() < this.lastUpdate;
}
getScore(): number {
return this.score;
}
getGossipScore(): number {
return this.gossipScore;
}
add(scoreDelta: number): number {
let newScore = this.lodestarScore + scoreDelta;
if (newScore > MAX_SCORE) newScore = MAX_SCORE;
if (newScore < MIN_SCORE) newScore = MIN_SCORE;
this.setLodestarScore(newScore);
return newScore;
}
applyReconnectionCoolDown(reason: GoodByeReasonCode): number {
let coolDownMin = NO_COOL_DOWN_APPLIED;
switch (reason) {
// let scoring system handle score decay by itself
case GoodByeReasonCode.BANNED:
case GoodByeReasonCode.SCORE_TOO_LOW:
return coolDownMin;
case GoodByeReasonCode.INBOUND_DISCONNECT:
case GoodByeReasonCode.TOO_MANY_PEERS:
coolDownMin = 5;
break;
case GoodByeReasonCode.ERROR:
case GoodByeReasonCode.CLIENT_SHUTDOWN:
coolDownMin = 60;
break;
case GoodByeReasonCode.IRRELEVANT_NETWORK:
coolDownMin = 240;
break;
}
// set banning period to time in ms in the future from now
this.lastUpdate = Date.now() + coolDownMin * 60 * 1000;
return coolDownMin;
}
/**
* Applies time-based logic such as decay rates to the score.
* This function should be called periodically.
*
* Return the new score.
*/
update(): number {
const nowMs = Date.now();
// Decay the current score
// Using exponential decay based on a constant half life.
const sinceLastUpdateMs = nowMs - this.lastUpdate;
// If peer was banned, lastUpdate will be in the future
if (sinceLastUpdateMs > 0) {
this.lastUpdate = nowMs;
// e^(-ln(2)/HL*t)
const decayFactor = Math.exp(HALFLIFE_DECAY_MS * sinceLastUpdateMs);
this.setLodestarScore(this.lodestarScore * decayFactor);
}
return this.lodestarScore;
}
updateGossipsubScore(newScore: number, ignore: boolean): void {
// we only update gossipsub if last_updated is in the past which means either the peer is
// not banned or the BANNED_BEFORE_DECAY time is over.
if (this.lastUpdate <= Date.now()) {
this.gossipScore = newScore;
this.ignoreNegativeGossipScore = ignore;
}
}
getStat(): PeerScoreStat {
return {
lodestarScore: this.lodestarScore,
gossipScore: this.gossipScore,
ignoreNegativeGossipScore: this.ignoreNegativeGossipScore,
score: this.score,
lastUpdate: this.lastUpdate,
};
}
/**
* Updating lodestarScore should always go through this method,
* so that we update this.score accordingly.
*/
private setLodestarScore(newScore: number): void {
this.lodestarScore = newScore;
this.updateState();
}
/**
* Compute the final score, ban peer if needed
*/
private updateState(): void {
const prevState = scoreToState(this.score);
this.recomputeScore();
const newState = scoreToState(this.score);
if (prevState !== ScoreState.Banned && newState === ScoreState.Banned) {
// ban this peer for at least BANNED_BEFORE_DECAY_MS seconds
this.lastUpdate = Date.now() + COOL_DOWN_BEFORE_DECAY_MS;
}
}
/**
* Compute the final score
*/
private recomputeScore(): void {
this.score = this.lodestarScore;
if (this.score <= MIN_LODESTAR_SCORE_BEFORE_BAN) {
// ignore all other scores, i.e. do nothing here
return;
}
if (this.gossipScore >= 0) {
this.score += this.gossipScore * GOSSIPSUB_POSITIVE_SCORE_WEIGHT;
} else if (!this.ignoreNegativeGossipScore) {
this.score += this.gossipScore * GOSSIPSUB_NEGATIVE_SCORE_WEIGHT;
}
}
}
/** An implementation of IPeerScore for testing */
export class MaxScore implements IPeerScore {
getScore(): number {
return MAX_SCORE;
}
getGossipScore(): number {
return DEFAULT_SCORE;
}
isCoolingDown(): boolean {
return false;
}
add(): number {
return DEFAULT_SCORE;
}
update(): number {
return MAX_SCORE;
}
applyReconnectionCoolDown(_reason: GoodByeReasonCode): number {
return NO_COOL_DOWN_APPLIED;
}
updateGossipsubScore(): void {}
getStat(): PeerScoreStat {
return {
lodestarScore: MAX_SCORE,
gossipScore: DEFAULT_SCORE,
ignoreNegativeGossipScore: false,
score: MAX_SCORE,
lastUpdate: Date.now(),
};
}
}