For the complete documentation index, see llms.txt. This page is also available as Markdown.

Key and Address Rotation

Rotating the signer key bound to a signer-manager under PoX-5: the order the calls go in, when the switch has to happen relative to the reward cycle, and how a staker changes a Bitcoin payout address.

An operator rotates the signer key their signer-manager is bound to, and the admin key that controls the manager itself. A staker changes the Bitcoin payout address their rewards are sent to. None of these requires anyone to unstake.

If you are arriving from the PoX-4 model, where stack-extend carried a signer-key argument and pools had an operator key that could not be rotated at all, read If you knew rotation under PoX-4 first.


Rotate a signer key

Rotating changes which key is bound to your signer-manager. Your stakers do nothing, their positions do not move, and the manager does not leave the signer set.

1. Write the new config file, but do not restart

Create the new config.toml with the new stacks_private_key alongside the running one. The signer reads its config only at startup, so writing the file changes nothing until you restart. Everything below uses this file, which means the key you are about to register and the key your signer will present come from the same source.

2. Generate the grant with the new key

The new key signs a SIP-018 message binding your signer-manager to an auth-id, under the pox-5-signer domain:

stacks-signer generate-staking-signature \
  --config /etc/stacks-signer/config-new.toml \
  --signer-manager SP1EXAMPLE.your-manager-name \
  --auth-id 2

Use a fresh auth-id. Each (signer-key, signer-manager, auth-id) triple can be consumed once, and reusing one fails with ERR_SIGNER_KEY_GRANT_USED (u12). That bites if you ever rotate back to a key you have used before. Generate a Signer Signature covers the command in full.

3. Call register-self on your manager

register-self performs grant-signer-key and register-signer in a single transaction. It is admin-only, and authorize-admin asserts that contract-caller equals tx-sender, so it has to come from an admin account directly and cannot be routed through another contract.

This call is not prepare-phase gated, but it does have a deadline. See When to register and when to restart.

4. Verify that the new key is registered

Do this before touching the signer. A rotation that silently missed its cycle looks identical to one that worked, right up until you restart.

Two commands: what the chain has, and what your signer will present

What the chain has. get-signer-info on pox-5 returns the key currently bound to a signer-manager. It takes the manager as a hex-serialized Clarity principal, which you can produce with:

Then read it back:

A registered manager returns okay: true and a result of 0x0a0200000021 followed by 66 hex characters, which are the signer key. 0a is some, 02 is a buffer and 00000021 is its length of 33 bytes. An unregistered manager returns 0x09, which is none.

Run against SPMPMA1V6P430M8C91QS1G9XJ95S59JS1TZFZ4Q4.fastpool-max500-signer-manager, whose serialized principal is 0x0616296a283b358830510c486f90c13d924b92a6590e1e66617374706f6f6c2d6d61783530302d7369676e65722d6d616e61676572, the response is:

What your signer will present. check-config prints the public key derived from a config file without starting the signer:

Among the fields it prints:

Those 66 characters are the same string the chain returns after 0x0a0200000021. If they match, the restart is safe. If they do not, the restart takes your signer out of the set.

Compare all 66 characters. Two different signer keys live on mainnet today that both begin 023d6e, so a glance at the start of a key proves nothing.

Seeing the old key on chain once the prepare phase has opened means the rotation missed this cycle. Do not restart the signer. Wait one cycle.

5. Restart the signer at the cycle boundary

Covered in the next section. This is the one step with no room in it.

6. Revoke the old grant, last and optionally

revoke-signer-grant is sent by the Stacks principal derived from the old signer key, not by the manager admin, and contract-caller is the authorization, so it cannot be forwarded through a contract. Its arguments are (signer-manager, signer-key), the reverse order of grant-signer-key. Revoking a grant that does not exist is a no-op rather than a revert.

The reason to do it is hygiene. After a rotation the old grant stays active, so any admin of your manager could re-register the old key with it. An outsider cannot, because register-signer requires contract-caller to be the manager.


When to register and when to restart

None of grant-signer-key, register-signer or revoke-signer-grant checks the reward phase. The constraint is the reward-set snapshot: the node computes each cycle's signer set once, at the first Stacks block whose burn view falls inside the prepare phase, reading the registered key live at that instant. Whichever key is registered then is frozen into that cycle.

Register before the prepare phase

The prepare phase is the last 100 Bitcoin blocks of a cycle. Numbering a cycle's 2,100 blocks from its first reward-paying block, blocks 2,001 to 2,100 are the prepare phase, so a register-self transaction has to confirm at block 2,000 or earlier to count for the next cycle.

Treat 2,000 as the boundary, not the target. A Bitcoin block is roughly ten minutes and fee estimation is not a guarantee, so submit with room and then verify.

Missing the deadline is not an outage. The set for the next cycle still carries the old key, so you keep signing with the old key for one more cycle and the rotation lands a cycle late. The damage comes from missing it without noticing and restarting the signer as though it had worked, which is why step 4 exists.

How the deadline is calculated

The node's prepare-phase test is:

With mainnet's cycle length of 2,100 and prepare length of 100, the prepare phase is reward_index 2,001 through 2,099, plus reward_index 0, which is the first block of the next cycle. The reward phase is 1 through 2,000.

The set is not computed at a fixed height. It is computed at the first Stacks block whose tenure burn height falls in that window and which finds the signer set still behind, guarded by a strict less-than that makes it fire once per cycle and never re-read pox-5 afterwards. Stacks blocks arrive about every ten seconds, so in practice that is moments after the prepare phase opens.

