Skip to content

feat: dynamic per-subnet validator sets with an admin-adjustable cap#26

Open
pawel-gebal-reef wants to merge 8 commits into
mainfrom
pg/valis-num---bump
Open

feat: dynamic per-subnet validator sets with an admin-adjustable cap#26
pawel-gebal-reef wants to merge 8 commits into
mainfrom
pg/valis-num---bump

Conversation

@pawel-gebal-reef

@pawel-gebal-reef pawel-gebal-reef commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the fixed 3-validator cap in ValidatorRegistry with an admin-adjustable maxValidators (default 20, hard limit 64) and makes per-subnet validator sets fully dynamic: attestations carry 1..maxValidators validators and the stored set, the getValidators ABI, and all of AlphaVault's consuming plumbing are sized by the actual attested count instead of fixed bytes32[3]/uint16[3] slots with a trailing-zero sentinel.

The EIP-712 WeightAttestation payload and typehash are unchanged (they were already dynamic), so attester tooling keeps signing the same struct.

Changes

ValidatorRegistry

  • Cap raised 3 → 20 and made admin-settable: maxValidators (public, default 20) with setMaxValidators gated on DEFAULT_ADMIN_ROLE, zero rejected, hard-capped by MAX_VALIDATORS_LIMIT = 64 (mirrors the MAX_SIGNERS careless-admin gas bound); lowering the cap does not shrink already-committed sets
  • ValidatorSet stores dynamic arrays; _commit is a plain delete+push (drops the old diff-write; readability over the write optimization, per the locked design in the cap research doc)
  • getValidators returns exact-size bytes32[]/uint16[]; empty means "not configured"

AlphaVault

  • All [3]/[6] plumbing generalized: _lastSeenHotkeys is bytes32[], rebalance targets/loops are count-sized, _totalStake merges current + last-seen sets of any sizes with _contains-based dedup
  • _resolveValidators derives the count from array length and additionally enforces hotkeys.length == weights.length — the structural guarantee the fixed-size ABI used to give for free
  • getBestValidators / lastSeenHotkeys ABI changed to dynamic arrays
  • Sell loop of unwrapForTao extracted to _sellAlphaForTao (also resolves a stack-too-deep under coverage's --ir-minimum)

Tooling

  • scripts/get_vault_state.py derives the validator count from array length and pre-fills 20 CSV columns

Gas

Count-3 hot paths pay for the dynamic arrays and the simpler commit/snapshot writes:

snapshot before after
wrap: first 478,831 505,429 (+5.6 %)
wrap: subsequent 251,575 261,067 (+3.8 %)
unwrap: full 167,625 176,980 (+5.6 %)
unwrap: partial 190,625 200,664 (+5.3 %)
rebalance 114,614 124,063 (+8.2 %)

The cap is a bound, not a target — worst-case cost scales with the attested count (up to 19 moveStake per rebalance; up to 2N merged entries on unwrapForTao after a full rotation). Real-precompile cost at count 20 is only exercised by mocks here; worth a localnet e2e pass before attesting large sets.

🤖 Generated with Claude Code

pawel-gebal-reef and others added 4 commits July 6, 2026 08:10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pawel-gebal-reef pawel-gebal-reef changed the title feat: raise validator cap to 20 and store dynamic per-subnet sets feat: dynamic per-subnet validator sets with an admin-adjustable cap Jul 6, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
return _generateHotkeys(count, "generated-hotkey");
}

function _generateHotkeys(uint256 count, string memory tag) internal pure returns (bytes32[] memory hotkeys) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it used except for _generarateHotkeys(uint256 count) call?

Comment thread test/AlphaVault.t.sol Outdated
assertEq(_totalVaultStakeAcrossHotkeys(NETUID1), 90 ether);
}

uint256 private constant FRESH_NETUID = 93;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the folowing pricate constants should be defined at the top of the file, together with other vars/constants

Comment thread test/AlphaVault.t.sol
_setValidators(FRESH_NETUID, hotkeys, weights);
_setRegBlock(FRESH_NETUID, 93);
_simulateAlphaDepositHotkey(alice, FRESH_NETUID, deposit, hotkeys[count - 1]);
_wrapHotkey(alice, FRESH_NETUID, hotkeys[count - 1]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name doesn't tell much. I would prefer a function name that better explain what the functions does.

Comment thread test/AlphaVault.t.sol Outdated
}
}

/// @dev Mirrors the documented weight-to-target rule: BPS share per slot, remainder on

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You refer to some kind of docs that are not part of this repo/project. Drop this comment, or explain better what the function does.
What I don't like in general is that the maths and the function is not obvious. IF it's used for expected values in assertions. That's bad. Those expected values should rather be hardcode than computed this way. Expected values have to be easily verifiable by a software dev "in-memory"

Comment thread test/AlphaVault.t.sol Outdated
(bytes32[] memory hotkeys, uint256[] memory weights, uint256 tokenId) = _wrapFreshSubnet(20, FRESH_DEPOSIT);

assertEq(vault.totalStake(tokenId), FRESH_DEPOSIT);
uint256[] memory targets = _expectedTargets(FRESH_DEPOSIT, weights);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was talking about in prev. comment. This shouldn't be computed here in some wierd way (likely duplicating what the code does). It's another surface for a potential error.

pawel-gebal-reef and others added 2 commits July 10, 2026 16:55
…cap changes

- restore the lost add_stake dispatch in chain_ops.py; all three localnet
  e2e jobs died at the first stake with zero RAO landed
- drop the hardcoded 500k gas on updateValidators in favor of
  eth_estimateGas (a 20-validator commit needs ~673k)
- pin threads = 4 in [profile.ci] so FOUNDRY_PROFILE=ci forge snapshot
  reproduces CI without a CLI flag
- cache the last-seen set length in _sweepRotatedStake and regenerate
  gas snapshots
- size get_vault_state columns by the attested set and tighten test
  names and assertions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
forge 1.7.x silently ignores `threads` in foundry.toml, so the previous
commit's [profile.ci] pin regenerated .gas-snapshot at the local core
count (2) while CI ran at 4 threads, diverging the fuzz input streams.
Restore the --threads 4 CLI flag and regenerate at 4 threads with the
CI-pinned forge v1.7.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant