diff --git a/apps/1kv-backend-staging/templates/kusama-otv-backend.yaml b/apps/1kv-backend-staging/templates/kusama-otv-backend.yaml index bd807506f..5e8cc3b4a 100644 --- a/apps/1kv-backend-staging/templates/kusama-otv-backend.yaml +++ b/apps/1kv-backend-staging/templates/kusama-otv-backend.yaml @@ -61,6 +61,11 @@ spec: "minSelfStake": 10000000000000, "commission": 150000000, "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": true, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX" ] + }, "forceClientVersion": "v1.10.0" }, "cron": { diff --git a/apps/1kv-backend-staging/templates/polkadot-otv-backend.yaml b/apps/1kv-backend-staging/templates/polkadot-otv-backend.yaml index 9e8225f6d..183e847e3 100644 --- a/apps/1kv-backend-staging/templates/polkadot-otv-backend.yaml +++ b/apps/1kv-backend-staging/templates/polkadot-otv-backend.yaml @@ -60,6 +60,11 @@ spec: "minSelfStake": 50000000000000, "commission": 50000000, "unclaimedEraThreshold": 1, + "sanctionedGeoArea": { + "skip": true, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX" ] + }, "forceClientVersion": "v1.10.0" }, "cron": { diff --git a/apps/1kv-backend/templates/kusama-otv-backend.yaml b/apps/1kv-backend/templates/kusama-otv-backend.yaml index 90e25cc79..712c255ee 100644 --- a/apps/1kv-backend/templates/kusama-otv-backend.yaml +++ b/apps/1kv-backend/templates/kusama-otv-backend.yaml @@ -17,7 +17,7 @@ spec: source: repoURL: https://w3f.github.io/helm-charts/ chart: otv-backend - targetRevision: v3.1.16 + targetRevision: v3.2.0 plugin: env: - name: HELM_VALUES @@ -59,6 +59,11 @@ spec: "minSelfStake": 10000000000000, "commission": 150000000, "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": true, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX" ] + }, "forceClientVersion": "v1.10.0" }, "cron": { diff --git a/apps/1kv-backend/templates/polkadot-otv-backend.yaml b/apps/1kv-backend/templates/polkadot-otv-backend.yaml index 67ce90603..d07e974ac 100644 --- a/apps/1kv-backend/templates/polkadot-otv-backend.yaml +++ b/apps/1kv-backend/templates/polkadot-otv-backend.yaml @@ -17,7 +17,7 @@ spec: source: repoURL: https://w3f.github.io/helm-charts/ chart: otv-backend - targetRevision: v3.1.16 + targetRevision: v3.2.0 plugin: env: - name: HELM_VALUES @@ -58,6 +58,11 @@ spec: "minSelfStake": 50000000000000, "commission": 50000000, "unclaimedEraThreshold": 1, + "sanctionedGeoArea": { + "skip": true, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX" ] + }, "forceClientVersion": "v1.10.0" }, "cron": { diff --git a/charts/otv-backend/Chart.yaml b/charts/otv-backend/Chart.yaml index af51c4177..7de1cb298 100644 --- a/charts/otv-backend/Chart.yaml +++ b/charts/otv-backend/Chart.yaml @@ -1,5 +1,5 @@ description: 1K Validators Backend name: otv-backend -version: v3.1.16 -appVersion: v3.1.16 +version: v3.2.0 +appVersion: v3.2.0 apiVersion: v2 diff --git a/charts/otv-backend/values.yaml b/charts/otv-backend/values.yaml index 102964ca4..861175842 100644 --- a/charts/otv-backend/values.yaml +++ b/charts/otv-backend/values.yaml @@ -48,7 +48,12 @@ config: | }, "constraints": { "skipConnectionTime": false, - "skipIdentity": false + "skipIdentity": false, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX" ] + } }, "db": { "mongo": { diff --git a/packages/common/package.json b/packages/common/package.json index 3a16729db..7d7156b6d 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/common", - "version": "3.1.16", + "version": "3.2.0", "description": "Services for running the Thousand Validator Program.", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index eb8aea873..015a700e4 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -31,6 +31,11 @@ export type ConfigSchema = { minSelfStake: number; commission: number; unclaimedEraThreshold: number; + sanctionedGeoArea?: { + skip: boolean; + sanctionedCountries: string[]; + sanctionedRegions: string[]; + }; }; cron: { monitor: string; diff --git a/packages/common/src/constraints/CheckCandidates.ts b/packages/common/src/constraints/CheckCandidates.ts index 7a8a61a5c..2d1492598 100644 --- a/packages/common/src/constraints/CheckCandidates.ts +++ b/packages/common/src/constraints/CheckCandidates.ts @@ -15,6 +15,7 @@ import { checkSelfStake, checkUnclaimed, checkValidateIntention, + checkSanctionedGeoArea, } from "./ValidityChecks"; import { percentage, timeRemaining } from "../utils/util"; @@ -129,6 +130,12 @@ export const checkCandidate = async ( logger.info(`${candidate.name} beefy keys not valid`, constraintsLabel); } + const sanctionedGeoAreaValid = + constraints.config?.constraints?.sanctionedGeoArea?.skip == true + ? true + : (await checkSanctionedGeoArea(constraints.config, candidate)) || + false; + valid = onlineValid && validateValid && @@ -142,7 +149,8 @@ export const checkCandidate = async ( blockedValid && kusamaValid && providerValid && - beefyValid; + beefyValid && + sanctionedGeoAreaValid; await setValid(candidate, valid); diff --git a/packages/common/src/constraints/ValidityChecks.ts b/packages/common/src/constraints/ValidityChecks.ts index e99d4adc4..969266e2e 100644 --- a/packages/common/src/constraints/ValidityChecks.ts +++ b/packages/common/src/constraints/ValidityChecks.ts @@ -15,6 +15,7 @@ import { setSelfStakeInvalidity, setUnclaimedInvalidity, setValidateIntentionValidity, + setSanctionedGeoAreaValidity, } from "../db"; import { ChainData, Config, Constants, queries, Util } from "../index"; import axios from "axios"; @@ -434,3 +435,61 @@ export const checkBeefyKeys = async ( throw new Error("could not make validity check"); } }; + +// Checks if the candidate is in a sanctioned location +export const checkSanctionedGeoArea = async ( + config: Config.ConfigSchema, + candidate: Candidate, +): Promise => { + try { + if ( + !config.constraints?.sanctionedGeoArea?.sanctionedCountries?.length && + !config.constraints?.sanctionedGeoArea?.sanctionedRegions?.length + ) { + await setSanctionedGeoAreaValidity(candidate, true); + return true; + } + + const location = await queries.getCandidateLocation(candidate.slotId); + if (!location?.region || !location?.country) { + await setSanctionedGeoAreaValidity(candidate, true); + return true; + } + + const sanctionedCountries = config.constraints?.sanctionedGeoArea + ?.sanctionedCountries + ? config.constraints.sanctionedGeoArea.sanctionedCountries.map((x) => + x.trim().toLowerCase(), + ) + : []; + const sanctionedRegions = config.constraints?.sanctionedGeoArea + ?.sanctionedRegions + ? config.constraints.sanctionedGeoArea.sanctionedRegions.map((x) => + x.trim().toLowerCase(), + ) + : []; + + if ( + sanctionedCountries.includes(location.country.trim().toLowerCase()) || + sanctionedRegions.includes(location.region.trim().toLowerCase()) + ) { + logger.info( + `${candidate.name} is in a sanctioned location: Country: ${location.country}, Region: ${location.region}`, + { + label: "Constraints", + }, + ); + await setSanctionedGeoAreaValidity(candidate, false); + return false; + } + + await setSanctionedGeoAreaValidity(candidate, true); + return true; + } catch (e) { + logger.error( + `Error checking location for sanctions: ${e}`, + constraintsLabel, + ); + throw new Error("could not make validity check"); + } +}; diff --git a/packages/common/src/db/models.ts b/packages/common/src/db/models.ts index 799e6483e..33cf506bb 100644 --- a/packages/common/src/db/models.ts +++ b/packages/common/src/db/models.ts @@ -88,6 +88,7 @@ export enum InvalidityReasonType { KUSAMA_RANK = "KUSAMA_RANK", PROVIDER = "PROVIDER", BEEFY = "BEEFY", + SANCTIONED_GEO_AREA = "SANCTIONED_GEO_AREA", } export interface InvalidityReason { @@ -117,6 +118,7 @@ export const InvalidityReason = new Schema({ "KUSAMA_RANK", "PROVIDER", "BEEFY", + "SANCTIONED_GEO_AREA", ], default: "NEW", }, diff --git a/packages/common/src/db/queries/Candidate.ts b/packages/common/src/db/queries/Candidate.ts index cde5ffc48..a389ab07d 100644 --- a/packages/common/src/db/queries/Candidate.ts +++ b/packages/common/src/db/queries/Candidate.ts @@ -339,7 +339,7 @@ export const getIdentityAddresses = async ( export const setCandidateOnlineValid = async ( candidate: Candidate, ): Promise => { - setCandidateInvalidity(candidate, InvalidityReasonType.ONLINE, true); + await setCandidateInvalidity(candidate, InvalidityReasonType.ONLINE, true); await CandidateModel.findOneAndUpdate( { @@ -355,7 +355,7 @@ export const setCandidateOnlineNotValid = async ( candidate: Candidate, ): Promise => { const invalidityMessage = `Candidate ${candidate.name} offline. Offline since ${Date.now()}.`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.ONLINE, false, @@ -827,7 +827,7 @@ export const setOnlineValidity = async ( ): Promise => { const candidate = await getCandidateBySlotId(slotId); const invalidityMessage = `${candidate.name} offline. Last seen online ${candidate.onlineSince}.`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.ONLINE, isValid, @@ -841,7 +841,7 @@ export const setValidateIntentionValidity = async ( isValid: boolean, ): Promise => { const invalidityMessage = `${candidate.name} does not have a validate intention.`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.VALIDATE_INTENTION, isValid, @@ -855,7 +855,7 @@ export const setLatestClientReleaseValidity = async ( isValid: boolean, ): Promise => { const invalidityMessage = `${candidate.name} is not on the latest client version`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.CLIENT_UPGRADE, isValid, @@ -870,7 +870,7 @@ export const setConnectionTimeInvalidity = async ( isValid: boolean, ): Promise => { const invalidityMessage = `${candidate.name} has not been connected for minimum length`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.CONNECTION_TIME, isValid, @@ -888,7 +888,7 @@ export const setIdentityInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} has not properly set their identity`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.IDENTITY, isValid, @@ -903,7 +903,7 @@ export const setOfflineAccumulatedInvalidity = async ( isValid: boolean, ): Promise => { const invalidityMessage = `${candidate.name} has been offline ${candidate.offlineAccumulated / 1000 / 60} minutes this week.`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.ACCUMULATED_OFFLINE_TIME, isValid, @@ -919,7 +919,7 @@ export const setRewardDestinationInvalidity = async ( isValid: boolean, ): Promise => { const invalidityMessage = `${candidate.name} does not have reward destination as Staked`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.REWARD_DESTINATION, isValid, @@ -937,7 +937,7 @@ export const setCommissionInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} has not properly set their commission`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.COMMISION, isValid, @@ -955,7 +955,7 @@ export const setSelfStakeInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} has not properly bonded enough self stake`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.SELF_STAKE, isValid, @@ -973,7 +973,7 @@ export const setUnclaimedInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} has not properly claimed era rewards`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.UNCLAIMED_REWARDS, isValid, @@ -991,7 +991,7 @@ export const setBlockedInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} blocks external nominations`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.BLOCKED, isValid, @@ -1009,7 +1009,7 @@ export const setProviderInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} has banned infrastructure provider`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.PROVIDER, isValid, @@ -1027,7 +1027,7 @@ export const setKusamaRankInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} has not properly claimed era rewards`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.KUSAMA_RANK, isValid, @@ -1044,7 +1044,7 @@ export const setBeefyKeysInvalidity = async ( const invalidityMessage = message ? message : `${candidate.name} does not have beefy keys`; - setCandidateInvalidity( + await setCandidateInvalidity( candidate, InvalidityReasonType.BEEFY, isValid, @@ -1053,6 +1053,20 @@ export const setBeefyKeysInvalidity = async ( ); }; +// Set Sanctions Validity Status +export const setSanctionedGeoAreaValidity = async ( + candidate: Candidate, + isValid: boolean, +): Promise => { + const invalidityMessage = `${candidate.name} in sanctioned area.`; + await setCandidateInvalidity( + candidate, + InvalidityReasonType.SANCTIONED_GEO_AREA, + isValid, + invalidityMessage, + ); +}; + // Sets valid boolean for node export const setValid = async ( candidate: Candidate, diff --git a/packages/common/src/db/queries/CandidateUtils.ts b/packages/common/src/db/queries/CandidateUtils.ts index 0abea84e2..24e7cea2c 100644 --- a/packages/common/src/db/queries/CandidateUtils.ts +++ b/packages/common/src/db/queries/CandidateUtils.ts @@ -35,25 +35,34 @@ export const setCandidateInvalidity = async ( ) => { if (skipIfNoData && !isCandidateInvaliditySet(candidate)) return; - const invalidityReasons = filterCandidateInvalidityFields( - candidate, - invalidityType, - ); + const invalidityReason: InvalidityReason = { + valid: isValid, + type: invalidityType, + updated: Date.now(), + details: isValid ? "" : `${invalidityMessage}`, + }; + + await CandidateModel.updateOne( + { + slotId: candidate.slotId, + "invalidity.type": invalidityType, + }, + { + $set: { + "invalidity.$": invalidityReason, + }, + }, + ).exec(); - await CandidateModel.findOneAndUpdate( + await CandidateModel.updateOne( { slotId: candidate.slotId, + "invalidity.type": { $ne: invalidityType }, }, { - invalidity: [ - ...invalidityReasons, - { - valid: isValid, - type: invalidityType, - updated: Date.now(), - details: isValid ? "" : `${invalidityMessage}`, - }, - ], + $push: { + invalidity: invalidityReason, + }, }, ).exec(); }; diff --git a/packages/core/config/kusama.current.dev.sample.json b/packages/core/config/kusama.current.dev.sample.json index 50606cb0a..28370f684 100644 --- a/packages/core/config/kusama.current.dev.sample.json +++ b/packages/core/config/kusama.current.dev.sample.json @@ -21,7 +21,12 @@ "forceClientVersion": "v0.9.30", "minSelfStake": 10000000000000, "commission": 150000000, - "unclaimedEraThreshold": 4 + "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "cron": { "monitor": "0 */15 * * * *", diff --git a/packages/core/config/kusama.current.sample.json b/packages/core/config/kusama.current.sample.json index a1efba2a8..371dbeb13 100644 --- a/packages/core/config/kusama.current.sample.json +++ b/packages/core/config/kusama.current.sample.json @@ -42,7 +42,12 @@ "forceClientVersion": "v0.9.30", "minSelfStake": 10000000000000, "commission": 150000000, - "unclaimedEraThreshold": 4 + "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "db": { "mongo": { diff --git a/packages/core/config/kusama.microservice.dev.sample.json b/packages/core/config/kusama.microservice.dev.sample.json index 70615363e..2fdcd6fa1 100644 --- a/packages/core/config/kusama.microservice.dev.sample.json +++ b/packages/core/config/kusama.microservice.dev.sample.json @@ -43,7 +43,12 @@ "forceClientVersion": "v0.9.30", "minSelfStake": 10000000000000, "commission": 150000000, - "unclaimedEraThreshold": 4 + "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "db": { "mongo": { diff --git a/packages/core/config/kusama.microservice.sample.json b/packages/core/config/kusama.microservice.sample.json index a948ebdcc..d25cb275d 100644 --- a/packages/core/config/kusama.microservice.sample.json +++ b/packages/core/config/kusama.microservice.sample.json @@ -22,7 +22,12 @@ "forceClientVersion": "v0.9.30", "minSelfStake": 10000000000000, "commission": 150000000, - "unclaimedEraThreshold": 4 + "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "db": { "mongo": { diff --git a/packages/core/config/main.sample.json b/packages/core/config/main.sample.json index 7658082e5..6bc510aec 100644 --- a/packages/core/config/main.sample.json +++ b/packages/core/config/main.sample.json @@ -26,7 +26,12 @@ "skipUnclaimed": true, "minSelfStake": 100000, "commission": 150000000, - "unclaimedEraThreshold": 4 + "unclaimedEraThreshold": 4, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "proxy": { "timeDelayBlocks": "35" diff --git a/packages/core/config/polkadot.current.dev.sample.json b/packages/core/config/polkadot.current.dev.sample.json index 3b1954b80..5ae6c494a 100644 --- a/packages/core/config/polkadot.current.dev.sample.json +++ b/packages/core/config/polkadot.current.dev.sample.json @@ -41,7 +41,12 @@ "skipUnclaimed": true, "minSelfStake": 50000000000000, "commission": 50000000, - "unclaimedEraThreshold": 1 + "unclaimedEraThreshold": 1, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "db": { "mongo": { diff --git a/packages/core/config/polkadot.current.sample.json b/packages/core/config/polkadot.current.sample.json index 4819472f0..3912aac70 100644 --- a/packages/core/config/polkadot.current.sample.json +++ b/packages/core/config/polkadot.current.sample.json @@ -18,7 +18,12 @@ "skipUnclaimed": true, "minSelfStake": 50000000000000, "commission": 50000000, - "unclaimedEraThreshold": 1 + "unclaimedEraThreshold": 1, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "cron": { "monitorEnabled": true, diff --git a/packages/core/config/polkadot.microservice.dev.sample.json b/packages/core/config/polkadot.microservice.dev.sample.json index e40880060..9c542ccea 100644 --- a/packages/core/config/polkadot.microservice.dev.sample.json +++ b/packages/core/config/polkadot.microservice.dev.sample.json @@ -20,7 +20,12 @@ "skipUnclaimed": true, "minSelfStake": 50000000000000, "commission": 50000000, - "unclaimedEraThreshold": 1 + "unclaimedEraThreshold": 1, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "db": { "mongo": { diff --git a/packages/core/config/polkadot.microservice.sample.json b/packages/core/config/polkadot.microservice.sample.json index 465ac5d3f..603658eb7 100644 --- a/packages/core/config/polkadot.microservice.sample.json +++ b/packages/core/config/polkadot.microservice.sample.json @@ -20,7 +20,12 @@ "skipUnclaimed": true, "minSelfStake": 50000000000000, "commission": 50000000, - "unclaimedEraThreshold": 1 + "unclaimedEraThreshold": 1, + "sanctionedGeoArea": { + "skip": false, + "sanctionedCountries": ["XXX"], + "sanctionedRegions": ["XXX"] + } }, "db": { "mongo": { diff --git a/packages/core/package.json b/packages/core/package.json index b45441b55..ac41fec11 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/core", - "version": "3.1.16", + "version": "3.2.0", "description": "Services for running the Thousand Validator Program.", "main": "index.js", "scripts": { diff --git a/packages/gateway/package.json b/packages/gateway/package.json index 7a4821e90..5e5b2795e 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/gateway", - "version": "3.1.16", + "version": "3.2.0", "description": "Services for running the Thousand Validator Program.", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/scorekeeper-status-ui/package.json b/packages/scorekeeper-status-ui/package.json index aa44345e3..eac6502d3 100644 --- a/packages/scorekeeper-status-ui/package.json +++ b/packages/scorekeeper-status-ui/package.json @@ -1,7 +1,7 @@ { "name": "@1kv/scorekeeper-status-ui", "private": true, - "version": "3.1.16", + "version": "3.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index ae4299cb3..42173a63b 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/telemetry", - "version": "3.1.16", + "version": "3.2.0", "description": "Services for running the Thousand Validator Program.", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 65c5e21c8..77ad78ac3 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/worker", - "version": "3.1.16", + "version": "3.2.0", "description": "Services for running the Thousand Validator Program.", "main": "build/index.js", "types": "build/index.d.ts",