Skip to content

Commit 0612ece

Browse files
committed
Document core resource signer deprecation
1 parent e851693 commit 0612ece

2 files changed

Lines changed: 300 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Movement Core Resource Signer Deprecation
2+
3+
## Status
4+
5+
Draft.
6+
7+
## Summary
8+
9+
Movement mainnet should stop relying on the core resource signer as a privileged path for framework
10+
governance execution. Mainnet framework changes should be authorized by validator governance, using
11+
the existing multi-step proposal flow. Testnet should keep the core resource signer path because it
12+
is useful for operational testing and fast iteration.
13+
14+
The migration has three phases:
15+
16+
1. Prove delegation-pool governance can create proposals and vote.
17+
2. Execute a mainnet framework upgrade that decommissions core resource authority on mainnet.
18+
3. Rotate the core resource account authentication key to an unrecoverable key as defense in depth.
19+
20+
## Goals
21+
22+
- Mainnet framework upgrades are executable only after a governance proposal receives sufficient
23+
validator voting power.
24+
- A core resource account signature can no longer obtain a framework signer on Movement mainnet.
25+
- Delegation-pool owners and voters can participate in proposal creation and voting through the
26+
delegation-pool governance path.
27+
- Testnet root-signer workflows remain available.
28+
- Historical proposal artifacts under `movement-migration/` remain unchanged.
29+
30+
## Non-Goals
31+
32+
- Remove the core resource account from genesis.
33+
- Remove testnet root-signer workflows.
34+
- Introduce a new release-builder execution mode if the existing `MultiStep` governance mode is
35+
sufficient.
36+
- Rewrite historical proposals that have already been generated or executed.
37+
38+
## Current State
39+
40+
Mainnet currently has two relevant authority paths:
41+
42+
- Governance path: proposals are created, voted on, and resolved by `aptos_governance`.
43+
- Core resource shortcut: scripts signed by `@core_resources` can call
44+
`aptos_governance::get_signer_testnet_only(core_resources, signer_address)` and receive a signer
45+
for a framework-controlled address.
46+
47+
The shortcut is intended for tests and testnets, but it is still callable wherever
48+
`system_addresses::assert_core_resource` accepts `@core_resources`.
49+
50+
The framework already contains a transient feature flag:
51+
52+
```move
53+
std::features::get_decommission_core_resources_enabled()
54+
```
55+
56+
`system_addresses::is_core_resource_address` returns false when this feature flag is enabled. Since
57+
`get_signer_testnet_only` calls `system_addresses::assert_core_resource`, enabling this feature
58+
prevents the core resource signer from using that function.
59+
60+
Movement chain IDs in Rust are:
61+
62+
- Movement mainnet: `126`
63+
- Movement testnet: `250`
64+
65+
## Proposed Design
66+
67+
### 1. Validate Delegation-Pool Governance
68+
69+
Add and run a focused test that verifies a delegation-pool owner can:
70+
71+
- create a governance proposal through `delegation_pool::create_proposal`;
72+
- vote on the proposal through `delegation_pool::vote`;
73+
- consume the expected voting power.
74+
75+
This validates the operator-facing path that Movement expects to use for mainnet proposal
76+
participation.
77+
78+
Validation command:
79+
80+
```bash
81+
RUST_MIN_STACK=16777216 TEST_FILTER=test_delegation_pool_owner_can_create_and_vote \
82+
cargo test -p aptos-framework --test move_unit_test move_framework_unit_tests -- --nocapture
83+
```
84+
85+
### 2. Use Existing Multi-Step Governance for Mainnet Releases
86+
87+
Movement mainnet should use the existing `MultiStep` proposal mode, even for a one-script proposal.
88+
For a single executable step, the proposal is still created with governance-v2 semantics and the
89+
last step uses an empty next execution hash.
90+
91+
The generated script should resolve governance before obtaining the framework signer:
92+
93+
```move
94+
let framework_signer = aptos_governance::resolve_multi_step_proposal(
95+
proposal_id,
96+
@aptos_framework,
97+
x"",
98+
);
99+
```
100+
101+
This means no new release-builder execution mode is required for the initial migration. Existing
102+
testnet `RootSigner` behavior should remain unchanged.
103+
104+
### 3. Mainnet Framework Upgrade: Decommission Core Resources
105+
106+
The first mainnet governance upgrade should enable the existing
107+
`DECOMMISSION_CORE_RESOURCES` feature flag.
108+
109+
After this feature flag is enabled:
110+
111+
- `system_addresses::is_core_resource_address(@core_resources)` returns false;
112+
- `system_addresses::assert_core_resource(core_resources)` aborts;
113+
- `aptos_governance::get_signer_testnet_only(core_resources, signer_address)` aborts before
114+
returning any signer;
115+
- scripts signed only by `@core_resources` can no longer mint a framework signer on mainnet.
116+
117+
This is the actual security cutover. Key rotation alone is not sufficient before this step, because
118+
the framework would still contain the privileged conversion path.
119+
120+
The implementation should use governance to enable the feature flag. If desired, a follow-up code
121+
change can make `get_signer_testnet_only` explicitly reject Movement mainnet by checking
122+
`chain_id::get() == 126`, but the feature flag is already wired into the core resource assertion and
123+
keeps testnet behavior intact.
124+
125+
### 4. Rotate Core Resource Authentication Key
126+
127+
After the decommissioning feature is active on mainnet, submit a separate governance proposal that
128+
rotates the `@core_resources` authentication key to an unrecoverable value.
129+
130+
This step is defense in depth:
131+
132+
- the framework already rejects core resource signer authority after phase 3;
133+
- the key rotation prevents the old key from submitting ordinary transactions as `@core_resources`;
134+
- operators can verify that transactions signed by the old key are rejected.
135+
136+
The key rotation proposal must resolve governance for the core resource address. It should not use
137+
the old core resource private key to perform the rotation.
138+
139+
## Operator Flow
140+
141+
### Proposal Creation
142+
143+
For stake-pool governance:
144+
145+
```bash
146+
movement governance propose \
147+
--pool-address <stake-pool-address> \
148+
--metadata-url <metadata-url> \
149+
--script-path <script-path> \
150+
--is-multi-step \
151+
--sender-account <delegated-voter-address>
152+
```
153+
154+
For delegation-pool governance:
155+
156+
```bash
157+
movement governance delegation-pool propose \
158+
--delegation-pool-address <delegation-pool-address> \
159+
--metadata-url <metadata-url> \
160+
--script-path <script-path> \
161+
--is-multi-step \
162+
--sender-account <voter-address>
163+
```
164+
165+
### Voting
166+
167+
For stake-pool governance:
168+
169+
```bash
170+
movement governance vote \
171+
--pool-addresses <stake-pool-address> \
172+
--proposal-id <proposal-id> \
173+
--yes \
174+
--sender-account <delegated-voter-address>
175+
```
176+
177+
For delegation-pool governance:
178+
179+
```bash
180+
movement governance delegation-pool vote \
181+
--delegation-pool-address <delegation-pool-address> \
182+
--proposal-id <proposal-id> \
183+
--yes \
184+
--sender-account <voter-address>
185+
```
186+
187+
### Execution
188+
189+
After the proposal succeeds:
190+
191+
```bash
192+
movement governance execute-proposal \
193+
--proposal-id <proposal-id> \
194+
--script-path <script-path> \
195+
--sender-account <executor-address>
196+
```
197+
198+
The executor does not need core resource authority. The script itself must resolve the successful
199+
governance proposal before obtaining the framework signer.
200+
201+
## Verification Checklist
202+
203+
Before mainnet execution:
204+
205+
- Delegation-pool owner create/vote test passes.
206+
- Generated proposal scripts use `resolve_multi_step_proposal`.
207+
- Proposal metadata and script hashes are published and independently verified by validators.
208+
- The decommission proposal enables `DECOMMISSION_CORE_RESOURCES`.
209+
- Testnet `RootSigner` generation is unchanged.
210+
211+
After mainnet execution:
212+
213+
- `std::features::get_decommission_core_resources_enabled()` returns true.
214+
- `system_addresses::is_core_resource_address(@core_resources)` returns false.
215+
- A script calling `get_signer_testnet_only` with `@core_resources` aborts on mainnet.
216+
- The old core resource key cannot execute privileged framework operations.
217+
- Testnet root-signer workflows continue to work on Movement testnet.
218+
219+
## Risks and Mitigations
220+
221+
### Incorrectly Breaking Testnet
222+
223+
Risk: disabling the core resource signer globally would break testnet operations.
224+
225+
Mitigation: use the feature flag only on Movement mainnet, and do not remove the testnet script
226+
generation path.
227+
228+
### Governance Participation Misconfiguration
229+
230+
Risk: delegation-pool operators may assume operator status is enough to vote.
231+
232+
Mitigation: document that delegation-pool voting depends on delegated voting power. The operator can
233+
vote only if they have their own voting power or delegators delegate voting power to them.
234+
235+
### Irreversible Key Rotation Before Framework Cutover
236+
237+
Risk: rotating the core resource key before decommissioning the framework path leaves the privileged
238+
path in code and may complicate rollback.
239+
240+
Mitigation: upgrade the framework first, verify the feature is active, then rotate the auth key in a
241+
separate proposal.
242+
243+
### Proposal Execution Failure
244+
245+
Risk: the decommission proposal succeeds but execution fails due to script or hash mismatch.
246+
247+
Mitigation: use the existing proposal verification tooling and require validators to independently
248+
verify script hash, metadata hash, and expected feature flag changes before voting.
249+
250+
## Rollback
251+
252+
Before execution, rollback is simple: do not execute the proposal.
253+
254+
After the decommissioning proposal executes, rollback would require another successful governance
255+
proposal to disable `DECOMMISSION_CORE_RESOURCES`. After the core resource key is rotated to an
256+
unknown key, rollback must not depend on the core resource account.
257+
258+
## Open Questions
259+
260+
- Should `aptos_governance::get_signer_testnet_only` also include an explicit Movement mainnet
261+
chain-id guard, or is the existing feature flag gate sufficient?
262+
- What exact unrecoverable authentication key should be used for `@core_resources` rotation?
263+
- Should operator runbooks require delegation-pool vote delegation to be configured before the
264+
decommission proposal is submitted?

