Skip to content

Commit af0cc88

Browse files
committed
fix(api): unlist removed validator profiles
1 parent f0ec393 commit af0cc88

File tree

7 files changed

+92
-10
lines changed

7 files changed

+92
-10
lines changed

server/api/[version]/status.get.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getRpcUrl } from '~~/server/utils/rpc'
1313
* - The addresses of the active validators in the current epoch
1414
* - The addresses of inactive validators
1515
* - The addresses of untracked validators (new validators)
16+
* - The addresses of active validators with removed profile metadata (`unlistedActiveValidators`)
1617
* - The addresses of all validators regardless of their status
1718
*
1819
* Blockchain information:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE `validators` ADD `is_listed` integer;

server/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const validators = sqliteTable('validators', {
1616
accentColor: text('accent_color').notNull(),
1717
website: text('website'),
1818
contact: text('contact', { mode: 'json' }),
19+
isListed: integer('is_listed', { mode: 'boolean' }),
1920
}, table => [
2021
uniqueIndex('validators_address_unique').on(table.address),
2122
check(

server/utils/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface SnapshotEpochValidators {
1212
electedValidators: (ElectedValidator | UnelectedValidator)[]
1313
unelectedValidators: (ElectedValidator | UnelectedValidator)[]
1414
deletedValidators: string[]
15+
unlistedActiveValidators: string[]
1516

1617
/**
1718
* Validators that are not tracked by the database. The untracked validators are

server/utils/validator-listing.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const UNKNOWN_VALIDATOR_NAME = 'Unknown validator'
2+
3+
interface ValidatorListState {
4+
isListed: boolean | null
5+
name: string
6+
}
7+
8+
interface ValidatorAddress {
9+
address: string
10+
}
11+
12+
interface StoredValidatorAddressState extends ValidatorAddress {
13+
isListed: boolean | null
14+
}
15+
16+
export function isKnownValidatorProfile({ isListed, name }: ValidatorListState) {
17+
if (typeof isListed === 'boolean')
18+
return isListed
19+
return name.toLowerCase() !== UNKNOWN_VALIDATOR_NAME.toLowerCase()
20+
}
21+
22+
export function getUnlistedAddresses(storedAddresses: string[], bundledAddresses: Set<string>) {
23+
return storedAddresses.filter(address => !bundledAddresses.has(address))
24+
}
25+
26+
export function getUnlistedActiveValidatorAddresses(
27+
epochValidators: ValidatorAddress[],
28+
storedValidators: StoredValidatorAddressState[],
29+
) {
30+
const unlistedAddresses = new Set(
31+
storedValidators
32+
.filter(v => v.isListed === false)
33+
.map(v => v.address),
34+
)
35+
return epochValidators
36+
.filter(v => unlistedAddresses.has(v.address))
37+
.map(v => v.address)
38+
}

server/utils/validators-bundle.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { Result } from 'nimiq-validator-trustscore/types'
22
import type { ValidatorJSON } from './schemas'
33
import { bundledValidatorsByNetwork } from '../generated/validators-bundle.generated'
44
import { validatorSchema } from './schemas'
5-
import { storeValidator } from './validators'
5+
import { getUnlistedAddresses } from './validator-listing'
6+
import { getStoredValidatorsAddress, markValidatorsAsUnlisted, storeValidator } from './validators'
67

78
interface ImportValidatorsBundledOptions {
89
shouldStore?: boolean
@@ -28,10 +29,16 @@ export async function importValidatorsBundled(nimiqNetwork?: string, options: Im
2829
if (!shouldStore)
2930
return [true, undefined, validators]
3031

31-
const results = await Promise.allSettled(validators.map(v => storeValidator(v.address, v, { upsert: true })))
32+
const bundledAddresses = new Set(validators.map(v => v.address))
33+
const storedAddresses = await getStoredValidatorsAddress()
34+
const unlistedAddresses = getUnlistedAddresses(storedAddresses, bundledAddresses)
35+
36+
const results = await Promise.allSettled(validators.map(v => storeValidator(v.address, v, { upsert: true, isListed: true })))
3237
const failures = results.filter(r => r.status === 'rejected')
3338
if (failures.length > 0)
3439
return [false, `Errors importing validators: ${failures.map((f: any) => f.reason).join(', ')}`, undefined]
3540

41+
await markValidatorsAsUnlisted(unlistedAddresses)
42+
3643
return [true, undefined, validators]
3744
}

server/utils/validators.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import { fetchSnapshotEpoch } from '~~/packages/nimiq-validator-trustscore/src/f
1010
import { tables, useDrizzle } from './drizzle'
1111
import { handleValidatorLogo } from './logo'
1212
import { defaultValidatorJSON } from './schemas'
13+
import { PayoutType } from './types'
14+
import { getUnlistedActiveValidatorAddresses, isKnownValidatorProfile } from './validator-listing'
1315

1416
export const getStoredValidatorsId = () => useDrizzle().select({ id: tables.validators.id }).from(tables.validators).execute().then(r => r.map(v => v.id))
1517
export const getStoredValidatorsAddress = () => useDrizzle().select({ address: tables.validators.address }).from(tables.validators).execute().then(r => r.map(v => v.address))
18+
export const getStoredValidatorsListState = () => useDrizzle().select({ address: tables.validators.address, isListed: tables.validators.isListed }).from(tables.validators).execute()
1619

1720
const validators = new Map<string, number>()
1821

@@ -22,6 +25,12 @@ interface StoreValidatorOptions {
2225
* @default false
2326
*/
2427
upsert?: boolean
28+
29+
/**
30+
* Controls if the validator should appear in `only-known=true`.
31+
* @default false
32+
*/
33+
isListed?: boolean
2534
}
2635

2736
export async function storeValidator(address: string, rest: ValidatorJSON = defaultValidatorJSON, options: StoreValidatorOptions = {}): Promise<number | undefined> {
@@ -34,7 +43,7 @@ export async function storeValidator(address: string, rest: ValidatorJSON = defa
3443
return
3544
}
3645

37-
const { upsert = false } = options
46+
const { upsert = false, isListed = false } = options
3847

3948
// If the validator is cached and upsert is not true, return it
4049
if (!upsert && validators.has(address)) {
@@ -63,14 +72,14 @@ export async function storeValidator(address: string, rest: ValidatorJSON = defa
6372
if (validatorId) {
6473
await useDrizzle()
6574
.update(tables.validators)
66-
.set({ ...rest, ...brandingParameters })
75+
.set({ ...rest, ...brandingParameters, isListed })
6776
.where(eq(tables.validators.id, validatorId))
6877
.execute()
6978
}
7079
else {
7180
validatorId = await useDrizzle()
7281
.insert(tables.validators)
73-
.values({ ...rest, address, ...brandingParameters })
82+
.values({ ...rest, address, ...brandingParameters, isListed })
7483
.returning()
7584
.get()
7685
.then(r => r.id)
@@ -84,6 +93,28 @@ export async function storeValidator(address: string, rest: ValidatorJSON = defa
8493
return validatorId
8594
}
8695

96+
export async function markValidatorsAsUnlisted(addresses: string[]) {
97+
await Promise.all(addresses.map(async (address) => {
98+
const brandingParameters = await handleValidatorLogo(address, defaultValidatorJSON)
99+
await useDrizzle()
100+
.update(tables.validators)
101+
.set({
102+
name: 'Unknown validator',
103+
description: null,
104+
fee: null,
105+
payoutType: PayoutType.None,
106+
payoutSchedule: '',
107+
isMaintainedByNimiq: false,
108+
website: null,
109+
contact: null,
110+
isListed: false,
111+
...brandingParameters,
112+
})
113+
.where(eq(tables.validators.address, address))
114+
.execute()
115+
}))
116+
}
117+
87118
export type FetchValidatorsOptions = MainQuerySchema & { epochNumber: number }
88119

89120
export async function fetchValidators(_event: H3Event, params: FetchValidatorsOptions): Result<FetchedValidator[]> {
@@ -98,16 +129,15 @@ export async function fetchValidators(_event: H3Event, params: FetchValidatorsOp
98129
const filters: SQLWrapper[] = []
99130
if (payoutType)
100131
filters.push(eq(tables.validators.payoutType, payoutType))
101-
if (onlyKnown)
102-
filters.push(sql`lower(${tables.validators.name}) NOT LIKE lower('%Unknown validator%')`)
103132

104133
try {
105134
const validatorsQuery = useDrizzle().select().from(tables.validators)
106135
const dbValidators = filters.length > 0
107136
? await validatorsQuery.where(and(...filters)).execute()
108137
: await validatorsQuery.execute()
109138

110-
const validatorIds = dbValidators.map(v => v.id)
139+
const visibleValidators = onlyKnown ? dbValidators.filter(isKnownValidatorProfile) : dbValidators
140+
const validatorIds = visibleValidators.map(v => v.id)
111141
if (validatorIds.length === 0)
112142
return [true, undefined, []]
113143

@@ -172,7 +202,7 @@ export async function fetchValidators(_event: H3Event, params: FetchValidatorsOp
172202
const scoresByValidatorId = new Map(scoresRows.map(row => [row.validatorId, row]))
173203
const activityByValidatorId = new Map(activityRows.map(row => [row.validatorId, row]))
174204

175-
const validators = dbValidators.map((validator) => {
205+
const validators = visibleValidators.map((validator) => {
176206
const { logo, contact, hasDefaultLogo, ...rest } = validator
177207
const scoreRow = scoresByValidatorId.get(validator.id)
178208
const activityRow = activityByValidatorId.get(validator.id)
@@ -301,17 +331,20 @@ export async function categorizeValidatorsSnapshotEpoch(): Result<SnapshotEpochV
301331
if (!epochOk)
302332
return [false, error, undefined]
303333

304-
const dbAddresses = await getStoredValidatorsAddress()
334+
const storedValidators = await getStoredValidatorsListState()
335+
const dbAddresses = storedValidators.map(v => v.address)
305336
const electedValidators = epoch.validators.filter(v => v.elected) as ElectedValidator[]
306337
const unelectedValidators = epoch.validators.filter(v => !v.elected) as UnelectedValidator[]
307338
const untrackedValidators = electedValidators.filter(v => !dbAddresses.includes(v.address)) as (ElectedValidator & UnelectedValidator)[]
308339
const deletedValidators = dbAddresses.filter(dbAddress => !epoch.validators.map(v => v.address).includes(dbAddress))
340+
const unlistedActiveValidators = getUnlistedActiveValidatorAddresses(epoch.validators, storedValidators)
309341

310342
return [true, undefined, {
311343
epochNumber: epoch.epochNumber,
312344
electedValidators,
313345
unelectedValidators,
314346
untrackedValidators,
315347
deletedValidators,
348+
unlistedActiveValidators,
316349
}]
317350
}

0 commit comments

Comments
 (0)