diff --git a/.changelog/unreleased/improvements/1621-utils-next-epoch.md b/.changelog/unreleased/improvements/1621-utils-next-epoch.md new file mode 100644 index 00000000000..5f663a8e008 --- /dev/null +++ b/.changelog/unreleased/improvements/1621-utils-next-epoch.md @@ -0,0 +1,2 @@ +- Added a command to wait for the next epoch: `client utils epoch-sleep`. + ([\#1621](https://github.com/anoma/namada/pull/1621)) \ No newline at end of file diff --git a/apps/src/bin/namada-client/cli.rs b/apps/src/bin/namada-client/cli.rs index 609588045d5..17b33318035 100644 --- a/apps/src/bin/namada-client/cli.rs +++ b/apps/src/bin/namada-client/cli.rs @@ -455,6 +455,18 @@ pub async fn main() -> Result<()> { Utils::DefaultBaseDir(DefaultBaseDir(args)) => { utils::default_base_dir(global_args, args) } + Utils::EpochSleep(EpochSleep(mut args)) => { + let mut ctx = cli::Context::new(global_args)?; + let client = HttpClient::new(utils::take_config_address( + &mut args.ledger_address, + )) + .unwrap(); + wait_until_node_is_synched(&client).await; + let client = + HttpClient::new(args.ledger_address.clone()).unwrap(); + let args = args.to_sdk(&mut ctx); + rpc::epoch_sleep(&client, args).await; + } }, } Ok(()) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 06d6f3f4068..40f459d10a1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1678,6 +1678,28 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct EpochSleep(pub args::Query); + + impl SubCmd for EpochSleep { + const CMD: &'static str = "epoch-sleep"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| Self(args::Query::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Query for the current epoch, then sleep until the next \ + epoch.", + ) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub enum Utils { JoinNetwork(JoinNetwork), @@ -1686,6 +1708,7 @@ pub mod cmds { InitGenesisValidator(InitGenesisValidator), PkToTmAddress(PkToTmAddress), DefaultBaseDir(DefaultBaseDir), + EpochSleep(EpochSleep), } impl SubCmd for Utils { @@ -1704,12 +1727,14 @@ pub mod cmds { SubCmd::parse(matches).map(Self::PkToTmAddress); let default_base_dir = SubCmd::parse(matches).map(Self::DefaultBaseDir); + let epoch_sleep = SubCmd::parse(matches).map(Self::EpochSleep); join_network .or(fetch_wasms) .or(init_network) .or(init_genesis) .or(pk_to_tm_address) .or(default_base_dir) + .or(epoch_sleep) }) } @@ -1722,6 +1747,7 @@ pub mod cmds { .subcommand(InitGenesisValidator::def()) .subcommand(PkToTmAddress::def()) .subcommand(DefaultBaseDir::def()) + .subcommand(EpochSleep::def()) .subcommand_required(true) .arg_required_else_help(true) } diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index 44955967d0d..2b0b71650e6 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -2030,6 +2030,21 @@ pub async fn query_result( } } +pub async fn epoch_sleep( + client: &C, + _args: args::Query, +) { + let start_epoch = query_and_print_epoch(client).await; + loop { + tokio::time::sleep(core::time::Duration::from_secs(1)).await; + let current_epoch = query_epoch(client).await; + if current_epoch > start_epoch { + println!("Reached epoch {}", current_epoch); + break; + } + } +} + pub async fn get_proposal_votes( client: &C, epoch: Epoch, diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 7c79fdfdb61..b4235c7347b 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -424,20 +424,29 @@ pub fn epoch_sleep( ledger_address: &str, timeout_secs: u64, ) -> Result { - let old_epoch = get_epoch(test, ledger_address)?; - let start = Instant::now(); - let loop_timeout = Duration::new(timeout_secs, 0); - loop { - if Instant::now().duration_since(start) > loop_timeout { - panic!("Timed out waiting for the next epoch"); - } - let epoch = get_epoch(test, ledger_address)?; - if epoch > old_epoch { - break Ok(epoch); - } else { - sleep(10); - } - } + let mut find = run!( + test, + Bin::Client, + &["utils", "epoch-sleep", "--node", ledger_address], + Some(timeout_secs) + )?; + parse_reached_epoch(&mut find) +} + +pub fn parse_reached_epoch(find: &mut NamadaCmd) -> Result { + let (unread, matched) = find.exp_regex("Reached epoch .*")?; + let epoch_str = strip_trailing_newline(&matched) + .trim() + .rsplit_once(' ') + .unwrap() + .1; + let epoch = u64::from_str(epoch_str).map_err(|e| { + eyre!(format!( + "Epoch: {} parsed from {}, Error: {}\n\nOutput: {}", + epoch_str, matched, e, unread + )) + })?; + Ok(Epoch(epoch)) } /// Wait for txs and VPs WASM compilations to finish. This is useful to avoid a diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 3f22402c2c2..373feebff25 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -41,6 +41,7 @@ use super::helpers::{ use super::setup::{get_all_wasms_hashes, set_ethereum_bridge_mode}; use crate::e2e::helpers::{ epoch_sleep, find_address, find_bonded_stake, get_actor_rpc, get_epoch, + parse_reached_epoch, }; use crate::e2e::setup::{self, default_port_offset, sleep, Bin, Who}; use crate::{run, run_as}; @@ -2150,7 +2151,7 @@ fn pos_bonds() -> Result<()> { delegation_withdrawable_epoch ); } - let epoch = get_epoch(&test, &validator_one_rpc)?; + let epoch = epoch_sleep(&test, &validator_one_rpc, 40)?; if epoch >= delegation_withdrawable_epoch { break; } @@ -2357,7 +2358,7 @@ fn pos_rewards() -> Result<()> { if Instant::now().duration_since(start) > loop_timeout { panic!("Timed out waiting for epoch: {}", wait_epoch); } - let epoch = get_epoch(&test, &validator_zero_rpc)?; + let epoch = epoch_sleep(&test, &validator_zero_rpc, 40)?; if dbg!(epoch) >= wait_epoch { break; } @@ -2440,7 +2441,7 @@ fn test_bond_queries() -> Result<()> { if Instant::now().duration_since(start) > loop_timeout { panic!("Timed out waiting for epoch: {}", 1); } - let epoch = get_epoch(&test, &validator_one_rpc)?; + let epoch = epoch_sleep(&test, &validator_one_rpc, 40)?; if epoch >= Epoch(4) { break; } @@ -2499,7 +2500,7 @@ fn test_bond_queries() -> Result<()> { // 6. Wait for withdraw_epoch loop { - let epoch = epoch_sleep(&test, &validator_one_rpc, 120)?; + let epoch = epoch_sleep(&test, &validator_one_rpc, 40)?; if epoch >= withdraw_epoch { break; } @@ -2739,7 +2740,7 @@ fn pos_init_validator() -> Result<()> { if Instant::now().duration_since(start) > loop_timeout { panic!("Timed out waiting for epoch: {}", earliest_update_epoch); } - let epoch = get_epoch(&test, &non_validator_rpc)?; + let epoch = epoch_sleep(&test, &non_validator_rpc, 40)?; if epoch >= earliest_update_epoch { break; } @@ -4663,6 +4664,52 @@ fn implicit_account_reveal_pk() -> Result<()> { Ok(()) } +#[test] +fn test_epoch_sleep() -> Result<()> { + // Use slightly longer epochs to give us time to sleep + let test = setup::network( + |genesis| { + let parameters = ParametersConfig { + epochs_per_year: epochs_per_year_from_min_duration(30), + min_num_of_blocks: 1, + ..genesis.parameters + }; + GenesisConfig { + parameters, + ..genesis + } + }, + None, + )?; + + // 1. Run the ledger node + let mut ledger = + run_as!(test, Who::Validator(0), Bin::Node, &["ledger"], Some(40))?; + wait_for_wasm_pre_compile(&mut ledger)?; + + let _bg_ledger = ledger.background(); + + let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); + + // 2. Query the current epoch + let start_epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + + // 3. Use epoch-sleep to sleep for an epoch + let args = ["utils", "epoch-sleep", "--node", &validator_one_rpc]; + let mut client = run!(test, Bin::Client, &args, None)?; + let reached_epoch = parse_reached_epoch(&mut client)?; + client.assert_success(); + + // 4. Confirm the current epoch is larger + // possibly badly, we assume we get here within 30 seconds of the last step + // should be fine haha (future debuggers: sorry) + let current_epoch = get_epoch(&test, &validator_one_rpc).unwrap(); + assert!(current_epoch > start_epoch); + assert_eq!(current_epoch, reached_epoch); + + Ok(()) +} + /// Prepare proposal data in the test's temp dir from the given source address. /// This can be submitted with "init-proposal" command. fn prepare_proposal_data(