Is your feature request related to a problem? Please describe.
Every slot on finney is full. Registering a subnet means taking one from somebody. The v440 notes also say the price of entry should fall toward the cost of the registration transaction, so it gets cheaper to do.
At SubnetLimit, a new registration evicts whichever subnet get_network_to_prune names: the lowest SubnetMovingPrice among non-immune subnets, ties broken by earliest registration. The owner gets no notice and no recourse.
The owner finds out afterward. Every miner and validator on that subnet is deregistered. destroy_alpha_in_out_stakes cashes out every alpha holder near the worst price the subnet ever had, and a low price is exactly why it got picked. The netuid goes, so every explorer listing, dashboard, API integration, Discord channel, website and validator config downstream has to move with it.
An owner may value all of that far above the current lock cost. It makes no difference, because there is no call they can make. They cannot outbid the newcomer and they cannot pay to stay.
"Just register again" is the answer this design assumes, and it is the right answer for a miner. A miner UID is a slot number. Lose it, register again, take whatever number is free, and nothing that identifies you has moved.
A netuid is not a slot number. It is the name. It is what the community calls the subnet, what every dashboard and explorer indexes, what validator configs point at, and what get_symbol_for_subnet derives the token ticker from, since SYMBOLS is indexed by netuid. Registering again gives you a different number, a different ticker, and an alpha token with no relationship to the one your holders were just cashed out of. There is no migration path between them: dissolution removes SubnetAlphaIn, SubnetAlphaOut and SubnetProtocolAlpha outright rather than moving them.
The owner's lock does not come back either. destroy_alpha_in_out_stakes refunds it only for subnets registered before NetworkRegistrationStartBlock. Under the current rules there is no refund at all.
SubnetMovingPrice is an EMA of the subnet's alpha price. update_moving_prices runs each block over the subnets currently receiving emission, moving each toward spot at SubnetMovingAlpha scaled by b/(b+h) for h = EMAPriceHalvingBlocks. A subnet outside that set has its EMA frozen, and membership there is root-gated, not owner-gated. Either way an owner cannot set the number and cannot move it quickly, because shifting the average takes sustained buying of that subnet's alpha. So someone who has built nothing can take the slot of someone who has, and the incumbent has no move available at any price.
Mainnet is at the cap right now: 128 live non-root entries in NetworksAdded against SubnetLimit 128, checked at block 8,739,071. So every registration on finney today takes the eviction path.
Issue #1651, which specified this deregistration design, lists "maintain root-only access to direct calls for now" among its goals. The "for now" is why I am raising this instead of treating the current rule as settled.
Describe the solution you'd like
When a registration would evict a subnet, open a window on it instead of pruning immediately.
- The window records the exact lock the challenger committed. That figure is the price, so the challenger sets it and not the owner.
- The owner has
FIRST_REFUSAL_WINDOW (7,200 blocks) to pay it. Paying recycles the TAO and renews immunity.
- Matching refunds the challenger and removes their queue entry, so a refused offer does not keep a place in line at a price that was just beaten.
- If the owner does not pay, the subnet is evicted on today's terms, though not at today's moment: a lapsed window is cleared by the next registration to reach the limit, and that registration is the one that prunes.
Owner-only, no bid parameter, no auction. Paying is the test of whether the subnet is worth keeping, so nothing is protected for free and the protocol makes no new judgement about subnet quality.
Two extrinsics: exercise_first_refusal(netuid) and cancel_network_registration(lock_id). The second exists because window expiry is lazy, so a challenger whose window lapses with no further registration arriving would otherwise sit with TAO locked and nothing block-driven to release it.
Describe alternatives you've considered
A standing renewal flag, where the owner pre-commits to auto-renew and the challenger's transaction collects. No deadline, no second call, much less code. That is the version I wrote first, and it does not work.
Collecting inside the challenger's transaction means the chain has to know the charge will clear. That depends on the owner's balance, and balances are public. The flag becomes a published price for making a specific owner spend money: watch a flagged owner's balance, wait for get_network_lock_cost to decay past it, then register. They pay, and you never wanted the slot at all. A window puts the decision after the offer instead, which is where a right of first refusal belongs. Nothing about the owner sits on chain beforehand, so there is no number to walk down to, and they can fund during the window rather than holding TAO against a challenge that may never come.
An auction or bidding parameter. Rejected: it prices the slot twice, adds a knob that needs a defensible starting value, and turns eviction into a negotiation the protocol has to arbitrate.
Raising SubnetLimit instead. This moves the date, not the problem. sudo_set_subnet_limit is root-only with no ceiling, so 128 could be 256 tomorrow, and the day 256 fills the owner is back here. The eviction rule is what is broken.
Letting the owner buy immunity whenever they want, rather than only under challenge. Rejected, though not on price. get_network_lock_cost quotes the same number to everyone in a given block, so an owner buying at will pays exactly what a registrant would pay then. What changes is whether anyone had to want the slot. Under challenge, the payment is what somebody actually committed for your subnet. Buy-anytime, it is a function of how long it has been since the last registration.
That is what breaks, and it compounds. Immunity takes a subnet out of the eviction pool, and once that pool is empty get_network_to_prune returns None and the registration fails with SubnetLimitReached. A failed registration never reaches set_network_last_lock, so the quote keeps decaying while the network is shut. get_lock_reduction_interval scales the stored 115,200 by the block emission, currently 0.5 TAO, giving an effective interval of 57,600 blocks and a fall to the 1 TAO floor about 16 days after the last successful registration. So owners who had all bought immunity would find each renewal cheaper than the last, with nobody able to get in and reprice it, and the 128 slots would belong permanently to whoever held them the day it started.
Gating to under-challenge inverts that. Buying requires a challenge, a challenge requires a registration that opened a window, and that registration ratchets the price up. The pool can only close where demand actually landed, and each closure makes the next one more expensive.
It does not close everything. do_exercise_first_refusal checks that the caller owns the subnet and never compares them to the challenger, and a matched challenger is refunded in full. So an owner who is already the prune target can register from a second coldkey, open the window on themselves, take the refund back, and match at whatever the quote was. After a full decay that quote is the floor. The gate costs them timing and the rate limit, not the price.
I think that residual is acceptable. The owner who can run it is by definition the subnet the scan was about to evict, so the case where the trick works is the case the feature exists to serve. If you read it the other way, the fix is a floor on what an owner may match, not another parameter on the challenge.
Doing nothing. This is the alternative I want to argue with, because the status quo is not neutral. Eviction falls on the lowest EMA alpha price, and a subnet sits low while it is being built rather than traded. So the builder pays the lock, builds, gets evicted, and pays the lock again, while a subnet with a high alpha price never faces the question at any price.
Under this proposal that same builder pays exactly what the challenger committed, and gets four more months. Not a premium and not a penalty: the same figure somebody else just put up for the same slot. It is a real cost and it lands hardest on whoever has least. What changes is that it becomes a price they can choose to pay. Today the mechanism never asks whether anyone wants the slot, it takes it from whoever the market has priced lowest, and that is often whoever is still building rather than trading.
Additional context
Implemented, tested and measured, and open as a PR so there is something concrete to argue with: #3023. It is 22 files against main, stacked on a benchmark correction I have up separately as #3022.
I am not attached to my version of it. If the mechanism is right and the implementation is wrong, that is a good outcome and I will rewrite it.
Costs I know about:
- Queue depth. An unanswered cycle leaves one extra entry in
NetworkRegistrationQueue, because a lapsed window is cleared by the next registration to arrive, and that registration is the one that prunes and then queues behind the original challenger. Depth climbs by one per unanswered cycle, bounded by the evictable pool. NetworkRegistrationQueue is a plain Vec decoded on every registration, so the cost is real and not cosmetic. I measured this rather than reasoning about it.
- Weights. Filling the queue in the benchmark fixture raises
register_network proof size by about 861 KB, which is 3.6x. Same storage annotations, larger values.
- Leased subnets cannot use this.
SubnetOwner is the derived lease coldkey and nobody can sign for it, and putting the call in the lease beneficiary proxy would let a beneficiary spend crowdloan funds. A perpetual lease has no end_block, so it can never terminate and can never answer a challenge.
It also depends on #3022: register_network's benchmark measures an empty subnet map, so the weight is understated on a chain that is at the cap. That one stands alone and should land regardless of what you decide here.
Is your feature request related to a problem? Please describe.
Every slot on finney is full. Registering a subnet means taking one from somebody. The v440 notes also say the price of entry should fall toward the cost of the registration transaction, so it gets cheaper to do.
At
SubnetLimit, a new registration evicts whichever subnetget_network_to_prunenames: the lowestSubnetMovingPriceamong non-immune subnets, ties broken by earliest registration. The owner gets no notice and no recourse.The owner finds out afterward. Every miner and validator on that subnet is deregistered.
destroy_alpha_in_out_stakescashes out every alpha holder near the worst price the subnet ever had, and a low price is exactly why it got picked. The netuid goes, so every explorer listing, dashboard, API integration, Discord channel, website and validator config downstream has to move with it.An owner may value all of that far above the current lock cost. It makes no difference, because there is no call they can make. They cannot outbid the newcomer and they cannot pay to stay.
"Just register again" is the answer this design assumes, and it is the right answer for a miner. A miner UID is a slot number. Lose it, register again, take whatever number is free, and nothing that identifies you has moved.
A netuid is not a slot number. It is the name. It is what the community calls the subnet, what every dashboard and explorer indexes, what validator configs point at, and what
get_symbol_for_subnetderives the token ticker from, sinceSYMBOLSis indexed by netuid. Registering again gives you a different number, a different ticker, and an alpha token with no relationship to the one your holders were just cashed out of. There is no migration path between them: dissolution removesSubnetAlphaIn,SubnetAlphaOutandSubnetProtocolAlphaoutright rather than moving them.The owner's lock does not come back either.
destroy_alpha_in_out_stakesrefunds it only for subnets registered beforeNetworkRegistrationStartBlock. Under the current rules there is no refund at all.SubnetMovingPriceis an EMA of the subnet's alpha price.update_moving_pricesruns each block over the subnets currently receiving emission, moving each toward spot atSubnetMovingAlphascaled byb/(b+h)forh=EMAPriceHalvingBlocks. A subnet outside that set has its EMA frozen, and membership there is root-gated, not owner-gated. Either way an owner cannot set the number and cannot move it quickly, because shifting the average takes sustained buying of that subnet's alpha. So someone who has built nothing can take the slot of someone who has, and the incumbent has no move available at any price.Mainnet is at the cap right now: 128 live non-root entries in
NetworksAddedagainstSubnetLimit128, checked at block 8,739,071. So every registration on finney today takes the eviction path.Issue #1651, which specified this deregistration design, lists "maintain root-only access to direct calls for now" among its goals. The "for now" is why I am raising this instead of treating the current rule as settled.
Describe the solution you'd like
When a registration would evict a subnet, open a window on it instead of pruning immediately.
FIRST_REFUSAL_WINDOW(7,200 blocks) to pay it. Paying recycles the TAO and renews immunity.Owner-only, no bid parameter, no auction. Paying is the test of whether the subnet is worth keeping, so nothing is protected for free and the protocol makes no new judgement about subnet quality.
Two extrinsics:
exercise_first_refusal(netuid)andcancel_network_registration(lock_id). The second exists because window expiry is lazy, so a challenger whose window lapses with no further registration arriving would otherwise sit with TAO locked and nothing block-driven to release it.Describe alternatives you've considered
A standing renewal flag, where the owner pre-commits to auto-renew and the challenger's transaction collects. No deadline, no second call, much less code. That is the version I wrote first, and it does not work.
Collecting inside the challenger's transaction means the chain has to know the charge will clear. That depends on the owner's balance, and balances are public. The flag becomes a published price for making a specific owner spend money: watch a flagged owner's balance, wait for
get_network_lock_costto decay past it, then register. They pay, and you never wanted the slot at all. A window puts the decision after the offer instead, which is where a right of first refusal belongs. Nothing about the owner sits on chain beforehand, so there is no number to walk down to, and they can fund during the window rather than holding TAO against a challenge that may never come.An auction or bidding parameter. Rejected: it prices the slot twice, adds a knob that needs a defensible starting value, and turns eviction into a negotiation the protocol has to arbitrate.
Raising
SubnetLimitinstead. This moves the date, not the problem.sudo_set_subnet_limitis root-only with no ceiling, so 128 could be 256 tomorrow, and the day 256 fills the owner is back here. The eviction rule is what is broken.Letting the owner buy immunity whenever they want, rather than only under challenge. Rejected, though not on price.
get_network_lock_costquotes the same number to everyone in a given block, so an owner buying at will pays exactly what a registrant would pay then. What changes is whether anyone had to want the slot. Under challenge, the payment is what somebody actually committed for your subnet. Buy-anytime, it is a function of how long it has been since the last registration.That is what breaks, and it compounds. Immunity takes a subnet out of the eviction pool, and once that pool is empty
get_network_to_prunereturnsNoneand the registration fails withSubnetLimitReached. A failed registration never reachesset_network_last_lock, so the quote keeps decaying while the network is shut.get_lock_reduction_intervalscales the stored 115,200 by the block emission, currently 0.5 TAO, giving an effective interval of 57,600 blocks and a fall to the 1 TAO floor about 16 days after the last successful registration. So owners who had all bought immunity would find each renewal cheaper than the last, with nobody able to get in and reprice it, and the 128 slots would belong permanently to whoever held them the day it started.Gating to under-challenge inverts that. Buying requires a challenge, a challenge requires a registration that opened a window, and that registration ratchets the price up. The pool can only close where demand actually landed, and each closure makes the next one more expensive.
It does not close everything.
do_exercise_first_refusalchecks that the caller owns the subnet and never compares them to the challenger, and a matched challenger is refunded in full. So an owner who is already the prune target can register from a second coldkey, open the window on themselves, take the refund back, and match at whatever the quote was. After a full decay that quote is the floor. The gate costs them timing and the rate limit, not the price.I think that residual is acceptable. The owner who can run it is by definition the subnet the scan was about to evict, so the case where the trick works is the case the feature exists to serve. If you read it the other way, the fix is a floor on what an owner may match, not another parameter on the challenge.
Doing nothing. This is the alternative I want to argue with, because the status quo is not neutral. Eviction falls on the lowest EMA alpha price, and a subnet sits low while it is being built rather than traded. So the builder pays the lock, builds, gets evicted, and pays the lock again, while a subnet with a high alpha price never faces the question at any price.
Under this proposal that same builder pays exactly what the challenger committed, and gets four more months. Not a premium and not a penalty: the same figure somebody else just put up for the same slot. It is a real cost and it lands hardest on whoever has least. What changes is that it becomes a price they can choose to pay. Today the mechanism never asks whether anyone wants the slot, it takes it from whoever the market has priced lowest, and that is often whoever is still building rather than trading.
Additional context
Implemented, tested and measured, and open as a PR so there is something concrete to argue with: #3023. It is 22 files against
main, stacked on a benchmark correction I have up separately as #3022.I am not attached to my version of it. If the mechanism is right and the implementation is wrong, that is a good outcome and I will rewrite it.
Costs I know about:
NetworkRegistrationQueue, because a lapsed window is cleared by the next registration to arrive, and that registration is the one that prunes and then queues behind the original challenger. Depth climbs by one per unanswered cycle, bounded by the evictable pool.NetworkRegistrationQueueis a plainVecdecoded on every registration, so the cost is real and not cosmetic. I measured this rather than reasoning about it.register_networkproof size by about 861 KB, which is 3.6x. Same storage annotations, larger values.SubnetOwneris the derived lease coldkey and nobody can sign for it, and putting the call in the lease beneficiary proxy would let a beneficiary spend crowdloan funds. A perpetual lease has noend_block, so it can never terminate and can never answer a challenge.It also depends on #3022:
register_network's benchmark measures an empty subnet map, so the weight is understated on a chain that is at the cap. That one stands alone and should land regardless of what you decide here.