From 171c9904ae4c4df1d4da49377288f52563811c67 Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 18 Dec 2025 11:16:51 +0000 Subject: [PATCH 1/4] feat: add --debug flag to validator-info command Shows raw unfiltered pending deposits/withdrawals and current epoch for debugging stake visibility issues. --- src/commands/staking/index.ts | 1 + src/commands/staking/stakingInfo.ts | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/commands/staking/index.ts b/src/commands/staking/index.ts index c04aaa8..a78f39c 100644 --- a/src/commands/staking/index.ts +++ b/src/commands/staking/index.ts @@ -228,6 +228,7 @@ export function initializeStakingCommands(program: Command) { .option("--network ", "Network to use (localnet, testnet-asimov)") .option("--rpc ", "RPC URL for the network") .option("--staking-address
", "Staking contract address (overrides chain config)") + .option("--debug", "Show raw unfiltered pending deposits/withdrawals") .action(async (validatorArg: string | undefined, options: StakingInfoOptions) => { const validator = validatorArg || options.validator; const action = new StakingInfoAction(); diff --git a/src/commands/staking/stakingInfo.ts b/src/commands/staking/stakingInfo.ts index 9f00729..8a5ca8d 100644 --- a/src/commands/staking/stakingInfo.ts +++ b/src/commands/staking/stakingInfo.ts @@ -9,6 +9,7 @@ const UNBONDING_PERIOD_EPOCHS = 7n; export interface StakingInfoOptions extends StakingConfig { validator?: string; + debug?: boolean; } export class StakingInfoAction extends StakingAction { @@ -38,6 +39,7 @@ export class StakingInfoAction extends StakingAction { const currentEpoch = epochInfo.currentEpoch; const result: Record = { + ...(options.debug && {currentEpoch: currentEpoch.toString()}), validator: info.address, owner: info.owner, operator: info.operator, @@ -52,22 +54,27 @@ export class StakingInfoAction extends StakingAction { live: info.live, banned: info.banned ? info.bannedEpoch?.toString() : "Not banned", selfStakePendingDeposits: (() => { - // Filter to only truly pending deposits (not yet active) - const pending = info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch); - return pending.length > 0 - ? pending.map(d => { + // In debug mode, show all deposits; otherwise filter to truly pending only + const deposits = options.debug + ? info.pendingDeposits + : info.pendingDeposits.filter(d => d.epoch + ACTIVATION_DELAY_EPOCHS > currentEpoch); + return deposits.length > 0 + ? deposits.map(d => { const depositEpoch = d.epoch; const activationEpoch = depositEpoch + ACTIVATION_DELAY_EPOCHS; const epochsUntilActive = activationEpoch - currentEpoch; + const isActivated = epochsUntilActive <= 0n; return { epoch: depositEpoch.toString(), stake: d.stake, shares: d.shares.toString(), activatesAtEpoch: activationEpoch.toString(), - epochsRemaining: epochsUntilActive.toString(), + ...(options.debug + ? {status: isActivated ? "ACTIVATED" : `pending (${epochsUntilActive} epochs)`} + : {epochsRemaining: epochsUntilActive.toString()}), }; }) - : "None"; + : options.debug ? `None (raw count: ${info.pendingDeposits.length})` : "None"; })(), selfStakePendingWithdrawals: info.pendingWithdrawals.length > 0 From cd50d5f3c72cb0c7d004f88931366ea9f39a14ed Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 18 Dec 2025 11:22:14 +0000 Subject: [PATCH 2/4] feat: use tree traversal for validator list, add Primed column - Traverse validatorsRoot tree to find ALL validators (including unprimed) - Add separate Primed column showing epoch and needs-priming status - Remove prime! from Status column (now in dedicated column) --- src/commands/staking/StakingAction.ts | 73 ++++++++++++++++++++++++++- src/commands/staking/stakingInfo.ts | 35 ++++++++----- 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/src/commands/staking/StakingAction.ts b/src/commands/staking/StakingAction.ts index 1251ccc..60ceec8 100644 --- a/src/commands/staking/StakingAction.ts +++ b/src/commands/staking/StakingAction.ts @@ -2,10 +2,21 @@ import {BaseAction, BUILT_IN_NETWORKS, resolveNetwork} from "../../lib/actions/B import {createClient, createAccount, formatStakingAmount, parseStakingAmount, abi} from "genlayer-js"; import type {GenLayerClient, GenLayerChain, Address} from "genlayer-js/types"; import {readFileSync, existsSync} from "fs"; -import {ethers} from "ethers"; +import {ethers, ZeroAddress} from "ethers"; import {createPublicClient, createWalletClient, http, type PublicClient, type WalletClient, type Chain, type Account} from "viem"; import {privateKeyToAccount} from "viem/accounts"; +// Extended ABI for tree traversal (not in SDK) +const STAKING_TREE_ABI = [ + { + name: "validatorsRoot", + type: "function", + stateMutability: "view", + inputs: [], + outputs: [{name: "", type: "address"}], + }, +] as const; + // Re-export for use by other staking commands export {BUILT_IN_NETWORKS}; @@ -193,4 +204,64 @@ export class StakingAction extends BaseAction { signerAddress: account.address as Address, }; } + + /** + * Get all validators by traversing the validator tree. + * This finds ALL validators including those not yet active/primed. + */ + protected async getAllValidatorsFromTree(config: StakingConfig): Promise { + const network = this.getNetwork(config); + const rpcUrl = config.rpc || network.rpcUrls.default.http[0]; + const stakingAddress = config.stakingAddress || network.stakingContract?.address; + + if (!stakingAddress) { + throw new Error("Staking contract address not configured"); + } + + const publicClient = createPublicClient({ + chain: network, + transport: http(rpcUrl), + }); + + // Get the root of the validator tree + const root = await publicClient.readContract({ + address: stakingAddress as `0x${string}`, + abi: STAKING_TREE_ABI, + functionName: "validatorsRoot", + }); + + if (root === ZeroAddress) { + return []; + } + + const validators: Address[] = []; + const stack: string[] = [root as string]; + const visited = new Set(); + + // Use validatorView from SDK's ABI (has left/right fields) + while (stack.length > 0) { + const addr = stack.pop()!; + + if (addr === ZeroAddress || visited.has(addr.toLowerCase())) continue; + visited.add(addr.toLowerCase()); + + validators.push(addr as Address); + + const info = await publicClient.readContract({ + address: stakingAddress as `0x${string}`, + abi: abi.STAKING_ABI, + functionName: "validatorView", + args: [addr as `0x${string}`], + }) as {left: string; right: string}; + + if (info.left !== ZeroAddress) { + stack.push(info.left); + } + if (info.right !== ZeroAddress) { + stack.push(info.right); + } + } + + return validators; + } } diff --git a/src/commands/staking/stakingInfo.ts b/src/commands/staking/stakingInfo.ts index 8a5ca8d..2c58048 100644 --- a/src/commands/staking/stakingInfo.ts +++ b/src/commands/staking/stakingInfo.ts @@ -369,7 +369,10 @@ export class StakingInfoAction extends StakingAction { // No account configured, that's fine } - // Fetch all data in parallel + // Use tree traversal to get ALL validators (including not-yet-primed) + const allTreeAddresses = await this.getAllValidatorsFromTree(options); + + // Also fetch status lists in parallel const [activeAddresses, quarantinedList, bannedList, epochInfo] = await Promise.all([ client.getActiveValidators(), client.getQuarantinedValidatorsDetailed(), @@ -380,15 +383,14 @@ export class StakingInfoAction extends StakingAction { // Build set of quarantined/banned for status lookup const quarantinedSet = new Map(quarantinedList.map(v => [v.validator.toLowerCase(), v])); const bannedSet = new Map(bannedList.map(v => [v.validator.toLowerCase(), v])); + const activeSet = new Set(activeAddresses.map(a => a.toLowerCase())); - // Combine all validators - const allAddresses = new Set([ - ...activeAddresses, - ...quarantinedList.map(v => v.validator), - ...(options.all ? bannedList.map(v => v.validator) : []), - ]); + // Filter out banned if not --all + const allAddresses = options.all + ? allTreeAddresses + : allTreeAddresses.filter(addr => !bannedSet.has(addr.toLowerCase())); - this.setSpinnerText(`Fetching details for ${allAddresses.size} validators...`); + this.setSpinnerText(`Fetching details for ${allAddresses.length} validators...`); // Fetch detailed info in batches to avoid rate limiting const BATCH_SIZE = 5; @@ -418,7 +420,7 @@ export class StakingInfoAction extends StakingAction { const addrLower = info.address.toLowerCase(); const isQuarantined = quarantinedSet.has(addrLower); const isBanned = bannedSet.has(addrLower); - const isActive = activeAddresses.some(a => a.toLowerCase() === addrLower); + const isActive = activeSet.has(addrLower); let status = ""; if (isBanned) { @@ -427,8 +429,6 @@ export class StakingInfoAction extends StakingAction { } else if (isQuarantined) { const qInfo = quarantinedSet.get(addrLower)!; status = `quarant(e${qInfo.untilEpoch})`; - } else if (info.needsPriming) { - status = "prime!"; } else if (isActive) { status = "active"; } else { @@ -492,6 +492,7 @@ export class StakingInfoAction extends StakingAction { chalk.cyan("Self"), chalk.cyan("Deleg"), chalk.cyan("Pending"), + chalk.cyan("Primed"), chalk.cyan("Weight"), chalk.cyan("Status"), ], @@ -540,13 +541,22 @@ export class StakingInfoAction extends StakingAction { ? `${moniker}${roleTag}\n${chalk.gray(info.address)}` : `${chalk.gray(info.address)}${roleTag}`; + // Primed status - color based on how current it is + let primedStr: string; + if (info.ePrimed >= currentEpoch) { + primedStr = chalk.green(`e${info.ePrimed}`); + } else if (info.ePrimed === currentEpoch - 1n) { + primedStr = chalk.yellow(`e${info.ePrimed}`); + } else { + primedStr = chalk.red(`e${info.ePrimed}!`); + } + // Status coloring let statusStr = status; if (status === "active") statusStr = chalk.green(status); else if (status === "BANNED") statusStr = chalk.red(status); else if (status.startsWith("quarant")) statusStr = chalk.yellow(status); else if (status.startsWith("banned")) statusStr = chalk.red(status); - else if (status === "prime!") statusStr = chalk.magenta(status); else if (status === "pending") statusStr = chalk.gray(status); table.push([ @@ -555,6 +565,7 @@ export class StakingInfoAction extends StakingAction { formatStake(info.vStake), formatStake(info.dStake), pendingStr, + primedStr, weightStr, statusStr, ]); From 53126e70d01c5fe48a79463a5ffd7351f0a33240 Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 18 Dec 2025 11:52:58 +0000 Subject: [PATCH 3/4] feat: add prime-all command to prime all validators at once Iterates through validator tree and attempts to prime each validator that needs priming. Continues on failure for max resilience. --- src/commands/staking/StakingAction.ts | 2 +- src/commands/staking/index.ts | 12 ++++++++ src/commands/staking/validatorPrime.ts | 38 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/commands/staking/StakingAction.ts b/src/commands/staking/StakingAction.ts index 60ceec8..e92be2e 100644 --- a/src/commands/staking/StakingAction.ts +++ b/src/commands/staking/StakingAction.ts @@ -143,7 +143,7 @@ export class StakingAction extends BaseAction { // Stop spinner before prompting for password this.stopSpinner(); const password = await this.promptPassword(`Enter password to unlock account '${accountName}':`); - this.startSpinner("Continuing..."); + this.startSpinner("Unlocking account..."); const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password); return wallet.privateKey; diff --git a/src/commands/staking/index.ts b/src/commands/staking/index.ts index a78f39c..f3650c5 100644 --- a/src/commands/staking/index.ts +++ b/src/commands/staking/index.ts @@ -116,6 +116,18 @@ export function initializeStakingCommands(program: Command) { await action.execute({...options, validator}); }); + staking + .command("prime-all") + .description("Prime all validators that need priming") + .option("--account ", "Account to use (pays gas)") + .option("--network ", "Network to use (localnet, testnet-asimov)") + .option("--rpc ", "RPC URL for the network") + .option("--staking-address
", "Staking contract address (overrides chain config)") + .action(async (options: StakingConfig) => { + const action = new ValidatorPrimeAction(); + await action.primeAll(options); + }); + staking .command("set-operator [validator] [operator]") .description("Change the operator address for a validator wallet") diff --git a/src/commands/staking/validatorPrime.ts b/src/commands/staking/validatorPrime.ts index 9a8c32c..bf7d429 100644 --- a/src/commands/staking/validatorPrime.ts +++ b/src/commands/staking/validatorPrime.ts @@ -1,5 +1,6 @@ import {StakingAction, StakingConfig} from "./StakingAction"; import type {Address} from "genlayer-js/types"; +import chalk from "chalk"; export interface ValidatorPrimeOptions extends StakingConfig { validator: string; @@ -32,4 +33,41 @@ export class ValidatorPrimeAction extends StakingAction { this.failSpinner("Failed to prime validator", error.message || error); } } + + async primeAll(options: StakingConfig): Promise { + this.startSpinner("Fetching validators..."); + + try { + const client = await this.getStakingClient(options); + + // Get all validators from tree + this.setSpinnerText("Fetching validators..."); + const allValidators = await this.getAllValidatorsFromTree(options); + + this.stopSpinner(); + console.log(`\nPriming ${allValidators.length} validators:\n`); + + let succeeded = 0; + let skipped = 0; + + for (const addr of allValidators) { + process.stdout.write(` ${addr} ... `); + + try { + const result = await client.validatorPrime({validator: addr}); + console.log(chalk.green(`primed ${result.transactionHash}`)); + succeeded++; + } catch (error: any) { + const msg = error.message || String(error); + const shortErr = msg.length > 60 ? msg.slice(0, 57) + "..." : msg; + console.log(chalk.gray(`skipped: ${shortErr}`)); + skipped++; + } + } + + console.log(`\n${chalk.green(`${succeeded} primed`)}, ${chalk.gray(`${skipped} skipped`)}\n`); + } catch (error: any) { + this.failSpinner("Failed to prime validators", error.message || error); + } + } } From 4f915b1ac9657c5694d354a2e34186cfa864d24a Mon Sep 17 00:00:00 2001 From: Edgars Date: Thu, 18 Dec 2025 12:46:09 +0000 Subject: [PATCH 4/4] docs: add prime-all, validator-prime, and primed status documentation --- README.md | 12 ++++++++++-- docs/validator-guide.md | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a852c5..40951f4 100644 --- a/README.md +++ b/README.md @@ -306,14 +306,16 @@ COMMANDS: validator-deposit [options] Make an additional deposit as a validator validator-exit [options] Exit as a validator by withdrawing shares validator-claim [options] Claim validator withdrawals after unbonding period + validator-prime [validator] Prime a validator for the next epoch + prime-all [options] Prime all validators that need priming delegator-join [options] Join as a delegator by staking with a validator delegator-exit [options] Exit as a delegator by withdrawing shares delegator-claim [options] Claim delegator withdrawals after unbonding period - validator-info [validator] Get information about a validator + validator-info [validator] Get information about a validator (--debug for raw data) validator-history [validator] Show slash and reward history for a validator delegation-info [validator] Get delegation info for a delegator with a validator epoch-info [options] Get current/previous epoch info (--epoch for specific) - validators [options] Show validator set with stake, status, and weight + validators [options] Show validator set with stake, primed status, and weight active-validators [options] List all active validators quarantined-validators List all quarantined validators banned-validators List all banned validators @@ -416,6 +418,12 @@ EXAMPLES: # Exit and claim (requires validator wallet address) genlayer staking validator-exit --validator 0x... --shares 100 genlayer staking validator-claim --validator 0x... + + # Prime a validator for next epoch + genlayer staking validator-prime 0x... + + # Prime all validators that need priming (anyone can call) + genlayer staking prime-all ``` ### Running the CLI from the repository diff --git a/docs/validator-guide.md b/docs/validator-guide.md index 7224653..48b19ad 100644 --- a/docs/validator-guide.md +++ b/docs/validator-guide.md @@ -235,6 +235,23 @@ Output will include: ## Managing Your Validator +### Priming Your Validator + +Validators must be "primed" each epoch to participate in consensus. Priming updates the validator's stake record for the upcoming epoch. + +```bash +# Prime your validator +genlayer staking validator-prime 0xYourValidator... + +# Or prime all validators at once (anyone can do this) +genlayer staking prime-all +``` + +The `validators` command shows priming status: +- **Green** `e11` - Primed for current epoch +- **Yellow** `e10` - Needs priming before next epoch +- **Red** `e9!` - Urgently needs priming (behind) + ### Add More Stake ```bash @@ -247,6 +264,16 @@ genlayer staking validator-deposit --validator 0xYourValidatorWallet... --amount genlayer staking active-validators ``` +### View Validator Set + +```bash +# Show all validators with stake, primed status, and weight +genlayer staking validators + +# Include banned validators +genlayer staking validators --all +``` + ### Exit as Validator ```bash