Account Key Rotation
Aptos Move accounts have a public address, an authentication key, a public key, and a private key. The public address is permanent, always matching the account’s initial authentication key, which is derived from the original private key.
The Aptos account model facilitates the unique ability to rotate an account’s private key. Since an account’s address is the initial authentication key, the ability to sign for an account can be transferred to another private key without changing its public address.
In this guide, we show examples of how to rotate an account’s authentication key using the CLI and few of the various Aptos SDKs.
Here are the installation links for the SDKs we will cover in this example:
Proven and unproven key rotations
Section titled “Proven and unproven key rotations”The onchain logic for key rotation is implemented through two Move APIs:
account::rotate_authentication_key
, which executes a “proven” rotation.account::rotate_authentication_key_call
, which executes an “unproven” rotation.
Proven key rotations
Section titled “Proven key rotations”The account::rotate_authentication_key
API requires a signed
account::RotationProofChallenge
, which proves that the rotation operation is
approved by the private key from both before and after the operation. When the
operation is successful, the account::OriginatingAddress
table is updated
with an entry that maps from the new authentication key to the corresponding
account address.
The account::OriginatingAddress
table is a reverse lookup table that allows
users to query an account address associated with a given authentication key,
and only allows for one entry per authentication key. Hence the requirement of a
signed account::RotationProofChallenge
to ensure that a malicious actor does
not rotate an account’s authentication key to a key that is already in the
table, as this attack would prevent lookup of the valid originating address that
the holder of an authentication key had previously approved.
Notably, the account::OriginatingAddress
table is only updated upon key
rotation, not upon standard account generation. This means that with proven key
rotations, a given private key can theoretically authenticate up to two accounts
at the same time:
- The account address derived from the private key during standard account generation, assuming the account has not undergone any key rotations.
- A second arbitrary address, which has had its authentication key rotated to the given private key.
However, it is considered best practice to only authenticate one account with
a given private key at a time, because whenever the
account::OriginatingAddress
table is updated, the underlying logic first
checks if the rotating account’s initial authentication key is in the table, and
if so, verifies that the rotating account’s address is the one mapped to in the
table.
This means that if an arbitrary account’s authentication key is rotated to
a given private key, the standard account whose address is originally derived
from the private key will not be able to execute its first authentication key
rotation while the associated authentication key is mapped to a second arbitrary
account address in the account::OriginatingAddress
table, because this
operation would fail the check that the rotating account’s address is the one
mapped to in the table (since the table is only updated during rotation, not
upon standard account generation).
To prevent this issue and ensure best practices are followed, you can always run
account::set_originating_address
after generating a new account (see below
CLI tutorial).
Unproven key rotations
Section titled “Unproven key rotations”Unlike account::rotate_authentication_key
, the
account::rotate_authentication_key_call
does not require a signed
account::RotationProofChallenge
. This means that the operation is not proven
in the sense the private key from after the operation has approved the
key rotation. Hence the account::OriginatingAddress
table is not updated
for unproven key rotations, and there is thus no restriction on the number of
accounts that can be authenticated with a given private key. Note that the
aptos
CLI does not currently support unproven key rotations.
While it is technically possible to authenticate as many accounts as you want with a given authentication key via unproven key rotations, it is not considered best practice because this approach does not ensure one-to-one mapping.
If you execute an unproven key rotation, it is suggested that you follow up with
account::set_originating_address
to ensure a one-to-one mapping from
authentication key to account address for ease of originating address lookup
(see below CLI tutorial).
Key rotation with the Aptos CLI
Section titled “Key rotation with the Aptos CLI”-
Start a localnet
Start a localnet:
Terminal window aptos node run-localnetThe localnet is ready when it prints out:
Terminal window Applying post startup steps...Setup is complete, you can now use the localnet! -
Generate a private key
Create a private key corresponding to an authentication key, and thus initial account address, that starts with the vanity prefix
0xaaa
:Terminal window aptos key generate \--assume-yes \--output-file private-key-a \--vanity-prefix 0xaaaExample output
Terminal window {"Result": {"Account Address:": "0xaaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b","PublicKey Path": "private-key-a.pub","PrivateKey Path": "private-key-a"}}This will generate two files:
- A private key at
private-key-a
. - A public key at
private-key-a.pub
.
Since there is not yet an account associated with the authentication key, the following command should fail with a corresponding message:
Terminal window aptos account lookup-address \--public-key-file private-key-a.pub \--url http://localhost:8080Example output
Terminal window {"Error": "API error: API error Error(AccountNotFound): Account not found by Address(0xaaafb224eb00e4d0ef520ce02038ede850893622562a4189b7f6e5d94454ccd9) and Ledger version(1206)"} - A private key at
-
Initialize a profile
Use the private key to initialize
test-profile-1
on the localnet:Terminal window aptos init \--assume-yes \--network local \--private-key-file private-key-a \--profile test-profile-1Example output
Terminal window Configuring for profile test-profile-1Configuring for network LocalUsing command line argument for private keyAccount 0xaaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b doesn\'t exist, creating it and funding it with 100000000 OctasAccount 0xaaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b funded successfully---Aptos CLI is now set up for account 0xaaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b as profile test-profile-1! Run `aptos --help` for more information about commands{"Result": "Success"}Note that you can always view the profile with:
Terminal window aptos config show-profiles --profile test-profile-1Example output
Terminal window {"Result": {"test-profile-1": {"has_private_key": true,"public_key": "0xe0bfe46f41c5be40e7a068e8dff4d6016126b226d947a39262f5b2347217a7e3","account": "aaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b","rest_url": "http://localhost:8080","faucet_url": "http://localhost:8081"}}}However, this will not show the private key, which is hidden by default. If you would like to show the private key:
Terminal window aptos config show-private-key --profile test-profile-1Example output
Terminal window {"Result": "0xcc3b0c38ad99e171263a7af930464313d1fb105d0d8e6a4b13f9b1140563a7dd"} -
Look up address
Now that there is an onchain account associated with the authentication key, you can look up the account address using
aptos account lookup-address
:Terminal window aptos account lookup-address \--public-key-file private-key-a.pub \--url http://localhost:8080Example output
Terminal window {"Result": "aaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b"}Store this address in a shell variable:
Terminal window ADDRESS_A=aaa... -
Look up authentication key
Recall that the address of an account is identical to its authentication key when it is initially created, which means that the account address
aaa...
is identical to the account’s authentication key:Terminal window aptos move view \--args address:$ADDRESS_A \--function-id 0x1::account::get_authentication_key \--url http://localhost:8080Example output
Terminal window {"Result": ["0xaaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b"]}Hence, store the authentication key in a shell variable:
Terminal window AUTH_KEY_A=$ADDRESS_ANote, however, since the account has not yet had its authentication key rotated, there is no corresponding entry in the
account::OriginatingAddress
table:Terminal window aptos move view \--args address:$AUTH_KEY_A \--function-id 0x1::account::originating_address \--url http://localhost:8080Example output
Terminal window {"Result": [{"vec": []}]} -
Set originating address
To ensure an entry in the
account::OriginatingAddress
table for this new account, you can runaccount::set_originating_address
:Terminal window aptos move run \--assume-yes \--function-id 0x1::account::set_originating_address \--profile test-profile-1Example output
Terminal window {"Result": {"transaction_hash": "0x216992ef37a3c2f42aa9f8fed8f94d9f945a00e952dfe96b46123bb5c387ab6c","gas_used": 444,"gas_unit_price": 100,"sender": "aaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b","sequence_number": 0,"success": true,"timestamp_us": 1717809169531279,"version": 3268,"vm_status": "Executed successfully"}}Then you should see an entry in the
account::OriginatingAddress
table:Terminal window aptos move view \--args address:$AUTH_KEY_A \--function-id 0x1::account::originating_address \--url http://localhost:8080Example output
Terminal window {"Result": [{"vec": ["0xaaa5131b4d3fcef8d33ee465c4ee65727e36039f283455be87b1164200572e5b"]}]} -
Rotate authentication key
Generate a new private key:
Terminal window aptos key generate \--assume-yes \--output-file private-key-b \--vanity-prefix 0xbbbExample output
Terminal window {"Result": {"PrivateKey Path": "private-key-b","Account Address:": "0xbbbdb12f4fa23b8fe8711b77f4ab7108f3a22077c5dfe787eed3d048a0b82734","PublicKey Path": "private-key-b.pub"}}Rotate the authentication key of the existing onchain account to the new private key:
Terminal window aptos account rotate-key \--assume-yes \--new-private-key-file private-key-b \--profile test-profile-1 \--save-to-profile test-profile-2Example output
Terminal window {"Result": {"message": "Saved new profile test-profile-2","transaction": {"transaction_hash": "0xe561b710390511203511d15eee6f019a2e43ba32f8e3b7ce6bf812232e3bd27f","gas_used": 449,"gas_unit_price": 100,"sender": "aaa8dc0f5e7a6e820f7b1906d99864412b12274ed259ad06bc2c2d8ee7b51e51","sequence_number": 1,"success": true,"timestamp_us": 1717810059696079,"version": 1109,"vm_status": "Executed successfully"}}} -
Compare profiles
Compare
test-profile-1
(which is now stale) withtest-profile-2
(which is current) noting that the public key has changed, but not the account address:Terminal window aptos config show-profiles --profile test-profile-1aptos config show-profiles --profile test-profile-2Example output
Terminal window {"Result": {"test-profile-1": {"has_private_key": true,"public_key": "0xb517173e68f4116e99c7fa1677058a6ee786a3b9e12447000db7fd85ab99dbdd","account": "aaa8dc0f5e7a6e820f7b1906d99864412b12274ed259ad06bc2c2d8ee7b51e51","rest_url": "http://localhost:8080","faucet_url": "http://localhost:8081"}}}{"Result": {"test-profile-2": {"has_private_key": true,"public_key": "0xadc3dd795fdd8569f59dc7b9900b38a5d7b95348b815de4eb5f00e2c2da07916","account": "aaa8dc0f5e7a6e820f7b1906d99864412b12274ed259ad06bc2c2d8ee7b51e51","rest_url": "http://localhost:8080","faucet_url": "http://localhost:8081"}}}Lookup the new authentication key:
Terminal window aptos move view \--args address:$ADDRESS_A \--function-id 0x1::account::get_authentication_key \--url http://localhost:8080Example output
Terminal window {"Result": ["0xbbbdb12f4fa23b8fe8711b77f4ab7108f3a22077c5dfe787eed3d048a0b82734"]}Store the authentication key in a shell variable:
Terminal window AUTH_KEY_B=bbb... -
Look up originating addresses
Check the originating address for the new authentication key:
Terminal window aptos move view \--args address:$AUTH_KEY_B \--function-id 0x1::account::originating_address \--url http://localhost:8080Example output
Terminal window {"Result": [{"vec": ["0xaaa8dc0f5e7a6e820f7b1906d99864412b12274ed259ad06bc2c2d8ee7b51e51"]}]}Check the originating address for the old authentication key:
Terminal window aptos move view \--args address:$AUTH_KEY_A \--function-id 0x1::account::originating_address \--url http://localhost:8080Example output
Terminal window {"Result": [{"vec": []}]} -
Attempt invalid rotation (same key)
Attempt an invalid rotation where the current authentication key is identical to the new authentication key:
Terminal window aptos account rotate-key \--assume-yes \--new-private-key-file private-key-b \--profile test-profile-2 \--skip-saving-profileExample output
Terminal window {"Error": "Invalid arguments: New public key cannot be the same as the current public key"} -
Attempt invalid rotation (new key already mapped)
Create another private key:
Terminal window aptos key generate \--assume-yes \--output-file private-key-c \--vanity-prefix 0xcccExample output
Terminal window {"Result": {"PrivateKey Path": "private-key-c","PublicKey Path": "private-key-c.pub","Account Address:": "0xccc79d46b2963cb87f2ff32c51eb6c6361e8aa108d334d3183c3016389542958"}}Initialize a new profile:
Terminal window aptos init \--assume-yes \--network local \--private-key-file private-key-c \--profile test-profile-3Example output
Terminal window Configuring for profile test-profile-3Configuring for network LocalUsing command line argument for private keyAccount 0xccc79d46b2963cb87f2ff32c51eb6c6361e8aa108d334d3183c3016389542958 doesn\'t exist, creating it and funding it with 100000000 OctasAccount 0xccc79d46b2963cb87f2ff32c51eb6c6361e8aa108d334d3183c3016389542958 funded successfully---Aptos CLI is now set up for account 0xccc79d46b2963cb87f2ff32c51eb6c6361e8aa108d334d3183c3016389542958 as profile test-profile-3! Run `aptos --help` for more information about commands{"Result": "Success"}Attempt an invalid rotation where the new authentication key is already mapped:
Terminal window aptos account rotate-key \--assume-yes \--max-gas 100000 \--new-private-key-file private-key-b \--profile test-profile-3 \--skip-saving-profile(
--max-gas
is specified here to skip local simulation, which does not print out as descriptive of an error as the actual transaction.)Example output
Terminal window {"Error": "API error: Unknown error Transaction committed on chain, but failed execution: Move abort in 0x1::account: ENEW_AUTH_KEY_ALREADY_MAPPED(0x10015): The new authentication key already has an entry in the `OriginatingAddress` table"} -
Attempt invalid rotation (invalid originating address)
Rotate the authentication key for account
0xaaa...
to use the authentication key for account0xccc...
:Terminal window aptos account rotate-key \--assume-yes \--new-private-key-file private-key-c \--profile test-profile-2 \--save-to-profile test-profile-4Example output
Terminal window {"Result": {"message": "Saved new profile test-profile-4","transaction": {"transaction_hash": "0xa5dec792d82ef7471cdf82b9c957fc79b5815da770ad1dd9232ae4692e4f0895","gas_used": 449,"gas_unit_price": 100,"sender": "aaa8dc0f5e7a6e820f7b1906d99864412b12274ed259ad06bc2c2d8ee7b51e51","sequence_number": 2,"success": true,"timestamp_us": 1717812312772580,"version": 5355,"vm_status": "Executed successfully"}}}Then try to rotate the authentication key for account
0xccc...
for the first time, an operation that is blocked because an entry for the authentication key was established in theaccount::OriginatingAddress
table during the last operation:Terminal window aptos account rotate-key \--assume-yes \--max-gas 100000 \--new-private-key-file private-key-b \--profile test-profile-3 \--skip-saving-profile(
--max-gas
is specified here to skip local simulation, which does not print out as descriptive of an error as the actual transaction.)Example output
Terminal window {"Error": "API error: Unknown error Transaction committed on chain, but failed execution: Move abort in 0x1::account: EINVALID_ORIGINATING_ADDRESS(0x6000d): Abort the transaction if the expected originating address is different from the originating address on-chain"} -
Clean up
Delete the test profiles:
Terminal window aptos config delete-profile --profile test-profile-1aptos config delete-profile --profile test-profile-2aptos config delete-profile --profile test-profile-3aptos config delete-profile --profile test-profile-4Then you can stop the localnet and delete the private and public key files.
-
Rotate keys for a Ledger
You can also perform authentication key rotation with a private key that is securely stored on a Ledger hardware wallet. For more information, see the Ledger authentication key rotation guide.
TypeScript key rotation example
Section titled “TypeScript key rotation example”This program creates two accounts on devnet, Alice and Bob, funds them, then rotates the Alice’s authentication key to that of Bob’s.
View the full example for this code here.
The function to rotate is very simple:
Commands to run the example script:
Navigate to the typescript SDK directory, install dependencies and run
Section titled “Navigate to the typescript SDK directory, install dependencies and run”rotate_key.ts
cd ~/aptos-core/ecosystem/typescript/sdk/examples/typescript-esmpnpm install && pnpm rotate_key
rotate_key.ts output
Section titled “rotate_key.ts output”Account Address Auth Key Private Key Public Key------------------------------------------------------------------------------------------------Alice 0x213d...031013 '0x213d...031013' '0x00a4...b2887b' '0x859e...08d2a9'Bob 0x1c06...ac3bb3 0x1c06...ac3bb3 0xf2be...9486aa 0xbbc1...abb808
...rotating...
Alice 0x213d...031013 '0x1c06...ac3bb3' '0xf2be...9486aa' '0xbbc1...abb808'Bob 0x1c06...ac3bb3 0x1c06...ac3bb3 0xf2be...9486aa 0xbbc1...abb808
Python key rotation example
Section titled “Python key rotation example”This program creates two accounts on devnet, Alice and Bob, funds them, then rotates the Alice’s authentication key to that of Bob’s.
View the full example for this code here.
Here’s the relevant code that rotates Alice’s keys to Bob’s:
Commands to run the example script:
Navigate to the python SDK directory, install dependencies and run
Section titled “Navigate to the python SDK directory, install dependencies and run”rotate_key.ts
cd aptos-core/ecosystem/python/sdkpoetry install && poetry run python -m examples.rotate-key
rotate_key.py output
Section titled “rotate_key.py output”Account Address Auth Key Private Key Public Key------------------------------------------------------------------------------------------------Alice 0x213d...031013 '0x213d...031013' '0x00a4...b2887b' '0x859e...08d2a9'Bob 0x1c06...ac3bb3 0x1c06...ac3bb3 0xf2be...9486aa 0xbbc1...abb808
...rotating...
Alice 0x213d...031013 '0x1c06...ac3bb3' '0xf2be...9486aa' '0xbbc1...abb808'Bob 0x1c06...ac3bb3 0x1c06...ac3bb3 0xf2be...9486aa 0xbbc1...abb808