Source: PoxConstants::static_is_in_prepare_phase in stackslib/src/burnchains/mod.rs at release 4.0.1.

Restart at the cycle boundary

For a rotation registered during cycle N:

Where you are
Which key the chain expects

Cycle N, blocks 1 to 2,000

Old. Register the new key here.

Cycle N, blocks 2,001 to 2,100

Old. The set for N+1 is computed here, carrying the new key.

Cycle N+1 onward

New.

So the restart belongs at the boundary between N and N+1. Restarting earlier, ten blocks before the prepare phase for instance, means signing with the new key while the chain still expects the old one for the rest of cycle N.

stacks-signer reads config.toml once at startup and holds one key. There is no reload signal and no way to carry both keys through the transition, so the switch is a hard cutover and the signer cannot sign while it restarts. Put that gap on the boundary, where the old key stops being valid anyway, rather than inside a cycle where it costs blocks. An operator carrying a large share of signer weight will want to sit on the boundary tightly. A small one can be looser.

A trap with no guard

pox-5 does not check whether a signer key is already bound to another manager, and there is no error code for it. The node sums managers sharing a key into a single signer entry, so reusing a key across two managers merges them silently.


Rotate the manager admin key

Admin is a map on your signer-manager, so this is two calls on your own contract and pox-5 never sees it. Both deployed managers expose (update-admin (admin principal) (enabled bool)).

  1. From the old admin, call update-admin with the new principal and enabled: true.

  2. From the new admin, call update-admin with the old principal and enabled: false.

Doing the removal from the new admin is the check. If the new principal did not take, step 2 fails with ERR_UNAUTHORIZED_ADMIN and the old admin is still in place. Removing the old admin from the old admin gives you no way to find that out.

On SPMPMA1V6P430M8C91QS1G9XJ95S59JS1TZFZ4Q4.fastpool-max500-signer-manager step 2 is the only route, because an admin cannot remove themselves and trying fails with ERR_CANNOT_REMOVE_SELF (u1015). Since the caller has already been established as an admin and cannot be the principal being removed, at least one admin always survives the call. SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.fastpool-1-signer-manager has no such guard, so an admin there can remove themselves and leave the contract with none.

Both check the caller the same way:

So the call has to come from the admin account directly rather than through another contract. Confirm the result with the read-only is-admin, which takes a principal. There is no cap on the number of admins.

Admin controls register-self, update-fees, withdraw-fees and sweep-fee-refunds, so a manager left with no admin can never register a signer key again and any accrued fees are stranded.


Rotate a Bitcoin payout address

This is a staker action, and it lives at the signer-manager layer rather than in pox-5. pox-5 has no pox-addr parameter anywhere: rewards are sBTC by default, and a native BTC payout is an election carried in signer-calldata alongside a staking action.

For the reference signer-manager, signer-calldata deserializes to { pox-addr, max-fee } and is stored per staker. Every staking action validates and rewrites that stored entry, so re-supplying calldata through stake-update changes the address. The election is not fixed for the term of the position.

Unlike the key rotation, these paths are prepare-phase gated and fail with ERR_STAKE_IN_PREPARE_PHASE (u47) during the last 100 Bitcoin blocks of a cycle.

The deployed signer-managers do not all take the same calldata. SP21YTSM60CAY6D011EZVEVNKXVW8FVZE198XEFFP.fastpool-1-signer-manager matches the reference implementation. SPMPMA1V6P430M8C91QS1G9XJ95S59JS1TZFZ4Q4.fastpool-max500-signer-manager is a separate rewrite with a different payout configuration. Build calldata against the contract you are staking to. Stake to an Existing Signer-Manager covers the differences.


Key hygiene

Keep the signer key and the manager admin key separate. They authorize different things and neither substitutes for the other. Only an admin can call register-self. Only the principal derived from the signer key can revoke a grant. In a normal setup they belong to different people.

Secure the admin key, and consider holding two. The admin key can be rotated, which is an improvement on the PoX-4 pool operator key, but it also controls the manager's fee and fee withdrawal. Two admin keys, one warm and one in deeper cold storage, means neither a lost key nor a compromised one is terminal. The case gets stronger the more the manager handles: a manager taking a high fee holds proportionally more of other people's rewards. Deploy a Signer-Manager Contract covers the differences between the deployed contracts.

Limit signer key exposure. It lives on the signer host and is the one key that has to be online. It signs the grant and it signs blocks, and it should do nothing else. See OpSec Best Practices.


If you knew rotation under PoX-4

The corrections to the old model, in one place. The source of each is the previous version of this page.

  • stack-extend and stack-increase took a signer-key argument, so rotation happened per transaction and per cycle. Those functions do not exist in pox-5. A signer-manager holds one key through a grant, and rotation is a contract call on the manager rather than a stacking parameter.

  • There is no DKG. PoX-4-era signers ran a distributed key generation round each cycle, which is why the old advice was to have the new key running before the prepare phase, called the DKG window. The node now validates individual signer signatures weighted by stake. The prepare phase still sets the deadline, for the reward-set snapshot described above, but not for DKG.

  • pox-addr is not a transaction argument. stack-extend, stack-aggregation-commit-indexed and delegate-stack-stx each took one. pox-5 has none of those functions and no pox-addr parameter.

  • There is no pool operator key. delegate-stx recorded the operator principal each member authorized, which is what made that key unrotatable without every member re-delegating. Delegation is gone. A pool under PoX-5 is a signer-manager that other people stake to, and its admin key can be rotated.


Additional resources

Last updated

Was this helpful?