diff --git a/.changelog/unreleased/features/2082-validator-re-de-activate.md b/.changelog/unreleased/features/2082-validator-re-de-activate.md new file mode 100644 index 00000000000..c1222bafabc --- /dev/null +++ b/.changelog/unreleased/features/2082-validator-re-de-activate.md @@ -0,0 +1,2 @@ +- Add transactions to deactivate and reactivate a validator + ([\#2082](https://github.com/anoma/namada/pull/2082)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index f3792dedbd7..ee25c6769c3 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -20,6 +20,7 @@ "e2e::ledger_tests::test_node_connectivity_and_consensus": 28, "e2e::ledger_tests::test_epoch_sleep": 12, "e2e::ledger_tests::wrapper_disposable_signer": 28, + "e2e::ledger_tests::deactivate_and_reactivate_validator": 67, "e2e::wallet_tests::wallet_address_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, diff --git a/apps/src/lib/bench_utils.rs b/apps/src/lib/bench_utils.rs index 4f12c5b6b19..d96ffc12291 100644 --- a/apps/src/lib/bench_utils.rs +++ b/apps/src/lib/bench_utils.rs @@ -107,6 +107,8 @@ pub const TX_CHANGE_VALIDATOR_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; pub const TX_IBC_WASM: &str = "tx_ibc.wasm"; pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; +pub const TX_DEACTIVATE_VALIDATOR_WASM: &str = "tx_deactivate_validator.wasm"; +pub const TX_REACTIVATE_VALIDATOR_WASM: &str = "tx_reactivate_validator.wasm"; pub const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; pub const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; pub const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index b9b4d157803..8ca857e2cdc 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -225,6 +225,8 @@ pub mod cmds { // PoS transactions .subcommand(TxInitValidator::def().display_order(2)) .subcommand(TxUnjailValidator::def().display_order(2)) + .subcommand(TxDeactivateValidator::def().display_order(2)) + .subcommand(TxReactivateValidator::def().display_order(2)) .subcommand(Bond::def().display_order(2)) .subcommand(Unbond::def().display_order(2)) .subcommand(Withdraw::def().display_order(2)) @@ -273,6 +275,10 @@ pub mod cmds { Self::parse_with_ctx(matches, TxInitValidator); let tx_unjail_validator = Self::parse_with_ctx(matches, TxUnjailValidator); + let tx_deactivate_validator = + Self::parse_with_ctx(matches, TxDeactivateValidator); + let tx_reactivate_validator = + Self::parse_with_ctx(matches, TxReactivateValidator); let tx_reveal_pk = Self::parse_with_ctx(matches, TxRevealPk); let tx_init_proposal = Self::parse_with_ctx(matches, TxInitProposal); @@ -330,6 +336,8 @@ pub mod cmds { .or(tx_init_validator) .or(tx_commission_rate_change) .or(tx_unjail_validator) + .or(tx_deactivate_validator) + .or(tx_reactivate_validator) .or(bond) .or(unbond) .or(withdraw) @@ -403,6 +411,8 @@ pub mod cmds { TxInitValidator(TxInitValidator), TxCommissionRateChange(TxCommissionRateChange), TxUnjailValidator(TxUnjailValidator), + TxDeactivateValidator(TxDeactivateValidator), + TxReactivateValidator(TxReactivateValidator), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), TxRevealPk(TxRevealPk), @@ -1380,6 +1390,55 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct TxDeactivateValidator( + pub args::TxDeactivateValidator, + ); + + impl SubCmd for TxDeactivateValidator { + const CMD: &'static str = "deactivate-validator"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + TxDeactivateValidator(args::TxDeactivateValidator::parse( + matches, + )) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Send a signed transaction to deactivate a validator.") + .add_args::>() + } + } + + #[derive(Clone, Debug)] + pub struct TxReactivateValidator( + pub args::TxReactivateValidator, + ); + + impl SubCmd for TxReactivateValidator { + const CMD: &'static str = "reactivate-validator"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + TxReactivateValidator(args::TxReactivateValidator::parse( + matches, + )) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about( + "Send a signed transaction to reactivate an inactive \ + validator.", + ) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct Bond(pub args::Bond); @@ -2648,6 +2707,8 @@ pub mod args { pub const TX_BRIDGE_POOL_WASM: &str = "tx_bridge_pool.wasm"; pub const TX_CHANGE_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; + pub const TX_DEACTIVATE_VALIDATOR_WASM: &str = + "tx_deactivate_validator.wasm"; pub const TX_IBC_WASM: &str = "tx_ibc.wasm"; pub const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; pub const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; @@ -2657,6 +2718,8 @@ pub mod args { pub const TX_TRANSFER_WASM: &str = "tx_transfer.wasm"; pub const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; + pub const TX_REACTIVATE_VALIDATOR_WASM: &str = + "tx_reactivate_validator.wasm"; pub const TX_REDELEGATE_WASM: &str = "tx_redelegate.wasm"; pub const TX_UPDATE_VP_WASM: &str = "tx_update_vp.wasm"; pub const TX_UPDATE_STEWARD_COMMISSION: &str = @@ -4914,9 +4977,75 @@ pub mod args { fn def(app: App) -> App { app.add_args::>().arg( - VALIDATOR.def().help( - "The address of the jailed validator to re-activate.", - ), + VALIDATOR + .def() + .help("The address of the jailed validator to unjail."), + ) + } + } + + impl CliToSdk> + for TxDeactivateValidator + { + fn to_sdk(self, ctx: &mut Context) -> TxDeactivateValidator { + TxDeactivateValidator:: { + tx: self.tx.to_sdk(ctx), + validator: ctx.borrow_chain_or_exit().get(&self.validator), + tx_code_path: self.tx_code_path.to_path_buf(), + } + } + } + + impl Args for TxDeactivateValidator { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let validator = VALIDATOR.parse(matches); + let tx_code_path = PathBuf::from(TX_DEACTIVATE_VALIDATOR_WASM); + Self { + tx, + validator, + tx_code_path, + } + } + + fn def(app: App) -> App { + app.add_args::>().arg( + VALIDATOR + .def() + .help("The address of the jailed validator to deactivate."), + ) + } + } + + impl CliToSdk> + for TxReactivateValidator + { + fn to_sdk(self, ctx: &mut Context) -> TxReactivateValidator { + TxReactivateValidator:: { + tx: self.tx.to_sdk(ctx), + validator: ctx.borrow_chain_or_exit().get(&self.validator), + tx_code_path: self.tx_code_path.to_path_buf(), + } + } + } + + impl Args for TxReactivateValidator { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let validator = VALIDATOR.parse(matches); + let tx_code_path = PathBuf::from(TX_REACTIVATE_VALIDATOR_WASM); + Self { + tx, + validator, + tx_code_path, + } + } + + fn def(app: App) -> App { + app.add_args::>().arg( + VALIDATOR + .def() + .help("The address of the jailed validator to deactivate."), ) } } diff --git a/apps/src/lib/cli/client.rs b/apps/src/lib/cli/client.rs index 9a886265615..729b0aed10a 100644 --- a/apps/src/lib/cli/client.rs +++ b/apps/src/lib/cli/client.rs @@ -245,6 +245,32 @@ impl CliApi { let namada = ctx.to_sdk(&client, io); tx::submit_unjail_validator(&namada, args).await?; } + Sub::TxDeactivateValidator(TxDeactivateValidator( + mut args, + )) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.tx.ledger_address, + ) + }); + client.wait_until_node_is_synced(io).await?; + let args = args.to_sdk(&mut ctx); + let namada = ctx.to_sdk(&client, io); + tx::submit_deactivate_validator(&namada, args).await?; + } + Sub::TxReactivateValidator(TxReactivateValidator( + mut args, + )) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.tx.ledger_address, + ) + }); + client.wait_until_node_is_synced(io).await?; + let args = args.to_sdk(&mut ctx); + let namada = ctx.to_sdk(&client, io); + tx::submit_reactivate_validator(&namada, args).await?; + } Sub::TxUpdateStewardCommission( TxUpdateStewardCommission(mut args), ) => { diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index d939d5691e0..753f0e8b5ac 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -1016,6 +1016,50 @@ where Ok(()) } +pub async fn submit_deactivate_validator<'a, N: Namada<'a>>( + namada: &N, + args: args::TxDeactivateValidator, +) -> Result<(), error::Error> +where + ::Error: std::fmt::Display, +{ + let (mut tx, signing_data, _fee_unshield_epoch) = + args.build(namada).await?; + signing::generate_test_vector(namada, &tx).await?; + + if args.tx.dump_tx { + tx::dump_tx(namada.io(), &args.tx, tx); + } else { + namada.sign(&mut tx, &args.tx, signing_data).await?; + + namada.submit(tx, &args.tx).await?; + } + + Ok(()) +} + +pub async fn submit_reactivate_validator<'a, N: Namada<'a>>( + namada: &N, + args: args::TxReactivateValidator, +) -> Result<(), error::Error> +where + ::Error: std::fmt::Display, +{ + let (mut tx, signing_data, _fee_unshield_epoch) = + args.build(namada).await?; + signing::generate_test_vector(namada, &tx).await?; + + if args.tx.dump_tx { + tx::dump_tx(namada.io(), &args.tx, tx); + } else { + namada.sign(&mut tx, &args.tx, signing_data).await?; + + namada.submit(tx, &args.tx).await?; + } + + Ok(()) +} + pub async fn submit_update_steward_commission<'a, N: Namada<'a>>( namada: &N, args: args::UpdateStewardCommission, diff --git a/benches/txs.rs b/benches/txs.rs index ac24b6ab4fa..92a0f1aa062 100644 --- a/benches/txs.rs +++ b/benches/txs.rs @@ -27,8 +27,9 @@ use namada::types::transaction::EllipticCurve; use namada_apps::bench_utils::{ generate_ibc_transfer_tx, generate_tx, BenchShell, BenchShieldedCtx, ALBERT_PAYMENT_ADDRESS, ALBERT_SPENDING_KEY, BERTHA_PAYMENT_ADDRESS, - TX_BOND_WASM, TX_CHANGE_VALIDATOR_COMMISSION_WASM, TX_INIT_ACCOUNT_WASM, - TX_INIT_PROPOSAL_WASM, TX_INIT_VALIDATOR_WASM, TX_REDELEGATE_WASM, + TX_BOND_WASM, TX_CHANGE_VALIDATOR_COMMISSION_WASM, + TX_DEACTIVATE_VALIDATOR_WASM, TX_INIT_ACCOUNT_WASM, TX_INIT_PROPOSAL_WASM, + TX_INIT_VALIDATOR_WASM, TX_REACTIVATE_VALIDATOR_WASM, TX_REDELEGATE_WASM, TX_REVEAL_PK_WASM, TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, TX_VOTE_PROPOSAL_WASM, TX_WITHDRAW_WASM, VP_VALIDATOR_WASM, @@ -718,6 +719,63 @@ fn unjail_validator(c: &mut Criterion) { }); } +fn deactivate_validator(c: &mut Criterion) { + let signed_tx = generate_tx( + TX_DEACTIVATE_VALIDATOR_WASM, + defaults::validator_address(), + None, + None, + Some(&defaults::validator_keypair()), + ); + + c.bench_function("deactivate_validator", |b| { + b.iter_batched_ref( + BenchShell::default, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + +fn reactivate_validator(c: &mut Criterion) { + let signed_tx = generate_tx( + TX_REACTIVATE_VALIDATOR_WASM, + defaults::validator_address(), + None, + None, + Some(&defaults::validator_keypair()), + ); + + c.bench_function("reactivate_validator", |b| { + b.iter_batched_ref( + || { + let mut shell = BenchShell::default(); + + // Deactivate the validator + let pos_params = read_pos_params(&shell.wl_storage).unwrap(); + let current_epoch = shell.wl_storage.storage.block.epoch; + proof_of_stake::deactivate_validator( + &mut shell.wl_storage, + &defaults::validator_address(), + current_epoch, + ) + .unwrap(); + + shell.wl_storage.commit_tx(); + shell.commit(); + // Advance by slash epoch offset epochs + for _ in 0..=pos_params.pipeline_len { + shell.advance_epoch(); + } + + shell + }, + |shell| shell.execute_tx(&signed_tx), + criterion::BatchSize::LargeInput, + ) + }); +} + criterion_group!( whitelisted_txs, transfer, @@ -733,6 +791,8 @@ criterion_group!( init_validator, change_validator_commission, ibc, - unjail_validator + unjail_validator, + deactivate_validator, + reactivate_validator ); criterion_main!(whitelisted_txs); diff --git a/proof_of_stake/src/error.rs b/proof_of_stake/src/error.rs index d3eeecb3c81..71288f8bfff 100644 --- a/proof_of_stake/src/error.rs +++ b/proof_of_stake/src/error.rs @@ -140,6 +140,29 @@ pub enum RedelegationError { NotAValidator(Address), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum DeactivationError { + #[error("The validator {0} is already inactive at the pipeline epoch {1}")] + AlreadyInactive(Address, Epoch), + #[error("The given address {0} is not a validator address")] + NotAValidator(Address), + #[error( + "The given address {0} is jailed at the pipeline epoch {1} and is \ + forbidden from being deactivated" + )] + ValidatorIsJailed(Address, Epoch), +} + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum ReactivationError { + #[error("The validator {0} is not inactive at the epoch {1}")] + NotInactive(Address, Epoch), + #[error("No state found for validator {0} in epoch {1}")] + NoStateFound(Address, Epoch), +} + impl From for storage_api::Error { fn from(err: BecomeValidatorError) -> Self { Self::new(err) @@ -187,3 +210,15 @@ impl From for storage_api::Error { Self::new(err) } } + +impl From for storage_api::Error { + fn from(err: DeactivationError) -> Self { + Self::new(err) + } +} + +impl From for storage_api::Error { + fn from(err: ReactivationError) -> Self { + Self::new(err) + } +} diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index f977ff4a0c9..187c26311c8 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -804,16 +804,6 @@ where let bond_handle = bond_handle(source, validator); let total_bonded_handle = total_bonded_handle(validator); - // Check that validator is not inactive at anywhere between the current - // epoch and pipeline offset - for epoch in current_epoch.iter_range(offset) { - if let Some(ValidatorState::Inactive) = - validator_state_handle.get(storage, epoch, ¶ms)? - { - return Err(BondError::InactiveValidator(validator.clone()).into()); - } - } - if tracing::level_enabled!(tracing::Level::DEBUG) { let bonds = find_bonds(storage, source, validator)?; tracing::debug!("\nBonds before incrementing: {bonds:#?}"); @@ -831,13 +821,11 @@ where // Update the validator set // Allow bonding even if the validator is jailed. However, if jailed, there // must be no changes to the validator set. Check at the pipeline epoch. - let is_jailed_at_pipeline = matches!( - validator_state_handle - .get(storage, offset_epoch, ¶ms)? - .unwrap(), - ValidatorState::Jailed + let is_jailed_or_inactive_at_pipeline = matches!( + validator_state_handle.get(storage, offset_epoch, ¶ms)?, + Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) ); - if !is_jailed_at_pipeline { + if !is_jailed_or_inactive_at_pipeline { update_validator_set( storage, ¶ms, @@ -1712,8 +1700,6 @@ where return Err(UnbondError::ValidatorIsFrozen(validator.clone()).into()); } - // TODO: check that validator is not inactive (when implemented)! - let source = source.unwrap_or(validator); let bonds_handle = bond_handle(source, validator); @@ -1961,13 +1947,15 @@ where // Update the validator set at the pipeline offset. Since unbonding from a // jailed validator who is no longer frozen is allowed, only update the // validator set if the validator is not jailed - let is_jailed_at_pipeline = matches!( - validator_state_handle(validator) - .get(storage, pipeline_epoch, ¶ms)? - .unwrap(), - ValidatorState::Jailed + let is_jailed_or_inactive_at_pipeline = matches!( + validator_state_handle(validator).get( + storage, + pipeline_epoch, + ¶ms + )?, + Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) ); - if !is_jailed_at_pipeline { + if !is_jailed_or_inactive_at_pipeline { update_validator_set( storage, ¶ms, @@ -5296,15 +5284,15 @@ where )?; // Update validator set for dest validator - let is_jailed_at_pipeline = matches!( + let is_jailed_or_inactive_at_pipeline = matches!( validator_state_handle(dest_validator).get( storage, pipeline_epoch, ¶ms )?, - Some(ValidatorState::Jailed) + Some(ValidatorState::Jailed) | Some(ValidatorState::Inactive) ); - if !is_jailed_at_pipeline { + if !is_jailed_or_inactive_at_pipeline { update_validator_set( storage, ¶ms, @@ -5335,6 +5323,203 @@ where Ok(()) } +/// De-activate a validator by removing it from any validator sets +pub fn deactivate_validator( + storage: &mut S, + validator: &Address, + current_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + let params = read_pos_params(storage)?; + let pipeline_epoch = current_epoch + params.pipeline_len; + + let pipeline_state = match validator_state_handle(validator).get( + storage, + pipeline_epoch, + ¶ms, + )? { + Some(state) => state, + None => { + return Err( + DeactivationError::NotAValidator(validator.clone()).into() + ); + } + }; + + let pipeline_stake = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; + + // Remove the validator from the validator set. If it is in the consensus + // set, promote the next validator. + match pipeline_state { + ValidatorState::Consensus => { + let consensus_set = consensus_validator_set_handle() + .at(&pipeline_epoch) + .at(&pipeline_stake); + // TODO: handle the unwrap better here + let val_position = validator_set_positions_handle() + .at(&pipeline_epoch) + .get(storage, validator)? + .unwrap(); + let removed = consensus_set.remove(storage, &val_position)?; + debug_assert_eq!(removed, Some(validator.clone())); + + // Remove position + validator_set_positions_handle() + .at(&pipeline_epoch) + .remove(storage, validator)?; + + // Now promote the next below-capacity validator to the consensus + // set + let below_cap_set = + below_capacity_validator_set_handle().at(&pipeline_epoch); + let max_below_capacity_validator_amount = + get_max_below_capacity_validator_amount( + &below_cap_set, + storage, + )?; + + if let Some(max_bc_amount) = max_below_capacity_validator_amount { + let below_cap_vals_max = + below_cap_set.at(&max_bc_amount.into()); + let lowest_position = + find_first_position(&below_cap_vals_max, storage)?.unwrap(); + let removed_max_below_capacity = below_cap_vals_max + .remove(storage, &lowest_position)? + .expect("Must have been removed"); + + insert_validator_into_set( + &consensus_validator_set_handle() + .at(&pipeline_epoch) + .at(&max_bc_amount), + storage, + &pipeline_epoch, + &removed_max_below_capacity, + )?; + validator_state_handle(&removed_max_below_capacity).set( + storage, + ValidatorState::Consensus, + pipeline_epoch, + 0, + )?; + } + } + ValidatorState::BelowCapacity => { + let below_capacity_set = below_capacity_validator_set_handle() + .at(&pipeline_epoch) + .at(&pipeline_stake.into()); + // TODO: handle the unwrap better here + let val_position = validator_set_positions_handle() + .at(&pipeline_epoch) + .get(storage, validator)? + .unwrap(); + let removed = below_capacity_set.remove(storage, &val_position)?; + debug_assert_eq!(removed, Some(validator.clone())); + + // Remove position + validator_set_positions_handle() + .at(&pipeline_epoch) + .remove(storage, validator)?; + } + ValidatorState::BelowThreshold => {} + ValidatorState::Inactive => { + return Err(DeactivationError::AlreadyInactive( + validator.clone(), + pipeline_epoch, + ) + .into()); + } + ValidatorState::Jailed => { + return Err(DeactivationError::ValidatorIsJailed( + validator.clone(), + pipeline_epoch, + ) + .into()); + } + } + + // Set the state to inactive + validator_state_handle(validator).set( + storage, + ValidatorState::Inactive, + current_epoch, + params.pipeline_len, + )?; + + Ok(()) +} + +/// Re-activate an inactive validator +pub fn reactivate_validator( + storage: &mut S, + validator: &Address, + current_epoch: Epoch, +) -> storage_api::Result<()> +where + S: StorageRead + StorageWrite, +{ + // TODO: need to additionally check for past slashes, in case a jailed + // validator tries to deactivate and then reactivate + let params = read_pos_params(storage)?; + let pipeline_epoch = current_epoch + params.pipeline_len; + + // Make sure state is Inactive at every epoch up through the pipeline + for epoch in Epoch::iter_bounds_inclusive(current_epoch, pipeline_epoch) { + let state = + validator_state_handle(validator).get(storage, epoch, ¶ms)?; + if let Some(state) = state { + if state != ValidatorState::Inactive { + return Err(ReactivationError::NotInactive( + validator.clone(), + epoch, + ) + .into()); + } + } else { + return Err(ReactivationError::NoStateFound( + validator.clone(), + epoch, + ) + .into()); + } + } + + // Check to see if the validator should still be jailed upon a reactivation + let last_slash_epoch = read_validator_last_slash_epoch(storage, validator)?; + if let Some(last_slash_epoch) = last_slash_epoch { + let eligible_epoch = + last_slash_epoch + params.slash_processing_epoch_offset(); + if current_epoch < eligible_epoch { + // The validator should be set back to jailed + validator_state_handle(validator).set( + storage, + ValidatorState::Jailed, + pipeline_epoch, + 0, + )?; + // End execution here? + return Ok(()); + } + } + + // Determine which validator set the validator should be added to again + let stake = + read_validator_stake(storage, ¶ms, validator, pipeline_epoch)?; + + insert_validator_into_validator_set( + storage, + ¶ms, + validator, + stake, + current_epoch, + params.pipeline_len, + )?; + + Ok(()) +} + #[cfg(any(test, feature = "testing"))] /// PoS related utility functions to help set up tests. pub mod test_utils { diff --git a/scripts/generator.sh b/scripts/generator.sh index c9635d498db..de994e14681 100755 --- a/scripts/generator.sh +++ b/scripts/generator.sh @@ -134,6 +134,10 @@ elif [ "$1" = "client" ]; then cargo run --bin namadac --features std -- unjail-validator --validator Bertha --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- deactivate-validator --validator Bertha --gas-token NAM --force --node 127.0.0.1:27657 + + cargo run --bin namadac --features std -- reactivate-validator --validator Bertha --gas-token NAM --force --node 127.0.0.1:27657 + cargo run --bin namadac --features std -- change-commission-rate --validator Bertha --commission-rate 0.02 --gas-token NAM --force --node 127.0.0.1:27657 PROPOSAL_ID_0=$(cargo run --bin namadac --features std -- init-proposal --force --data-path proposal_default.json --node 127.0.0.1:27657 | grep -o -P '(?<=/proposal/).*(?=/author)') diff --git a/sdk/src/args.rs b/sdk/src/args.rs index f8454648b50..5e1e7940c4a 100644 --- a/sdk/src/args.rs +++ b/sdk/src/args.rs @@ -1420,6 +1420,104 @@ impl TxUnjailValidator { } } +#[derive(Clone, Debug)] +/// Deactivate validator args +pub struct TxDeactivateValidator { + /// Common tx arguments + pub tx: Tx, + /// Validator address (should be self) + pub validator: C::Address, + /// Path to the TX WASM code file + pub tx_code_path: PathBuf, +} + +impl TxBuilder for TxDeactivateValidator { + fn tx(self, func: F) -> Self + where + F: FnOnce(Tx) -> Tx, + { + TxDeactivateValidator { + tx: func(self.tx), + ..self + } + } +} + +impl TxDeactivateValidator { + /// Validator address (should be self) + pub fn validator(self, validator: C::Address) -> Self { + Self { validator, ..self } + } + + /// Path to the TX WASM code file + pub fn tx_code_path(self, tx_code_path: PathBuf) -> Self { + Self { + tx_code_path, + ..self + } + } +} + +impl TxDeactivateValidator { + /// Build a transaction from this builder + pub async fn build<'a>( + &self, + context: &impl Namada<'a>, + ) -> crate::error::Result<(crate::proto::Tx, SigningTxData, Option)> + { + tx::build_deactivate_validator(context, self).await + } +} + +#[derive(Clone, Debug)] +/// Re-activate a deactivated validator args +pub struct TxReactivateValidator { + /// Common tx arguments + pub tx: Tx, + /// Validator address (should be self) + pub validator: C::Address, + /// Path to the TX WASM code file + pub tx_code_path: PathBuf, +} + +impl TxBuilder for TxReactivateValidator { + fn tx(self, func: F) -> Self + where + F: FnOnce(Tx) -> Tx, + { + TxReactivateValidator { + tx: func(self.tx), + ..self + } + } +} + +impl TxReactivateValidator { + /// Validator address (should be self) + pub fn validator(self, validator: C::Address) -> Self { + Self { validator, ..self } + } + + /// Path to the TX WASM code file + pub fn tx_code_path(self, tx_code_path: PathBuf) -> Self { + Self { + tx_code_path, + ..self + } + } +} + +impl TxReactivateValidator { + /// Build a transaction from this builder + pub async fn build<'a>( + &self, + context: &impl Namada<'a>, + ) -> crate::error::Result<(crate::proto::Tx, SigningTxData, Option)> + { + tx::build_reactivate_validator(context, self).await + } +} + #[derive(Clone, Debug)] /// Sign a transaction offline pub struct SignTx { diff --git a/sdk/src/error.rs b/sdk/src/error.rs index 3512d1a9f23..c02b7e7ad18 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -151,6 +151,17 @@ pub enum TxError { restored." )] ValidatorNotCurrentlyJailed(Address), + /// Already inactive at pipeline epoch + #[error( + "The validator address {0} is inactive at the pipeline epoch {1}." + )] + ValidatorInactive(Address, Epoch), + /// Validator not inactive + #[error( + "The validator address {0} is not inactive at epoch {1} and so cannot \ + be reactivated." + )] + ValidatorNotInactive(Address, Epoch), /// Validator still frozen and ineligible to be unjailed #[error( "The validator address {0} is currently frozen and ineligible to be \ diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index e53b7cfb696..463d5d137d3 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -65,9 +65,10 @@ use crate::signing::SigningTxData; use crate::token::DenominatedAmount; use crate::tx::{ ProcessTxResponse, TX_BOND_WASM, TX_BRIDGE_POOL_WASM, - TX_CHANGE_COMMISSION_WASM, TX_IBC_WASM, TX_INIT_PROPOSAL, - TX_INIT_VALIDATOR_WASM, TX_RESIGN_STEWARD, TX_REVEAL_PK, TX_TRANSFER_WASM, - TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, + TX_CHANGE_COMMISSION_WASM, TX_DEACTIVATE_VALIDATOR_WASM, TX_IBC_WASM, + TX_INIT_PROPOSAL, TX_INIT_VALIDATOR_WASM, TX_REACTIVATE_VALIDATOR_WASM, + TX_RESIGN_STEWARD, TX_REVEAL_PK, TX_TRANSFER_WASM, TX_UNBOND_WASM, + TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, TX_UPDATE_STEWARD_COMMISSION, TX_VOTE_PROPOSAL, TX_WITHDRAW_WASM, VP_USER_WASM, }; @@ -332,6 +333,32 @@ pub trait Namada<'a>: Sized { } } + /// Make a TxDeactivateValidator builder from the given minimum set of + /// arguments + fn new_deactivate_validator( + &self, + validator: Address, + ) -> args::TxDeactivateValidator { + args::TxDeactivateValidator { + validator, + tx_code_path: PathBuf::from(TX_DEACTIVATE_VALIDATOR_WASM), + tx: self.tx_builder(), + } + } + + /// Make a TxReactivateValidator builder from the given minimum set of + /// arguments + fn new_reactivate_validator( + &self, + validator: Address, + ) -> args::TxReactivateValidator { + args::TxReactivateValidator { + validator, + tx_code_path: PathBuf::from(TX_REACTIVATE_VALIDATOR_WASM), + tx: self.tx_builder(), + } + } + /// Make a Withdraw builder from the given minimum set of arguments fn new_withdraw(&self, validator: Address) -> args::Withdraw { args::Withdraw { diff --git a/sdk/src/signing.rs b/sdk/src/signing.rs index 381df346347..5a77a9d8041 100644 --- a/sdk/src/signing.rs +++ b/sdk/src/signing.rs @@ -48,10 +48,11 @@ use crate::masp::make_asset_type; use crate::proto::{MaspBuilder, Section, Tx}; use crate::rpc::{query_wasm_code_hash, validate_amount}; use crate::tx::{ - TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM, TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, - TX_INIT_PROPOSAL, TX_INIT_VALIDATOR_WASM, TX_REVEAL_PK, TX_TRANSFER_WASM, - TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, TX_UPDATE_ACCOUNT_WASM, - TX_VOTE_PROPOSAL, TX_WITHDRAW_WASM, VP_USER_WASM, + TX_BOND_WASM, TX_CHANGE_COMMISSION_WASM, TX_DEACTIVATE_VALIDATOR_WASM, + TX_IBC_WASM, TX_INIT_ACCOUNT_WASM, TX_INIT_PROPOSAL, + TX_INIT_VALIDATOR_WASM, TX_REACTIVATE_VALIDATOR_WASM, TX_REVEAL_PK, + TX_TRANSFER_WASM, TX_UNBOND_WASM, TX_UNJAIL_VALIDATOR_WASM, + TX_UPDATE_ACCOUNT_WASM, TX_VOTE_PROPOSAL, TX_WITHDRAW_WASM, VP_USER_WASM, }; pub use crate::wallet::store::AddressVpType; use crate::wallet::{Wallet, WalletIo}; @@ -916,6 +917,10 @@ pub async fn to_ledger_vector<'a>( let user_hash = query_wasm_code_hash(context, VP_USER_WASM).await?; let unjail_validator_hash = query_wasm_code_hash(context, TX_UNJAIL_VALIDATOR_WASM).await?; + let deactivate_validator_hash = + query_wasm_code_hash(context, TX_DEACTIVATE_VALIDATOR_WASM).await?; + let reactivate_validator_hash = + query_wasm_code_hash(context, TX_REACTIVATE_VALIDATOR_WASM).await?; // To facilitate lookups of human-readable token names let tokens: HashMap = context @@ -1476,6 +1481,40 @@ pub async fn to_ledger_vector<'a>( format!("Validator : {}", address), ]); + tv.output_expert.push(format!("Validator : {}", address)); + } else if code_hash == deactivate_validator_hash { + let address = Address::try_from_slice( + &tx.data() + .ok_or_else(|| Error::Other("Invalid Data".to_string()))?, + ) + .map_err(|err| { + Error::from(EncodingError::Conversion(err.to_string())) + })?; + + tv.name = "Deactivate_Validator_0".to_string(); + + tv.output.extend(vec![ + format!("Type : Deactivate Validator"), + format!("Validator : {}", address), + ]); + + tv.output_expert.push(format!("Validator : {}", address)); + } else if code_hash == reactivate_validator_hash { + let address = Address::try_from_slice( + &tx.data() + .ok_or_else(|| Error::Other("Invalid Data".to_string()))?, + ) + .map_err(|err| { + Error::from(EncodingError::Conversion(err.to_string())) + })?; + + tv.name = "Reactivate_Validator_0".to_string(); + + tv.output.extend(vec![ + format!("Type : Reactivate Validator"), + format!("Validator : {}", address), + ]); + tv.output_expert.push(format!("Validator : {}", address)); } else { tv.name = "Custom_0".to_string(); diff --git a/sdk/src/tx.rs b/sdk/src/tx.rs index 2a7919895ba..47c65b65a74 100644 --- a/sdk/src/tx.rs +++ b/sdk/src/tx.rs @@ -74,6 +74,10 @@ pub const TX_INIT_ACCOUNT_WASM: &str = "tx_init_account.wasm"; pub const TX_INIT_VALIDATOR_WASM: &str = "tx_init_validator.wasm"; /// Unjail validator transaction WASM path pub const TX_UNJAIL_VALIDATOR_WASM: &str = "tx_unjail_validator.wasm"; +/// Deactivate validator transaction WASM path +pub const TX_DEACTIVATE_VALIDATOR_WASM: &str = "tx_deactivate_validator.wasm"; +/// Reactivate validator transaction WASM path +pub const TX_REACTIVATE_VALIDATOR_WASM: &str = "tx_reactivate_validator.wasm"; /// Initialize proposal transaction WASM path pub const TX_INIT_PROPOSAL: &str = "tx_init_proposal.wasm"; /// Vote transaction WASM path @@ -813,6 +817,147 @@ pub async fn build_unjail_validator<'a>( .map(|(tx, epoch)| (tx, signing_data, epoch)) } +/// Submit transaction to deactivate a validator +pub async fn build_deactivate_validator<'a>( + context: &impl Namada<'a>, + args::TxDeactivateValidator { + tx: tx_args, + validator, + tx_code_path, + }: &args::TxDeactivateValidator, +) -> Result<(Tx, SigningTxData, Option)> { + let default_signer = Some(validator.clone()); + let signing_data = signing::aux_signing_data( + context, + tx_args, + Some(validator.clone()), + default_signer, + ) + .await?; + + // Check if the validator address is actually a validator + if !rpc::is_validator(context.client(), validator).await? { + edisplay_line!( + context.io(), + "The given address {} is not a validator.", + &validator + ); + if !tx_args.force { + return Err(Error::from(TxError::InvalidValidatorAddress( + validator.clone(), + ))); + } + } + + let params: PosParams = rpc::get_pos_params(context.client()).await?; + let current_epoch = rpc::query_epoch(context.client()).await?; + let pipeline_epoch = current_epoch + params.pipeline_len; + + let validator_state_at_pipeline = rpc::get_validator_state( + context.client(), + validator, + Some(pipeline_epoch), + ) + .await?; + if validator_state_at_pipeline == Some(ValidatorState::Inactive) { + edisplay_line!( + context.io(), + "The given validator address {} is already inactive at the \ + pipeline epoch {}.", + &validator, + &pipeline_epoch + ); + if !tx_args.force { + return Err(Error::from(TxError::ValidatorInactive( + validator.clone(), + pipeline_epoch, + ))); + } + } + + build( + context, + tx_args, + tx_code_path.clone(), + validator.clone(), + do_nothing, + &signing_data.fee_payer, + None, + ) + .await + .map(|(tx, epoch)| (tx, signing_data, epoch)) +} + +/// Submit transaction to deactivate a validator +pub async fn build_reactivate_validator<'a>( + context: &impl Namada<'a>, + args::TxReactivateValidator { + tx: tx_args, + validator, + tx_code_path, + }: &args::TxReactivateValidator, +) -> Result<(Tx, SigningTxData, Option)> { + let default_signer = Some(validator.clone()); + let signing_data = signing::aux_signing_data( + context, + tx_args, + Some(validator.clone()), + default_signer, + ) + .await?; + + // Check if the validator address is actually a validator + if !rpc::is_validator(context.client(), validator).await? { + edisplay_line!( + context.io(), + "The given address {} is not a validator.", + &validator + ); + if !tx_args.force { + return Err(Error::from(TxError::InvalidValidatorAddress( + validator.clone(), + ))); + } + } + + let params: PosParams = rpc::get_pos_params(context.client()).await?; + let current_epoch = rpc::query_epoch(context.client()).await?; + let pipeline_epoch = current_epoch + params.pipeline_len; + + for epoch in Epoch::iter_bounds_inclusive(current_epoch, pipeline_epoch) { + let validator_state = + rpc::get_validator_state(context.client(), validator, Some(epoch)) + .await?; + + if validator_state != Some(ValidatorState::Inactive) { + edisplay_line!( + context.io(), + "The given validator address {} is not inactive at epoch {}.", + &validator, + &epoch + ); + if !tx_args.force { + return Err(Error::from(TxError::ValidatorNotInactive( + validator.clone(), + epoch, + ))); + } + } + } + + build( + context, + tx_args, + tx_code_path.clone(), + validator.clone(), + do_nothing, + &signing_data.fee_payer, + None, + ) + .await + .map(|(tx, epoch)| (tx, signing_data, epoch)) +} + /// Redelegate bonded tokens from one validator to another pub async fn build_redelegation<'a>( context: &impl Namada<'a>, @@ -907,6 +1052,32 @@ pub async fn build_redelegation<'a>( } } + // Give a redelegation warning based on the pipeline state of the dest + // validator + let pipeline_epoch = current_epoch + params.pipeline_len; + let dest_validator_state_at_pipeline = rpc::get_validator_state( + context.client(), + &dest_validator, + Some(pipeline_epoch), + ) + .await?; + if dest_validator_state_at_pipeline == Some(ValidatorState::Inactive) + && !tx_args.force + { + edisplay_line!( + context.io(), + "WARNING: the given destination validator address {} is inactive \ + at the pipeline epoch {}. If you would still like to bond to the \ + inactive validator, use the --force option.", + &dest_validator, + &pipeline_epoch + ); + return Err(Error::from(TxError::ValidatorInactive( + dest_validator.clone(), + pipeline_epoch, + ))); + } + // There must be at least as many tokens in the bond as the requested // redelegation amount let bond_amount = @@ -1258,6 +1429,33 @@ pub async fn build_bond<'a>( None => Ok(source.clone()), }?; + // Give a bonding warning based on the pipeline state + let params: PosParams = rpc::get_pos_params(context.client()).await?; + let current_epoch = rpc::query_epoch(context.client()).await?; + let pipeline_epoch = current_epoch + params.pipeline_len; + let validator_state_at_pipeline = rpc::get_validator_state( + context.client(), + &validator, + Some(pipeline_epoch), + ) + .await?; + if validator_state_at_pipeline == Some(ValidatorState::Inactive) + && !tx_args.force + { + edisplay_line!( + context.io(), + "WARNING: the given validator address {} is inactive at the \ + pipeline epoch {}. If you would still like to bond to the \ + inactive validator, use the --force option.", + &validator, + &pipeline_epoch + ); + return Err(Error::from(TxError::ValidatorInactive( + validator.clone(), + pipeline_epoch, + ))); + } + let default_address = source.clone().unwrap_or(validator.clone()); let default_signer = Some(default_address.clone()); let signing_data = signing::aux_signing_data( diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 809feb334d4..f88c80a17e6 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -3199,3 +3199,177 @@ fn prepare_proposal_data( ); valid_proposal_json_path } + +#[test] +fn deactivate_and_reactivate_validator() -> Result<()> { + let pipeline_len = 2; + let unbonding_len = 4; + let test = setup::network( + |mut genesis, base_dir: &_| { + genesis.parameters.pos_params.pipeline_len = pipeline_len; + genesis.parameters.pos_params.unbonding_len = unbonding_len; + // genesis.parameters.parameters.min_num_of_blocks = 6; + // genesis.parameters.parameters.max_expected_time_per_block = 1; + // genesis.parameters.parameters.epochs_per_year = 31_536_000; + let mut genesis = setup::set_validators( + 2, + genesis, + base_dir, + default_port_offset, + ); + let bonds = genesis.transactions.bond.unwrap(); + genesis.transactions.bond = Some( + bonds + .into_iter() + .filter(|bond| { + (&bond.data.validator).as_ref() != "validator-1" + }) + .collect(), + ); + genesis + }, + None, + )?; + + set_ethereum_bridge_mode( + &test, + &test.net.chain_id, + &Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); + + // 1. Run the ledger node + let _bg_validator_0 = + start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))? + .background(); + + let _bg_validator_1 = + start_namada_ledger_node_wait_wasm(&test, Some(1), Some(40))? + .background(); + + let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); + + // put money in the validator-1 account from its balance account so that it + // can deactivate and reactivate + let tx_args = vec![ + "transfer", + "--source", + "validator-1-balance-key", + "--target", + "validator-1-validator-key", + "--amount", + "100.0", + "--token", + "NAM", + "--node", + &validator_1_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(1), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + // Check the state of validator-0 + let tx_args = vec![ + "validator-state", + "--validator", + "validator-1", + "--node", + &validator_1_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_regex(r"Validator [a-z0-9]+ is in the below-threshold set")?; + client.assert_success(); + + // Deactivate validator-1 + let tx_args = vec![ + "deactivate-validator", + "--validator", + "validator-1", + "--signing-keys", + "validator-1-validator-key", + "--node", + &validator_1_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(1), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + let deactivate_epoch = get_epoch(&test, &validator_1_rpc)?; + let start = Instant::now(); + let loop_timeout = Duration::new(120, 0); + loop { + if Instant::now().duration_since(start) > loop_timeout { + panic!( + "Timed out waiting for epoch: {}", + deactivate_epoch + pipeline_len + ); + } + let epoch = epoch_sleep(&test, &validator_1_rpc, 40)?; + if epoch >= deactivate_epoch + pipeline_len { + break; + } + } + + // Check the state of validator-0 again + let tx_args = vec![ + "validator-state", + "--validator", + "validator-1", + "--node", + &validator_1_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_regex(r"Validator [a-z0-9]+ is inactive")?; + client.assert_success(); + + // Reactivate validator-1 + let tx_args = vec![ + "reactivate-validator", + "--validator", + "validator-1", + "--signing-keys", + "validator-1-validator-key", + "--node", + &validator_1_rpc, + ]; + let mut client = + run_as!(test, Who::Validator(1), Bin::Client, tx_args, Some(40))?; + client.exp_string("Transaction applied with result:")?; + client.exp_string("Transaction is valid.")?; + client.assert_success(); + + let reactivate_epoch = get_epoch(&test, &validator_1_rpc)?; + let start = Instant::now(); + let loop_timeout = Duration::new(120, 0); + loop { + if Instant::now().duration_since(start) > loop_timeout { + panic!( + "Timed out waiting for epoch: {}", + reactivate_epoch + pipeline_len + ); + } + let epoch = epoch_sleep(&test, &validator_1_rpc, 40)?; + if epoch >= reactivate_epoch + pipeline_len { + break; + } + } + + // Check the state of validator-0 again + let tx_args = vec![ + "validator-state", + "--validator", + "validator-1", + "--node", + &validator_1_rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_regex(r"Validator [a-z0-9]+ is in the below-threshold set")?; + client.assert_success(); + + Ok(()) +} diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index 2582287af28..fedad4d1c3a 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -7,8 +7,9 @@ use namada_core::types::{key, token}; pub use namada_proof_of_stake::parameters::PosParams; use namada_proof_of_stake::{ become_validator, bond_tokens, change_validator_commission_rate, - read_pos_params, redelegate_tokens, unbond_tokens, unjail_validator, - withdraw_tokens, BecomeValidator, + deactivate_validator, reactivate_validator, read_pos_params, + redelegate_tokens, unbond_tokens, unjail_validator, withdraw_tokens, + BecomeValidator, }; pub use namada_proof_of_stake::{parameters, types, ResultSlashing}; @@ -137,4 +138,16 @@ impl Ctx { Ok(validator_address) } + + /// Deactivate validator + pub fn deactivate_validator(&mut self, validator: &Address) -> TxResult { + let current_epoch = self.get_block_epoch()?; + deactivate_validator(self, validator, current_epoch) + } + + /// Reactivate validator + pub fn reactivate_validator(&mut self, validator: &Address) -> TxResult { + let current_epoch = self.get_block_epoch()?; + reactivate_validator(self, validator, current_epoch) + } } diff --git a/wasm/checksums.json b/wasm/checksums.json index 38a44429e53..7df400ae62f 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,23 +1,25 @@ { - "tx_bond.wasm": "tx_bond.f371b0615f8931ef71e12ecb11ff361d1b52904d2197b1bcd0b245c4b6dc6b85.wasm", - "tx_bridge_pool.wasm": "tx_bridge_pool.fd90aa41331ba118a8ad4c8a6eaff633620f9c13f7f0689d5193650ed0fe5441.wasm", - "tx_change_validator_commission.wasm": "tx_change_validator_commission.f6905933556182a5a8e63a26687c5b7dc08a597caa4d95243362dc9bc9872200.wasm", - "tx_ibc.wasm": "tx_ibc.71fc5960466ebfb2e2304fd2da36206f37ffee463309e80490de0efd05a9774e.wasm", - "tx_init_account.wasm": "tx_init_account.ddaf039045f7a438781afa6359a984d967c31919d0ed624026b7c807359f4ed1.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.ae97d0ce0a99d41e977d0df5e8f1b7840d1b55c56925a33c340e809b332008f7.wasm", - "tx_init_validator.wasm": "tx_init_validator.0d80054aec7009c96b86e2760c17f96acbec2916d4625826b968875fc9080992.wasm", - "tx_redelegate.wasm": "tx_redelegate.201a6d7fe5ba0c8a549398979d96547bb4e2ab2c590346814512e6c6dfbd4b33.wasm", - "tx_resign_steward.wasm": "tx_resign_steward.de1e20e7c59bf3ca16a6eeaa38d3e9d84d0b767ad67693a968982ce5bf29c580.wasm", - "tx_reveal_pk.wasm": "tx_reveal_pk.f54ef16721912c58c9b82729c384857d5e9a755679e54d36d051fed9516e3986.wasm", - "tx_transfer.wasm": "tx_transfer.dbb8e3391339072c6947036b54ddbda386405ed17fdd550ce0e223479d8dfe33.wasm", - "tx_unbond.wasm": "tx_unbond.5fbc7162efa1361257bdb56d7df327013cb918ab8a0ebd49cdebe948a9bb8a0f.wasm", - "tx_unjail_validator.wasm": "tx_unjail_validator.948a6c50d04ad9a0da1a4936d49380f0ba457225110788abc9443761a4fb8ec0.wasm", - "tx_update_account.wasm": "tx_update_account.1bc336c242913481719c6ba756e5403f9f569b97d05cf3a6b117573dc73f4e45.wasm", - "tx_update_steward_commission.wasm": "tx_update_steward_commission.9cadf5b48ad25f6371bbc0ac3e3e010664ce4b9b9104f916f4b87ad2e81eff29.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.a8ae56dc352635a01d7e6e953c188c03309810d2e84983516a30832f34a12533.wasm", - "tx_withdraw.wasm": "tx_withdraw.690ad6b17768e24f8523dd5e09ed458fce4b2a3b85710f81731306fac1f28ac7.wasm", - "vp_implicit.wasm": "vp_implicit.05fa0994305859d2e76950744982bb245be36a58d6e6413e6f7eb4fb1d59bce3.wasm", - "vp_masp.wasm": "vp_masp.adbb83cced017ca4f06a5eec4764f81ab30b56d73a179303aa12aa2c03b26964.wasm", - "vp_user.wasm": "vp_user.f50b392a7bc587a126f1377d287ec159cf2859ad2f737c189a97964b3fab7bc9.wasm", - "vp_validator.wasm": "vp_validator.d1fd61cca046ce85e1fd63cc85245e19c7b06c23f5705eb4949f855a8b3ed1a4.wasm" -} \ No newline at end of file + "tx_bond.wasm": "tx_bond.70e7863eade59d637217f133cf8dacbb28fbe7c22936396055e8228c87199f83.wasm", + "tx_bridge_pool.wasm": "tx_bridge_pool.69971943e6ad81a537197ee4d7fe8f1211882a17de572b28e71906572e8193af.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.94615c76f6f32b627adbbb1279a345bd06c6bd0d4eb65a6286c0038f78e91321.wasm", + "tx_deactivate_validator.wasm": "tx_deactivate_validator.38bdc4c2cda3f09c9c233ccea8f8df64e1d3b7916f67723d079248f36042ec82.wasm", + "tx_ibc.wasm": "tx_ibc.a74c56e43a62bc89018b2ccc19a4daf02d24a2f7c25ccc7b8e57c1f8ce272b82.wasm", + "tx_init_account.wasm": "tx_init_account.67cf30a5dc66ed3456e209f6983bb2d17c44b0dc952aeecccee6499939c5c58d.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.42f0735b45cebb72c10c4f2a6298d838adf89c1ab2d8eb31e0c726e33b1efdbd.wasm", + "tx_init_validator.wasm": "tx_init_validator.b33e386e3695a676a028040fbe6ce32762c9299e41f673f8636feb959535982d.wasm", + "tx_reactivate_validator.wasm": "tx_reactivate_validator.60f635af60a6172d2f32ae69332185cd658a0a49eecf2ad232f372d35a96c905.wasm", + "tx_redelegate.wasm": "tx_redelegate.5fc225fed027677e5f374a6e03ba14a97ffe730e2b600fccf23f4990ca7a19d8.wasm", + "tx_resign_steward.wasm": "tx_resign_steward.0852161a0947dc129fd1a581f79e5725c63760aed6cc71bbf3e2b554f913a540.wasm", + "tx_reveal_pk.wasm": "tx_reveal_pk.57f11af958daa0d52db27c1036a3d82eea964e976238561ad48baf89423b110b.wasm", + "tx_transfer.wasm": "tx_transfer.c3cecec1bbd1eb452a5aef6f70d45e1ed8590da4ff670fafe28163847405e934.wasm", + "tx_unbond.wasm": "tx_unbond.fc0a8f16733ee5cedb4a3e56e211bba168cb5f97e977d2eeebdf2c28ff26a4a6.wasm", + "tx_unjail_validator.wasm": "tx_unjail_validator.2f973ea13bf7a0060fdec2faa4a30d95fd018b54aa4bb6283b2a53fe21b5ab77.wasm", + "tx_update_account.wasm": "tx_update_account.a4b02b42cc0428030085ffde5b5169ecb4b5d02c8f316b162df5bb1e3ea15c3a.wasm", + "tx_update_steward_commission.wasm": "tx_update_steward_commission.17439af7a44254111415e28cc8a5d8ab62e0a67d2825a82dcb2eb5666689fad9.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.8b5d3d2a258f08eb0fe7727c81f55b60134985bb30ac2e13814d1f3a38082cb2.wasm", + "tx_withdraw.wasm": "tx_withdraw.26952ab447ba48aa08ffa7d12700877b1beef3c73b3e8fb2b8ddb1839e80fa8f.wasm", + "vp_implicit.wasm": "vp_implicit.32f0dee40e3c6730327193a32a191bdd6bfca768d9176f02bb63da04eb97a17f.wasm", + "vp_masp.wasm": "vp_masp.a5fc24c02e34d2345353abfd355f7fc665b980b185676e921f0994c02ca6bb6d.wasm", + "vp_user.wasm": "vp_user.238f0f88b402059130a1e4868ff62e4880320725155c275c626798d69d04cecf.wasm", + "vp_validator.wasm": "vp_validator.e8721ed7520d7b0940adb4d8f195ab4d1d2eeca2fa36688b618e6ecd545f25fc.wasm" +} diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 87e22c96aba..aebe4a95377 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -15,11 +15,13 @@ crate-type = ["cdylib"] tx_bond = ["namada_tx_prelude"] tx_bridge_pool = ["namada_tx_prelude"] tx_change_validator_commission = ["namada_tx_prelude"] +tx_deactivate_validator = ["namada_tx_prelude"] tx_from_intent = ["namada_tx_prelude"] tx_ibc = ["namada_tx_prelude"] tx_init_account = ["namada_tx_prelude"] tx_init_proposal = ["namada_tx_prelude"] tx_init_validator = ["namada_tx_prelude"] +tx_reactivate_validator = ["namada_tx_prelude"] tx_redelegate = ["namada_tx_prelude"] tx_reveal_pk = ["namada_tx_prelude"] tx_transfer = ["namada_tx_prelude"] diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index e78237c89d1..6f7faac2509 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -8,11 +8,13 @@ nightly := $(shell cat ../../rust-nightly-version) wasms := tx_bond wasms += tx_bridge_pool wasms += tx_change_validator_commission +wasms += tx_deactivate_validator wasms += tx_ibc wasms += tx_init_account wasms += tx_init_proposal wasms += tx_init_validator wasms += tx_redelegate +wasms += tx_reactivate_validator wasms += tx_reveal_pk wasms += tx_transfer wasms += tx_unbond diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index 139835fe9f2..673f3b78c11 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -4,6 +4,8 @@ pub mod tx_bond; pub mod tx_bridge_pool; #[cfg(feature = "tx_change_validator_commission")] pub mod tx_change_validator_commission; +#[cfg(feature = "tx_deactivate_validator")] +pub mod tx_deactivate_validator; #[cfg(feature = "tx_ibc")] pub mod tx_ibc; #[cfg(feature = "tx_init_account")] @@ -12,6 +14,8 @@ pub mod tx_init_account; pub mod tx_init_proposal; #[cfg(feature = "tx_init_validator")] pub mod tx_init_validator; +#[cfg(feature = "tx_reactivate_validator")] +pub mod tx_reactivate_validator; #[cfg(feature = "tx_redelegate")] pub mod tx_redelegate; #[cfg(feature = "tx_resign_steward")] diff --git a/wasm/wasm_source/src/tx_deactivate_validator.rs b/wasm/wasm_source/src/tx_deactivate_validator.rs new file mode 100644 index 00000000000..cd62efc1140 --- /dev/null +++ b/wasm/wasm_source/src/tx_deactivate_validator.rs @@ -0,0 +1,12 @@ +//! A tx to deactivate a validator. + +use namada_tx_prelude::*; + +#[transaction(gas = 340000)] +fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { + let signed = tx_data; + let data = signed.data().ok_or_err_msg("Missing data")?; + let validator = Address::try_from_slice(&data[..]) + .wrap_err("failed to decode an Address")?; + ctx.deactivate_validator(&validator) +} diff --git a/wasm/wasm_source/src/tx_reactivate_validator.rs b/wasm/wasm_source/src/tx_reactivate_validator.rs new file mode 100644 index 00000000000..19bfd746484 --- /dev/null +++ b/wasm/wasm_source/src/tx_reactivate_validator.rs @@ -0,0 +1,12 @@ +//! A tx to reactivate a validator. + +use namada_tx_prelude::*; + +#[transaction(gas = 340000)] +fn apply_tx(ctx: &mut Ctx, tx_data: Tx) -> TxResult { + let signed = tx_data; + let data = signed.data().ok_or_err_msg("Missing data")?; + let validator = Address::try_from_slice(&data[..]) + .wrap_err("failed to decode an Address")?; + ctx.reactivate_validator(&validator) +}