diff --git a/Cargo.lock b/Cargo.lock index 7e4ac01391b19..049550fc11e00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11011,6 +11011,22 @@ dependencies = [ "log", ] +[[package]] +name = "l1-migration" +version = "0.1.0" +dependencies = [ + "anyhow", + "aptos-config", + "aptos-crypto", + "aptos-db", + "aptos-storage-interface", + "aptos-types", + "bcs 0.1.4", + "clap 4.5.21", + "hex", + "serde_yaml 0.8.26", +] + [[package]] name = "lalrpop" version = "0.19.12" @@ -11472,6 +11488,17 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "mach2" version = "0.4.2" @@ -18643,6 +18670,33 @@ dependencies = [ "serde", ] +[[package]] +name = "validation-tool" +version = "0.1.0" +dependencies = [ + "anyhow", + "aptos-config", + "aptos-db", + "aptos-rest-client", + "aptos-sdk", + "aptos-storage-interface", + "aptos-types", + "bcs 0.1.4", + "bytes", + "clap 4.5.21", + "either", + "hex", + "move-core-types", + "serde_json", + "tar", + "tempfile", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", + "xz2", +] + [[package]] name = "valuable" version = "0.1.0" @@ -19358,6 +19412,15 @@ dependencies = [ "rustix 0.38.28", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 4c7e4fc0d759d..c526e08054706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,6 +153,7 @@ members = [ "keyless/pepper/example-client-rust", "keyless/pepper/service", "mempool", + "movement-migration/validation-tool", "network/benchmark", "network/builder", "network/discovery", @@ -252,6 +253,7 @@ members = [ "third_party/move/tools/move-unit-test", "tools/calc-dep-sizes", "tools/compute-module-expansion-size", + "tools/l1-migration", "types", "vm-validator", ] @@ -827,6 +829,7 @@ which = "4.2.5" whoami = "1.5.0" # This allows for zeroize 1.6 to be used. Version 1.2.0 of x25519-dalek locks zeroize to 1.3. x25519-dalek = { git = "https://github.com/aptos-labs/x25519-dalek", rev = "b9cdbaf36bf2a83438d9f660e5a708c82ed60d8e" } +xz2 = "0.1" z3tracer = "0.8.0" # MOVE DEPENDENCIES diff --git a/consensus/consensus-types/src/block.rs b/consensus/consensus-types/src/block.rs index 3af591dbbb7ef..463475e9304fd 100644 --- a/consensus/consensus-types/src/block.rs +++ b/consensus/consensus-types/src/block.rs @@ -177,7 +177,11 @@ impl Block { /// Construct new genesis block for next epoch deterministically from the end-epoch LedgerInfo /// We carry over most fields except round and block id pub fn make_genesis_block_from_ledger_info(ledger_info: &LedgerInfo) -> Self { - let block_data = BlockData::new_genesis_from_ledger_info(ledger_info); + let block_data = if ledger_info.ends_epoch() { + BlockData::new_genesis_from_ledger_info(ledger_info) + } else { + BlockData::make_genesis_block_from_any_ledger_info(ledger_info) + }; Block { id: block_data.hash(), block_data, diff --git a/consensus/consensus-types/src/block_data.rs b/consensus/consensus-types/src/block_data.rs index a7e923c80e0d4..51f5046e78ca2 100644 --- a/consensus/consensus-types/src/block_data.rs +++ b/consensus/consensus-types/src/block_data.rs @@ -188,6 +188,22 @@ impl BlockData { } } + pub fn make_genesis_block_from_any_ledger_info(ledger_info: &LedgerInfo) -> Self { + let ancestor = ledger_info.commit_info().to_owned(); + + // Genesis carries a placeholder quorum certificate to its parent id with LedgerInfo + // carrying information about version from the last LedgerInfo of previous epoch. + let genesis_quorum_cert = QuorumCert::new( + VoteData::new(ancestor.clone(), ancestor.clone()), + LedgerInfoWithSignatures::new( + LedgerInfo::new(ancestor, HashValue::zero()), + AggregateSignature::empty(), + ), + ); + + BlockData::new_starting_block(ledger_info.timestamp_usecs(), genesis_quorum_cert) + } + pub fn new_genesis_from_ledger_info(ledger_info: &LedgerInfo) -> Self { assert!(ledger_info.ends_epoch()); let ancestor = BlockInfo::new( @@ -246,9 +262,22 @@ impl BlockData { #[allow(unexpected_cfgs)] pub fn new_genesis(timestamp_usecs: u64, quorum_cert: QuorumCert) -> Self { + assume!(quorum_cert.certified_block().epoch() < u64::MAX); // unlikely to be false in this universe + let genesis_epoch = quorum_cert.certified_block().epoch() + 1; + Self { + epoch: genesis_epoch, + round: 0, + timestamp_usecs, + quorum_cert, + block_type: BlockType::Genesis, + } + } + + #[allow(unexpected_cfgs)] + pub fn new_starting_block(timestamp_usecs: u64, quorum_cert: QuorumCert) -> Self { assume!(quorum_cert.certified_block().epoch() < u64::MAX); // unlikely to be false in this universe Self { - epoch: quorum_cert.certified_block().epoch() + 1, + epoch: quorum_cert.certified_block().epoch(), round: 0, timestamp_usecs, quorum_cert, diff --git a/consensus/consensus-types/src/pipelined_block.rs b/consensus/consensus-types/src/pipelined_block.rs index b23b50b1f4753..8b346b8ae2b17 100644 --- a/consensus/consensus-types/src/pipelined_block.rs +++ b/consensus/consensus-types/src/pipelined_block.rs @@ -571,10 +571,11 @@ impl PipelinedBlock { } pub async fn wait_for_compute_result(&self) -> ExecutorResult<(StateComputeResult, Duration)> { - self.pipeline_futs() - .ok_or(ExecutorError::InternalError { - error: "Pipeline aborted".to_string(), - })? + let pipeline_futs = self.pipeline_futs().ok_or(ExecutorError::InternalError { + error: "Pipeline aborted".to_string(), + })?; + + pipeline_futs .ledger_update_fut .await .map(|(compute_result, execution_time, _)| (compute_result, execution_time)) diff --git a/consensus/consensus-types/src/quorum_cert.rs b/consensus/consensus-types/src/quorum_cert.rs index 5cdc1033f220d..863c587cdfd04 100644 --- a/consensus/consensus-types/src/quorum_cert.rs +++ b/consensus/consensus-types/src/quorum_cert.rs @@ -82,31 +82,51 @@ impl QuorumCert { /// - the accumulator root hash of the LedgerInfo is set to the last executed state of previous /// epoch. /// - the map of signatures is empty because genesis block is implicitly agreed. + // TODO(l1-migration): This is for recovery when we lost consensu DB data + // We create this virual block and don't want to add 1 since it is not epoch_ending block + pub fn certificate_for_genesis_from_ledger_info( ledger_info: &LedgerInfo, genesis_id: HashValue, ) -> QuorumCert { - let ancestor = BlockInfo::new( + let ancestor_epoch = if ledger_info.ends_epoch() { ledger_info .epoch() .checked_add(1) - .expect("Integer overflow when creating cert for genesis from ledger info"), - 0, - genesis_id, - ledger_info.transaction_accumulator_hash(), - ledger_info.version(), - ledger_info.timestamp_usecs(), - None, - ); + .expect("Integer overflow when creating cert for genesis from ledger info") + } else { + ledger_info.epoch() + }; + + let ancestor = if ledger_info.ends_epoch() { + BlockInfo::new( + ancestor_epoch, + 0, + genesis_id, + ledger_info.transaction_accumulator_hash(), + ledger_info.version(), + ledger_info.timestamp_usecs(), + None, + ) + } else { + BlockInfo::new( + ancestor_epoch, + 0, + genesis_id, + ledger_info.transaction_accumulator_hash(), + ledger_info.version(), + ledger_info.timestamp_usecs(), + None, + ) + }; let vote_data = VoteData::new(ancestor.clone(), ancestor.clone()); let li = LedgerInfo::new(ancestor, vote_data.hash()); let validator_set_size = ledger_info .next_epoch_state() - .expect("Next epoch state not found in ledger info") - .verifier - .len(); + .map(|epoch_state| epoch_state.verifier.len()) + .unwrap_or(1); QuorumCert::new( vote_data, diff --git a/consensus/safety-rules/src/safety_rules.rs b/consensus/safety-rules/src/safety_rules.rs index d7ddaa5444180..af8e30627b2dd 100644 --- a/consensus/safety-rules/src/safety_rules.rs +++ b/consensus/safety-rules/src/safety_rules.rs @@ -131,6 +131,7 @@ impl SafetyRules { let mut updated = false; let one_chain = qc.certified_block().round(); let two_chain = qc.parent_block().round(); + if one_chain > safety_data.one_chain_round { safety_data.one_chain_round = one_chain; trace!( @@ -260,6 +261,7 @@ impl SafetyRules { let last_li = proof .verify(&waypoint) .map_err(|e| Error::InvalidEpochChangeProof(format!("{}", e)))?; + let ledger_info = last_li.ledger_info(); let epoch_state = ledger_info .next_epoch_state() diff --git a/consensus/src/block_storage/block_store.rs b/consensus/src/block_storage/block_store.rs index a91e547d508c3..c2a294708f4d2 100644 --- a/consensus/src/block_storage/block_store.rs +++ b/consensus/src/block_storage/block_store.rs @@ -311,6 +311,9 @@ impl BlockStore { let block_to_commit = self .get_block(block_id_to_commit) .ok_or_else(|| format_err!("Committed block id not found"))?; + if block_to_commit.block().is_genesis_block() && block_to_commit.round() == 0 { + return Ok(()); + } // First make sure that this commit is new. ensure!( diff --git a/consensus/src/consensus_observer/observer/consensus_observer.rs b/consensus/src/consensus_observer/observer/consensus_observer.rs index 0a90bf96d0315..e4deb765daf22 100644 --- a/consensus/src/consensus_observer/observer/consensus_observer.rs +++ b/consensus/src/consensus_observer/observer/consensus_observer.rs @@ -1077,16 +1077,17 @@ impl ConsensusObserver { .start_epoch( sk, epoch_state.clone(), - dummy_signer.clone(), + dummy_signer, payload_manager, &consensus_config, &execution_config, &randomness_config, - None, - None, + None, // rand_config + None, // fast_rand_config rand_msg_rx, - 0, - self.pipeline_enabled(), + 0, // highest_committed_round + self.observer_epoch_state.pipeline_enabled(), + None, // Consensus observer doesn't use virtual genesis ) .await; if self.pipeline_enabled() { diff --git a/consensus/src/epoch_manager.rs b/consensus/src/epoch_manager.rs index c5673730609d9..e3211152e55e9 100644 --- a/consensus/src/epoch_manager.rs +++ b/consensus/src/epoch_manager.rs @@ -844,6 +844,13 @@ impl EpochManager

{ let safety_rules_container = Arc::new(Mutex::new(safety_rules)); + // Extract virtual genesis block ID if consensus created one + let virtual_genesis_block_id = if recovery_data.commit_root_block().is_genesis_block() { + Some(recovery_data.commit_root_block().id()) + } else { + None + }; + self.execution_client .start_epoch( consensus_key.clone(), @@ -858,6 +865,7 @@ impl EpochManager

{ rand_msg_rx, recovery_data.commit_root_block().round(), self.config.enable_pipeline, + virtual_genesis_block_id, ) .await; let consensus_sk = consensus_key; @@ -1422,6 +1430,7 @@ impl EpochManager

{ rand_msg_rx, highest_committed_round, self.config.enable_pipeline, + None, // DAG doesn't use virtual genesis ) .await; @@ -1476,6 +1485,7 @@ impl EpochManager

{ tokio::spawn(bootstrapper.start(dag_rpc_rx, dag_shutdown_rx)); } + //TODO(l1-migration) turn on quorum store fn enable_quorum_store(&mut self, onchain_config: &OnChainConsensusConfig) -> bool { fail_point!("consensus::start_new_epoch::disable_qs", |_| false); onchain_config.quorum_store_enabled() diff --git a/consensus/src/execution_pipeline.rs b/consensus/src/execution_pipeline.rs index f37c37fd45eed..d2f470841bb6d 100644 --- a/consensus/src/execution_pipeline.rs +++ b/consensus/src/execution_pipeline.rs @@ -288,6 +288,7 @@ impl ExecutionPipeline { { counters::APPLY_LEDGER_WAIT_TIME.observe_duration(command_creation_time.elapsed()); debug!("ledger_apply stage received block {}.", block_id); + let res = async { let execution_duration = execution_time?; let executor = executor.clone(); diff --git a/consensus/src/liveness/leader_reputation.rs b/consensus/src/liveness/leader_reputation.rs index 90652e22f8979..26dbbb5a2c0de 100644 --- a/consensus/src/liveness/leader_reputation.rs +++ b/consensus/src/liveness/leader_reputation.rs @@ -665,18 +665,21 @@ impl LeaderReputation { if chosen { // do not treat chain as unhealthy, if chain just started, and we don't have enough history to decide. - let voting_power_participation_ratio: VotingPowerRatio = - if history.len() < *participants_window_size && self.epoch <= 2 { - 1.0 - } else if total_voting_power >= 1.0 { - participating_voting_power / total_voting_power - } else { - error!( - "Total voting power is {}, should never happen", - total_voting_power - ); - 1.0 - }; + // Also handle single validator case where participation should always be 1.0 + let voting_power_participation_ratio: VotingPowerRatio = if history.len() + < *participants_window_size + && (self.epoch <= 2 || candidates.len() == 1) + { + 1.0 + } else if total_voting_power >= 1.0 { + participating_voting_power / total_voting_power + } else { + error!( + "Total voting power is {}, should never happen", + total_voting_power + ); + 1.0 + }; CHAIN_HEALTH_REPUTATION_PARTICIPATING_VOTING_POWER_FRACTION .set(voting_power_participation_ratio); result = Some(voting_power_participation_ratio); diff --git a/consensus/src/persistent_liveness_storage.rs b/consensus/src/persistent_liveness_storage.rs index 7e4f93852fd90..86f229e981466 100644 --- a/consensus/src/persistent_liveness_storage.rs +++ b/consensus/src/persistent_liveness_storage.rs @@ -109,25 +109,28 @@ impl LedgerRecoveryData { ) -> Result { // We start from the block that storage's latest ledger info, if storage has end-epoch // LedgerInfo, we generate the virtual genesis block - let (latest_commit_id, latest_ledger_info_sig) = - if self.storage_ledger.ledger_info().ends_epoch() { - let genesis = - Block::make_genesis_block_from_ledger_info(self.storage_ledger.ledger_info()); - let genesis_qc = QuorumCert::certificate_for_genesis_from_ledger_info( - self.storage_ledger.ledger_info(), - genesis.id(), - ); - let genesis_ledger_info = genesis_qc.ledger_info().clone(); - let genesis_id = genesis.id(); - blocks.push(genesis); - quorum_certs.push(genesis_qc); - (genesis_id, genesis_ledger_info) - } else { - ( - self.storage_ledger.ledger_info().consensus_block_id(), - self.storage_ledger.clone(), - ) - }; + let ends_epoch = self.storage_ledger.ledger_info().ends_epoch(); + //TODO(l1-migration): when blocks is empty and we have to create virtual genesis from storage to start consensus. + // This is only required for migration when we only have a single validator network + let blocks_empty = blocks.is_empty(); + let (latest_commit_id, latest_ledger_info_sig) = if ends_epoch || blocks_empty { + let genesis = + Block::make_genesis_block_from_ledger_info(self.storage_ledger.ledger_info()); + let genesis_qc = QuorumCert::certificate_for_genesis_from_ledger_info( + self.storage_ledger.ledger_info(), + genesis.id(), + ); + let genesis_ledger_info = genesis_qc.ledger_info().clone(); + let genesis_id = genesis.id(); + blocks.push(genesis); + quorum_certs.push(genesis_qc); + (genesis_id, genesis_ledger_info) + } else { + ( + self.storage_ledger.ledger_info().consensus_block_id(), + self.storage_ledger.clone(), + ) + }; // sort by (epoch, round) to guarantee the topological order of parent <- child blocks.sort_by_key(|b| (b.epoch(), b.round())); @@ -209,7 +212,10 @@ impl LedgerRecoveryData { ) -> Result { // We start from the block that storage's latest ledger info, if storage has end-epoch // LedgerInfo, we generate the virtual genesis block - let (root_id, latest_ledger_info_sig) = if self.storage_ledger.ledger_info().ends_epoch() { + let ends_epoch = self.storage_ledger.ledger_info().ends_epoch(); + // TODO(l1-migration): This is for single validator network boostrap. We created virtual genesis to start the consensus + let blocks_empty = blocks.is_empty(); + let (root_id, latest_ledger_info_sig) = if ends_epoch || blocks_empty { let genesis = Block::make_genesis_block_from_ledger_info(self.storage_ledger.ledger_info()); let genesis_qc = QuorumCert::certificate_for_genesis_from_ledger_info( @@ -228,9 +234,7 @@ impl LedgerRecoveryData { ) }; - // sort by (epoch, round) to guarantee the topological order of parent <- child blocks.sort_by_key(|b| (b.epoch(), b.round())); - let root_idx = blocks .iter() .position(|block| block.id() == root_id) diff --git a/consensus/src/pipeline/execution_client.rs b/consensus/src/pipeline/execution_client.rs index b1f107d04af19..74a3f4e829197 100644 --- a/consensus/src/pipeline/execution_client.rs +++ b/consensus/src/pipeline/execution_client.rs @@ -72,6 +72,7 @@ pub trait TExecutionClient: Send + Sync { rand_msg_rx: aptos_channel::Receiver, highest_committed_round: Round, new_pipeline_enabled: bool, + virtual_genesis_block_id: Option, ); /// This is needed for some DAG tests. Clean this up as a TODO. @@ -325,8 +326,9 @@ impl TExecutionClient for ExecutionProxyClient { rand_msg_rx: aptos_channel::Receiver, highest_committed_round: Round, new_pipeline_enabled: bool, + virtual_genesis_block_id: Option, ) { - let maybe_rand_msg_tx = self.spawn_decoupled_execution( + self.spawn_decoupled_execution( maybe_consensus_key, commit_signer_provider, epoch_state.clone(), @@ -357,9 +359,8 @@ impl TExecutionClient for ExecutionProxyClient { transaction_deduper, randomness_enabled, onchain_consensus_config.order_vote_enabled(), + virtual_genesis_block_id, ); - - maybe_rand_msg_tx } fn get_execution_channel(&self) -> Option> { @@ -546,6 +547,7 @@ impl TExecutionClient for DummyExecutionClient { _rand_msg_rx: aptos_channel::Receiver, _highest_committed_round: Round, _new_pipeline_enabled: bool, + _virtual_genesis_block_id: Option, ) { } diff --git a/consensus/src/state_computer.rs b/consensus/src/state_computer.rs index 945a210a2b63f..31b22f29e505e 100644 --- a/consensus/src/state_computer.rs +++ b/consensus/src/state_computer.rs @@ -492,7 +492,17 @@ impl StateComputer for ExecutionProxy { transaction_deduper: Arc, randomness_enabled: bool, order_vote_enabled: bool, + virtual_genesis_block_id: Option, ) { + // Reset the executor with the virtual genesis block ID if provided + if let Some(virtual_genesis_id) = virtual_genesis_block_id { + self.executor + .reset_with_virtual_genesis(Some(virtual_genesis_id)) + .expect("Failed to reset executor with virtual genesis"); + } else { + self.executor.reset().expect("Failed to reset executor"); + } + *self.state.write() = Some(MutableState { validators: epoch_state .verifier @@ -668,6 +678,7 @@ async fn test_commit_sync_race() { create_transaction_deduper(TransactionDeduperType::NoDedup), false, false, + None, ); executor .commit(vec![], generate_li(1, 1), callback.clone()) diff --git a/consensus/src/state_computer_tests.rs b/consensus/src/state_computer_tests.rs index 611bbdba785b9..9d93efa9e4efc 100644 --- a/consensus/src/state_computer_tests.rs +++ b/consensus/src/state_computer_tests.rs @@ -195,6 +195,7 @@ async fn should_see_and_notify_validator_txns() { Arc::new(NoOpDeduper {}), false, false, + None, ); // Ensure the dummy executor has received the txns. diff --git a/consensus/src/state_replication.rs b/consensus/src/state_replication.rs index 444691be5b8c3..18e5e0fa313bf 100644 --- a/consensus/src/state_replication.rs +++ b/consensus/src/state_replication.rs @@ -73,6 +73,7 @@ pub trait StateComputer: Send + Sync { transaction_deduper: Arc, randomness_enabled: bool, order_vote_enabled: bool, + virtual_genesis_block_id: Option, ); // Reconfigure to clear epoch state at end of epoch. diff --git a/consensus/src/test_utils/mock_execution_client.rs b/consensus/src/test_utils/mock_execution_client.rs index 84fcfc32cca67..767d76c7034b0 100644 --- a/consensus/src/test_utils/mock_execution_client.rs +++ b/consensus/src/test_utils/mock_execution_client.rs @@ -107,6 +107,7 @@ impl TExecutionClient for MockExecutionClient { _rand_msg_rx: aptos_channel::Receiver, _highest_committed_round: Round, _new_pipeline_enabled: bool, + _virtual_genesis_block_id: Option, ) { } diff --git a/consensus/src/test_utils/mock_state_computer.rs b/consensus/src/test_utils/mock_state_computer.rs index b578318a99981..11e11a6c0eb3f 100644 --- a/consensus/src/test_utils/mock_state_computer.rs +++ b/consensus/src/test_utils/mock_state_computer.rs @@ -91,6 +91,7 @@ impl StateComputer for EmptyStateComputer { _: Arc, _: bool, _: bool, + _: Option, ) { } @@ -179,6 +180,7 @@ impl StateComputer for RandomComputeResultStateComputer { _: Arc, _: bool, _: bool, + _: Option, ) { } diff --git a/execution/executor-types/src/lib.rs b/execution/executor-types/src/lib.rs index 8bb4b3d756216..e39c7a43533d8 100644 --- a/execution/executor-types/src/lib.rs +++ b/execution/executor-types/src/lib.rs @@ -124,6 +124,15 @@ pub trait BlockExecutorTrait: Send + Sync { /// Reset the internal state including cache with newly fetched latest committed block from storage. fn reset(&self) -> Result<()>; + /// Reset with a virtual genesis block ID (used for consensus recovery) + fn reset_with_virtual_genesis( + &self, + _virtual_genesis_block_id: Option, + ) -> Result<()> { + // Default implementation just calls reset() for backward compatibility + self.reset() + } + /// Executes a block - TBD, this API will be removed in favor of `execute_and_state_checkpoint`, followed /// by `ledger_update` once we have ledger update as a separate pipeline phase. #[cfg(any(test, feature = "fuzzing"))] diff --git a/execution/executor/src/block_executor/block_tree/mod.rs b/execution/executor/src/block_executor/block_tree/mod.rs index 9bcea40d75d27..377ac58af52f2 100644 --- a/execution/executor/src/block_executor/block_tree/mod.rs +++ b/execution/executor/src/block_executor/block_tree/mod.rs @@ -11,7 +11,7 @@ use crate::{ logging::{LogEntry, LogSchema}, types::partial_state_compute_result::PartialStateComputeResult, }; -use anyhow::{anyhow, ensure, Result}; +use anyhow::{anyhow, ensure, Ok, Result}; use aptos_consensus_types::block::Block as ConsensusBlock; use aptos_crypto::HashValue; use aptos_drop_helper::DEFAULT_DROPPER; @@ -24,7 +24,6 @@ use std::{ collections::{hash_map::Entry, HashMap}, sync::{mpsc::Receiver, Arc, Weak}, }; - pub struct Block { pub id: HashValue, pub output: PartialStateComputeResult, @@ -178,14 +177,25 @@ pub struct BlockTree { impl BlockTree { pub fn new(db: &Arc) -> Result { + Self::new_with_virtual_genesis(db, None) + } + + pub fn new_with_virtual_genesis( + db: &Arc, + virtual_genesis_block_id: Option, + ) -> Result { let block_lookup = Arc::new(BlockLookup::new()); - let root = Mutex::new(Self::root_from_db(&block_lookup, db)?); + let root = Mutex::new(Self::root_from_db( + &block_lookup, + db, + virtual_genesis_block_id, + )?); Ok(Self { root, block_lookup }) } pub fn reset(&self, db: &Arc) -> Result<()> { - *self.root.lock() = Self::root_from_db(&self.block_lookup, db)?; + *self.root.lock() = Self::root_from_db(&self.block_lookup, db, None)?; Ok(()) } @@ -205,7 +215,11 @@ impl BlockTree { self.block_lookup.multi_get(ids) } - fn root_from_db(block_lookup: &Arc, db: &Arc) -> Result> { + fn root_from_db( + block_lookup: &Arc, + db: &Arc, + virtual_genesis_block_id: Option, + ) -> Result> { let ledger_info_with_sigs = db.get_latest_ledger_info()?; let ledger_info = ledger_info_with_sigs.ledger_info(); let ledger_summary = db.get_pre_committed_ledger_summary()?; @@ -217,13 +231,21 @@ impl BlockTree { ledger_info.version(), ); - let id = if ledger_info.ends_epoch() { + let id = if let Some(virtual_genesis_id) = virtual_genesis_block_id { + // Use the virtual genesis block ID provided by consensus + info!( + "Using virtual genesis block ID from consensus: {:x}", + virtual_genesis_id + ); + virtual_genesis_id + } else if ledger_info.ends_epoch() { epoch_genesis_block_id(ledger_info) } else { ledger_info.consensus_block_id() }; - let output = PartialStateComputeResult::new_empty(ledger_summary); + let output: PartialStateComputeResult = + PartialStateComputeResult::new_empty(ledger_summary); block_lookup.fetch_or_add_block(id, output, None) } diff --git a/execution/executor/src/block_executor/mod.rs b/execution/executor/src/block_executor/mod.rs index 2195c36ab1a47..0cf65f88360e9 100644 --- a/execution/executor/src/block_executor/mod.rs +++ b/execution/executor/src/block_executor/mod.rs @@ -93,6 +93,19 @@ where Ok(()) } + fn reset_with_virtual_genesis( + &self, + virtual_genesis_block_id: Option, + ) -> Result<()> { + let _guard = CONCURRENCY_GAUGE.concurrency_with(&["block", "reset"]); + + *self.inner.write() = Some(BlockExecutorInner::new_with_virtual_genesis( + self.db.clone(), + virtual_genesis_block_id, + )?); + Ok(()) + } + fn execute_and_update_state( &self, block: ExecutableBlock, @@ -162,7 +175,14 @@ where V: VMBlockExecutor, { pub fn new(db: DbReaderWriter) -> Result { - let block_tree = BlockTree::new(&db.reader)?; + Self::new_with_virtual_genesis(db, None) + } + + pub fn new_with_virtual_genesis( + db: DbReaderWriter, + virtual_genesis_block_id: Option, + ) -> Result { + let block_tree = BlockTree::new_with_virtual_genesis(&db.reader, virtual_genesis_block_id)?; Ok(Self { db, block_tree, diff --git a/movement-migration/validation-tool/Cargo.toml b/movement-migration/validation-tool/Cargo.toml new file mode 100644 index 0000000000000..7da1bfbab1d3d --- /dev/null +++ b/movement-migration/validation-tool/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "validation-tool" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +publish.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +aptos-config = { workspace = true } +aptos-db = { workspace = true } +aptos-rest-client = { workspace = true } +aptos-sdk = { workspace = true } +aptos-storage-interface = { workspace = true } +aptos-types = { workspace = true } +bcs = { workspace = true } +bytes = { workspace = true } +clap = { workspace = true } +either = { workspace = true } +hex = { workspace = true } +serde_json = { workspace = true } +move-core-types = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[dev-dependencies] +tar = { workspace = true } +tempfile = { workspace = true } +xz2 = { workspace = true } \ No newline at end of file diff --git a/movement-migration/validation-tool/assets/maptos.tar.xz b/movement-migration/validation-tool/assets/maptos.tar.xz new file mode 100644 index 0000000000000..fd4d54edec7c6 Binary files /dev/null and b/movement-migration/validation-tool/assets/maptos.tar.xz differ diff --git a/movement-migration/validation-tool/script/clean_and_sync.sh b/movement-migration/validation-tool/script/clean_and_sync.sh new file mode 100755 index 0000000000000..9885c7c8fddf5 --- /dev/null +++ b/movement-migration/validation-tool/script/clean_and_sync.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Check if exactly 2 arguments are provided +if [ $# -ne 2 ]; then + echo "Usage: $0 " + echo "Recursively deletes files from folder_to_clean that don't exist in reference_folder," + echo "then syncs reference_folder to folder_to_clean using rsync" + exit 1 +fi + +folder_to_clean="$1" +reference_folder="$2" + +# Check if both directories exist +if [ ! -d "$folder_to_clean" ]; then + echo "Error: Directory '$folder_to_clean' does not exist" + exit 1 +fi + +if [ ! -d "$reference_folder" ]; then + echo "Error: Directory '$reference_folder' does not exist" + exit 1 +fi + +# Convert to absolute paths to avoid issues +folder_to_clean=$(realpath "$folder_to_clean") +reference_folder=$(realpath "$reference_folder") + +echo "Cleaning folder: $folder_to_clean" +echo "Reference folder: $reference_folder" +echo + +# Ask for confirmation before proceeding +echo "This will:" +echo "1. Delete all files in '$folder_to_clean' that don't exist in '$reference_folder'" +echo "2. Remove empty directories" +echo "3. Sync '$reference_folder' to '$folder_to_clean' using rsync" +echo +read -p "Do you want to continue? (y/N): " -n 1 -r +echo + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Operation cancelled." + exit 0 +fi + +echo + +# Phase 1: Delete files that don't exist in reference folder +echo "=== Phase 1: Cleaning up files not in reference folder ===" +deleted_count=0 + +# Use find to get all files in folder_to_clean, preserving the relative path structure +while IFS= read -r -d '' file; do + # Get the relative path from folder_to_clean + relative_path="${file#"$folder_to_clean"/}" + + # Check if this file exists in the same relative location in reference folder + reference_file="$reference_folder/$relative_path" + + if [ ! -f "$reference_file" ]; then + echo "Deleting: $file" + rm "$file" + ((deleted_count++)) + fi +done < <(find "$folder_to_clean" -type f -print0) + +echo "Files deleted: $deleted_count" +echo + +# Phase 2: Remove empty directories +echo "=== Phase 2: Removing empty directories ===" +# Find and remove empty directories (bottom-up) +find "$folder_to_clean" -type d -empty -delete 2>/dev/null +echo "Empty directories removed" +echo + +# Phase 3: Sync with rsync +echo "=== Phase 3: Syncing reference folder to cleaned folder ===" +echo "Running: rsync -av --progress \"$reference_folder/\" \"$folder_to_clean/\"" +echo + +# Use rsync to sync the reference folder to the cleaned folder +# -a: archive mode (preserves permissions, timestamps, etc.) +# -v: verbose +# --progress: show progress +# Note the trailing slashes are important for rsync behavior +rsync -av --progress "$reference_folder/" "$folder_to_clean/" + +sync_exit_code=$? + +if [ $sync_exit_code -eq 0 ]; then + echo + echo "=== Cleanup and sync completed successfully ===" + echo "The folder '$folder_to_clean' now matches '$reference_folder'" +else + echo + echo "=== WARNING: rsync failed with exit code $sync_exit_code ===" + echo "Please check the rsync output above for errors" + exit $sync_exit_code +fi diff --git a/movement-migration/validation-tool/src/checks.rs b/movement-migration/validation-tool/src/checks.rs new file mode 100644 index 0000000000000..66657b389f19a --- /dev/null +++ b/movement-migration/validation-tool/src/checks.rs @@ -0,0 +1,6 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +pub mod api; +pub mod error; +pub mod node; diff --git a/movement-migration/validation-tool/src/checks/api.rs b/movement-migration/validation-tool/src/checks/api.rs new file mode 100644 index 0000000000000..cb7301d82712f --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api.rs @@ -0,0 +1,36 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::checks::api::active_feature_flags::GlobalFeatureCheck; +use crate::checks::api::cmp_transactions::CompareTransactions; +use crate::checks::api::submit_transaction::SubmitTransaction; +use crate::checks::api::transactions::GetTransactions; +use clap::Subcommand; + +mod active_feature_flags; +mod cmp_transactions; +mod submit_transaction; +mod transactions; + +#[derive(Subcommand)] +#[clap( + name = "migration-api-tool", + about = "Validates api conformity after movement migration" +)] +pub enum ApiTool { + ActiveFeatures(GlobalFeatureCheck), + Transactions(GetTransactions), + CompareTransactions(CompareTransactions), + SubmitTransaction(SubmitTransaction), +} + +impl ApiTool { + pub async fn run(self) -> anyhow::Result<()> { + match self { + ApiTool::ActiveFeatures(tool) => tool.run().await, + ApiTool::Transactions(tool) => tool.run().await, + ApiTool::CompareTransactions(tool) => tool.run().await, + ApiTool::SubmitTransaction(tool) => tool.run().await, + } + } +} diff --git a/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs b/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs new file mode 100644 index 0000000000000..65622ad20c826 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/active_feature_flags.rs @@ -0,0 +1,180 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::checks::error::ValidationError; +use crate::types::api::MovementAptosRestClient; +use aptos_rest_client::aptos_api_types::ViewFunction; +use clap::Parser; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::ModuleId; +use std::str::FromStr; +use tracing::debug; + +#[derive(Parser)] +#[clap( + name = "migration-api-validation", + about = "Validates api conformity after movement migration." +)] +pub struct GlobalFeatureCheck { + #[clap(value_parser)] + #[clap( + long = "movement-aptos", + help = "The url of the Movement Aptos REST endpoint." + )] + pub movement_aptos_rest_api_url: String, +} + +impl GlobalFeatureCheck { + pub async fn run(self) -> anyhow::Result<()> { + let movement_aptos_rest_client = + MovementAptosRestClient::new(&self.movement_aptos_rest_api_url)?; + + satisfies(&movement_aptos_rest_client).await?; + + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + GlobalFeatureCheck::command().debug_assert() +} + +pub async fn satisfies( + movement_aptos_rest_client: &MovementAptosRestClient, +) -> Result<(), ValidationError> { + let mut errors = vec![]; + let expected_active: Vec = vec![ + 1, // FeatureFlag::CODE_DEPENDENCY_CHECK + 2, // FeatureFlag::TREAT_FRIEND_AS_PRIVATE + 3, // FeatureFlag::SHA_512_AND_RIPEMD_160_NATIVES + 4, // FeatureFlag::APTOS_STD_CHAIN_ID_NATIVES + 5, // FeatureFlag::VM_BINARY_FORMAT_V6 + 7, // FeatureFlag::MULTI_ED25519_PK_VALIDATE_V2_NATIVES + 8, // FeatureFlag::BLAKE2B_256_NATIVE + 9, // FeatureFlag::RESOURCE_GROUPS + 10, // FeatureFlag::MULTISIG_ACCOUNTS + 11, // FeatureFlag::DELEGATION_POOLS + 12, // FeatureFlag::CRYPTOGRAPHY_ALGEBRA_NATIVES + 13, // FeatureFlag::BLS12_381_STRUCTURES + 14, // FeatureFlag::ED25519_PUBKEY_VALIDATE_RETURN_FALSE_WRONG_LENGTH + 15, // FeatureFlag::STRUCT_CONSTRUCTORS + 18, // FeatureFlag::SIGNATURE_CHECKER_V2 + 19, // FeatureFlag::STORAGE_SLOT_METADATA + 20, // FeatureFlag::CHARGE_INVARIANT_VIOLATION + 22, // FeatureFlag::GAS_PAYER_ENABLED + 23, // FeatureFlag::APTOS_UNIQUE_IDENTIFIERS + 24, // FeatureFlag::BULLETPROOFS_NATIVES + 25, // FeatureFlag::SIGNER_NATIVE_FORMAT_FIX + 26, // FeatureFlag::MODULE_EVENT + 27, // FeatureFlag::EMIT_FEE_STATEMENT + 28, // FeatureFlag::STORAGE_DELETION_REFUND + 29, // FeatureFlag::SIGNATURE_CHECKER_V2_SCRIPT_FIX + 30, // FeatureFlag::AGGREGATOR_V2_API + 31, // FeatureFlag::SAFER_RESOURCE_GROUPS + 32, // FeatureFlag::SAFER_METADATA + 33, // FeatureFlag::SINGLE_SENDER_AUTHENTICATOR + 34, // FeatureFlag::SPONSORED_AUTOMATIC_ACCOUNT_V1_CREATION + 35, // FeatureFlag::FEE_PAYER_ACCOUNT_OPTIONAL + 36, // FeatureFlag::AGGREGATOR_V2_DELAYED_FIELDS + 37, // FeatureFlag::CONCURRENT_TOKEN_V2 + 38, // FeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH + 39, // FeatureFlag::OPERATOR_BENEFICIARY_CHANGE + 41, // FeatureFlag::RESOURCE_GROUPS_SPLIT_IN_VM_CHANGE_SET + 42, // FeatureFlag::COMMISSION_CHANGE_DELEGATION_POOL + 43, // FeatureFlag::BN254_STRUCTURES + 44, // FeatureFlag::WEBAUTHN_SIGNATURE + 46, // FeatureFlag::KEYLESS_ACCOUNTS + 47, // FeatureFlag::KEYLESS_BUT_ZKLESS_ACCOUNTS + 48, // FeatureFlag::REMOVE_DETAILED_ERROR_FROM_HASH + 49, // FeatureFlag::JWK_CONSENSUS + 50, // FeatureFlag::CONCURRENT_FUNGIBLE_ASSETS + 51, // FeatureFlag::REFUNDABLE_BYTES + 52, // FeatureFlag::OBJECT_CODE_DEPLOYMENT + 53, // FeatureFlag::MAX_OBJECT_NESTING_CHECK + 54, // FeatureFlag::KEYLESS_ACCOUNTS_WITH_PASSKEYS + 55, // FeatureFlag::MULTISIG_V2_ENHANCEMENT + 56, // FeatureFlag::DELEGATION_POOL_ALLOWLISTING + 57, // FeatureFlag::MODULE_EVENT_MIGRATION + 58, // FeatureFlag::REJECT_UNSTABLE_BYTECODE + 59, // FeatureFlag::TRANSACTION_CONTEXT_EXTENSION + 60, // FeatureFlag::COIN_TO_FUNGIBLE_ASSET_MIGRATION + 62, // FeatureFlag::OBJECT_NATIVE_DERIVED_ADDRESS + 63, // FeatureFlag::DISPATCHABLE_FUNGIBLE_ASSET + 66, // FeatureFlag::AGGREGATOR_V2_IS_AT_LEAST_API + 67, // FeatureFlag::CONCURRENT_FUNGIBLE_BALANCE + 69, // FeatureFlag::LIMIT_VM_TYPE_SIZE + 70, // FeatureFlag::ABORT_IF_MULTISIG_PAYLOAD_MISMATCH + 73, // FeatureFlag::GOVERNED_GAS_POOL + ]; + + let module = + ModuleId::from_str("0x1::features").map_err(|e| ValidationError::Internal(e.into()))?; + let function = + Identifier::from_str("is_enabled").map_err(|e| ValidationError::Internal(e.into()))?; + + let mut view_function = ViewFunction { + module, + function, + ty_args: vec![], + args: vec![], + }; + + for feature_id in expected_active { + debug!("checking feature flag {}", feature_id); + let bytes = bcs::to_bytes(&feature_id).map_err(|e| ValidationError::Internal(e.into()))?; + view_function.args = vec![bytes]; + + // Check feature for Maptos executor + let maptos_active = movement_aptos_rest_client + .view_bcs_with_json_response(&view_function, None) + .await + .map_err(|e| { + ValidationError::Internal( + format!( + "failed to get Movement feature flag {}: {:?}", + feature_id, e + ) + .into(), + ) + })? + .into_inner(); + + let maptos_active = maptos_active.get(0).ok_or_else(|| { + ValidationError::Internal( + format!( + "failed to get Movement feature flag {}: response is empty", + feature_id + ) + .into(), + ) + })?; + + let maptos_active = maptos_active.as_bool().ok_or_else(|| { + ValidationError::Internal( + format!( + "failed to get Movement feature flag {}: can't convert {:?} into a bool", + feature_id, maptos_active + ) + .into(), + ) + })?; + + if !maptos_active { + errors.push(format!( + "Feature {}: Aptos={} — expected to be active", + feature_id, maptos_active, + )); + } + + // Slow down to avoid Cloudflare rate limiting + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + + if !errors.is_empty() { + return Err(ValidationError::Unsatisfied(errors.join("\n").into())); + } + + Ok(()) +} diff --git a/movement-migration/validation-tool/src/checks/api/cmp_transactions.rs b/movement-migration/validation-tool/src/checks/api/cmp_transactions.rs new file mode 100644 index 0000000000000..585f20633fc15 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/cmp_transactions.rs @@ -0,0 +1,78 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::api::MovementAptosRestClient; +use aptos_rest_client::aptos_api_types::{TransactionData, TransactionOnChainData}; +use clap::Parser; +use std::path::PathBuf; +use tokio::fs::File; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tracing::{error, info}; + +#[derive(Parser)] +#[clap( + name = "compare-transactions", + about = "Compares transactions with the same transactions from a remote validator node" +)] +pub struct CompareTransactions { + #[clap(value_parser)] + #[clap(long = "url", help = "The url of the Movement Aptos REST endpoint.")] + pub rest_api_url: String, + #[clap(long = "in", help = "Input path file name")] + pub path: PathBuf, +} + +impl CompareTransactions { + pub async fn run(self) -> anyhow::Result<()> { + let rest_client = MovementAptosRestClient::new(&self.rest_api_url)?; + compare_transactions(&rest_client, self.path).await?; + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + CompareTransactions::command().debug_assert() +} + +async fn compare_transactions( + rest_client: &MovementAptosRestClient, + path: PathBuf, +) -> anyhow::Result<()> { + let file = File::open(path).await?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + let mut error = false; + + while let Some(line) = lines.next_line().await? { + let bytes = hex::decode(line.trim_end())?; + let tx_data_local = bcs::from_bytes::<'_, TransactionOnChainData>(&bytes)?; + let hash = tx_data_local.info.transaction_hash(); + if let Ok(response) = rest_client.get_transaction_by_hash_bcs(hash).await { + if let TransactionData::OnChain(tx_data_remote) = response.into_inner() { + if tx_data_local == tx_data_remote { + info!("Checked transaction with hash {}", hash); + } else { + error!("Remote transaction with hash {} mismatch", hash); + error!("Local transaction:\n{:?}", tx_data_local); + error!("Remote transaction:\n{:?}", tx_data_remote); + error = true; + } + } else { + // should be never the case + error!("Remote transaction with hash {} is pending", hash); + error = true; + }; + } else { + error!("Remote transaction with hash {:?} not found", hash); + error = true; + } + } + + if error { + Err(anyhow::Error::msg("Validation failed")) + } else { + Ok(()) + } +} diff --git a/movement-migration/validation-tool/src/checks/api/submit_transaction.rs b/movement-migration/validation-tool/src/checks/api/submit_transaction.rs new file mode 100644 index 0000000000000..956aecf5f6ea5 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/submit_transaction.rs @@ -0,0 +1,259 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::api::MovementAptosRestClient; +use anyhow::Context; +use aptos_rest_client::aptos_api_types::TransactionOnChainData; +use aptos_sdk::transaction_builder::TransactionBuilder; +use aptos_sdk::types::{ + account_address::AccountAddress, + chain_id::ChainId, + transaction::{EntryFunction, SignedTransaction, TransactionPayload}, + LocalAccount, +}; +use clap::Parser; +use move_core_types::identifier::Identifier; +use move_core_types::language_storage::{ModuleId, TypeTag}; +use std::str::FromStr; +use std::time::{SystemTime, UNIX_EPOCH}; +use tracing::{info, warn}; + +#[derive(Parser)] +#[clap( + name = "submit-transaction", + about = "Transfers funds from one account to another account" +)] +pub struct SubmitTransaction { + #[clap(value_parser)] + #[clap(long = "movement-url", help = "The url of the Movement REST endpoint.")] + pub movement_rest_api_url: String, + #[clap(value_parser)] + #[clap(long = "aptos-url", help = "The url of the Aptos REST endpoint.")] + pub aptos_rest_api_url: String, + #[clap(value_parser)] + #[clap(long = "receiver", help = "The receiver account address.")] + pub receiver_address: String, + #[clap(value_parser)] + #[clap(long = "amount", help = "The amount of tokens to be sent.")] + pub send_amount: u64, +} + +impl SubmitTransaction { + pub async fn run(self) -> anyhow::Result<()> { + let private_key = std::env::var("MOVE_ACCOUNT_PRIVATE_KEY") + .context("MOVE_ACCOUNT_PRIVATE_KEY variable is not set")?; + let local_account = LocalAccount::from_private_key(&private_key, 0)?; + let remote_account = + AccountAddress::from_hex(self.receiver_address.trim_start_matches("0x"))?; + + let rest_client_movement = MovementAptosRestClient::new(&self.movement_rest_api_url)?; + let rest_client_aptos = MovementAptosRestClient::new(&self.aptos_rest_api_url)?; + let chain_id = check_chain_id(&rest_client_movement, &rest_client_aptos).await?; + check_sequence_number(&rest_client_movement, &rest_client_aptos, &local_account).await?; + check_balance( + &rest_client_movement, + &rest_client_aptos, + local_account.address(), + self.send_amount, + ) + .await?; + let transaction = + create_transaction(&local_account, remote_account, self.send_amount, chain_id)?; + // let json = serde_json::to_string_pretty(&transaction)?; + // info!("Transaction created:\n{}", json); + submit_transaction(&rest_client_movement, &rest_client_aptos, &transaction).await?; + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + SubmitTransaction::command().debug_assert() +} + +async fn check_chain_id( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, +) -> anyhow::Result { + let chain_id_movement = rest_client_movement + .get_index_bcs() + .await? + .into_inner() + .chain_id; + let chain_id_aptos = rest_client_aptos + .get_index_bcs() + .await? + .into_inner() + .chain_id; + + if chain_id_aptos == chain_id_movement { + info!("Chain-Id: {}", chain_id_movement); + Ok(ChainId::new(chain_id_movement)) + } else { + Err(anyhow::anyhow!( + "Chain-Id mismatch. Movement chain-id: {}. Aptos chain-id: {}.", + chain_id_movement, + chain_id_aptos + )) + } +} + +async fn check_sequence_number( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, + local_account: &LocalAccount, +) -> anyhow::Result<()> { + let sequence_number_movement = rest_client_movement + .get_account_bcs(local_account.address()) + .await + .context(format!( + "Can't get the Movement account for the address {}", + local_account.address() + ))? + .into_inner() + .sequence_number(); + let sequence_number_aptos = rest_client_aptos + .get_account_bcs(local_account.address()) + .await + .context(format!( + "Can't get the Aptos account for the address {}", + local_account.address() + ))? + .into_inner() + .sequence_number(); + + if sequence_number_movement == sequence_number_aptos { + info!("Account sequence number: {}", sequence_number_movement); + local_account.set_sequence_number(sequence_number_movement); + Ok(()) + } else { + Err(anyhow::anyhow!( + "Sequence number mismatch. Movement sequence number: {}. Aptos sequence number: {}.", + sequence_number_movement, + sequence_number_aptos + )) + } +} + +async fn check_balance( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, + account: AccountAddress, + amount: u64, +) -> anyhow::Result<()> { + let balance_movement = rest_client_movement + .view_apt_account_balance(account) + .await + .context(format!( + "Can't get the Movement balance for address {}", + account + ))? + .into_inner(); + let balance_aptos = rest_client_aptos + .view_apt_account_balance(account) + .await + .context(format!( + "Can't get the Aptos balance for address {}", + account + ))? + .into_inner(); + + if balance_movement == balance_aptos { + info!("Account address: {}", account); + info!("Account balance: {}", balance_movement); + + if amount <= balance_movement { + Ok(()) + } else { + Err(anyhow::anyhow!( + "The account balance is less than the amount to transfer" + )) + } + } else { + Err(anyhow::anyhow!( + "Balance mismatch. Movement account balance: {}. Aptos account balance: {}.", + balance_movement, + balance_aptos + )) + } +} + +fn create_transaction( + from_account: &LocalAccount, + to_account: AccountAddress, + amount: u64, + chain_id: ChainId, +) -> anyhow::Result { + info!( + "Sending {} Octas from {} to {}", + amount, + from_account.address(), + to_account + ); + let coin_type = "0x1::aptos_coin::AptosCoin"; + let max_gas_amount = 5_000; + let gas_unit_price = 100; + let transaction_builder = TransactionBuilder::new( + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new(AccountAddress::ONE, Identifier::new("coin")?), + Identifier::new("transfer")?, + vec![TypeTag::from_str(coin_type)?], + vec![bcs::to_bytes(&to_account)?, bcs::to_bytes(&amount)?], + )), + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 60, + chain_id, + ) + .sender(from_account.address()) + .sequence_number(from_account.sequence_number()) + .max_gas_amount(max_gas_amount) + .gas_unit_price(gas_unit_price); + + Ok(from_account.sign_with_transaction_builder(transaction_builder)) +} + +async fn submit_transaction( + rest_client_movement: &MovementAptosRestClient, + rest_client_aptos: &MovementAptosRestClient, + transaction: &SignedTransaction, +) -> anyhow::Result<()> { + // Send the transaction to the Aptos node first. + // If the submission fails, we can fix the problem and reset the DB. + let tx_on_chain_data_aptos = rest_client_aptos + .submit_and_wait_bcs(transaction) + .await + .context("Failed to submit the transaction to Aptos")? + .into_inner(); + + log_tx_on_chain_data(&tx_on_chain_data_aptos, "Aptos")?; + + let tx_on_chain_data_movement = rest_client_movement + .submit_and_wait_bcs(transaction) + .await + .context("Failed to submit the transaction to Movement")? + .into_inner(); + + log_tx_on_chain_data(&tx_on_chain_data_movement, "Movement")?; + + if tx_on_chain_data_movement == tx_on_chain_data_aptos { + info!("Transaction on-chain data on Aptos equals the data on Movement"); + } else { + warn!("Transaction on-chain data on Aptos is different than the data on Movement"); + } + + Ok(()) +} + +fn log_tx_on_chain_data(data: &TransactionOnChainData, name: &str) -> anyhow::Result<()> { + let bytes = bcs::to_bytes(data)?; + let str = hex::encode(&bytes); + + info!("Transaction on-chain data ({}):\n{}", name, str); + info!( + "Transaction info ({}):\n{}", + name, + serde_json::to_string_pretty(&data.info)? + ); + + Ok(()) +} diff --git a/movement-migration/validation-tool/src/checks/api/transactions.rs b/movement-migration/validation-tool/src/checks/api/transactions.rs new file mode 100644 index 0000000000000..3a4d51b7c2055 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/api/transactions.rs @@ -0,0 +1,110 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::api::MovementAptosRestClient; +use clap::Parser; +use std::cmp::min; +use std::path::{Path, PathBuf}; +use tokio::fs::File; +use tokio::io::{AsyncWriteExt, BufWriter}; +use tracing::info; + +#[derive(Parser)] +#[clap( + name = "transactions", + about = "Gets a list of transactions from a validator node and stores them into a file" +)] +pub struct GetTransactions { + #[clap(value_parser)] + #[clap(long = "url", help = "The url of the Movement Aptos REST endpoint.")] + pub rest_api_url: String, + #[clap(long = "start", help = "The start ledger version")] + pub start: u64, + #[clap( + long = "limit", + help = "Limit how many ledger versions should be queried" + )] + pub limit: Option, + #[clap(long = "out", help = "Output path file name")] + pub output_path: PathBuf, +} + +impl GetTransactions { + pub async fn run(self) -> anyhow::Result<()> { + let rest_client = MovementAptosRestClient::new(&self.rest_api_url)?; + get_transactions(&rest_client, self.start, self.limit, self.output_path).await?; + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + GetTransactions::command().debug_assert() +} + +async fn get_transactions( + rest_client: &MovementAptosRestClient, + start: u64, + limit: Option, + output_path: impl AsRef, +) -> Result<(), anyhow::Error> { + let response = rest_client.get_index_bcs().await?; + let latest_ledger_version: u64 = response.into_inner().ledger_version.into(); + let max_ledger_version = if let Some(limit) = limit { + let ledger_version = start + limit as u64; + min(ledger_version, latest_ledger_version) + } else { + latest_ledger_version + }; + let mut current_ledger_version = start; + let mut user_tx_count = 0usize; + let mut total_tx_count = 0usize; + let file = File::create(output_path).await?; + let mut writer = BufWriter::new(file); + + info!("Latest ledger version is {}", latest_ledger_version); + + while current_ledger_version < max_ledger_version { + info!( + "Getting transactions from version {}", + current_ledger_version + ); + let txs = rest_client + .get_transactions_bcs(Some(current_ledger_version), Some(100)) + .await? + .into_inner(); + + if txs.is_empty() { + info!("Transactions not found"); + break; + } + + let mut user_transactions = 0usize; + + for tx in txs.iter() { + if tx.transaction.try_as_signed_user_txn().is_some() { + let bytes = bcs::to_bytes(tx)?; + let str = format!("{}\n", hex::encode(&bytes)); + writer.write_all(str.as_bytes()).await?; + user_transactions += 1; + } + current_ledger_version = tx.version; + } + + current_ledger_version += 1; + user_tx_count += user_transactions; + total_tx_count += txs.len(); + info!( + "Node returned {} transactions ({} signed user transactions)", + txs.len(), + user_transactions + ); + } + + info!("Total transaction count is {}", total_tx_count); + info!("Total signed user transaction count is {}", user_tx_count); + + writer.flush().await?; + Ok(()) +} diff --git a/movement-migration/validation-tool/src/checks/error.rs b/movement-migration/validation-tool/src/checks/error.rs new file mode 100644 index 0000000000000..3625157251a4e --- /dev/null +++ b/movement-migration/validation-tool/src/checks/error.rs @@ -0,0 +1,10 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, thiserror::Error)] +pub enum ValidationError { + #[error("the criterion was not satisfied: {0}")] + Unsatisfied(#[source] Box), + #[error("criterion encountered an internal error: {0}")] + Internal(#[source] Box), +} diff --git a/movement-migration/validation-tool/src/checks/node.rs b/movement-migration/validation-tool/src/checks/node.rs new file mode 100644 index 0000000000000..0d31dabca6756 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/node.rs @@ -0,0 +1,43 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + checks::node::global_storage_includes::GlobalStorageIncludes, + types::storage::{MovementAptosStorage, MovementStorage}, +}; +use clap::Parser; +use std::path::PathBuf; + +mod global_storage_includes; + +#[derive(Parser)] +#[clap( + name = "migration-node-validation", + about = "Validates data conformity after movement migration." +)] +pub struct Command { + #[clap(long = "movement", help = "The path to the movement database.")] + pub movement_db: PathBuf, + #[clap( + long = "movement-aptos", + help = "The path to the movement Aptos database." + )] + pub movement_aptos_db: PathBuf, +} + +impl Command { + pub async fn run(self) -> anyhow::Result<()> { + let movement_storage = MovementStorage::open(&self.movement_db)?; + let movement_aptos_storage = MovementAptosStorage::open(&self.movement_aptos_db)?; + + GlobalStorageIncludes::satisfies(&movement_storage, &movement_aptos_storage)?; + + Ok(()) + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + Command::command().debug_assert() +} diff --git a/movement-migration/validation-tool/src/checks/node/global_storage_includes.rs b/movement-migration/validation-tool/src/checks/node/global_storage_includes.rs new file mode 100644 index 0000000000000..32e899c9ea5b9 --- /dev/null +++ b/movement-migration/validation-tool/src/checks/node/global_storage_includes.rs @@ -0,0 +1,227 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + checks::error::ValidationError, + types::storage::{MovementAptosStorage, MovementStorage}, +}; +use aptos_types::{ + access_path::Path, + account_config::{AccountResource, CoinStoreResourceUntyped}, + state_store::{ + state_key::{inner::StateKeyInner, StateKey}, + TStateView, + }, +}; +use bytes::Bytes; +use move_core_types::{account_address::AccountAddress, language_storage::StructTag}; +use std::str::FromStr; +use tracing::{debug, info}; + +/// This check iterates over all global state keys starting at ledger version 0. +/// For each state key it fetches the state view for the latest ledger version, +/// from the old Movment database and the new Aptos database. The state view bytes +/// from both databases need to match. If the state key has no value in the latest +/// ledger version of the old Movement database then it should also have no value +/// in the new Aptos database. +/// Account Resources and Coin Stores are deserialized from BSC before comparison. +/// In case of Coin Stores, only the balances are compared. +pub struct GlobalStorageIncludes; + +impl GlobalStorageIncludes { + pub fn satisfies( + movement_storage: &MovementStorage, + movement_aptos_storage: &MovementAptosStorage, + ) -> Result<(), ValidationError> { + let account = StructTag::from_str("0x1::account::Account").unwrap(); + let coin = StructTag::from_str("0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>").unwrap(); + + // get the latest ledger version from the movement storage + let movement_ledger_version = movement_storage + .latest_ledger_version() + .map_err(|e| ValidationError::Internal(e.into()))?; + + info!("checking global state keys and values"); + debug!("movement_ledger_version: {:?}", movement_ledger_version); + + // get the latest state view from the movement storage + let movement_state_view = movement_storage + .state_view_at_version(Some(movement_ledger_version)) + .map_err(|e| ValidationError::Internal(e.into()))?; + + // get the latest state view from the maptos storage + let maptos_state_view = movement_aptos_storage + .state_view_at_version(Some(movement_ledger_version)) + .map_err(|e| ValidationError::Internal(e.into()))?; + + // the movement state view is the domain, so the maptos state view is the codomain + let movement_global_state_keys_iterator = + movement_storage.global_state_keys_from_version(None); + let movement_global_state_keys = movement_global_state_keys_iterator + .iter() + .map_err(|e| ValidationError::Internal(e.into()))?; + + let mut count = 0; + for movement_state_key in movement_global_state_keys { + debug!( + "processing movement_state_key {}: {:?}", + count, movement_state_key + ); + + let movement_state_key = + movement_state_key.map_err(|e| ValidationError::Internal(e.into()))?; + + let movement_value = movement_state_view + .get_state_value_bytes(&movement_state_key) + .map_err(|e| ValidationError::Internal(e.into()))?; + + match movement_value { + Some(movement_value) => { + let maptos_state_value = maptos_state_view + .get_state_value_bytes(&movement_state_key) + .map_err(|e| ValidationError::Internal(e.into()))? + .ok_or(ValidationError::Unsatisfied( + format!( + "Movement Aptos is missing a value for {:?}", + movement_state_key + ) + .into(), + ))?; + + if let StateKeyInner::AccessPath(p) = movement_state_key.inner() { + match p.get_path() { + Path::Resource(tag) if tag == account => Self::compare_accounts( + p.address, + movement_value, + maptos_state_value, + )?, + Path::Resource(tag) if tag == coin => Self::compare_balances( + p.address, + movement_value, + maptos_state_value, + )?, + _ => Self::compare_raw_state( + movement_state_key, + movement_value, + maptos_state_value, + )?, + } + } else { + Self::compare_raw_state( + movement_state_key, + movement_value, + maptos_state_value, + )?; + } + }, + None => { + debug!("Value from a previous version has been removed at the latest ledger version"); + + match maptos_state_view + .get_state_value(&movement_state_key) + .map_err(|e| ValidationError::Internal(e.into()))? + { + Some(_) => { + return Err(ValidationError::Unsatisfied( + format!( + "Movement Aptos is unexpectedly not missing a value for {:?}", + movement_state_key + ) + .into(), + )); + }, + None => {}, + } + }, + } + count += 1; + } + + Ok(()) + } + + fn compare_raw_state( + movement_state_key: StateKey, + movement_value: Bytes, + maptos_state_value: Bytes, + ) -> Result<(), ValidationError> { + if movement_value != maptos_state_value { + Err(ValidationError::Unsatisfied( + format!( + "Movement state value for {:?} is {:?}, while Movement Aptos state value is {:?}", + movement_state_key, + movement_value, + maptos_state_value + ) + .into(), + )) + } else { + Ok(()) + } + } + + fn compare_accounts( + address: AccountAddress, + movement_value: Bytes, + maptos_state_value: Bytes, + ) -> Result<(), ValidationError> { + let movement_account = bcs::from_bytes::(&movement_value) + .map_err(|e| ValidationError::Internal(e.into()))?; + let movement_aptos_account = bcs::from_bytes::(&maptos_state_value) + .map_err(|e| ValidationError::Internal(e.into()))?; + + debug!( + "movement account at 0x{}: {:?}", + address.short_str_lossless(), + movement_account + ); + + if movement_account != movement_aptos_account { + Err(ValidationError::Unsatisfied( + format!( + "Movement account for {:?} is {:?}, while Movement Aptos account is {:?}", + address.to_standard_string(), + movement_account, + movement_aptos_account + ) + .into(), + )) + } else { + Ok(()) + } + } + + fn compare_balances( + address: AccountAddress, + movement_value: Bytes, + maptos_state_value: Bytes, + ) -> Result<(), ValidationError> { + let movement_balance = bcs::from_bytes::(&movement_value) + .map_err(|e| ValidationError::Internal(e.into()))? + .coin(); + let movement_aptos_balance = + bcs::from_bytes::(&maptos_state_value) + .map_err(|e| ValidationError::Internal(e.into()))? + .coin(); + + debug!( + "movement balance at 0x{}: {} coins", + address.short_str_lossless(), + movement_balance + ); + + if movement_balance != movement_aptos_balance { + Err(ValidationError::Unsatisfied( + format!( + "Movement balance for 0x{} is {} coin(s), while Movement Aptos balance is {} coin(s)", + address.short_str_lossless(), + movement_balance, + movement_aptos_balance + ) + .into(), + )) + } else { + Ok(()) + } + } +} diff --git a/movement-migration/validation-tool/src/lib.rs b/movement-migration/validation-tool/src/lib.rs new file mode 100644 index 0000000000000..b43c5d6d56361 --- /dev/null +++ b/movement-migration/validation-tool/src/lib.rs @@ -0,0 +1,34 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; + +pub mod checks; +mod types; + +#[derive(Parser)] +#[clap( + name = "Movement post-migration validation tool", + author, + disable_version_flag = true +)] +pub enum ValidationTool { + #[clap(subcommand)] + Api(checks::api::ApiTool), + Node(checks::node::Command), +} + +impl ValidationTool { + pub async fn run(self) -> anyhow::Result<()> { + match self { + ValidationTool::Api(tool) => tool.run().await, + ValidationTool::Node(cmd) => cmd.run().await, + } + } +} + +#[test] +fn verify_tool() { + use clap::CommandFactory; + ValidationTool::command().debug_assert() +} diff --git a/movement-migration/validation-tool/src/main.rs b/movement-migration/validation-tool/src/main.rs new file mode 100644 index 0000000000000..2ac783968fb67 --- /dev/null +++ b/movement-migration/validation-tool/src/main.rs @@ -0,0 +1,16 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + use clap::Parser; + use tracing_subscriber::EnvFilter; + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + validation_tool::ValidationTool::parse().run().await +} diff --git a/movement-migration/validation-tool/src/types.rs b/movement-migration/validation-tool/src/types.rs new file mode 100644 index 0000000000000..f2f1dda281db1 --- /dev/null +++ b/movement-migration/validation-tool/src/types.rs @@ -0,0 +1,5 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +pub mod api; +pub mod storage; diff --git a/movement-migration/validation-tool/src/types/api.rs b/movement-migration/validation-tool/src/types/api.rs new file mode 100644 index 0000000000000..fa0c7b7775dc4 --- /dev/null +++ b/movement-migration/validation-tool/src/types/api.rs @@ -0,0 +1,45 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use aptos_rest_client::Client; +use std::ops::Deref; + +// pub struct MovementRestClient(Client); +// +// impl MovementRestClient { +// pub fn new(url: &str) -> Result { +// let client = Client::new( +// url.parse() +// .map_err(|e| anyhow::anyhow!("failed to parse Movement rest api url: {}", e))?, +// ); +// Ok(Self(client)) +// } +// } +// +// impl Deref for MovementRestClient { +// type Target = Client; +// +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +// } + +pub struct MovementAptosRestClient(Client); + +impl MovementAptosRestClient { + pub fn new(url: &str) -> Result { + let client = + Client::new(url.parse().map_err(|e| { + anyhow::anyhow!("failed to parse Movement Aptos rest api url: {}", e) + })?); + Ok(Self(client)) + } +} + +impl Deref for MovementAptosRestClient { + type Target = Client; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/movement-migration/validation-tool/src/types/storage.rs b/movement-migration/validation-tool/src/types/storage.rs new file mode 100644 index 0000000000000..d8a8fe615f292 --- /dev/null +++ b/movement-migration/validation-tool/src/types/storage.rs @@ -0,0 +1,136 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use aptos_config::config::{StorageConfig, StorageDirPaths, NO_OP_STORAGE_PRUNER_CONFIG}; +use aptos_db::AptosDB; +use aptos_storage_interface::{ + state_store::state_view::db_state_view::{DbStateView, DbStateViewAtVersion}, + DbReader, +}; +use aptos_types::state_store::state_key::StateKey; +use either::Either; +use std::{ops::Deref, path::PathBuf, sync::Arc}; + +pub struct Storage(Arc); + +impl Storage { + pub fn open(path: &PathBuf) -> anyhow::Result { + let config = StorageConfig::default(); + let aptos_db = AptosDB::open( + StorageDirPaths::from_path(path), + true, + NO_OP_STORAGE_PRUNER_CONFIG, + Default::default(), + false, + config.buffered_state_target_items, + config.max_num_nodes_per_lru_cache_shard, + None, + ) + .context("failed to open aptos db")?; + + Ok(Self(Arc::new(aptos_db))) + } + + /// Gets an [Arc] to the db reader. + fn db_reader(&self) -> Arc { + self.0.clone() + } + + /// Gets the latest version of the ledger. + pub fn latest_ledger_version(&self) -> Result { + let latest_ledger_info = self + .db_reader() + .get_latest_ledger_info() + .context("failed to get latest ledger info")?; + + Ok(latest_ledger_info.ledger_info().version()) + } + + /// Gets the state view at a given version. + pub fn state_view_at_version( + &self, + version: Option, + ) -> Result { + let state_view = self.db_reader().state_view_at_version(version)?; + + Ok(state_view) + } + + /// Gets the all [StateKey]s in the global storage dating back to an original version. None is treated as 0 or all versions. + pub fn global_state_keys_from_version(&self, version: Option) -> GlobalStateKeyIterable { + GlobalStateKeyIterable { + db_reader: self.db_reader(), + version: version.unwrap_or(0), + } + } +} + +pub struct MovementStorage(Storage); + +impl MovementStorage { + pub fn open(path: &PathBuf) -> anyhow::Result { + let storage = Storage::open(path)?; + Ok(Self(storage)) + } +} + +impl Deref for MovementStorage { + type Target = Storage; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +pub struct MovementAptosStorage(Storage); + +impl MovementAptosStorage { + pub fn open(path: &PathBuf) -> anyhow::Result { + let storage = Storage::open(path)?; + Ok(Self(storage)) + } +} + +impl Deref for MovementAptosStorage { + type Target = Storage; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// An iterable of [StateKey]s in the global storage dating back to an original version. +/// +/// This helps deal with lifetime issues. +pub struct GlobalStateKeyIterable { + db_reader: Arc, + version: u64, +} + +const MAX_WRITE_SET_SIZE: u64 = 20_000; + +impl GlobalStateKeyIterable { + pub fn iter( + &self, + ) -> Result> + '_>, anyhow::Error> { + let write_set_iterator = self + .db_reader + .get_write_set_iterator(self.version, MAX_WRITE_SET_SIZE)?; + + // We want to iterate lazily over the write set iterator because there could be a lot of them. + let iter = write_set_iterator.flat_map(move |res| match res { + Ok(write_set) => { + // It should be okay to collect because there should not be that many state keys in a write set. + let items: Vec<_> = write_set + .expect_v0() + .iter() + .map(|(key, _)| Ok(key.clone())) + .collect(); + Either::Left(items.into_iter()) + }, + Err(e) => Either::Right(std::iter::once(Err(e.into()))), + }); + + Ok(Box::new(iter)) + } +} diff --git a/movement-migration/validation-tool/tests/validate_db.rs b/movement-migration/validation-tool/tests/validate_db.rs new file mode 100644 index 0000000000000..b5c78a5afb62e --- /dev/null +++ b/movement-migration/validation-tool/tests/validate_db.rs @@ -0,0 +1,58 @@ +// Copyright (c) Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use std::path::Path; +use tracing::info; + +#[tokio::test] +async fn validate_node_aptos_db() -> Result<(), anyhow::Error> { + tracing_subscriber::fmt::init(); + + let archive_path = Path::new("./assets/maptos.tar.xz"); + assert!( + archive_path.exists(), + "Archive file {} not found", + archive_path.display() + ); + + // Create temporary directory + let temp_dir_old = tempfile::Builder::new().prefix("movement_db-").tempdir()?; + let temp_dir_new = tempfile::Builder::new() + .prefix("movement_aptos_db-") + .tempdir()?; + let movement_db = temp_dir_old.path(); + let movement_aptos_db = temp_dir_new.path(); + + extract_tar_archive(&archive_path, movement_db)?; + extract_tar_archive(&archive_path, movement_aptos_db)?; + + let mut movement_db = movement_db.to_path_buf(); + movement_db.push(".maptos"); + let mut movement_aptos_db = movement_aptos_db.to_path_buf(); + movement_aptos_db.push(".maptos"); + + let cmd = validation_tool::checks::node::Command { + movement_db, + movement_aptos_db, + }; + let node = validation_tool::ValidationTool::Node(cmd); + + node.run().await?; + + Ok(()) +} + +fn extract_tar_archive(archive_path: &Path, temp_dir: &Path) -> std::io::Result<()> { + info!( + "Extracting tarball {} to {}", + archive_path.display(), + temp_dir.display() + ); + let file = std::fs::File::open(archive_path)?; + let buf_reader = std::io::BufReader::new(file); + let decoder = xz2::read::XzDecoder::new(buf_reader); + let mut archive = tar::Archive::new(decoder); + archive.unpack(temp_dir)?; + + Ok(()) +} diff --git a/peer_set.yaml b/peer_set.yaml new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/peer_set.yaml @@ -0,0 +1 @@ +{} diff --git a/state-sync/inter-component/event-notifications/src/lib.rs b/state-sync/inter-component/event-notifications/src/lib.rs index c7fb9481a52a8..18002ac7e0c89 100644 --- a/state-sync/inter-component/event-notifications/src/lib.rs +++ b/state-sync/inter-component/event-notifications/src/lib.rs @@ -294,12 +294,26 @@ impl EventSubscriptionService { error )) })?; - let epoch = ConfigurationResource::fetch_config(&db_state_view) + + let mut epoch = ConfigurationResource::fetch_config(&db_state_view) .ok_or_else(|| { Error::UnexpectedErrorEncountered("Configuration resource does not exist!".into()) })? .epoch(); + let db_epoch_state = self + .storage + .read() + .reader + .get_latest_epoch_state() + .map_err(|_e| { + Error::UnexpectedErrorEncountered("Cannot read epoch state from DB".into()) + })?; + // TODO(l1-migration): update once config fixed + if epoch < db_epoch_state.epoch { + epoch = db_epoch_state.epoch; + } + // Return the new on-chain config payload (containing all found configs at this version). Ok(OnChainConfigPayload::new( epoch, diff --git a/state-sync/state-sync-driver/src/driver.rs b/state-sync/state-sync-driver/src/driver.rs index 2542665851a25..fb7dcc56ad561 100644 --- a/state-sync/state-sync-driver/src/driver.rs +++ b/state-sync/state-sync-driver/src/driver.rs @@ -48,7 +48,6 @@ use tokio_stream::wrappers::IntervalStream; // Useful constants for the driver const DRIVER_INFO_LOG_FREQ_SECS: u64 = 2; const DRIVER_ERROR_LOG_FREQ_SECS: u64 = 3; - /// The configuration of the state sync driver #[derive(Clone)] pub struct DriverConfiguration { @@ -627,7 +626,6 @@ impl< if !self.bootstrapper.is_bootstrapped() && self.is_consensus_or_observer_enabled() && self.driver_configuration.config.enable_auto_bootstrapping - && self.driver_configuration.waypoint.version() == 0 { if let Some(start_time) = self.start_time { if let Some(connection_deadline) = start_time.checked_add(Duration::from_secs( diff --git a/storage/db-tool/src/replay_on_archive.rs b/storage/db-tool/src/replay_on_archive.rs index 78c98703b9ae6..c0e4731ea6a7d 100644 --- a/storage/db-tool/src/replay_on_archive.rs +++ b/storage/db-tool/src/replay_on_archive.rs @@ -79,11 +79,9 @@ impl Opt { let all_errors = verifier.run()?; if !all_errors.is_empty() { error!("{} failed transactions", all_errors.len()); - /* errors were printed as found. for e in all_errors { error!("Failed: {}", e); } - */ process::exit(2); } Ok(()) diff --git a/tools/l1-migration/Cargo.toml b/tools/l1-migration/Cargo.toml new file mode 100644 index 0000000000000..3f8f1b5852191 --- /dev/null +++ b/tools/l1-migration/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "l1-migration" +version = "0.1.0" + +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } + + +[dependencies] +anyhow = "1.0" +aptos-config = { workspace = true } +aptos-crypto = { workspace = true } +aptos-db = { workspace = true } +aptos-storage-interface = { workspace = true } +aptos-types = { workspace = true } +bcs = { workspace = true } +clap = { workspace = true } +serde_yaml = { workspace = true } +hex = "0.4" diff --git a/tools/l1-migration/data/mainnet/genesis.blob b/tools/l1-migration/data/mainnet/genesis.blob new file mode 100644 index 0000000000000..21bb1227f779a Binary files /dev/null and b/tools/l1-migration/data/mainnet/genesis.blob differ diff --git a/tools/l1-migration/data/mainnet/waypoint.txt b/tools/l1-migration/data/mainnet/waypoint.txt new file mode 100644 index 0000000000000..2144a7018c95c --- /dev/null +++ b/tools/l1-migration/data/mainnet/waypoint.txt @@ -0,0 +1 @@ +8740519:1848ea5e9a417e1a90b1bf57f7bf22d0c627da736c5cc884971f9ef5018316d5 \ No newline at end of file diff --git a/tools/l1-migration/local-node-configs/full_node.yaml b/tools/l1-migration/local-node-configs/full_node.yaml new file mode 100644 index 0000000000000..39eaf37b9dd3c --- /dev/null +++ b/tools/l1-migration/local-node-configs/full_node.yaml @@ -0,0 +1,26 @@ +base: + data_dir: /opt/etc + role: full_node + waypoint: + from_file: /opt/etc/waypoint.txt +execution: + genesis_file_location: /opt/etc/genesis.blob +full_node_networks: + - listen_address: /ip4/127.0.0.1/tcp/6184 + discovery_method: onchain + network_id: public +api: + enabled: true + address: 0.0.0.0:8081 +admin_service: + enabled: true + address: 0.0.0.0 + port: 9104 +storage: + backup_service_address: 0.0.0.0:6188 + rocksdb_configs: + enable_storage_sharding: false +state_sync: + state_sync_driver: + bootstrapping_mode: ApplyTransactionOutputsFromGenesis + continuous_syncing_mode: ExecuteTransactionsOrApplyOutputs diff --git a/tools/l1-migration/local-node-configs/validator_node.yaml b/tools/l1-migration/local-node-configs/validator_node.yaml new file mode 100644 index 0000000000000..05991540aa2d9 --- /dev/null +++ b/tools/l1-migration/local-node-configs/validator_node.yaml @@ -0,0 +1,46 @@ +base: + data_dir: /Users/bowu/data/.maptos # update to your path + role: validator + waypoint: + from_file: /Users/bowu/data/.maptos/waypoint.txt # update to your path +consensus: + vote_back_pressure_limit: 50 + safety_rules: + service: + type: local + backend: + type: on_disk_storage + path: /Users/bowu/data/.maptos/secure-data.json # update to your path + namespace: null + initial_safety_rules_config: + from_file: + waypoint: + from_file: /Users/bowu/data/.maptos/waypoint.txt # update to your path + identity_blob_path: /Users/bowu/data/.maptos/operator_keys/mainnet/validator-identity.yaml # update to your path + +execution: + genesis_file_location: /Users/bowu/data/.maptos/genesis.blob # update to your path +storage: + backup_service_address: 0.0.0.0:6186 + rocksdb_configs: + enable_storage_sharding: false +validator_network: + discovery_method: none + mutual_authentication: true + identity: + type: from_file + path: /Users/bowu/data/.maptos/operator_keys/mainnet/validator-identity.yaml # update to your path + listen_address: /ip4/0.0.0.0/tcp/6180 +api: + enabled: true + address: 0.0.0.0:8080 +admin_service: + enabled: true + address: 0.0.0.0 + port: 9102 +state_sync: + state_sync_driver: + bootstrapping_mode: ExecuteOrApplyFromGenesis + continuous_syncing_mode: ExecuteTransactionsOrApplyOutputs + enable_auto_bootstrapping: true + max_connection_deadline_secs: 1 diff --git a/tools/l1-migration/src/lib.rs b/tools/l1-migration/src/lib.rs new file mode 100644 index 0000000000000..f9e67da99919c --- /dev/null +++ b/tools/l1-migration/src/lib.rs @@ -0,0 +1,3 @@ +pub mod utils; + +pub use utils::extract_genesis_and_waypoint; diff --git a/tools/l1-migration/src/main.rs b/tools/l1-migration/src/main.rs new file mode 100644 index 0000000000000..135de1243a4d9 --- /dev/null +++ b/tools/l1-migration/src/main.rs @@ -0,0 +1,57 @@ +use anyhow::{anyhow, Result}; +use aptos_crypto::x25519; +use clap::Parser; +use l1_migration::extract_genesis_and_waypoint; +use std::path::PathBuf; + +/// L1 Migration Tool - Extract genesis and waypoint from database +#[derive(Parser)] +#[command(name = "l1-migration")] +#[command(about = "adhoc command for l1 migration")] +#[command(version)] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[derive(Parser)] +enum Commands { + /// Generate waypoint and genesis files from database + GenerateWaypointGenesis { + /// Path to the database directory + db_path: PathBuf, + /// Destination directory for extracted files + destination_path: PathBuf, + }, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + match args.command { + Commands::GenerateWaypointGenesis { + db_path, + destination_path, + } => { + // Validate that the database path exists + if !db_path.exists() { + eprintln!( + "Error: Database path '{}' does not exist", + db_path.display() + ); + std::process::exit(1); + } + + // Create destination directory if it doesn't exist + if !destination_path.exists() { + std::fs::create_dir_all(&destination_path)?; + } + + // Call the extraction function from the module + let db_path_str = db_path.to_string_lossy(); + let destination_path_str = destination_path.to_string_lossy(); + + extract_genesis_and_waypoint(&db_path_str, &destination_path_str) + }, + } +} diff --git a/tools/l1-migration/src/utils.rs b/tools/l1-migration/src/utils.rs new file mode 100644 index 0000000000000..5579bb1047f47 --- /dev/null +++ b/tools/l1-migration/src/utils.rs @@ -0,0 +1,141 @@ +use anyhow::Result; +use aptos_config::config::{ + Peer, PeerRole, PeerSet, RocksdbConfigs, StorageDirPaths, NO_OP_STORAGE_PRUNER_CONFIG, +}; +use aptos_crypto::{x25519, ValidCryptoMaterialStringExt}; +use aptos_db::AptosDB; +use aptos_storage_interface::DbReader; +use aptos_types::{ + account_address::from_identity_public_key, network_address::NetworkAddress, + transaction::Transaction, waypoint::Waypoint, PeerId, +}; +use serde_yaml; +use std::{ + collections::{HashMap, HashSet}, + fs, + path::Path, + str::FromStr, +}; + +/// Extract genesis transaction and waypoint from an Aptos database +pub fn extract_genesis_and_waypoint(db_path: &str, output_dir: &str) -> Result<()> { + println!("Opening database at: {}", db_path); + + // Create storage directory paths + let storage_dir_paths = StorageDirPaths::from_path(Path::new(db_path)); + + // Open the database with correct API + let db = AptosDB::open( + storage_dir_paths, + true, // readonly + NO_OP_STORAGE_PRUNER_CONFIG, // pruner_config + RocksdbConfigs::default(), + false, // enable_indexer + 1, // buffered_state_target_items + 10000, // max_num_nodes_per_lru_cache_shard + None, // internal_indexer_db + )?; + + println!("Database opened successfully"); + + // Get the latest version to understand the database state + let latest_version = db.get_synced_version()?; + println!("Latest synced version: {:?}", latest_version); + + if latest_version.is_none() { + return Err(anyhow::anyhow!("Database has no synced version")); + } + + let latest_ver = latest_version.unwrap(); + + // Extract genesis transaction + extract_genesis_transaction(&db, latest_ver, output_dir)?; + + // Extract waypoint + extract_waypoint(&db, output_dir)?; + + println!("✓ Genesis extraction completed successfully!"); + println!(" - genesis.blob: Contains the BCS-serialized genesis transaction"); + println!(" - waypoint.txt: Contains the initial waypoint for bootstrapping"); + + Ok(()) +} + +/// Extract the genesis transaction from the database +fn extract_genesis_transaction(db: &AptosDB, latest_ver: u64, output_dir: &str) -> Result<()> { + println!("Extracting genesis transaction (version 0)..."); + let genesis_txn_with_proof = db.get_transaction_by_version(0, latest_ver, false)?; + let genesis_transaction = genesis_txn_with_proof.transaction; + + // Serialize the genesis transaction using BCS + let genesis_bytes = bcs::to_bytes(&genesis_transaction)?; + + // Write genesis.blob + let genesis_path = format!("{}/genesis.blob", output_dir); + fs::write(&genesis_path, &genesis_bytes)?; + println!("Genesis transaction written to: {}", genesis_path); + println!("Genesis blob size: {} bytes", genesis_bytes.len()); + + // Print information about the genesis transaction + print_genesis_transaction_info(&genesis_transaction); + + Ok(()) +} + +/// Extract the waypoint from the database using proper waypoint conversion +fn extract_waypoint(db: &AptosDB, output_dir: &str) -> Result<()> { + // Get the ledger info to extract waypoint + let ledger_info_with_sigs = db.get_latest_ledger_info()?; + let ledger_info = ledger_info_with_sigs.ledger_info(); + + // Generate waypoint using the proper converter + let waypoint = Waypoint::new_any(ledger_info); + + // Write waypoint.txt + let waypoint_path = format!("{}/waypoint.txt", output_dir); + fs::write(&waypoint_path, waypoint.to_string())?; + println!("Waypoint written to: {}", waypoint_path); + println!("Waypoint: {}", waypoint); + + Ok(()) +} + +/// Print detailed information about the genesis transaction +fn print_genesis_transaction_info(genesis_transaction: &Transaction) { + match genesis_transaction { + Transaction::GenesisTransaction(genesis_payload) => { + println!("✓ Found GenesisTransaction (WriteSet transaction)"); + // Access the payload correctly + match genesis_payload { + aptos_types::transaction::WriteSetPayload::Direct(change_set) => { + println!(" Direct WriteSet payload"); + println!( + " Change set size: {} bytes", + bcs::to_bytes(change_set).unwrap_or_default().len() + ); + }, + aptos_types::transaction::WriteSetPayload::Script { .. } => { + println!(" Script-based WriteSet"); + }, + } + }, + Transaction::BlockMetadata(_) => { + println!("⚠ Transaction 0 is BlockMetadata (unexpected for genesis)"); + }, + Transaction::BlockMetadataExt(_) => { + println!("⚠ Transaction 0 is BlockMetadataExt (unexpected for genesis)"); + }, + Transaction::BlockEpilogue(_) => { + println!("⚠ Transaction 0 is BlockEpilogue (unexpected for genesis)"); + }, + Transaction::UserTransaction(_) => { + println!("⚠ Transaction 0 is UserTransaction (unexpected for genesis)"); + }, + Transaction::StateCheckpoint(_) => { + println!("⚠ Transaction 0 is StateCheckpoint (unexpected for genesis)"); + }, + Transaction::ValidatorTransaction(_) => { + println!("⚠ Transaction 0 is ValidatorTransaction (unexpected for genesis)"); + }, + } +} diff --git a/types/src/chain_id.rs b/types/src/chain_id.rs index 3092a08f1db94..aa2c1707e140a 100644 --- a/types/src/chain_id.rs +++ b/types/src/chain_id.rs @@ -22,6 +22,8 @@ pub enum NamedChain { DEVNET = 3, TESTING = 4, PREMAINNET = 5, + MOVEMAINNET = 126, + MOVETESTNET = 250, } const MAINNET: &str = "mainnet"; @@ -29,6 +31,8 @@ const TESTNET: &str = "testnet"; const DEVNET: &str = "devnet"; const TESTING: &str = "testing"; const PREMAINNET: &str = "premainnet"; +const MOVEMENT_MAINNET: &str = "movement_mainnet"; +const MOVEMENT_TESTNET: &str = "movement_testnet"; impl NamedChain { fn str_to_chain_id(string: &str) -> Result { @@ -48,6 +52,8 @@ impl NamedChain { 3 => Ok(NamedChain::DEVNET), // TODO: this is not correct and should removed. The devnet chain ID changes. 4 => Ok(NamedChain::TESTING), 5 => Ok(NamedChain::PREMAINNET), + 126 => Ok(NamedChain::MOVEMAINNET), + 250 => Ok(NamedChain::MOVETESTNET), _ => Err(format!("Not a named chain. Given ID: {:?}", chain_id)), } } @@ -87,6 +93,14 @@ impl ChainId { self.matches_named_chain(NamedChain::MAINNET) } + pub fn is_movement_mainnet(&self) -> bool { + self.matches_named_chain(NamedChain::MOVEMAINNET) + } + + pub fn is_movement_testnet(&self) -> bool { + self.matches_named_chain(NamedChain::MOVETESTNET) + } + /// Returns true iff the chain ID matches the given named chain fn matches_named_chain(&self, expected_chain: NamedChain) -> bool { if let Ok(named_chain) = NamedChain::from_chain_id(self) { @@ -157,6 +171,8 @@ impl fmt::Display for NamedChain { NamedChain::MAINNET => MAINNET, NamedChain::TESTING => TESTING, NamedChain::PREMAINNET => PREMAINNET, + NamedChain::MOVEMAINNET => MOVEMENT_MAINNET, + NamedChain::MOVETESTNET => MOVEMENT_TESTNET, }) } } diff --git a/types/src/on_chain_config/timed_features.rs b/types/src/on_chain_config/timed_features.rs index 6894b165dc1b0..80b83287f2560 100644 --- a/types/src/on_chain_config/timed_features.rs +++ b/types/src/on_chain_config/timed_features.rs @@ -69,6 +69,10 @@ impl TimedFeatureFlag { use TimedFeatureFlag::*; match (self, chain_id) { + (_, MOVEMAINNET | MOVETESTNET) => Los_Angeles + .with_ymd_and_hms(2025, 8, 11, 17, 0, 0) + .unwrap() + .with_timezone(&Utc), // Enabled from the beginning of time. (DisableInvariantViolationCheckInSwapLoc, TESTNET) => BEGINNING_OF_TIME, (DisableInvariantViolationCheckInSwapLoc, MAINNET) => BEGINNING_OF_TIME, diff --git a/types/src/transaction/mod.rs b/types/src/transaction/mod.rs index 75b7e27de026a..bdf4c011d174b 100644 --- a/types/src/transaction/mod.rs +++ b/types/src/transaction/mod.rs @@ -1774,6 +1774,15 @@ impl TransactionOutput { const ERR_MSG: &str = "TransactionOutput does not match TransactionInfo"; let expected_txn_status: TransactionStatus = txn_info.status().clone().into(); + match &expected_txn_status { + TransactionStatus::Keep(stat) => { + match stat { + ExecutionStatus::MiscellaneousError(_) => return Ok(()), // skip all the debug info mismatch for now + _ => (), + } + }, + _ => (), + } ensure!( self.status() == &expected_txn_status, "{}: version:{}, status:{:?}, auxiliary data:{:?}, expected:{:?}", diff --git a/types/src/waypoint.rs b/types/src/waypoint.rs index a2b848ae7c964..c90f843f3642b 100644 --- a/types/src/waypoint.rs +++ b/types/src/waypoint.rs @@ -21,7 +21,6 @@ use std::{ // The delimiter between the version and the hash. const WAYPOINT_DELIMITER: char = ':'; - /// Waypoint keeps information about the LedgerInfo on a given version, which provides an /// off-chain mechanism to verify the sync process right after the restart. /// At high level, a trusted waypoint verifies the LedgerInfo for a certain epoch change.