Skip to content

Commit 9b31922

Browse files
authored
fix(api): unlist removed validator profiles (#139)
1 parent 44247cb commit 9b31922

File tree

8 files changed

+244
-31
lines changed

8 files changed

+244
-31
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:

server/db/migrations/0003_add_cron_runs.sql

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CREATE TABLE `cron_runs` (
1+
CREATE TABLE IF NOT EXISTS `cron_runs` (
22
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
33
`cron` text NOT NULL,
44
`network` text NOT NULL,
@@ -10,5 +10,4 @@ CREATE TABLE `cron_runs` (
1010
`meta` text
1111
);
1212
--> statement-breakpoint
13-
CREATE INDEX `idx_cron_runs_started_at` ON `cron_runs` (`started_at`);
14-
13+
CREATE INDEX IF NOT EXISTS `idx_cron_runs_started_at` ON `cron_runs` (`started_at`);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- The `is_listed` column was added manually in remote D1 databases.
2+
-- Keep this as a no-op migration so deploy-time migrations don't fail
3+
-- with "duplicate column name: is_listed".
4+
SELECT 1;

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: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Result } from 'nimiq-validator-trustscore/types'
22
import type { ValidatorJSON } from './schemas'
3-
import { bundledValidatorsByNetwork } from '../generated/validators-bundle.generated'
43
import { validatorSchema } from './schemas'
5-
import { storeValidator } from './validators'
4+
import { getUnlistedAddresses } from './validator-listing'
5+
import { getStoredValidatorsAddress, markValidatorsAsUnlisted, storeValidator } from './validators'
66

77
interface ImportValidatorsBundledOptions {
88
shouldStore?: boolean
@@ -13,25 +13,37 @@ export async function importValidatorsBundled(nimiqNetwork?: string, options: Im
1313
return [false, 'Nimiq network is required', undefined]
1414

1515
const { shouldStore = true } = options
16-
const bundledValidators = bundledValidatorsByNetwork[nimiqNetwork as keyof typeof bundledValidatorsByNetwork]
17-
if (!bundledValidators || bundledValidators.length === 0)
18-
return [false, `No bundled validators found for network: ${nimiqNetwork}`, undefined]
16+
const storage = useStorage('assets:public')
17+
const keys = await storage.getKeys(`validators/${nimiqNetwork}`)
1918

2019
const validators: ValidatorJSON[] = []
21-
for (const data of bundledValidators) {
20+
for (const key of keys) {
21+
if (!key.endsWith('.json') || key.endsWith('.example.json'))
22+
continue
23+
24+
const data = await storage.getItem(key)
2225
const parsed = validatorSchema.safeParse(data)
2326
if (!parsed.success)
24-
return [false, `Invalid bundled validator data: ${parsed.error}`, undefined]
27+
return [false, `Invalid validator data at ${key}: ${parsed.error}`, undefined]
2528
validators.push(parsed.data)
2629
}
2730

2831
if (!shouldStore)
2932
return [true, undefined, validators]
3033

31-
const results = await Promise.allSettled(validators.map(v => storeValidator(v.address, v, { upsert: true })))
34+
if (validators.length === 0)
35+
return [false, `No bundled validators found for network: ${nimiqNetwork}`, undefined]
36+
37+
const bundledAddresses = new Set(validators.map(v => v.address))
38+
const storedAddresses = await getStoredValidatorsAddress()
39+
const unlistedAddresses = getUnlistedAddresses(storedAddresses, bundledAddresses)
40+
41+
const results = await Promise.allSettled(validators.map(v => storeValidator(v.address, v, { upsert: true, isListed: true })))
3242
const failures = results.filter(r => r.status === 'rejected')
3343
if (failures.length > 0)
3444
return [false, `Errors importing validators: ${failures.map((f: any) => f.reason).join(', ')}`, undefined]
3545

46+
await markValidatorsAsUnlisted(unlistedAddresses)
47+
3648
return [true, undefined, validators]
3749
}

0 commit comments

Comments
 (0)