Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions cli/src/validator/deploy/deployValidatorMainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { colors } from '@cliffy/colors'
import rpcLog from '/lib/config/rpcLog.ts'
import { listValidators } from '/src/validator/listValidators.ts'
import { getAnsibleHosts } from '/lib/yml/getAnsibleHost.ts'
import { registerBlsPubkey } from '/src/validator/registerBlsPubkey.ts'

const deployValidatorMainnet = async (limit?: string) => {
const inventoryType = 'mainnet_validators'
Expand All @@ -29,6 +30,19 @@ const deployValidatorMainnet = async (limit?: string) => {
if (result) {
console.log('Successfully deployed validator on mainnet')
rpcLog(ansibleHosts)
// SIMD-0387: register the BLS public key on each vote account post-deploy.
// No-op on mainnet until the feature gate is active there.
try {
await registerBlsPubkey('mainnet', limit)
} catch (e) {
console.warn(
colors.yellow(
`⚠️ BLS pubkey registration skipped: ${
e instanceof Error ? e.message : e
}`,
),
)
}
return true
}
console.log('Failed to deploy validator on mainnet')
Expand Down
13 changes: 13 additions & 0 deletions cli/src/validator/deploy/deployValidatorTestnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { colors } from '@cliffy/colors'
import rpcLog from '/lib/config/rpcLog.ts'
import { listValidators } from '/src/validator/listValidators.ts'
import { getAnsibleHosts } from '/lib/yml/getAnsibleHost.ts'
import { registerBlsPubkey } from '/src/validator/registerBlsPubkey.ts'

const deployValidatorTestnet = async (limit?: string) => {
const inventoryType = 'testnet_validators'
Expand All @@ -29,6 +30,18 @@ const deployValidatorTestnet = async (limit?: string) => {
if (result) {
console.log('Successfully deployed validator on testnet')
rpcLog(ansibleHosts)
// SIMD-0387: register the BLS public key on each vote account post-deploy.
try {
await registerBlsPubkey('testnet', limit)
} catch (e) {
console.warn(
colors.yellow(
`⚠️ BLS pubkey registration skipped: ${
e instanceof Error ? e.message : e
}`,
),
)
}
return true
}
console.log('Failed to deploy validator on testnet')
Expand Down
24 changes: 24 additions & 0 deletions cli/src/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { transformValidatorTypeFile } from '/lib/migrate/transformValidatorTypes
import { copyTemplateDirs } from '/src/rpc/init.ts'
import { registerDoubleZeroCommands } from '/lib/doublezero.ts'
import { registerSha256PatchCommands } from '/lib/sha256Patch.ts'
import { registerBlsPubkey } from '/src/validator/registerBlsPubkey.ts'

export const validatorCmd = new Command()
.description('🛠️ Manage Solana Validator Nodes 🛠️')
Expand Down Expand Up @@ -60,6 +61,29 @@ validatorCmd.command('deploy')
}
})

validatorCmd.command('register:bls')
.description(
'🔑 Register the BLS public key on vote accounts (SIMD-0387). Runs automatically after deploy; use this to re-run.',
)
.option('-n, --network <network>', 'Solana Network')
.option('-p, --pubkey <pubkey>', 'Inventory host name (defaults to all)')
.action(async (options) => {
let network = options.network as NetworkType | undefined
if (!network) {
const res = await prompt([
{
name: 'network',
message: 'Select Solana Network',
type: Select,
options: ['testnet', 'mainnet'],
default: 'testnet',
},
])
network = res.network as NetworkType
}
await registerBlsPubkey(network, options.pubkey)
})

validatorCmd.command('list')
.description('📋 List validators')
.option('-n, --network <network>', 'Solana Network', {
Expand Down
4 changes: 4 additions & 0 deletions cli/src/validator/init/initMainnetConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import type { SolanaNodeType } from '@cmn/types/config.ts'
import { findNearestSnapshotUrl } from '/lib/snapshot/findNearestSnapshot.ts'
import { getAllRegions } from '/lib/jito/jitoRegions.ts'
import { promptXdpConfig } from '/src/validator/init/promptXdpConfig.ts'

const initMainnetConfig = async (
sshConnection: SSHConnection,
Expand Down Expand Up @@ -136,6 +137,8 @@ const initMainnetConfig = async (
const blockEngineRegion = getNearRegion.info.blockEngineUrl
const shredstream_address = getNearRegion.info.shredReceiver
const relayer_url = getNearRegion.info.relayerUrl
// XDP retransmit acceleration (agave/jito only)
const xdpConfig = await promptXdpConfig(validatorType as SolanaNodeType)
// Generate Vote Key
const { voteAccount, authAccount } = await genVoteKey(identityAccount)
const configMainnet: Partial<ValidatorMainnetConfig> = {
Expand All @@ -152,6 +155,7 @@ const initMainnetConfig = async (
shred_receiver_address: String(shredstream_address),
snapshot_url: snapshotUrl,
staked_rpc_identity_account: rpcAccount,
...xdpConfig,
}
// await updateAllowedSshIps()
// await updateAllowedIps()
Expand Down
4 changes: 4 additions & 0 deletions cli/src/validator/init/initTestnetConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SolanaNodeTypes } from '@cmn/constants/config.ts'
import { findNearestJitoRegion } from '/lib/jito/findNearestRegion.ts'
import type { RegionLatency } from '/lib/jito/findNearestRegion.ts'
import { getAllRegions } from '/lib/jito/jitoRegions.ts'
import { promptXdpConfig } from '/src/validator/init/promptXdpConfig.ts'

const initTestnetConfig = async (
sshConnection: SSHConnection,
Expand Down Expand Up @@ -94,6 +95,8 @@ const initTestnetConfig = async (
console.log(colors.red('❌ Failed to measure latencies. Please try again.'))
return
}
// XDP retransmit acceleration (agave/jito only)
const xdpConfig = await promptXdpConfig(validatorType as SolanaNodeType)
// Generate Vote Key
const { voteAccount, authAccount } = await genVoteKey(identityAccount)
// Generate or Add Inventory
Expand Down Expand Up @@ -124,6 +127,7 @@ const initTestnetConfig = async (
snapshot_url: '',
port_rpc: 7211,
dynamic_port_range: '8900-8925',
...xdpConfig,
}
await updateInventory(name, configTestnet)
// Create solv User on Ubuntu Server
Expand Down
99 changes: 99 additions & 0 deletions cli/src/validator/init/promptXdpConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Confirm, Input, prompt } from '@cliffy/prompt'
import { colors } from '@cliffy/colors'
import type { SolanaNodeType } from '@cmn/types/config.ts'

export interface XdpConfig {
xdp_enabled?: boolean
xdp_interface?: string
xdp_cpu_cores?: number
xdp_zero_copy?: boolean
xdp_poh_pinned_cpu_core?: number
}

// Parses a non-negative integer from prompt input, or returns `fallback` when
// the input is blank or not a valid integer. Prevents NaN/garbage from reaching
// the inventory (and the validator launch flags) on a fat-fingered entry.
const parseNonNegativeInt = (
raw: unknown,
fallback: number | null,
): number | null => {
const s = String(raw ?? '').trim()
if (!s) return fallback
const n = Number(s)
if (!Number.isInteger(n) || n < 0) {
console.log(
colors.yellow(`⚠️ "${s}" is not a valid non-negative integer — ignored.`),
)
return fallback
}
return n
}

// XDP (eXpress Data Path) accelerates Turbine retransmit. Only Agave/Jito
// validators take these flags; Firedancer uses its own XDP path natively, so
// for those types we return an empty config and skip the prompt entirely.
const promptXdpConfig = async (
validatorType: SolanaNodeType,
): Promise<XdpConfig> => {
if (validatorType !== 'agave' && validatorType !== 'jito') {
return {}
}
const { enable } = await prompt([{
name: 'enable',
message: '⚡ Enable XDP retransmit acceleration for this validator?',
type: Confirm,
default: true,
}])
if (!enable) {
return { xdp_enabled: false }
}
console.log(colors.yellow(
'⚠️ XDP requires a recent kernel (6.8+, igb driver needs 6.14+) and grants\n' +
' the validator CAP_NET_RAW/CAP_NET_ADMIN/CAP_BPF/CAP_PERFMON via systemd.',
))
const answers = await prompt([
{
name: 'iface',
message: 'XDP network interface (bond member NIC, e.g. enp196s0f0np0)',
type: Input,
},
{
name: 'cores',
message: 'XDP retransmit CPU cores (count)',
type: Input,
default: '1',
},
{
name: 'zeroCopy',
message: 'Enable XDP zero-copy? (do NOT enable on bnxt_en / ice drivers)',
type: Confirm,
default: false,
},
{
name: 'pohCore',
message: 'PoH pinned CPU core (leave blank to skip)',
type: Input,
default: '',
},
])
const iface = String(answers.iface || '').trim()
if (!iface) {
console.log(
colors.yellow('⚠️ No interface given — disabling XDP for this host.'),
)
return { xdp_enabled: false }
}
const cfg: XdpConfig = {
xdp_enabled: true,
xdp_interface: iface,
xdp_cpu_cores: parseNonNegativeInt(answers.cores, 1) ?? 1,
xdp_zero_copy: Boolean(answers.zeroCopy),
}
const poh = parseNonNegativeInt(answers.pohCore, null)
if (poh !== null) {
cfg.xdp_poh_pinned_cpu_core = poh
}
return cfg
}

export { promptXdpConfig }
Loading
Loading