diff --git a/Cargo.lock b/Cargo.lock index 53b2d35e6..d3de09041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2730,6 +2730,37 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "aptos-framework-core-resources-rotation-test" +version = "0.3.4" +dependencies = [ + "anyhow", + "aptos-crypto 0.0.3 (git+https://github.com/movementlabsxyz/aptos-core?rev=1d1cdbbd7fabb80dcb95ba5e23213faa072fab67)", + "aptos-framework 0.1.0 (git+https://github.com/movementlabsxyz/aptos-core?rev=1d1cdbbd7fabb80dcb95ba5e23213faa072fab67)", + "aptos-framework-release-script-release", + "aptos-framework-set-feature-flags-release", + "aptos-framework-upgrade-gas-release", + "aptos-gas-schedule 0.1.0 (git+https://github.com/movementlabsxyz/aptos-core?rev=1d1cdbbd7fabb80dcb95ba5e23213faa072fab67)", + "aptos-release-builder", + "aptos-sdk 0.0.3 (git+https://github.com/movementlabsxyz/aptos-core?rev=1d1cdbbd7fabb80dcb95ba5e23213faa072fab67)", + "aptos-types 0.0.3 (git+https://github.com/movementlabsxyz/aptos-core?rev=1d1cdbbd7fabb80dcb95ba5e23213faa072fab67)", + "bcs 0.1.6 (git+https://github.com/movementlabsxyz/bcs.git?rev=bc16d2d39cabafaabd76173dd1b04b2aa170cf0c)", + "dot-movement", + "ed25519-dalek 2.1.1", + "hex", + "maptos-framework-release-util", + "movement-client", + "movement-config", + "once_cell", + "rand 0.7.3", + "serde", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "url", +] + [[package]] name = "aptos-framework-elsa-release" version = "0.3.4" diff --git a/networks/movement/movement-client/Cargo.toml b/networks/movement/movement-client/Cargo.toml index 2f72744d5..ed0fcd61e 100644 --- a/networks/movement/movement-client/Cargo.toml +++ b/networks/movement/movement-client/Cargo.toml @@ -58,6 +58,10 @@ path = "src/bin/e2e/key_rotation.rs" name = "movement-tests-sequence-number-ooo" path = "src/bin/e2e/sequence_number_ooo.rs" +[[bin]] +name = "movement-tests-core-resources-rotation" +path = "src/bin/e2e/core_resources_rotation.rs" + [dependencies] aptos-language-e2e-tests = { workspace = true } aptos-sdk = { workspace = true } diff --git a/networks/movement/movement-client/src/bin/e2e/core_resources_rotation.rs b/networks/movement/movement-client/src/bin/e2e/core_resources_rotation.rs new file mode 100644 index 000000000..d8e3e61d0 --- /dev/null +++ b/networks/movement/movement-client/src/bin/e2e/core_resources_rotation.rs @@ -0,0 +1,339 @@ +use anyhow::Context; +use aptos_sdk::{ + coin_client::CoinClient, + crypto::{SigningKey, ValidCryptoMaterialStringExt}, + move_types::{ + identifier::Identifier, + language_storage::{ModuleId, TypeTag}, + }, + rest_client::{Client, FaucetClient, Transaction}, + transaction_builder::TransactionFactory, + types::{account_address::AccountAddress, transaction::TransactionPayload}, +}; +use aptos_types::{ + account_config::{RotationProofChallenge, CORE_CODE_ADDRESS}, + chain_id::ChainId, + transaction::EntryFunction, +}; +use movement_client::types::LocalAccount; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +//use tokio::process::Command; +use movement_client::crypto::ed25519::Ed25519PrivateKey; +use tracing::info; +use tracing_subscriber::EnvFilter; +use url::Url; + +/// limit of gas unit +const GAS_UNIT_LIMIT: u64 = 100000; +/// minimum price of gas unit of aptos chains +pub const GAS_UNIT_PRICE: u64 = 100; + +static SUZUKA_CONFIG: Lazy = Lazy::new(|| { + let dot_movement = dot_movement::DotMovement::try_from_env().unwrap(); + dot_movement.try_get_config_from_json::().unwrap() +}); + +static NODE_URL: Lazy = Lazy::new(|| { + let node_connection_address = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_hostname + .clone(); + let node_connection_port = + SUZUKA_CONFIG.execution_config.maptos_config.client.maptos_rest_connection_port; + let node_connection_url = + format!("http://{}:{}", node_connection_address, node_connection_port); + Url::from_str(&node_connection_url).unwrap() +}); + +static FAUCET_URL: Lazy = Lazy::new(|| { + let faucet_listen_address = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_hostname + .clone(); + let faucet_listen_port = SUZUKA_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_port; + + let faucet_listen_url = format!("http://{}:{}", faucet_listen_address, faucet_listen_port); + + Url::from_str(faucet_listen_url.as_str()).unwrap() +}); + +#[derive(Serialize, Deserialize)] +struct RotationCapabilityOfferProofChallengeV2 { + account_address: AccountAddress, + module_name: String, + struct_name: String, + chain_id: u8, + sequence_number: u64, + source_address: AccountAddress, + recipient_address: AccountAddress, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + // Initialize clients + let rest_client = Client::new(NODE_URL.clone()); + let faucet_client = FaucetClient::new(FAUCET_URL.clone(), NODE_URL.clone()); + let coin_client = CoinClient::new(&rest_client); + + // Load core resource account + let raw_private_key = SUZUKA_CONFIG + .execution_config + .maptos_config + .chain + .maptos_private_key_signer_identifier + .try_raw_private_key()?; + let private_key = Ed25519PrivateKey::try_from(raw_private_key.as_slice())?; + let mut core_resources_account = + LocalAccount::from_private_key(private_key.to_encoded_string()?.as_str(), 0)?; + info!( + "Core Resources Account keypairs: {:?}, {:?}", + core_resources_account.private_key(), + core_resources_account.public_key() + ); + info!("Core Resources Account address: {}", core_resources_account.address()); + + // Fund the account + faucet_client.fund(core_resources_account.address(), 100_000_000_000).await?; + + let state = rest_client + .get_ledger_information() + .await + .context("Failed in getting chain id")? + .into_inner(); + + // Generate recipient account + let recipient = LocalAccount::generate(&mut rand::rngs::OsRng); + + faucet_client.fund(recipient.address(), 100_000_000_000).await?; + + let recipient_bal = coin_client + .get_account_balance(&recipient.address()) + .await + .context("Failed to get recipient's account balance")?; + + let core_resource_bal = coin_client + .get_account_balance(&core_resources_account.address()) + .await + .context("Failed to get core resources account balance")?; + + info!("Recipient's balance: {:?}", recipient_bal); + info!("Core Resources Account balance: {:?}", core_resource_bal); + + // --- Offer Rotation Capability --- + let rotation_capability_proof = RotationCapabilityOfferProofChallengeV2 { + account_address: CORE_CODE_ADDRESS, + module_name: String::from("account"), + struct_name: String::from("RotationCapabilityOfferProofChallengeV2"), + chain_id: state.chain_id, + sequence_number: core_resources_account.increment_sequence_number(), + source_address: core_resources_account.address(), + recipient_address: recipient.address(), + }; + + let rotation_capability_proof_msg = bcs::to_bytes(&rotation_capability_proof) + .context("Failed to serialize rotation capability proof challenge")?; + let rotation_proof_signed = core_resources_account + .private_key() + .sign_arbitrary_message(&rotation_capability_proof_msg); + + let is_valid = verify_signature( + &core_resources_account.public_key().to_bytes(), + &rotation_capability_proof_msg, + &rotation_proof_signed.to_bytes(), + )?; + + assert!(is_valid, "Signature verification failed!"); + info!("Signature successfully verified!"); + + let offer_payload = make_entry_function_payload( + CORE_CODE_ADDRESS, + "account", + "offer_rotation_capability", + vec![], + vec![ + bcs::to_bytes(&rotation_proof_signed.to_bytes().to_vec()) + .context("Failed to serialize rotation capability signature")?, + bcs::to_bytes(&0u8).context("Failed to serialize account scheme")?, + bcs::to_bytes(&core_resources_account.public_key().to_bytes().to_vec()) + .context("Failed to serialize public key bytes")?, + bcs::to_bytes(&recipient.address()).context("Failed to serialize recipient address")?, + ], + )?; + + core_resources_account.decrement_sequence_number(); + + let offer_response = + send_aptos_transaction(&rest_client, &mut core_resources_account, offer_payload).await?; + info!("Offer transaction response: {:?}", offer_response); + + // --- Rotate Authentication Key --- + let rotation_proof = RotationProofChallenge { + account_address: CORE_CODE_ADDRESS, + module_name: String::from("account"), + struct_name: String::from("RotationProofChallenge"), + sequence_number: core_resources_account.increment_sequence_number(), + originator: core_resources_account.address(), + current_auth_key: AccountAddress::from_bytes(core_resources_account.authentication_key())?, + new_public_key: recipient.public_key().to_bytes().to_vec(), + }; + + let rotation_message = + bcs::to_bytes(&rotation_proof).context("Failed to serialize rotation proof challenge")?; + + let signature_by_curr_privkey = + core_resources_account.private_key().sign_arbitrary_message(&rotation_message); + let signature_by_new_privkey = + recipient.private_key().sign_arbitrary_message(&rotation_message); + + let rotate_payload = make_entry_function_payload( + AccountAddress::from_hex_literal("0x1").context("Invalid hex literal for account")?, + "account", + "rotate_authentication_key", + vec![], + vec![ + bcs::to_bytes(&0u8).context("Failed to serialize from_scheme")?, + bcs::to_bytes(&core_resources_account.public_key().to_bytes().to_vec()) + .context("Failed to serialize from_public_key_bytes")?, + bcs::to_bytes(&0u8).context("Failed to serialize to_scheme")?, + bcs::to_bytes(&recipient.public_key().to_bytes().to_vec()) + .context("Failed to serialize to_public_key_bytes")?, + bcs::to_bytes(&signature_by_curr_privkey.to_bytes().to_vec()) + .context("Failed to serialize cap_rotate_key")?, + bcs::to_bytes(&signature_by_new_privkey.to_bytes().to_vec()) + .context("Failed to serialize cap_update_table")?, + ], + )?; + + core_resources_account.decrement_sequence_number(); + + let rotate_response = + send_aptos_transaction(&rest_client, &mut core_resources_account, rotate_payload).await?; + info!("Rotate transaction response: {:?}", rotate_response); + + // Try to transfer with the old private key it should fail + let transfer_result = coin_client + .transfer(&mut core_resources_account, recipient.address(), 42, None) + .await; + + match transfer_result { + Ok(resp) => { + panic!( + "Expected transfer to fail due to INVALID_AUTH_KEY, but got success: {:#?}", + resp + ); + } + Err(err) => { + // Convert error to string or inspect its downcasted type + let err_str = format!("{:?}", err); + assert!( + err_str.contains("INVALID_AUTH_KEY"), + "Expected INVALID_AUTH_KEY error, but got: {}", + err_str + ); + println!("✅ Transfer failed as expected with INVALID_AUTH_KEY error."); + } + } + + // Reconstruct LocalAccount using the recipient's private key (new key post-rotation) + let mut rotated_core_account = LocalAccount::new( + core_resources_account.address(), // Must match the old account's address + recipient.private_key().clone(), // New private key + core_resources_account.sequence_number(), // Continue with correct sequence number + ); + + // Check that the address is unchanged + assert_eq!( + rotated_core_account.address(), + core_resources_account.address(), + "Rotated key should control the same account address" + ); + + // Test signing: send a small transfer or dummy txn + let transfer_check = coin_client + .transfer(&mut rotated_core_account, recipient.address(), 1, None) + .await?; + + println!( + "✅ New private key successfully signed transaction for same account {}: {:#?}", + rotated_core_account.address(), + transfer_check + ); + + Ok(()) +} + +fn make_entry_function_payload( + package_address: AccountAddress, + module_name: &'static str, + function_name: &'static str, + ty_args: Vec, + args: Vec>, +) -> Result { + tracing::info!("Creating entry function payload for package address: {:?}", package_address); + + let module_id = ModuleId::new( + package_address, + Identifier::new(module_name).context("Invalid module name")?, + ); + + let function_id = Identifier::new(function_name).context("Invalid function name")?; + + Ok(TransactionPayload::EntryFunction(EntryFunction::new(module_id, function_id, ty_args, args))) +} + +fn verify_signature( + public_key_bytes: &[u8; 32], + message: &[u8], + signature_bytes: &[u8; 64], +) -> Result { + use ed25519_dalek::{Signature, Verifier, VerifyingKey}; + + let verifying_key = + VerifyingKey::from_bytes(public_key_bytes).context("Failed to parse public key bytes")?; + + let signature = Signature::from_bytes(signature_bytes); + + Ok(verifying_key.verify(message, &signature).is_ok()) +} + +async fn send_aptos_transaction( + client: &Client, + signer: &mut LocalAccount, + payload: TransactionPayload, +) -> anyhow::Result { + let state = client + .get_ledger_information() + .await + .context("Failed to retrieve ledger information")? + .into_inner(); + + let transaction_factory = TransactionFactory::new(ChainId::new(state.chain_id)) + .with_gas_unit_price(100) + .with_max_gas_amount(GAS_UNIT_LIMIT); + + let signed_tx = signer.sign_with_transaction_builder(transaction_factory.payload(payload)); + + let response = client + .submit_and_wait(&signed_tx) + .await + .context("Failed to submit and wait for transaction")? + .into_inner(); + + Ok(response) +} diff --git a/process-compose/movement-full-node/process-compose.test-core-resources-rotation.yml b/process-compose/movement-full-node/process-compose.test-core-resources-rotation.yml new file mode 100644 index 000000000..c7f2d5dee --- /dev/null +++ b/process-compose/movement-full-node/process-compose.test-core-resources-rotation.yml @@ -0,0 +1,24 @@ +version: "3" + +environment: + +processes: + + setup: + environment: + - APTOS_ACCOUNT_WHITELIST=$DOT_MOVEMENT_PATH/default_signer_address_whitelist + - MAPTOS_PRIVATE_KEY=random + + movement-faucet: + command : | + movement-faucet-service run-simple --do-not-delegate + + test-core-resources-rotation: + command : | + cargo run --bin movement-tests-e2e-key-rotation + depends_on: + movement-full-node: + condition: process_healthy + movement-faucet: + condition: process_healthy + diff --git a/process-compose/movement-full-node/process-compose.test-upgrade-framework-rotated-key.yml b/process-compose/movement-full-node/process-compose.test-upgrade-framework-rotated-key.yml new file mode 100644 index 000000000..fb574982e --- /dev/null +++ b/process-compose/movement-full-node/process-compose.test-upgrade-framework-rotated-key.yml @@ -0,0 +1,24 @@ +version: "3" + +environment: + +processes: + + setup: + environment: + - "KNOWN_FRAMEWORK_RELEASE=biarritz-rc1" + - APTOS_ACCOUNT_WHITELIST=$DOT_MOVEMENT_PATH/default_signer_address_whitelist + - MAPTOS_PRIVATE_KEY=random + + movement-faucet: + command : | + movement-faucet-service run-simple --do-not-delegate + + test-framework-upgrade-with-rotated-key: + command: | + cargo run --bin aptos-framework-core-resources-rotation + depends_on: + movement-full-node: + condition: process_healthy + movement-faucet: + condition: process_healthy diff --git a/protocol-units/execution/maptos/framework/releases/core-resources-rotation/Cargo.toml b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/Cargo.toml new file mode 100644 index 000000000..50be7bfcd --- /dev/null +++ b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "aptos-framework-core-resources-rotation-test" +description = "Core Resources Rotation Release Test" +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +version = { workspace = true } + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "aptos-framework-core-resources-rotation" +path = "src/bin/release.rs" + +[dependencies] +aptos-crypto = { workspace = true } +serde = { workspace = true } +maptos-framework-release-util = { workspace = true } +movement-config = { workspace = true } +movement-client = { workspace = true } +once_cell = { workspace = true } +aptos-framework = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +url = { workspace = true } +dot-movement = { workspace = true } +tempfile = { workspace = true } +bcs = { workspace = true } +aptos-framework-upgrade-gas-release = { workspace = true } +aptos-framework-set-feature-flags-release = { workspace = true } +aptos-framework-release-script-release = { workspace = true } +aptos-types = { workspace = true } +aptos-gas-schedule = { workspace = true } +aptos-sdk = { workspace = true } +aptos-release-builder = { workspace = true } +hex = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +rand = { workspace = true } +ed25519-dalek = { workspace = true } + +[build-dependencies] +maptos-framework-release-util = { workspace = true } +movement-config = { workspace = true } +movement-client = { workspace = true } +once_cell = { workspace = true } +aptos-framework = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } +url = { workspace = true } +dot-movement = { workspace = true } +tempfile = { workspace = true } +bcs = { workspace = true } diff --git a/protocol-units/execution/maptos/framework/releases/core-resources-rotation/build.rs b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/build.rs new file mode 100644 index 000000000..64875ef83 --- /dev/null +++ b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/build.rs @@ -0,0 +1,11 @@ +use maptos_framework_release_util::commit_hash_with_script; + +// change +commit_hash_with_script!( + PreL1Merge, // Struct name + "https://github.com/movementlabsxyz/aptos-core.git", // Repository URL + "edafe2e5ed6ce462fa81d08faf5d5008fa836ca2", // Commit hash + 6, // Bytecode version + "pre-l1-merge.mrb", // MRB file name + "CACHE_PRE_L1_MERGE_FRAMEWORK_RELEASE" // Cache environment variable +); diff --git a/protocol-units/execution/maptos/framework/releases/core-resources-rotation/mrb_cache/edafe2e5ed6ce462fa81d08faf5d5008fa836ca2-pre-l1-merge.mrb b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/mrb_cache/edafe2e5ed6ce462fa81d08faf5d5008fa836ca2-pre-l1-merge.mrb new file mode 100644 index 000000000..99fa96bf8 Binary files /dev/null and b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/mrb_cache/edafe2e5ed6ce462fa81d08faf5d5008fa836ca2-pre-l1-merge.mrb differ diff --git a/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/bin/release.rs b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/bin/release.rs new file mode 100644 index 000000000..d0b5bd941 --- /dev/null +++ b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/bin/release.rs @@ -0,0 +1,252 @@ +use aptos_crypto::ed25519::Ed25519PublicKey; +use aptos_framework_core_resources_rotation_test::cached::PreL1Merge; +use aptos_sdk::crypto::SigningKey; +use aptos_sdk::{ + rest_client::{Client, FaucetClient, Transaction}, + transaction_builder::TransactionFactory, + types::{account_address::AccountAddress, transaction::TransactionPayload}, +}; +use aptos_types::transaction::authenticator::AuthenticationKey; +use aptos_types::{ + account_config::{RotationProofChallenge, CORE_CODE_ADDRESS}, + chain_id::ChainId, + transaction::EntryFunction, +}; +use maptos_framework_release_util::{LocalAccountReleaseSigner, Release}; +use movement_client::types::{account_config::aptos_test_root_address, LocalAccount}; +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tracing::info; +use url::Url; + +const GAS_UNIT_LIMIT: u64 = 100_000; + +static MOVEMENT_CONFIG: Lazy = Lazy::new(|| { + let dot_movement = dot_movement::DotMovement::try_from_env().unwrap(); + dot_movement.try_get_config_from_json::().unwrap() +}); + +static NODE_URL: Lazy = Lazy::new(|| { + let addr = MOVEMENT_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_hostname + .clone(); + let port = MOVEMENT_CONFIG + .execution_config + .maptos_config + .client + .maptos_rest_connection_port + .clone(); + Url::from_str(&format!("http://{}:{}", addr, port)).unwrap() +}); + +static FAUCET_URL: Lazy = Lazy::new(|| { + let addr = MOVEMENT_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_hostname + .clone(); + let port = MOVEMENT_CONFIG + .execution_config + .maptos_config + .client + .maptos_faucet_rest_connection_port; + Url::from_str(&format!("http://{}:{}", addr, port)).unwrap() +}); + +#[derive(Serialize, Deserialize)] +struct RotationCapabilityOfferProofChallengeV2 { + account_address: AccountAddress, + module_name: String, + struct_name: String, + chain_id: u8, + sequence_number: u64, + source_address: AccountAddress, + recipient_address: AccountAddress, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + tracing_subscriber::fmt().with_env_filter("info").init(); + + let rest_client = Client::new(NODE_URL.clone()); + let faucet_client = FaucetClient::new(FAUCET_URL.clone(), NODE_URL.clone()); + + // Governance release object + let pre_l1_merge = PreL1Merge::new(); + + // Core resources (aptos_test_root) address + let gov_root_address = aptos_test_root_address(); + info!("aptos_test_root_address() (constant): {}", gov_root_address); + + // Load *core_resources* private key (from your config/genesis) + let raw_private_key = MOVEMENT_CONFIG + .execution_config + .maptos_config + .chain + .maptos_private_key_signer_identifier + .try_raw_private_key()?; + let gov_priv = + movement_client::crypto::ed25519::Ed25519PrivateKey::try_from(raw_private_key.as_slice())?; + + // Build signer by *forcing* core_resources address + current on-chain seq + let mut gov_root_account = { + let onchain = rest_client.get_account(gov_root_address).await?.into_inner(); + LocalAccount::new(gov_root_address, gov_priv.clone(), onchain.sequence_number) + }; + info!("Signer (gov_root_account) address: {}", gov_root_account.address()); + + // Fund signer and recipient (localnet) + faucet_client.fund(gov_root_account.address(), 100_000_000_000).await?; + let recipient = LocalAccount::generate(&mut rand::rngs::OsRng); + faucet_client.fund(recipient.address(), 100_000_000_000).await?; + + // Helper to refresh the sequence JUST-IN-TIME before each tx + async fn refresh_seq(client: &Client, acct: &mut LocalAccount) -> anyhow::Result { + let info = client.get_account(acct.address()).await?.into_inner(); + // LocalAccount in movement_client exposes a constructor; safest is to rebuild: + let pk = acct.private_key().clone(); + *acct = LocalAccount::new(acct.address(), pk, info.sequence_number); + Ok(info.sequence_number) + } + + // --- Offer Rotation Capability --- + let ledger_info = rest_client.get_ledger_information().await?.into_inner(); + + // Use fresh seq for the *challenge* (do not pre-increment) + let seq_for_offer = refresh_seq(&rest_client, &mut gov_root_account).await?; + let rotation_capability_proof = RotationCapabilityOfferProofChallengeV2 { + account_address: CORE_CODE_ADDRESS, + module_name: "account".to_string(), + struct_name: "RotationCapabilityOfferProofChallengeV2".to_string(), + chain_id: ledger_info.chain_id, + sequence_number: seq_for_offer, + source_address: gov_root_account.address(), + recipient_address: recipient.address(), + }; + + let proof_msg = bcs::to_bytes(&rotation_capability_proof)?; + let proof_signed = gov_root_account.private_key().sign_arbitrary_message(&proof_msg); + + let offer_payload = make_entry_function_payload( + CORE_CODE_ADDRESS, + "account", + "offer_rotation_capability", + vec![], + vec![ + bcs::to_bytes(&proof_signed.to_bytes().to_vec())?, + bcs::to_bytes(&0u8)?, // from_scheme = Ed25519 + bcs::to_bytes(&gov_root_account.public_key().to_bytes().to_vec())?, + bcs::to_bytes(&recipient.address())?, + ], + )?; + + // Refresh seq again right before submit (in case another tx raced) + refresh_seq(&rest_client, &mut gov_root_account).await?; + send_aptos_transaction(&rest_client, &mut gov_root_account, offer_payload).await?; + info!(" Offer rotation capability submitted."); + + // --- Rotate Authentication Key --- + let seq_for_rotate = refresh_seq(&rest_client, &mut gov_root_account).await?; + let rotation_proof = RotationProofChallenge { + account_address: CORE_CODE_ADDRESS, + module_name: "account".to_string(), + struct_name: "RotationProofChallenge".to_string(), + sequence_number: seq_for_rotate, + originator: gov_root_account.address(), + current_auth_key: AccountAddress::from_bytes(gov_root_account.authentication_key())?, + new_public_key: recipient.public_key().to_bytes().to_vec(), + }; + + let rotation_msg = bcs::to_bytes(&rotation_proof)?; + let sig_curr = gov_root_account.private_key().sign_arbitrary_message(&rotation_msg); + let sig_new = recipient.private_key().sign_arbitrary_message(&rotation_msg); + + let rotate_payload = make_entry_function_payload( + CORE_CODE_ADDRESS, + "account", + "rotate_authentication_key", + vec![], + vec![ + bcs::to_bytes(&0u8)?, // from_scheme = Ed25519 + bcs::to_bytes(&gov_root_account.public_key().to_bytes().to_vec())?, + bcs::to_bytes(&0u8)?, // to_scheme = Ed25519 + bcs::to_bytes(&recipient.public_key().to_bytes().to_vec())?, + bcs::to_bytes(&sig_curr.to_bytes().to_vec())?, + bcs::to_bytes(&sig_new.to_bytes().to_vec())?, + ], + )?; + + // Refresh seq again just before submit + refresh_seq(&rest_client, &mut gov_root_account).await?; + send_aptos_transaction(&rest_client, &mut gov_root_account, rotate_payload).await?; + info!("✅ Authentication key rotated successfully."); + + // --- Verify Rotation (read back the *same* account you rotated) --- + let updated_info = rest_client.get_account(gov_root_account.address()).await?.into_inner(); + + // Compute expected auth key exactly as the framework + let recip_pub = Ed25519PublicKey::try_from(recipient.public_key().to_bytes().as_slice()) + .expect("recipient pubkey parse"); + let expected_auth_key = AuthenticationKey::ed25519(&recip_pub); + + info!("on-chain auth_key: {:?}", updated_info.authentication_key); + info!("expected auth_key: {:?}", expected_auth_key); + info!("helper auth_key(?): {:?}", recipient.authentication_key()); + + assert_eq!( + updated_info.authentication_key, expected_auth_key, + "On-chain authentication key must match expected ed25519 recipient key" + ); + + // --- Build rotated governance signer for subsequent governance actions --- + let rotated_gov_account = LocalAccount::new( + gov_root_account.address(), + recipient.private_key().clone(), + updated_info.sequence_number, + ); + let rotated_release_signer = + LocalAccountReleaseSigner::new(rotated_gov_account, Some(aptos_test_root_address())); + + // --- Submit governance proposal with rotated signer --- + let rest_client = movement_client::rest_client::Client::new(NODE_URL.clone()); + pre_l1_merge + .release(&rotated_release_signer, 2_000_000, 100, 60, &rest_client) + .await?; + + info!("✅ Governance release successfully signed using rotated aptos_test_root_address!"); + Ok(()) +} + +// --- Helpers --- +fn make_entry_function_payload( + package_address: AccountAddress, + module_name: &'static str, + function_name: &'static str, + ty_args: Vec, + args: Vec>, +) -> Result { + let module_id = aptos_sdk::move_types::language_storage::ModuleId::new( + package_address, + aptos_sdk::move_types::identifier::Identifier::new(module_name)?, + ); + let function_id = aptos_sdk::move_types::identifier::Identifier::new(function_name)?; + Ok(TransactionPayload::EntryFunction(EntryFunction::new(module_id, function_id, ty_args, args))) +} + +async fn send_aptos_transaction( + client: &Client, + signer: &mut LocalAccount, + payload: TransactionPayload, +) -> anyhow::Result { + let state = client.get_ledger_information().await?.into_inner(); + let factory = TransactionFactory::new(ChainId::new(state.chain_id)) + .with_gas_unit_price(100) + .with_max_gas_amount(GAS_UNIT_LIMIT); + let signed_tx = signer.sign_with_transaction_builder(factory.payload(payload)); + Ok(client.submit_and_wait(&signed_tx).await?.into_inner()) +} diff --git a/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/cached.rs b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/cached.rs new file mode 100644 index 000000000..76ccadeac --- /dev/null +++ b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/cached.rs @@ -0,0 +1,74 @@ +use aptos_framework_upgrade_gas_release::generate_gas_upgrade_module; +use maptos_framework_release_util::mrb_release; + +mrb_release!(PreL1Merge, BIARRTIZ_RC1, "edafe2e5ed6ce462fa81d08faf5d5008fa836ca2-pre-l1-merge.mrb"); + +generate_gas_upgrade_module!(gas_upgrade, PreL1Merge, { + let mut gas_parameters = AptosGasParameters::initial(); + gas_parameters.vm.txn.max_transaction_size_in_bytes = GasQuantity::new(100_000_000); + gas_parameters.vm.txn.max_execution_gas = GasQuantity::new(10_000_000_000); + gas_parameters.vm.txn.gas_unit_scaling_factor = GasQuantity::new(50_000); + aptos_types::on_chain_config::GasScheduleV2 { + feature_version: aptos_gas_schedule::LATEST_GAS_FEATURE_VERSION, + entries: gas_parameters + .to_on_chain_gas_schedule(aptos_gas_schedule::LATEST_GAS_FEATURE_VERSION), + } +}); + +pub mod script { + use super::gas_upgrade::PreL1Merge; + use aptos_framework_release_script_release::generate_script_module; + + generate_script_module!(script, PreL1Merge, { + r#" +script { + use aptos_framework::aptos_governance; + use aptos_framework::gas_schedule; + use aptos_framework::governed_gas_pool; + use aptos_framework::aptos_coin; + use aptos_framework::signer; + use aptos_framework::version; + + fun main(core_resources: &signer) { + let core_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + + let core_address: address = signer::address_of(core_resources); + + // this initialize function is idempotent, already initialized GGP will not error. + governed_gas_pool::initialize(&core_signer, b"aptos_framework::governed_gas_pool"); + } +} +"# + .to_string() + }); +} + +pub mod full { + + use super::script::script::PreL1Merge; + use aptos_framework_set_feature_flags_release::generate_feature_upgrade_module; + + generate_feature_upgrade_module!(feature_upgrade, PreL1Merge, { + use aptos_release_builder::components::feature_flags::FeatureFlag; + use aptos_types::on_chain_config::FeatureFlag as AptosFeatureFlag; + + // start with the default features and append the Governed Gas Pool feature + let mut enable_feature_flags = AptosFeatureFlag::default_features(); + // Note: when testing into the future, you may have to use a different revision of [aptos_types] in this crate's Cargo.toml + // Or, I suppose you can keep and GOVERNED_GAS_POOL feature flag and a GOVERNED_GAS_POOL_V2 feature flag and just make sure you're disabling the former and enabling the latter. Thereafter, it won't matter what happens to the GOVERNED_GAS_POOL feature flag, i.e., it can be replaced. + enable_feature_flags.push(AptosFeatureFlag::GOVERNED_GAS_POOL); + + // Note: before the upgrade to the newest version to the Aptos framework + // we need to activate features that are currently active on the Aptos testnet + // See: https://github.com/movementlabsxyz/movement-migration/issues/30#issuecomment-2862738427 + // aptos_feature_flags.push(AptosFeatureFlag::PERIODICAL_REWARD_RATE_DECREASE); + enable_feature_flags.push(AptosFeatureFlag::PARTIAL_GOVERNANCE_VOTING); + enable_feature_flags.push(AptosFeatureFlag::DELEGATION_POOL_PARTIAL_GOVERNANCE_VOTING); + enable_feature_flags.push(AptosFeatureFlag::VM_BINARY_FORMAT_V7); + + Features { + enabled: enable_feature_flags.into_iter().map(FeatureFlag::from).collect(), + disabled: vec![AptosFeatureFlag::REMOVE_DETAILED_ERROR_FROM_HASH.into()], + } + }); +} diff --git a/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/lib.rs b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/lib.rs new file mode 100644 index 000000000..041e5c91c --- /dev/null +++ b/protocol-units/execution/maptos/framework/releases/core-resources-rotation/src/lib.rs @@ -0,0 +1,29 @@ +pub mod cached; + +use aptos_framework_upgrade_gas_release::generate_gas_upgrade_module; +/// Expose the `maptos_framework_release_util` crate for use in the `pre-l1-merge` release. +pub use maptos_framework_release_util; +use maptos_framework_release_util::commit_hash_with_script; + +// We are going to test the upgrade with the same commit hash +// as the pre-l1-merge, but with the rotated core-resource-account +commit_hash_with_script!( + PreL1Merge, // Struct name + "https://github.com/movementlabsxyz/aptos-core.git", // Repository URL + "edafe2e5ed6ce462fa81d08faf5d5008fa836ca2", // Commit hash + 6, // Bytecode version + "pre-l1-merge.mrb", // MRB file name + "CACHE_PRE_L1_MERGE_FRAMEWORK_RELEASE" // Cache environment variable +); + +generate_gas_upgrade_module!(gas_upgrade, PreL1Merge, { + let mut gas_parameters = AptosGasParameters::initial(); + gas_parameters.vm.txn.max_transaction_size_in_bytes = GasQuantity::new(100_000_000); + gas_parameters.vm.txn.max_execution_gas = GasQuantity::new(10_000_000_000); + + aptos_types::on_chain_config::GasScheduleV2 { + feature_version: aptos_gas_schedule::LATEST_GAS_FEATURE_VERSION, + entries: gas_parameters + .to_on_chain_gas_schedule(aptos_gas_schedule::LATEST_GAS_FEATURE_VERSION), + } +}); diff --git a/protocol-units/execution/maptos/framework/releases/pre-l1-merge/Cargo.toml b/protocol-units/execution/maptos/framework/releases/pre-l1-merge/Cargo.toml index a1ff3eb97..5a93d534b 100644 --- a/protocol-units/execution/maptos/framework/releases/pre-l1-merge/Cargo.toml +++ b/protocol-units/execution/maptos/framework/releases/pre-l1-merge/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aptos-framework-pre-l1-merge-release" -description = "Executor for the AptosVM" +description = "Pre L1 Merge Release" authors = { workspace = true } edition = { workspace = true } homepage = { workspace = true } @@ -23,7 +23,7 @@ movement-client = { workspace = true } once_cell = { workspace = true } aptos-framework = { workspace = true } tokio = { workspace = true } -anyhow = { workspace = true} +anyhow = { workspace = true } url = { workspace = true } dot-movement = { workspace = true } tempfile = { workspace = true } @@ -46,9 +46,8 @@ movement-client = { workspace = true } once_cell = { workspace = true } aptos-framework = { workspace = true } tokio = { workspace = true } -anyhow = { workspace = true} +anyhow = { workspace = true } url = { workspace = true } dot-movement = { workspace = true } tempfile = { workspace = true } bcs = { workspace = true } -