aptos-move/framework/aptos-framework/sources/delegation_pool.move

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4252,6 +4252,42 @@ module aptos_framework::delegation_pool {
42524252
);
42534253
}
42544254

4255+
#[test(aptos_framework = @aptos_framework, validator = @0x123)]
4256+
public entry fun test_delegation_pool_owner_can_create_and_vote(
4257+
aptos_framework: &signer,
4258+
validator: &signer,
4259+
) acquires DelegationPoolOwnership, DelegationPool, GovernanceRecords, BeneficiaryForOperator, NextCommissionPercentage, DelegationPoolAllowlisting {
4260+
initialize_for_test(aptos_framework);
4261+
aptos_governance::initialize_for_test(
4262+
aptos_framework,
4263+
(10 * ONE_APT as u128),
4264+
100 * ONE_APT,
4265+
1000,
4266+
);
4267+
initialize_test_validator(validator, 100 * ONE_APT, true, false);
4268+
end_aptos_epoch();
4269+
4270+
let validator_address = signer::address_of(validator);
4271+
let pool_address = get_owned_pool_address(validator_address);
4272+
assert!(stake::get_delegated_voter(pool_address) == pool_address, 1);
4273+
assert!(partial_governance_voting_enabled(pool_address), 2);
4274+
assert!(calculate_and_update_voter_total_voting_power(pool_address, validator_address) == 100 * ONE_APT, 3);
4275+
4276+
let execution_hash = vector::empty<u8>();
4277+
vector::push_back(&mut execution_hash, 1);
4278+
create_proposal(
4279+
validator,
4280+
pool_address,
4281+
execution_hash,
4282+
b"",
4283+
b"",
4284+
true,
4285+
);
4286+
4287+
vote(validator, pool_address, 0, 100 * ONE_APT, true);
4288+
assert!(calculate_and_update_remaining_voting_power(pool_address, validator_address, 0) == 0, 4);
4289+
}
4290+
42554291
#[test(
42564292
aptos_framework = @aptos_framework,
42574293
validator = @0x123,

0 commit comments

Comments
 (0)