From a14b68b51d12f4199c085cb4e7945b1754ff4a5e Mon Sep 17 00:00:00 2001 From: zhoujunma Date: Fri, 1 Mar 2024 15:04:25 -0800 Subject: [PATCH] randomness #9: smoke tests from randomnet (#12282) * smoke test deps and 1st case from randomnet * update * more smoke tests --- Cargo.lock | 5 + .../move-examples/on_chain_dice/Move.toml | 9 + .../on_chain_dice/sources/dice.move | 21 ++ aptos-move/vm-genesis/src/lib.rs | 24 +- crates/aptos-genesis/src/builder.rs | 4 +- crates/aptos-genesis/src/lib.rs | 5 +- crates/aptos-genesis/src/mainnet.rs | 8 +- crates/aptos/src/genesis/mod.rs | 2 + crates/aptos/src/test/mod.rs | 13 +- testsuite/smoke-test/Cargo.toml | 5 + testsuite/smoke-test/src/lib.rs | 2 + .../src/randomness/disable_feature_0.rs | 101 +++++++ .../src/randomness/disable_feature_1.rs | 111 ++++++++ .../src/randomness/dkg_with_validator_down.rs | 59 ++++ .../dkg_with_validator_join_leave.rs | 144 ++++++++++ .../src/randomness/e2e_basic_consumption.rs | 107 +++++++ .../src/randomness/e2e_correctness.rs | 82 ++++++ .../src/randomness/enable_feature_0.rs | 132 +++++++++ .../src/randomness/enable_feature_1.rs | 133 +++++++++ .../src/randomness/enable_feature_2.rs | 111 ++++++++ testsuite/smoke-test/src/randomness/mod.rs | 260 ++++++++++++++++++ .../validator_restart_during_dkg.rs | 109 ++++++++ 22 files changed, 1439 insertions(+), 8 deletions(-) create mode 100644 aptos-move/move-examples/on_chain_dice/Move.toml create mode 100644 aptos-move/move-examples/on_chain_dice/sources/dice.move create mode 100644 testsuite/smoke-test/src/randomness/disable_feature_0.rs create mode 100644 testsuite/smoke-test/src/randomness/disable_feature_1.rs create mode 100644 testsuite/smoke-test/src/randomness/dkg_with_validator_down.rs create mode 100644 testsuite/smoke-test/src/randomness/dkg_with_validator_join_leave.rs create mode 100644 testsuite/smoke-test/src/randomness/e2e_basic_consumption.rs create mode 100644 testsuite/smoke-test/src/randomness/e2e_correctness.rs create mode 100644 testsuite/smoke-test/src/randomness/enable_feature_0.rs create mode 100644 testsuite/smoke-test/src/randomness/enable_feature_1.rs create mode 100644 testsuite/smoke-test/src/randomness/enable_feature_2.rs create mode 100644 testsuite/smoke-test/src/randomness/mod.rs create mode 100644 testsuite/smoke-test/src/randomness/validator_restart_during_dkg.rs diff --git a/Cargo.lock b/Cargo.lock index 2d19ba66e1eb2f..9881b2689e6eca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14975,6 +14975,8 @@ dependencies = [ "aptos-consensus", "aptos-crypto", "aptos-db", + "aptos-debugger", + "aptos-dkg", "aptos-faucet-core", "aptos-forge", "aptos-framework", @@ -15004,16 +15006,19 @@ dependencies = [ "base64 0.13.1", "bcs 0.1.4", "diesel", + "digest 0.9.0", "futures", "hex", "hyper", "move-core-types", + "num-traits", "num_cpus", "once_cell", "proptest", "rand 0.7.3", "regex", "reqwest", + "serde", "serde_json", "serde_yaml 0.8.26", "tokio", diff --git a/aptos-move/move-examples/on_chain_dice/Move.toml b/aptos-move/move-examples/on_chain_dice/Move.toml new file mode 100644 index 00000000000000..32c79fb57df7f6 --- /dev/null +++ b/aptos-move/move-examples/on_chain_dice/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "OnChainDice" +version = "0.0.0" + +[addresses] +module_owner = "_" + +[dependencies] +AptosFramework = { local = "../../framework/aptos-framework" } diff --git a/aptos-move/move-examples/on_chain_dice/sources/dice.move b/aptos-move/move-examples/on_chain_dice/sources/dice.move new file mode 100644 index 00000000000000..304b6852845482 --- /dev/null +++ b/aptos-move/move-examples/on_chain_dice/sources/dice.move @@ -0,0 +1,21 @@ +module module_owner::dice { + use std::signer::address_of; + use std::vector; + use aptos_framework::randomness; + + struct DiceRollHistory has key { + rolls: vector, + } + + entry fun roll(account: signer) acquires DiceRollHistory { + let addr = address_of(&account); + let roll_history = if (exists(addr)) { + move_from(addr) + } else { + DiceRollHistory { rolls: vector[] } + }; + let new_roll = randomness::u64_range(0, 6); + vector::push_back(&mut roll_history.rolls, new_roll); + move_to(&account, roll_history); + } +} diff --git a/aptos-move/vm-genesis/src/lib.rs b/aptos-move/vm-genesis/src/lib.rs index 9e08eb83f37e3c..0e59ce8ca874a7 100644 --- a/aptos-move/vm-genesis/src/lib.rs +++ b/aptos-move/vm-genesis/src/lib.rs @@ -80,6 +80,7 @@ pub struct GenesisConfiguration { pub voting_power_increase_limit: u64, pub employee_vesting_start: u64, pub employee_vesting_period_duration: u64, + pub initial_features_override: Option, } pub static GENESIS_KEYPAIR: Lazy<(Ed25519PrivateKey, Ed25519PublicKey)> = Lazy::new(|| { @@ -140,7 +141,13 @@ pub fn encode_aptos_mainnet_genesis_transaction( &execution_config, &gas_schedule, ); - initialize_features(&mut session); + initialize_features( + &mut session, + genesis_config + .initial_features_override + .clone() + .map(Features::into_flag_vec), + ); initialize_aptos_coin(&mut session); initialize_on_chain_governance(&mut session, genesis_config); create_accounts(&mut session, accounts); @@ -249,7 +256,13 @@ pub fn encode_genesis_change_set( execution_config, gas_schedule, ); - initialize_features(&mut session); + initialize_features( + &mut session, + genesis_config + .initial_features_override + .clone() + .map(Features::into_flag_vec), + ); if genesis_config.is_test { initialize_core_resources_and_aptos_coin(&mut session, core_resources_key); } else { @@ -472,8 +485,9 @@ pub fn default_features() -> Vec { ] } -fn initialize_features(session: &mut SessionExt) { - let features: Vec = default_features() +fn initialize_features(session: &mut SessionExt, features_override: Option>) { + let features: Vec = features_override + .unwrap_or_else(default_features) .into_iter() .map(|feature| feature as u64) .collect(); @@ -930,6 +944,7 @@ pub fn generate_test_genesis( voting_power_increase_limit: 50, employee_vesting_start: 1663456089, employee_vesting_period_duration: 5 * 60, // 5 minutes + initial_features_override: None, }, &OnChainConsensusConfig::default_for_genesis(), &OnChainExecutionConfig::default_for_genesis(), @@ -977,6 +992,7 @@ fn mainnet_genesis_config() -> GenesisConfiguration { voting_power_increase_limit: 30, employee_vesting_start: 1663456089, employee_vesting_period_duration: 5 * 60, // 5 minutes + initial_features_override: None, } } diff --git a/crates/aptos-genesis/src/builder.rs b/crates/aptos-genesis/src/builder.rs index 2f9992df2358a6..7ad40777cbcd0e 100644 --- a/crates/aptos-genesis/src/builder.rs +++ b/crates/aptos-genesis/src/builder.rs @@ -27,7 +27,7 @@ use aptos_keygen::KeyGen; use aptos_logger::prelude::*; use aptos_types::{ chain_id::ChainId, - on_chain_config::{GasScheduleV2, OnChainConsensusConfig, OnChainExecutionConfig}, + on_chain_config::{Features, GasScheduleV2, OnChainConsensusConfig, OnChainExecutionConfig}, transaction::Transaction, waypoint::Waypoint, }; @@ -431,6 +431,7 @@ pub struct GenesisConfiguration { pub consensus_config: OnChainConsensusConfig, pub execution_config: OnChainExecutionConfig, pub gas_schedule: GasScheduleV2, + pub initial_features_override: Option, } pub type InitConfigFn = Arc; @@ -648,6 +649,7 @@ impl Builder { consensus_config: OnChainConsensusConfig::default_for_genesis(), execution_config: OnChainExecutionConfig::default_for_genesis(), gas_schedule: default_gas_schedule(), + initial_features_override: None, }; if let Some(init_genesis_config) = &self.init_genesis_config { (init_genesis_config)(&mut genesis_config); diff --git a/crates/aptos-genesis/src/lib.rs b/crates/aptos-genesis/src/lib.rs index 73633e2d2c73c3..68d61db174eee1 100644 --- a/crates/aptos-genesis/src/lib.rs +++ b/crates/aptos-genesis/src/lib.rs @@ -23,7 +23,7 @@ use aptos_storage_interface::DbReaderWriter; use aptos_temppath::TempPath; use aptos_types::{ chain_id::ChainId, - on_chain_config::{GasScheduleV2, OnChainConsensusConfig, OnChainExecutionConfig}, + on_chain_config::{Features, GasScheduleV2, OnChainConsensusConfig, OnChainExecutionConfig}, transaction::Transaction, waypoint::Waypoint, }; @@ -70,6 +70,7 @@ pub struct GenesisInfo { pub consensus_config: OnChainConsensusConfig, pub execution_config: OnChainExecutionConfig, pub gas_schedule: GasScheduleV2, + pub initial_features_override: Option, } impl GenesisInfo { @@ -106,6 +107,7 @@ impl GenesisInfo { consensus_config: genesis_config.consensus_config.clone(), execution_config: genesis_config.execution_config.clone(), gas_schedule: genesis_config.gas_schedule.clone(), + initial_features_override: genesis_config.initial_features_override.clone(), }) } @@ -138,6 +140,7 @@ impl GenesisInfo { voting_power_increase_limit: self.voting_power_increase_limit, employee_vesting_start: 1663456089, employee_vesting_period_duration: 5 * 60, // 5 minutes + initial_features_override: self.initial_features_override.clone(), }, &self.consensus_config, &self.execution_config, diff --git a/crates/aptos-genesis/src/mainnet.rs b/crates/aptos-genesis/src/mainnet.rs index 7e1aa674dd72d5..068f919b170e15 100644 --- a/crates/aptos-genesis/src/mainnet.rs +++ b/crates/aptos-genesis/src/mainnet.rs @@ -10,7 +10,9 @@ use aptos_db::AptosDB; use aptos_framework::ReleaseBundle; use aptos_storage_interface::DbReaderWriter; use aptos_temppath::TempPath; -use aptos_types::{chain_id::ChainId, transaction::Transaction, waypoint::Waypoint}; +use aptos_types::{ + chain_id::ChainId, on_chain_config::Features, transaction::Transaction, waypoint::Waypoint, +}; use aptos_vm::AptosVM; use aptos_vm_genesis::{AccountBalance, EmployeePool, ValidatorWithCommissionRate}; @@ -54,6 +56,8 @@ pub struct MainnetGenesisInfo { employee_vesting_start: u64, /// Duration of each vesting period (in seconds). employee_vesting_period_duration: u64, + + initial_features_override: Option, } impl MainnetGenesisInfo { @@ -93,6 +97,7 @@ impl MainnetGenesisInfo { voting_power_increase_limit: genesis_config.voting_power_increase_limit, employee_vesting_start, employee_vesting_period_duration, + initial_features_override: genesis_config.initial_features_override.clone(), }) } @@ -126,6 +131,7 @@ impl MainnetGenesisInfo { voting_power_increase_limit: self.voting_power_increase_limit, employee_vesting_start: self.employee_vesting_start, employee_vesting_period_duration: self.employee_vesting_period_duration, + initial_features_override: self.initial_features_override.clone(), }, ) } diff --git a/crates/aptos/src/genesis/mod.rs b/crates/aptos/src/genesis/mod.rs index 9c9155645175b1..830eddfa51bd01 100644 --- a/crates/aptos/src/genesis/mod.rs +++ b/crates/aptos/src/genesis/mod.rs @@ -257,6 +257,7 @@ pub fn fetch_mainnet_genesis_info(git_options: GitOptions) -> CliTypedResult CliTypedResult PathBuf { + pub fn aptos_framework_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") diff --git a/testsuite/smoke-test/Cargo.toml b/testsuite/smoke-test/Cargo.toml index acbb13ca842561..501e639fa553bb 100644 --- a/testsuite/smoke-test/Cargo.toml +++ b/testsuite/smoke-test/Cargo.toml @@ -21,6 +21,8 @@ aptos-config = { workspace = true } aptos-consensus = { workspace = true } aptos-crypto = { workspace = true } aptos-db = { workspace = true } +aptos-debugger = { workspace = true } +aptos-dkg = { workspace = true } aptos-faucet-core = { workspace = true } aptos-forge = { workspace = true } aptos-framework = { workspace = true } @@ -49,11 +51,14 @@ diesel = { workspace = true, features = [ "numeric", "serde_json", ] } +digest = { workspace = true } hex = { workspace = true } hyper = { workspace = true } move-core-types = { workspace = true } +num-traits = { workspace = true } proptest = { workspace = true } reqwest = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } url = { workspace = true } diff --git a/testsuite/smoke-test/src/lib.rs b/testsuite/smoke-test/src/lib.rs index b72bdf51340267..dfc612e30e0625 100644 --- a/testsuite/smoke-test/src/lib.rs +++ b/testsuite/smoke-test/src/lib.rs @@ -31,6 +31,8 @@ mod network; #[cfg(test)] mod oidb; #[cfg(test)] +mod randomness; +#[cfg(test)] mod rest_api; #[cfg(test)] mod rosetta; diff --git a/testsuite/smoke-test/src/randomness/disable_feature_0.rs b/testsuite/smoke-test/src/randomness/disable_feature_0.rs new file mode 100644 index 00000000000000..949c20e654b6e4 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/disable_feature_0.rs @@ -0,0 +1,101 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, get_on_chain_resource, verify_dkg_transcript}, + smoke_test_environment::SwarmBuilder, +}; +use aptos_forge::{Node, Swarm, SwarmExt}; +use aptos_logger::{debug, info}; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, + randomness::PerBlockRandomness, +}; +use std::{sync::Arc, time::Duration}; + +/// Disable on-chain randomness by only disabling feature `RECONFIGURE_WITH_DKG`. +#[tokio::test] +async fn disable_feature_0() { + let epoch_duration_secs = 20; + + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + conf.allow_new_validators = true; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let root_addr = swarm.chain_info().root_account().address(); + let root_idx = cli.add_account_with_address_to_cli(swarm.root_key(), root_addr); + + let decrypt_key_map = decrypt_key_map(&swarm); + + let client_endpoint = swarm.validators().nth(1).unwrap().rest_api_endpoint(); + let client = aptos_rest_client::Client::new(client_endpoint.clone()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(3, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 3."); + + info!("Now in epoch 3. Disabling feature RECONFIGURE_WITH_DKG."); + let disable_dkg_script = r#" +script { + use aptos_framework::aptos_governance; + fun main(core_resources: &signer) { + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let dkg_feature_id: u64 = std::features::get_reconfigure_with_dkg_feature(); + aptos_governance::toggle_features(&framework_signer, vector[], vector[dkg_feature_id]); + } +} +"#; + + let txn_summary = cli + .run_script(root_idx, disable_dkg_script) + .await + .expect("Txn execution error."); + debug!("disabling_dkg_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(4, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 4."); + + info!("Now in epoch 4. DKG transcript should still be available. Randomness seed should be unavailable."); + let dkg_session = get_on_chain_resource::(&client) + .await + .last_completed + .expect("dkg result for epoch 4 should be present"); + assert_eq!(4, dkg_session.target_epoch()); + assert!(verify_dkg_transcript(&dkg_session, &decrypt_key_map).is_ok()); + + let randomness_seed = get_on_chain_resource::(&client).await; + assert!(randomness_seed.seed.is_none()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(5, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 5."); + + info!("Now in epoch 5. DKG transcript should be unavailable. Randomness seed should be unavailable."); + let maybe_last_complete = get_on_chain_resource::(&client) + .await + .last_completed; + assert!( + maybe_last_complete.is_none() || maybe_last_complete.as_ref().unwrap().target_epoch() != 5 + ); + + let randomness_seed = get_on_chain_resource::(&client).await; + assert!(randomness_seed.seed.is_none()); +} diff --git a/testsuite/smoke-test/src/randomness/disable_feature_1.rs b/testsuite/smoke-test/src/randomness/disable_feature_1.rs new file mode 100644 index 00000000000000..07a281aff50714 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/disable_feature_1.rs @@ -0,0 +1,111 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, get_on_chain_resource, verify_dkg_transcript}, + smoke_test_environment::SwarmBuilder, + utils::get_current_consensus_config, +}; +use aptos_forge::{Node, Swarm, SwarmExt}; +use aptos_logger::{debug, info}; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, + randomness::PerBlockRandomness, +}; +use std::{sync::Arc, time::Duration}; + +/// Disable on-chain randomness by only disabling validator transactions. +#[tokio::test] +async fn disable_feature_1() { + let epoch_duration_secs = 20; + + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + conf.allow_new_validators = true; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let root_addr = swarm.chain_info().root_account().address(); + let root_idx = cli.add_account_with_address_to_cli(swarm.root_key(), root_addr); + + let decrypt_key_map = decrypt_key_map(&swarm); + + let client_endpoint = swarm.validators().nth(1).unwrap().rest_api_endpoint(); + let client = aptos_rest_client::Client::new(client_endpoint.clone()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(3, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 3."); + + info!("Now in epoch 3. Disabling validator transactions."); + let mut config = get_current_consensus_config(&client).await; + assert!(config.is_vtxn_enabled()); + config.disable_validator_txns(); + let config_bytes = bcs::to_bytes(&config).unwrap(); + let disable_vtxn_script = format!( + r#" +script {{ + use aptos_framework::aptos_governance; + use aptos_framework::consensus_config; + fun main(core_resources: &signer) {{ + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let config_bytes = vector{:?}; + consensus_config::set_for_next_epoch(&framework_signer, config_bytes); + aptos_governance::reconfigure(&framework_signer); + }} +}} +"#, + config_bytes + ); + debug!("disable_vtxn_script={}", disable_vtxn_script); + let txn_summary = cli + .run_script(root_idx, disable_vtxn_script.as_str()) + .await + .expect("Txn execution error."); + debug!("disabling_vtxn_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(4, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 4."); + + info!("Now in epoch 4. DKG transcript should still be available. Randomness seed should be unavailable."); + let dkg_session = get_on_chain_resource::(&client) + .await + .last_completed + .expect("dkg result for epoch 4 should be present"); + assert_eq!(4, dkg_session.target_epoch()); + assert!(verify_dkg_transcript(&dkg_session, &decrypt_key_map).is_ok()); + + let randomness_seed = get_on_chain_resource::(&client).await; + assert!(randomness_seed.seed.is_none()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(5, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 5."); + + info!("Now in epoch 5. DKG transcript should be unavailable. Randomness seed should be unavailable."); + let maybe_last_complete = get_on_chain_resource::(&client) + .await + .last_completed; + assert!( + maybe_last_complete.is_none() || maybe_last_complete.as_ref().unwrap().target_epoch() != 5 + ); + + let randomness_seed = get_on_chain_resource::(&client).await; + assert!(randomness_seed.seed.is_none()); +} diff --git a/testsuite/smoke-test/src/randomness/dkg_with_validator_down.rs b/testsuite/smoke-test/src/randomness/dkg_with_validator_down.rs new file mode 100644 index 00000000000000..dd1e2b3212593b --- /dev/null +++ b/testsuite/smoke-test/src/randomness/dkg_with_validator_down.rs @@ -0,0 +1,59 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, verify_dkg_transcript, wait_for_dkg_finish}, + smoke_test_environment::SwarmBuilder, +}; +use aptos_forge::NodeExt; +use aptos_types::on_chain_config::{FeatureFlag, Features}; +use std::sync::Arc; + +#[tokio::test] +async fn dkg_with_validator_down() { + let epoch_duration_secs = 10; + let estimated_dkg_latency_secs = 20; + let time_limit_secs = epoch_duration_secs + estimated_dkg_latency_secs; + + let mut swarm = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(|conf| { + conf.epoch_duration_secs = 10; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build() + .await; + let decrypt_key_map = decrypt_key_map(&swarm); + + let client = swarm.validators().last().unwrap().rest_client(); + println!("Wait for an epoch start."); + let dkg_session_1 = wait_for_dkg_finish(&client, None, time_limit_secs).await; + + println!("Current epoch is {}.", dkg_session_1.target_epoch()); + + println!("Take one validator down."); + swarm.validators_mut().take(1).for_each(|v| { + v.stop(); + }); + + println!( + "Wait until we fully entered epoch {}.", + dkg_session_1.target_epoch() + 1 + ); + + let dkg_session_2 = wait_for_dkg_finish( + &client, + Some(dkg_session_1.target_epoch() + 1), + time_limit_secs, + ) + .await; + + assert!(verify_dkg_transcript(&dkg_session_2, &decrypt_key_map).is_ok()); +} diff --git a/testsuite/smoke-test/src/randomness/dkg_with_validator_join_leave.rs b/testsuite/smoke-test/src/randomness/dkg_with_validator_join_leave.rs new file mode 100644 index 00000000000000..3ba10aae34db3d --- /dev/null +++ b/testsuite/smoke-test/src/randomness/dkg_with_validator_join_leave.rs @@ -0,0 +1,144 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, num_validators, verify_dkg_transcript, wait_for_dkg_finish}, + smoke_test_environment::SwarmBuilder, +}; +use aptos::test::CliTestFramework; +use aptos_forge::{Node, Swarm}; +use aptos_types::on_chain_config::{FeatureFlag, Features}; +use std::sync::Arc; + +#[tokio::test] +async fn dkg_with_validator_join_leave() { + let epoch_duration_secs = 40; + let estimated_dkg_latency_secs = 80; + let time_limit_secs = epoch_duration_secs + estimated_dkg_latency_secs; + + let mut swarm = SwarmBuilder::new_local(7) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + conf.allow_new_validators = true; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build() + .await; + + let decrypt_key_map = decrypt_key_map(&swarm); + + println!("Wait for a moment when DKG is not running."); + let client_endpoint = swarm.validators().nth(1).unwrap().rest_api_endpoint(); + let client = aptos_rest_client::Client::new(client_endpoint.clone()); + let dkg_session_1 = wait_for_dkg_finish(&client, None, time_limit_secs).await; + println!( + "Current epoch is {}. Number of validators: {}.", + dkg_session_1.target_epoch(), + num_validators(&dkg_session_1) + ); + + println!( + "Wait until we fully entered epoch {}.", + dkg_session_1.target_epoch() + 1 + ); + let dkg_session_2 = wait_for_dkg_finish( + &client, + Some(dkg_session_1.target_epoch() + 1), + time_limit_secs, + ) + .await; + + println!( + "Current epoch is {}. Number of validators: {}.", + dkg_session_2.target_epoch(), + num_validators(&dkg_session_2) + ); + + println!("Letting one of the validators leave."); + let (victim_validator_sk, victim_validator_addr) = { + let victim_validator = swarm.validators().next().unwrap(); + let sk = victim_validator + .account_private_key() + .clone() + .unwrap() + .private_key(); + let addr = victim_validator.peer_id(); + (sk, addr) + }; + + println!("Give the victim some money so it can first send transactions."); + let mut public_info = swarm.chain_info().into_aptos_public_info(); + public_info + .mint(victim_validator_addr, 100000000000000) + .await + .unwrap(); + + println!("Send the txn to request leave."); + let faucet_endpoint: reqwest::Url = "http://localhost:8081".parse().unwrap(); + let mut cli = CliTestFramework::new( + client_endpoint, + faucet_endpoint, + /*num_cli_accounts=*/ 0, + ) + .await; + let idx = cli.add_account_to_cli(victim_validator_sk); + let txn_result = cli.leave_validator_set(idx, None).await.unwrap(); + println!("Txn result: {:?}", txn_result); + + println!( + "Wait until we fully entered epoch {}.", + dkg_session_2.target_epoch() + 1 + ); + let dkg_session_3 = wait_for_dkg_finish( + &client, + Some(dkg_session_2.target_epoch() + 1), + time_limit_secs, + ) + .await; + + println!( + "Current epoch is {}. Number of validators: {}.", + dkg_session_3.target_epoch(), + num_validators(&dkg_session_3) + ); + + assert!(verify_dkg_transcript(&dkg_session_3, &decrypt_key_map).is_ok()); + assert_eq!( + num_validators(&dkg_session_3), + num_validators(&dkg_session_2) - 1 + ); + + println!("Now re-join."); + let txn_result = cli.join_validator_set(idx, None).await; + println!("Txn result: {:?}", txn_result); + println!( + "Wait until we fully entered epoch {}.", + dkg_session_3.target_epoch() + 1 + ); + let dkg_session_4 = wait_for_dkg_finish( + &client, + Some(dkg_session_3.target_epoch() + 1), + time_limit_secs, + ) + .await; + + println!( + "Current epoch is {}. Number of validators: {}.", + dkg_session_4.target_epoch(), + num_validators(&dkg_session_4) + ); + + assert!(verify_dkg_transcript(&dkg_session_4, &decrypt_key_map).is_ok()); + assert_eq!( + num_validators(&dkg_session_4), + num_validators(&dkg_session_3) + 1 + ); +} diff --git a/testsuite/smoke-test/src/randomness/e2e_basic_consumption.rs b/testsuite/smoke-test/src/randomness/e2e_basic_consumption.rs new file mode 100644 index 00000000000000..f11d02e255eb6d --- /dev/null +++ b/testsuite/smoke-test/src/randomness/e2e_basic_consumption.rs @@ -0,0 +1,107 @@ +// Copyright © Aptos Foundation + +use crate::smoke_test_environment::SwarmBuilder; +use aptos::{move_tool::MemberId, test::CliTestFramework}; +use aptos_forge::{NodeExt, Swarm, SwarmExt}; +use aptos_logger::info; +use aptos_types::on_chain_config::{FeatureFlag, Features}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, str::FromStr, sync::Arc, time::Duration}; + +/// Publish the `on-chain-dice` example module, +/// run its function that consume on-chain randomness, and +/// print out the random results. +#[tokio::test] +async fn e2e_basic_consumption() { + let epoch_duration_secs = 20; + + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let rest_client = swarm.validators().next().unwrap().rest_client(); + + info!("Wait for epoch 2. Epoch 1 does not have randomness."); + swarm + .wait_for_all_nodes_to_catchup_to_epoch(2, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Epoch 2 taking too long to arrive!"); + + let root_address = swarm.chain_info().root_account().address(); + info!("Root account: {}", root_address); + let _root_idx = cli.add_account_with_address_to_cli(swarm.root_key(), root_address); + + info!("Publishing OnChainDice module."); + publish_on_chain_dice_module(&mut cli, 0).await; + + info!("Rolling the dice."); + let account = cli.account_id(0).to_hex_literal(); + let roll_func_id = MemberId::from_str(&format!("{}::dice::roll", account)).unwrap(); + for _ in 0..10 { + let txn_summary = cli + .run_function(0, None, roll_func_id.clone(), vec![], vec![]) + .await + .unwrap(); + info!("Roll txn summary: {:?}", txn_summary); + } + + info!("Collecting roll history."); + let dice_roll_history = rest_client + .get_account_resource_bcs::( + root_address, + format!("{}::dice::DiceRollHistory", account).as_str(), + ) + .await + .unwrap() + .into_inner(); + + info!("Roll history: {:?}", dice_roll_history.rolls); +} + +#[derive(Deserialize, Serialize)] +struct DiceRollHistory { + rolls: Vec, +} + +async fn publish_on_chain_dice_module(cli: &mut CliTestFramework, publisher_account_idx: usize) { + cli.init_move_dir(); + let mut package_addresses = BTreeMap::new(); + package_addresses.insert("module_owner", "_"); + + cli.init_package( + "OnChainDice".to_string(), + package_addresses, + Some(CliTestFramework::aptos_framework_dir()), + ) + .await + .unwrap(); + + let content = + include_str!("../../../../aptos-move/move-examples/on_chain_dice/sources/dice.move") + .to_string(); + cli.add_file_in_package("sources/dice.move", content); + + cli.wait_for_account(publisher_account_idx).await.unwrap(); + + info!("Move package dir: {}", cli.move_dir().display()); + + let mut named_addresses = BTreeMap::new(); + let account_str = cli.account_id(publisher_account_idx).to_string(); + named_addresses.insert("module_owner", account_str.as_str()); + cli.publish_package(0, None, named_addresses, None) + .await + .unwrap(); +} diff --git a/testsuite/smoke-test/src/randomness/e2e_correctness.rs b/testsuite/smoke-test/src/randomness/e2e_correctness.rs new file mode 100644 index 00000000000000..56a96cf2397353 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/e2e_correctness.rs @@ -0,0 +1,82 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{ + decrypt_key_map, get_current_version, get_on_chain_resource, verify_dkg_transcript, + verify_randomness, + }, + smoke_test_environment::SwarmBuilder, +}; +use aptos_forge::{NodeExt, SwarmExt}; +use aptos_logger::info; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, +}; +use std::{sync::Arc, time::Duration}; + +/// Verify the correctness of DKG transcript and block-level randomness seed. +#[tokio::test] +async fn randomness_correctness() { + let epoch_duration_secs = 20; + + let (swarm, _cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let decrypt_key_map = decrypt_key_map(&swarm); + let rest_client = swarm.validators().next().unwrap().rest_client(); + + info!("Wait for epoch 2. Epoch 1 does not have randomness."); + swarm + .wait_for_all_nodes_to_catchup_to_epoch(2, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Epoch 2 taking too long to arrive!"); + + info!("Verify DKG correctness for epoch 2."); + let dkg_session = get_on_chain_resource::(&rest_client).await; + assert!(verify_dkg_transcript(dkg_session.last_complete(), &decrypt_key_map).is_ok()); + + // Verify the randomness in 5 versions. + for _ in 0..5 { + let cur_txn_version = get_current_version(&rest_client).await; + info!("Verifying WVUF output for version {}.", cur_txn_version); + let wvuf_verify_result = + verify_randomness(&decrypt_key_map, &rest_client, cur_txn_version).await; + println!("wvuf_verify_result={:?}", wvuf_verify_result); + assert!(wvuf_verify_result.is_ok()); + } + + info!("Wait for epoch 3."); + swarm + .wait_for_all_nodes_to_catchup_to_epoch(3, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Epoch 3 taking too long to arrive!"); + + info!("Verify DKG correctness for epoch 3."); + let dkg_session = get_on_chain_resource::(&rest_client).await; + assert!(verify_dkg_transcript(dkg_session.last_complete(), &decrypt_key_map).is_ok()); + + // Again, verify the randomness in 5 versions. + for _ in 0..5 { + let cur_txn_version = get_current_version(&rest_client).await; + info!("Verifying WVUF output for version {}.", cur_txn_version); + let wvuf_verify_result = + verify_randomness(&decrypt_key_map, &rest_client, cur_txn_version).await; + println!("wvuf_verify_result={:?}", wvuf_verify_result); + assert!(wvuf_verify_result.is_ok()); + } +} diff --git a/testsuite/smoke-test/src/randomness/enable_feature_0.rs b/testsuite/smoke-test/src/randomness/enable_feature_0.rs new file mode 100644 index 00000000000000..54659dedf62ff9 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/enable_feature_0.rs @@ -0,0 +1,132 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, get_on_chain_resource, verify_dkg_transcript}, + smoke_test_environment::SwarmBuilder, + utils::get_current_consensus_config, +}; +use aptos_forge::{Node, Swarm, SwarmExt}; +use aptos_logger::{debug, info}; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, +}; +use std::{sync::Arc, time::Duration}; + +/// Enable on-chain randomness in the following steps. +/// - Enable feature `RECONFIGURE_WITH_DKG` in epoch `e`. +/// - Enable validator transactions in consensus config in epoch `e + 1`. +#[tokio::test] +async fn enable_feature_0() { + let epoch_duration_secs = 20; + let estimated_dkg_latency_secs = 40; + + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + conf.allow_new_validators = true; + + // start with vtxn disabled. + conf.consensus_config.disable_validator_txns(); + + // start with dkg disabled. + let mut features = Features::default(); + features.disable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let root_addr = swarm.chain_info().root_account().address(); + let root_idx = cli.add_account_with_address_to_cli(swarm.root_key(), root_addr); + + let decrypt_key_map = decrypt_key_map(&swarm); + + let client_endpoint = swarm.validators().nth(1).unwrap().rest_api_endpoint(); + let client = aptos_rest_client::Client::new(client_endpoint.clone()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(3, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 3."); + + info!("Now in epoch 3. Enabling feature RECONFIGURE_WITH_DKG."); + let enable_dkg_script = r#" +script { + use aptos_framework::aptos_governance; + fun main(core_resources: &signer) { + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let dkg_feature_id: u64 = std::features::get_reconfigure_with_dkg_feature(); + aptos_governance::toggle_features(&framework_signer, vector[dkg_feature_id], vector[]); + } +} +"#; + + let txn_summary = cli + .run_script(root_idx, enable_dkg_script) + .await + .expect("Txn execution error."); + debug!("enabling_dkg_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(4, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 4."); + + info!("Now in epoch 4. Enabling validator transactions."); + let mut config = get_current_consensus_config(&client).await; + config.enable_validator_txns(); + let config_bytes = bcs::to_bytes(&config).unwrap(); + let enable_vtxn_script = format!( + r#" +script {{ + use aptos_framework::aptos_governance; + use aptos_framework::consensus_config; + fun main(core_resources: &signer) {{ + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let config_bytes = vector{:?}; + consensus_config::set_for_next_epoch(&framework_signer, config_bytes); + aptos_governance::reconfigure(&framework_signer); + }} +}} +"#, + config_bytes + ); + debug!("enable_vtxn_script={}", enable_vtxn_script); + let txn_summary = cli + .run_script(root_idx, enable_vtxn_script.as_str()) + .await + .expect("Txn execution error."); + debug!("enabling_vtxn_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(5, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 5."); + + info!("Now in epoch 5. Both DKG and vtxn are enabled. There should be no randomness since DKG did not happen at the end of last epoch."); + let maybe_last_complete = get_on_chain_resource::(&client) + .await + .last_completed; + assert!( + maybe_last_complete.is_none() || maybe_last_complete.as_ref().unwrap().target_epoch() != 5 + ); + + info!("Waiting for epoch 6."); + swarm + .wait_for_all_nodes_to_catchup_to_epoch( + 6, + Duration::from_secs(epoch_duration_secs + estimated_dkg_latency_secs), + ) + .await + .expect("Waited too long for epoch 6."); + + let dkg_session = get_on_chain_resource::(&client) + .await + .last_completed + .expect("dkg result for epoch 6 should be present"); + assert_eq!(6, dkg_session.target_epoch()); + assert!(verify_dkg_transcript(&dkg_session, &decrypt_key_map).is_ok()); +} diff --git a/testsuite/smoke-test/src/randomness/enable_feature_1.rs b/testsuite/smoke-test/src/randomness/enable_feature_1.rs new file mode 100644 index 00000000000000..e4b124dae90f11 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/enable_feature_1.rs @@ -0,0 +1,133 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, get_on_chain_resource, verify_dkg_transcript}, + smoke_test_environment::SwarmBuilder, + utils::get_current_consensus_config, +}; +use aptos_forge::{Node, Swarm, SwarmExt}; +use aptos_logger::{debug, info}; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, +}; +use std::{sync::Arc, time::Duration}; + +/// Enable on-chain randomness in the following steps. +/// - Enable validator transactions in consensus config in epoch `e`. +/// - Enable feature `RECONFIGURE_WITH_DKG` in epoch `e + 1`. +#[tokio::test] +async fn enable_feature_1() { + let epoch_duration_secs = 20; + let estimated_dkg_latency_secs = 40; + + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + conf.allow_new_validators = true; + + // start with vtxn disabled. + conf.consensus_config.disable_validator_txns(); + + // start with dkg disabled. + let mut features = Features::default(); + features.disable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let root_addr = swarm.chain_info().root_account().address(); + let root_idx = cli.add_account_with_address_to_cli(swarm.root_key(), root_addr); + + let decrypt_key_map = decrypt_key_map(&swarm); + + let client_endpoint = swarm.validators().nth(1).unwrap().rest_api_endpoint(); + let client = aptos_rest_client::Client::new(client_endpoint.clone()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(3, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 3."); + + info!("Now in epoch 3. Enabling validator transactions."); + let mut config = get_current_consensus_config(&client).await; + config.enable_validator_txns(); + let config_bytes = bcs::to_bytes(&config).unwrap(); + let enable_vtxn_script = format!( + r#" +script {{ + use aptos_framework::aptos_governance; + use aptos_framework::consensus_config; + fun main(core_resources: &signer) {{ + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let config_bytes = vector{:?}; + consensus_config::set_for_next_epoch(&framework_signer, config_bytes); + aptos_governance::reconfigure(&framework_signer); + }} +}} +"#, + config_bytes + ); + + debug!("enable_vtxn_script={}", enable_vtxn_script); + let txn_summary = cli + .run_script(root_idx, enable_vtxn_script.as_str()) + .await + .expect("Txn execution error."); + debug!("enabling_vtxn_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(4, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 4."); + + info!("Now in epoch 4. Enabling feature RECONFIGURE_WITH_DKG."); + let enable_dkg_script = r#" +script { + use aptos_framework::aptos_governance; + fun main(core_resources: &signer) { + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let dkg_feature_id: u64 = std::features::get_reconfigure_with_dkg_feature(); + aptos_governance::toggle_features(&framework_signer, vector[dkg_feature_id], vector[]); + } +} +"#; + + let txn_summary = cli + .run_script(root_idx, enable_dkg_script) + .await + .expect("Txn execution error."); + debug!("enabling_dkg_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(5, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 5."); + + info!("Now in epoch 5. Both DKG and vtxn are enabled. There should be no randomness since DKG did not happen at the end of last epoch."); + let maybe_last_complete = get_on_chain_resource::(&client) + .await + .last_completed; + assert!( + maybe_last_complete.is_none() || maybe_last_complete.as_ref().unwrap().target_epoch() != 5 + ); + + info!("Waiting for epoch 6."); + swarm + .wait_for_all_nodes_to_catchup_to_epoch( + 6, + Duration::from_secs(epoch_duration_secs + estimated_dkg_latency_secs), + ) + .await + .expect("Waited too long for epoch 6."); + + let dkg_session = get_on_chain_resource::(&client) + .await + .last_completed + .expect("dkg result for epoch 6 should be present"); + assert_eq!(6, dkg_session.target_epoch()); + assert!(verify_dkg_transcript(&dkg_session, &decrypt_key_map).is_ok()); +} diff --git a/testsuite/smoke-test/src/randomness/enable_feature_2.rs b/testsuite/smoke-test/src/randomness/enable_feature_2.rs new file mode 100644 index 00000000000000..a291258d2ad7c4 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/enable_feature_2.rs @@ -0,0 +1,111 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{decrypt_key_map, get_on_chain_resource, verify_dkg_transcript}, + smoke_test_environment::SwarmBuilder, + utils::get_current_consensus_config, +}; +use aptos_forge::{Node, Swarm, SwarmExt}; +use aptos_logger::{debug, info}; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, +}; +use std::{sync::Arc, time::Duration}; + +/// Enable on-chain randomness by enabling validator transactions and feature `RECONFIGURE_WITH_DKG` simultaneously. +#[tokio::test] +async fn enable_feature_2() { + let epoch_duration_secs = 20; + let estimated_dkg_latency_secs = 40; + + let (mut swarm, mut cli, _faucet) = SwarmBuilder::new_local(4) + .with_num_fullnodes(1) + .with_aptos() + .with_init_genesis_config(Arc::new(move |conf| { + conf.epoch_duration_secs = epoch_duration_secs; + conf.allow_new_validators = true; + + // start with vtxn disabled. + conf.consensus_config.disable_validator_txns(); + + // start with dkg disabled. + let mut features = Features::default(); + features.disable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build_with_cli(0) + .await; + + let root_addr = swarm.chain_info().root_account().address(); + let root_idx = cli.add_account_with_address_to_cli(swarm.root_key(), root_addr); + + let decrypt_key_map = decrypt_key_map(&swarm); + + let client_endpoint = swarm.validators().nth(1).unwrap().rest_api_endpoint(); + let client = aptos_rest_client::Client::new(client_endpoint.clone()); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(3, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 3."); + + info!("Now in epoch 3. Enabling features."); + let mut config = get_current_consensus_config(&client).await; + config.enable_validator_txns(); + let config_bytes = bcs::to_bytes(&config).unwrap(); + let script = format!( + r#" +script {{ + use aptos_framework::aptos_governance; + use aptos_framework::consensus_config; + use std::features; + fun main(core_resources: &signer) {{ + let framework_signer = aptos_governance::get_signer_testnet_only(core_resources, @0000000000000000000000000000000000000000000000000000000000000001); + let config_bytes = vector{:?}; + consensus_config::set_for_next_epoch(&framework_signer, config_bytes); + let dkg_feature_id: u64 = features::get_reconfigure_with_dkg_feature(); + features::change_feature_flags_for_next_epoch(&framework_signer, vector[dkg_feature_id], vector[]); + aptos_governance::reconfigure(&framework_signer); + }} +}} +"#, + config_bytes + ); + + debug!("script={}", script); + let txn_summary = cli + .run_script(root_idx, script.as_str()) + .await + .expect("Txn execution error."); + debug!("txn_summary={:?}", txn_summary); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(4, Duration::from_secs(epoch_duration_secs * 2)) + .await + .expect("Waited too long for epoch 4."); + + info!("Now in epoch 4. Both DKG and vtxn are enabled. There should be no randomness since DKG did not happen at the end of last epoch."); + let maybe_last_complete = get_on_chain_resource::(&client) + .await + .last_completed; + assert!( + maybe_last_complete.is_none() || maybe_last_complete.as_ref().unwrap().target_epoch() != 4 + ); + + info!("Waiting for epoch 5."); + swarm + .wait_for_all_nodes_to_catchup_to_epoch( + 5, + Duration::from_secs(epoch_duration_secs + estimated_dkg_latency_secs), + ) + .await + .expect("Waited too long for epoch 5."); + + let dkg_session = get_on_chain_resource::(&client) + .await + .last_completed + .expect("dkg result for epoch 6 should be present"); + assert_eq!(5, dkg_session.target_epoch()); + assert!(verify_dkg_transcript(&dkg_session, &decrypt_key_map).is_ok()); +} diff --git a/testsuite/smoke-test/src/randomness/mod.rs b/testsuite/smoke-test/src/randomness/mod.rs new file mode 100644 index 00000000000000..b68da06af1c35e --- /dev/null +++ b/testsuite/smoke-test/src/randomness/mod.rs @@ -0,0 +1,260 @@ +// Copyright © Aptos Foundation + +use anyhow::{anyhow, ensure, Result}; +use aptos_crypto::{compat::Sha3_256, Uniform}; +use aptos_dkg::weighted_vuf::traits::WeightedVUF; +use aptos_forge::LocalSwarm; +use aptos_logger::info; +use aptos_rest_client::Client; +use aptos_types::{ + dkg::{DKGSessionState, DKGState, DKGTrait, DefaultDKG}, + on_chain_config::OnChainConfig, + randomness::{PerBlockRandomness, RandMetadataToSign, WVUF}, + validator_verifier::ValidatorConsensusInfo, +}; +use digest::Digest; +use move_core_types::{account_address::AccountAddress, language_storage::CORE_CODE_ADDRESS}; +use rand::{prelude::StdRng, SeedableRng}; +use std::{collections::HashMap, time::Duration}; +use tokio::time::Instant; + +mod disable_feature_0; +mod disable_feature_1; +mod dkg_with_validator_down; +mod dkg_with_validator_join_leave; +mod e2e_basic_consumption; +mod e2e_correctness; +mod enable_feature_0; +mod enable_feature_1; +mod enable_feature_2; +mod validator_restart_during_dkg; + +#[allow(dead_code)] +async fn get_current_version(rest_client: &Client) -> u64 { + rest_client + .get_ledger_information() + .await + .unwrap() + .inner() + .version +} + +async fn get_on_chain_resource(rest_client: &Client) -> T { + let maybe_response = rest_client + .get_account_resource_bcs::(CORE_CODE_ADDRESS, T::struct_tag().to_string().as_str()) + .await; + let response = maybe_response.unwrap(); + response.into_inner() +} + +#[allow(dead_code)] +async fn get_on_chain_resource_at_version( + rest_client: &Client, + version: u64, +) -> T { + let maybe_response = rest_client + .get_account_resource_at_version_bcs::( + CORE_CODE_ADDRESS, + T::struct_tag().to_string().as_str(), + version, + ) + .await; + let response = maybe_response.unwrap(); + response.into_inner() +} + +/// Poll the on-chain state until we see a DKG session finishes. +/// Return a `DKGSessionState` of the DKG session seen. +#[allow(dead_code)] +async fn wait_for_dkg_finish( + client: &Client, + target_epoch: Option, + time_limit_secs: u64, +) -> DKGSessionState { + let mut dkg_state = get_on_chain_resource::(client).await; + let timer = Instant::now(); + while timer.elapsed().as_secs() < time_limit_secs + && !(dkg_state.in_progress.is_none() + && dkg_state.last_completed.is_some() + && (target_epoch.is_none() + || dkg_state + .last_completed + .as_ref() + .map(|session| session.metadata.dealer_epoch + 1) + == target_epoch)) + { + tokio::time::sleep(Duration::from_secs(1)).await; + dkg_state = get_on_chain_resource::(client).await; + } + assert!(timer.elapsed().as_secs() < time_limit_secs); + dkg_state.last_complete().clone() +} + +/// Verify that DKG transcript of epoch i (stored in `new_dkg_state`) is correctly generated +/// by the validator set in epoch i-1 (stored in `new_dkg_state`). +fn verify_dkg_transcript( + dkg_session: &DKGSessionState, + decrypt_key_map: &HashMap::NewValidatorDecryptKey>, +) -> Result<()> { + info!( + "Verifying the transcript generated in epoch {}.", + dkg_session.metadata.dealer_epoch, + ); + let pub_params = DefaultDKG::new_public_params(&dkg_session.metadata); + let transcript = bcs::from_bytes(dkg_session.transcript.as_slice()).map_err(|e| { + anyhow!("DKG transcript verification failed with transcript deserialization error: {e}") + })?; + println!("transcript={:?}", transcript); + DefaultDKG::verify_transcript(&pub_params, &transcript)?; + + info!("Double-verifying by reconstructing the dealt secret."); + let dealt_secret_from_shares = dealt_secret_from_shares( + dkg_session + .metadata + .target_validator_consensus_infos_cloned(), + decrypt_key_map, + &pub_params, + &transcript, + ); + + println!("dealt_secret_from_shares={:?}", dealt_secret_from_shares); + + let dealt_secret_from_inputs = dealt_secret_from_input( + &transcript, + &pub_params, + &pub_params.session_metadata.dealer_consensus_infos_cloned(), + ); + println!("dealt_secret_from_inputs={:?}", dealt_secret_from_inputs); + + ensure!( + dealt_secret_from_shares == dealt_secret_from_inputs, + "dkg transcript verification failed with final check failure" + ); + Ok(()) +} + +fn dealt_secret_from_shares( + target_validator_set: Vec, + decrypt_key_map: &HashMap::NewValidatorDecryptKey>, + pub_params: &::PublicParams, + transcript: &::Transcript, +) -> ::DealtSecret { + let player_share_pairs = target_validator_set + .iter() + .enumerate() + .map(|(idx, validator_info)| { + let dk = decrypt_key_map.get(&validator_info.address).unwrap(); + let (secret_share, _pub_key_share) = DefaultDKG::decrypt_secret_share_from_transcript( + pub_params, transcript, idx as u64, dk, + ) + .unwrap(); + (idx as u64, secret_share) + }) + .collect(); + + DefaultDKG::reconstruct_secret_from_shares(pub_params, player_share_pairs).unwrap() +} + +fn dealt_secret_from_input( + trx: &::Transcript, + pub_params: &::PublicParams, + dealer_validator_infos: &[ValidatorConsensusInfo], +) -> ::DealtSecret { + let dealers = DefaultDKG::get_dealers(trx); + println!("dealers={:?}", dealers); + let input_secrets = dealers + .into_iter() + .map(|dealer_idx| { + let cur_addr = dealer_validator_infos[dealer_idx as usize].address; + // Same seed is used in `DKGManager::setup_deal_broadcast` for smoke tests. + let mut rng = StdRng::from_seed(cur_addr.into_bytes()); + ::InputSecret::generate(&mut rng) + }) + .collect(); + + let aggregated_input_secret = DefaultDKG::aggregate_input_secret(input_secrets); + DefaultDKG::dealt_secret_from_input(pub_params, &aggregated_input_secret) +} + +#[allow(dead_code)] +fn num_validators(dkg_state: &DKGSessionState) -> usize { + dkg_state.metadata.target_validator_set.len() +} + +fn decrypt_key_map( + swarm: &LocalSwarm, +) -> HashMap::NewValidatorDecryptKey> { + swarm + .validators() + .map(|validator| { + let dk = validator + .config() + .consensus + .safety_rules + .initial_safety_rules_config + .identity_blob() + .unwrap() + .try_into_dkg_new_validator_decrypt_key() + .unwrap(); + (validator.peer_id(), dk) + }) + .collect::>() +} + +/// Fetch the DKG result and the block randomness (from aggregation) for a specific version. +/// Derive the distributed secret from DKG result. +/// Verify that the randomness from aggregation (the actual one store on chain) equals to +/// the randomness from direct evaluation using the distributed secret (the expected one). +async fn verify_randomness( + decrypt_key_map: &HashMap::NewValidatorDecryptKey>, + rest_client: &Client, + version: u64, +) -> Result<()> { + // Fetch resources. + let (dkg_state, on_chain_block_randomness) = tokio::join!( + get_on_chain_resource_at_version::(rest_client, version), + get_on_chain_resource_at_version::(rest_client, version) + ); + + ensure!( + on_chain_block_randomness.seed.is_some(), + "randomness verification failed with seed missing" + ); + + // Derive the shared secret. + let dkg_session = dkg_state + .last_completed + .ok_or_else(|| anyhow!("randomness verification failed with missing dkg result"))?; + let dkg_pub_params = DefaultDKG::new_public_params(&dkg_session.metadata); + let transcript = + bcs::from_bytes::<::Transcript>(dkg_session.transcript.as_slice()) + .map_err(|_| { + anyhow!( + "randomness verification failed with on-chain dkg transcript deserialization error" + ) + })?; + let dealt_secret = dealt_secret_from_shares( + dkg_session + .metadata + .target_validator_consensus_infos_cloned(), + decrypt_key_map, + &dkg_pub_params, + &transcript, + ); + + // Compare the outputs from 2 paths. + let rand_metadata = RandMetadataToSign { + epoch: on_chain_block_randomness.epoch, + round: on_chain_block_randomness.round, + }; + let input = bcs::to_bytes(&rand_metadata).unwrap(); + let output = WVUF::eval(&dealt_secret, input.as_slice()); + let output_serialized = bcs::to_bytes(&output).unwrap(); + let expected_randomness_seed = Sha3_256::digest(output_serialized.as_slice()).to_vec(); + + ensure!( + expected_randomness_seed == on_chain_block_randomness.seed.clone().unwrap(), + "randomness verification failed with final check failure" + ); + Ok(()) +} diff --git a/testsuite/smoke-test/src/randomness/validator_restart_during_dkg.rs b/testsuite/smoke-test/src/randomness/validator_restart_during_dkg.rs new file mode 100644 index 00000000000000..4c1fbd748a9eb2 --- /dev/null +++ b/testsuite/smoke-test/src/randomness/validator_restart_during_dkg.rs @@ -0,0 +1,109 @@ +// Copyright © Aptos Foundation + +use crate::{ + randomness::{ + decrypt_key_map, get_on_chain_resource, verify_dkg_transcript, wait_for_dkg_finish, + }, + smoke_test_environment::SwarmBuilder, +}; +use aptos_forge::{NodeExt, SwarmExt}; +use aptos_logger::{debug, info}; +use aptos_rest_client::Client; +use aptos_types::{ + dkg::DKGState, + on_chain_config::{FeatureFlag, Features}, +}; +use futures::future::join_all; +use std::{sync::Arc, time::Duration}; + +#[tokio::test] +async fn validator_restart_during_dkg() { + let epoch_duration_secs = 30; + let estimated_dkg_latency_secs = 30; + let time_limit_secs = epoch_duration_secs + estimated_dkg_latency_secs; + let num_validators = 4; + let num_validators_to_restart = 3; + let mut swarm = SwarmBuilder::new_local(num_validators) + .with_num_fullnodes(1) + .with_aptos() + .with_init_config(Arc::new(|_, conf, _| { + conf.api.failpoints_enabled = true; + })) + .with_init_genesis_config(Arc::new(|conf| { + conf.epoch_duration_secs = 30; + + // Ensure vtxn is enabled. + conf.consensus_config.enable_validator_txns(); + + // Ensure randomness flag is set. + let mut features = Features::default(); + features.enable(FeatureFlag::RECONFIGURE_WITH_DKG); + conf.initial_features_override = Some(features); + })) + .build() + .await; + + swarm + .wait_for_all_nodes_to_catchup_to_epoch(2, Duration::from_secs(epoch_duration_secs * 10)) + .await + .unwrap(); + + let decrypt_key_map = decrypt_key_map(&swarm); + + info!("Wait for an epoch start."); + let validator_clients: Vec = + swarm.validators().map(|node| node.rest_client()).collect(); + let dkg_session_1 = wait_for_dkg_finish(&validator_clients[3], None, time_limit_secs).await; + + info!( + "Current epoch is {}.", + dkg_session_1.metadata.dealer_epoch + 1 + ); + + info!("Inject fault to all validators so they get stuck upon the first DKG message received."); + let tasks = validator_clients + .iter() + .take(num_validators_to_restart) + .map(|client| { + client.set_failpoint( + "dkg::process_dkg_start_event".to_string(), + "panic".to_string(), + ) + }) + .collect::>(); + let aptos_results = join_all(tasks).await; + debug!("aptos_results={:?}", aptos_results); + + info!("Restart nodes after they panic."); + for (node_idx, node) in swarm + .validators_mut() + .enumerate() + .take(num_validators_to_restart) + { + while node.health_check().await.is_ok() { + tokio::time::sleep(Duration::from_secs(1)).await; + } + info!("node {} panicked", node_idx); + node.restart().await.unwrap(); + info!("node {} restarted", node_idx); + } + + info!( + "DKG should be able to continue. Wait until we fully entered epoch {}.", + dkg_session_1.target_epoch() + 1 + ); + + swarm + .wait_for_all_nodes_to_catchup_to_epoch( + dkg_session_1.target_epoch() + 1, + Duration::from_secs(time_limit_secs), + ) + .await + .unwrap(); + let dkg_session_2 = get_on_chain_resource::(&validator_clients[3]) + .await + .last_completed + .clone() + .unwrap(); + assert!(verify_dkg_transcript(&dkg_session_2, &decrypt_key_map).is_ok()); +}