diff --git a/.changelog/unreleased/features/1395-pgf-ibc.md b/.changelog/unreleased/features/1395-pgf-ibc.md new file mode 100644 index 0000000000..e4743c49fa --- /dev/null +++ b/.changelog/unreleased/features/1395-pgf-ibc.md @@ -0,0 +1 @@ +- PGF over IBC ([\#1395](https://github.com/anoma/namada/issues/1395)) \ No newline at end of file diff --git a/.changelog/unreleased/testing/773-ibc-e2e-hermes.md b/.changelog/unreleased/testing/773-ibc-e2e-hermes.md new file mode 100644 index 0000000000..13d55e0548 --- /dev/null +++ b/.changelog/unreleased/testing/773-ibc-e2e-hermes.md @@ -0,0 +1,2 @@ +- Add IBC E2E test with Hermes + ([\#773](https://github.com/anoma/namada/issues/773)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index 8817c9b6d7..b5438cfbc8 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -1,6 +1,8 @@ { "e2e::eth_bridge_tests::everything": 4, "e2e::ibc_tests::run_ledger_ibc": 155, + "e2e::ibc_tests::run_ledger_ibc_with_hermes": 130, + "e2e::ibc_tests::pgf_over_ibc_with_hermes": 240, "e2e::eth_bridge_tests::test_add_to_bridge_pool": 10, "e2e::ledger_tests::double_signing_gets_slashed": 12, "e2e::ledger_tests::invalid_transactions": 13, @@ -32,4 +34,4 @@ "e2e::wallet_tests::wallet_encrypted_key_cmds": 1, "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, "e2e::wallet_tests::wallet_unencrypted_key_cmds": 1 -} \ No newline at end of file +} diff --git a/.github/workflows/scripts/schedule-e2e.py b/.github/workflows/scripts/schedule-e2e.py index d174fb4c75..b207377e40 100644 --- a/.github/workflows/scripts/schedule-e2e.py +++ b/.github/workflows/scripts/schedule-e2e.py @@ -8,7 +8,7 @@ NIGHTLY_VERSION = open("rust-nightly-version", "r").read().strip() E2E_FILE = ".github/workflows/scripts/e2e.json" -CARGO_TEST_COMMAND = "cargo +{} test {} -- --test-threads=1 --nocapture" +CARGO_TEST_COMMAND = "cargo +{} test {} -- --test-threads=1 --nocapture --exact" MACHINES = [{'tasks': [], 'time': [], 'total_time': 0} for _ in range(N_OF_MACHINES)] @@ -73,4 +73,4 @@ def find_freer_machine(): print(" Run locally with: {}".format(test_command)) if has_failures: - exit(1) \ No newline at end of file + exit(1) diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index bc57f58f14..26538b8b85 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -1304,8 +1304,8 @@ pub async fn query_pgf(context: &impl Namada, _args: args::QueryPgf) { context.io(), "{:4}- {} for {}", "", - funding.detail.target, - funding.detail.amount.to_string_native() + funding.detail.target(), + funding.detail.amount().to_string_native() ); } } diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 0e46577a0d..d41957582f 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -143,7 +143,7 @@ where // Invariant: Process slashes before inflation as they may affect // the rewards in the current epoch. self.process_slashes(); - self.apply_inflation(current_epoch)?; + self.apply_inflation(current_epoch, &mut response)?; } // Consensus set liveness check @@ -659,7 +659,11 @@ where /// account, then update the reward products of the validators. This is /// executed while finalizing the first block of a new epoch and is applied /// with respect to the previous epoch. - fn apply_inflation(&mut self, current_epoch: Epoch) -> Result<()> { + fn apply_inflation( + &mut self, + current_epoch: Epoch, + response: &mut shim::response::FinalizeBlock, + ) -> Result<()> { let last_epoch = current_epoch.prev(); // Get the number of blocks in the last epoch @@ -682,6 +686,13 @@ where // Pgf inflation pgf_inflation::apply_inflation(&mut self.wl_storage)?; + for ibc_event in self.wl_storage.write_log_mut().take_ibc_events() { + let mut event = Event::from(ibc_event.clone()); + // Add the height for IBC event query + let height = self.wl_storage.storage.get_last_block_height() + 1; + event["height"] = height.to_string(); + response.events.push(event); + } Ok(()) } diff --git a/apps/src/lib/node/ledger/shell/governance.rs b/apps/src/lib/node/ledger/shell/governance.rs index 4991310d09..42d97b0bea 100644 --- a/apps/src/lib/node/ledger/shell/governance.rs +++ b/apps/src/lib/node/ledger/shell/governance.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use namada::core::ledger::governance::storage::keys as gov_storage; use namada::core::ledger::governance::storage::proposal::{ - AddRemove, PGFAction, ProposalType, StoragePgfFunding, + AddRemove, PGFAction, PGFTarget, ProposalType, StoragePgfFunding, }; use namada::core::ledger::governance::utils::{ compute_proposal_result, ProposalVotes, TallyResult, TallyType, TallyVote, @@ -18,7 +18,7 @@ use namada::ledger::pos::BondId; use namada::ledger::protocol; use namada::ledger::storage::types::encode; use namada::ledger::storage::{DBIter, StorageHasher, DB}; -use namada::ledger::storage_api::{pgf, token, StorageWrite}; +use namada::ledger::storage_api::{ibc, pgf, token, StorageWrite}; use namada::proof_of_stake::bond_amount; use namada::proof_of_stake::parameters::PosParams; use namada::proof_of_stake::storage::read_total_stake; @@ -136,6 +136,20 @@ where id ); + for ibc_event in + shell.wl_storage.write_log_mut().take_ibc_events() + { + let mut event = Event::from(ibc_event.clone()); + // Add the height for IBC event query + let height = shell + .wl_storage + .storage + .get_last_block_height() + + 1; + event["height"] = height.to_string(); + response.events.push(event); + } + ProposalEvent::pgf_payments_proposal_event(id, result) .into() } @@ -341,14 +355,15 @@ where Ok(true) } -fn execute_pgf_payment_proposal( - storage: &mut S, +fn execute_pgf_payment_proposal( + storage: &mut WlStorage, token: &Address, payments: Vec, proposal_id: u64, ) -> Result where - S: StorageRead + StorageWrite, + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, { for payment in payments { match payment { @@ -356,49 +371,55 @@ where AddRemove::Add(target) => { pgf_storage::fundings_handle().insert( storage, - target.target.clone(), + target.target().clone(), StoragePgfFunding::new(target.clone(), proposal_id), )?; tracing::info!( "Execute ContinousPgf from proposal id {}: set {} to \ {}.", proposal_id, - target.amount.to_string_native(), - target.target + target.amount().to_string_native(), + target.target() ); } AddRemove::Remove(target) => { pgf_storage::fundings_handle() - .remove(storage, &target.target)?; + .remove(storage, &target.target())?; tracing::info!( "Execute ContinousPgf from proposal id {}: set {} to \ {}.", proposal_id, - target.amount.to_string_native(), - target.target + target.amount().to_string_native(), + target.target() ); } }, PGFAction::Retro(target) => { - match token::transfer( - storage, - token, - &ADDRESS, - &target.target, - target.amount, - ) { + let result = match &target { + PGFTarget::Internal(target) => token::transfer( + storage, + token, + &ADDRESS, + &target.target, + target.amount, + ), + PGFTarget::Ibc(target) => { + ibc::transfer_over_ibc(storage, token, &ADDRESS, target) + } + }; + match result { Ok(()) => tracing::info!( "Execute RetroPgf from proposal id {}: sent {} to {}.", proposal_id, - target.amount.to_string_native(), - target.target + target.amount().to_string_native(), + target.target() ), Err(e) => tracing::warn!( "Error in RetroPgf transfer from proposal id {}, \ amount {} to {}: {}", proposal_id, - target.amount.to_string_native(), - target.target, + target.amount().to_string_native(), + target.target(), e ), } diff --git a/core/src/ledger/governance/cli/onchain.rs b/core/src/ledger/governance/cli/onchain.rs index 60e70ddf7d..bbfd25ca70 100644 --- a/core/src/ledger/governance/cli/onchain.rs +++ b/core/src/ledger/governance/cli/onchain.rs @@ -10,6 +10,7 @@ use super::validation::{ ProposalValidation, }; use crate::ledger::governance::parameters::GovernanceParameters; +use crate::ledger::governance::storage::proposal::PGFTarget; use crate::ledger::storage_api::token; use crate::types::address::Address; use crate::types::storage::Epoch; @@ -277,9 +278,9 @@ impl PgfAction { )] pub struct PgfFunding { /// Pgf continuous funding - pub continuous: Vec, + pub continuous: Vec, /// pgf retro fundings - pub retro: Vec, + pub retro: Vec, } /// Pgf continous funding @@ -288,7 +289,7 @@ pub struct PgfFunding { )] pub struct PgfContinous { /// Pgf target - pub target: PgfFundingTarget, + pub target: PGFTarget, /// Pgf action pub action: PgfAction, } @@ -299,18 +300,7 @@ pub struct PgfContinous { )] pub struct PgfRetro { /// Pgf retro target - pub target: PgfFundingTarget, -} - -/// Pgf Target -#[derive( - Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, -)] -pub struct PgfFundingTarget { - /// Target amount - pub amount: token::Amount, - /// Target address - pub address: Address, + pub target: PGFTarget, } /// Represent an proposal vote diff --git a/core/src/ledger/governance/storage/proposal.rs b/core/src/ledger/governance/storage/proposal.rs index 13e1aecae1..9b91baac45 100644 --- a/core/src/ledger/governance/storage/proposal.rs +++ b/core/src/ledger/governance/storage/proposal.rs @@ -5,6 +5,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use crate::ibc::core::host::types::identifiers::{ChannelId, PortId}; use crate::ledger::governance::cli::onchain::{ PgfAction, PgfContinous, PgfRetro, PgfSteward, StewardsUpdate, }; @@ -81,13 +82,130 @@ pub enum AddRemove { Eq, PartialOrd, )] -pub struct PGFTarget { +pub enum PGFTarget { + /// Funding target on this chain + Internal(PGFInternalTarget), + /// Funding target on another chain + Ibc(PGFIbcTarget), +} + +impl PGFTarget { + /// Returns the funding target as String + pub fn target(&self) -> String { + match self { + PGFTarget::Internal(t) => t.target.to_string(), + PGFTarget::Ibc(t) => t.target.clone(), + } + } + + /// Returns the funding amount + pub fn amount(&self) -> Amount { + match self { + PGFTarget::Internal(t) => t.amount, + PGFTarget::Ibc(t) => t.amount, + } + } +} + +/// The target of a PGF payment +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + Serialize, + Deserialize, + Ord, + Eq, + PartialOrd, +)] +pub struct PGFInternalTarget { /// The target address pub target: Address, /// The amount of token to fund the target address pub amount: Amount, } +/// The target of a PGF payment +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Ord, Eq, PartialOrd, +)] +pub struct PGFIbcTarget { + /// The target address on the target chain + pub target: String, + /// The amount of token to fund the target address + pub amount: Amount, + /// Port ID to fund + pub port_id: PortId, + /// Channel ID to fund + pub channel_id: ChannelId, +} + +impl BorshSerialize for PGFIbcTarget { + fn serialize( + &self, + writer: &mut W, + ) -> std::io::Result<()> { + BorshSerialize::serialize(&self.target, writer)?; + BorshSerialize::serialize(&self.amount, writer)?; + BorshSerialize::serialize(&self.port_id.to_string(), writer)?; + BorshSerialize::serialize(&self.channel_id.to_string(), writer) + } +} + +impl borsh::BorshDeserialize for PGFIbcTarget { + fn deserialize_reader( + reader: &mut R, + ) -> std::io::Result { + use std::io::{Error, ErrorKind}; + let target: String = BorshDeserialize::deserialize_reader(reader)?; + let amount: Amount = BorshDeserialize::deserialize_reader(reader)?; + let port_id: String = BorshDeserialize::deserialize_reader(reader)?; + let port_id: PortId = port_id.parse().map_err(|err| { + Error::new( + ErrorKind::InvalidData, + format!("Error decoding port ID: {}", err), + ) + })?; + let channel_id: String = BorshDeserialize::deserialize_reader(reader)?; + let channel_id: ChannelId = channel_id.parse().map_err(|err| { + Error::new( + ErrorKind::InvalidData, + format!("Error decoding channel ID: {}", err), + ) + })?; + Ok(Self { + target, + amount, + port_id, + channel_id, + }) + } +} + +impl borsh::BorshSchema for PGFIbcTarget { + fn add_definitions_recursively( + definitions: &mut BTreeMap< + borsh::schema::Declaration, + borsh::schema::Definition, + >, + ) { + let fields = borsh::schema::Fields::NamedFields(vec![ + ("target".into(), String::declaration()), + ("amount".into(), Amount::declaration()), + ("port_id".into(), String::declaration()), + ("channel_id".into(), String::declaration()), + ]); + let definition = borsh::schema::Definition::Struct { fields }; + definitions.insert(Self::declaration(), definition); + } + + fn declaration() -> borsh::schema::Declaration { + std::any::type_name::().into() + } +} + /// The actions that a PGF Steward can propose to execute #[derive( Debug, @@ -168,35 +286,22 @@ impl TryFrom for AddRemove
{ } } -impl TryFrom for PGFAction { - type Error = ProposalTypeError; - - fn try_from(value: PgfContinous) -> Result { +impl From for PGFAction { + fn from(value: PgfContinous) -> Self { match value.action { PgfAction::Add => { - Ok(PGFAction::Continuous(AddRemove::Add(PGFTarget { - target: value.target.address, - amount: value.target.amount, - }))) + PGFAction::Continuous(AddRemove::Add(value.target)) } PgfAction::Remove => { - Ok(PGFAction::Continuous(AddRemove::Remove(PGFTarget { - target: value.target.address, - amount: value.target.amount, - }))) + PGFAction::Continuous(AddRemove::Remove(value.target)) } } } } -impl TryFrom for PGFAction { - type Error = ProposalTypeError; - - fn try_from(value: PgfRetro) -> Result { - Ok(PGFAction::Retro(PGFTarget { - target: value.target.address, - amount: value.target.amount, - })) +impl From for PGFAction { + fn from(value: PgfRetro) -> Self { + PGFAction::Retro(value.target) } } @@ -319,6 +424,9 @@ pub mod testing { use proptest::{collection, option, prop_compose}; use super::*; + use crate::ledger::governance::storage::proposal::{ + PGFInternalTarget, PGFTarget, + }; use crate::types::address::testing::arb_non_internal_address; use crate::types::hash::testing::arb_hash; use crate::types::token::testing::arb_amount; @@ -341,10 +449,10 @@ pub mod testing { target in arb_non_internal_address(), amount in arb_amount(), ) -> PGFTarget { - PGFTarget { + PGFTarget::Internal(PGFInternalTarget { target, amount, - } + }) } } diff --git a/core/src/ledger/pgf/inflation.rs b/core/src/ledger/pgf/inflation.rs index 30dd4191fd..a647829345 100644 --- a/core/src/ledger/pgf/inflation.rs +++ b/core/src/ledger/pgf/inflation.rs @@ -1,18 +1,24 @@ //! PGF lib code. +use crate::ledger::governance::storage::proposal::PGFTarget; use crate::ledger::parameters::storage as params_storage; +use crate::ledger::storage::{DBIter, StorageHasher, WlStorage, DB}; +use crate::ledger::storage_api::ibc::transfer_over_ibc; use crate::ledger::storage_api::pgf::{ get_parameters, get_payments, get_stewards, }; -use crate::ledger::storage_api::token::credit_tokens; -use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::ledger::storage_api::token::{credit_tokens, transfer}; +use crate::ledger::storage_api::{self, StorageRead}; use crate::types::dec::Dec; use crate::types::token; /// Apply the PGF inflation. -pub fn apply_inflation(storage: &mut S) -> storage_api::Result<()> +pub fn apply_inflation( + storage: &mut WlStorage, +) -> storage_api::Result<()> where - S: StorageRead + StorageWrite, + D: DB + for<'iter> DBIter<'iter> + Sync + 'static, + H: StorageHasher + Sync + 'static, { let pgf_parameters = get_parameters(storage)?; let staking_token = storage.get_native_token()?; @@ -46,26 +52,36 @@ where pgf_fundings.sort_by(|a, b| a.id.cmp(&b.id)); for funding in pgf_fundings { - if storage_api::token::transfer( - storage, - &staking_token, - &super::ADDRESS, - &funding.detail.target, - funding.detail.amount, - ) - .is_ok() - { - tracing::info!( - "Paying {} tokens for {} project.", - funding.detail.amount.to_string_native(), - &funding.detail.target, - ); - } else { - tracing::warn!( - "Failed to pay {} tokens for {} project.", - funding.detail.amount.to_string_native(), - &funding.detail.target, - ); + let result = match &funding.detail { + PGFTarget::Internal(target) => transfer( + storage, + &staking_token, + &super::ADDRESS, + &target.target, + target.amount, + ), + PGFTarget::Ibc(target) => transfer_over_ibc( + storage, + &staking_token, + &super::ADDRESS, + target, + ), + }; + match result { + Ok(()) => { + tracing::info!( + "Paying {} tokens for {} project.", + funding.detail.amount().to_string_native(), + &funding.detail.target(), + ); + } + Err(_) => { + tracing::warn!( + "Failed to pay {} tokens for {} project.", + funding.detail.amount().to_string_native(), + &funding.detail.target(), + ); + } } } diff --git a/core/src/ledger/pgf/storage/keys.rs b/core/src/ledger/pgf/storage/keys.rs index 96768ac494..8ca22f8303 100644 --- a/core/src/ledger/pgf/storage/keys.rs +++ b/core/src/ledger/pgf/storage/keys.rs @@ -63,7 +63,7 @@ pub fn fundings_key_prefix() -> Key { } /// LazyMap handler for the pgf fundings substorage -pub fn fundings_handle() -> LazyMap { +pub fn fundings_handle() -> LazyMap { LazyMap::open(fundings_key_prefix()) } diff --git a/core/src/ledger/storage_api/ibc.rs b/core/src/ledger/storage_api/ibc.rs new file mode 100644 index 0000000000..9fc69ff784 --- /dev/null +++ b/core/src/ledger/storage_api/ibc.rs @@ -0,0 +1,182 @@ +//! Implementation of `IbcActions` with the protocol storage + +use std::cell::RefCell; +use std::rc::Rc; + +use crate::ibc::apps::transfer::types::msgs::transfer::MsgTransfer; +use crate::ibc::apps::transfer::types::packet::PacketData; +use crate::ibc::apps::transfer::types::PrefixedCoin; +use crate::ibc::core::channel::types::timeout::TimeoutHeight; +use crate::ibc::primitives::Msg; +use crate::ledger::governance::storage::proposal::PGFIbcTarget; +use crate::ledger::ibc::{IbcActions, IbcCommonContext, IbcStorageContext}; +use crate::ledger::parameters::read_epoch_duration_parameter; +use crate::ledger::storage::wl_storage::WriteLogAndStorage; +use crate::ledger::storage::write_log::{self, WriteLog}; +use crate::ledger::storage::{DBIter, Storage, StorageHasher, WlStorage, DB}; +use crate::ledger::storage_api::{self, token, Error, ResultExt, StorageWrite}; +use crate::tendermint::Time as TmTime; +use crate::types::address::{Address, InternalAddress}; +use crate::types::hash::Hash; +use crate::types::ibc::{IbcEvent, IbcShieldedTransfer}; +use crate::types::time::DateTimeUtc; +use crate::types::token::DenominatedAmount; + +/// IBC protocol context +#[derive(Debug)] +pub struct IbcProtocolContext<'a, D, H> +where + D: DB + for<'iter> DBIter<'iter>, + H: StorageHasher, +{ + wl_storage: &'a mut WlStorage, +} + +impl WriteLogAndStorage for IbcProtocolContext<'_, D, H> +where + D: DB + for<'iter> DBIter<'iter>, + H: StorageHasher, +{ + type D = D; + type H = H; + + fn write_log(&self) -> &WriteLog { + self.wl_storage.write_log() + } + + fn write_log_mut(&mut self) -> &mut WriteLog { + self.wl_storage.write_log_mut() + } + + fn storage(&self) -> &Storage { + self.wl_storage.storage() + } + + fn split_borrow(&mut self) -> (&mut WriteLog, &Storage) { + self.wl_storage.split_borrow() + } + + fn write_tx_hash(&mut self, hash: Hash) -> write_log::Result<()> { + self.wl_storage.write_tx_hash(hash) + } +} + +impl IbcStorageContext for IbcProtocolContext<'_, D, H> +where + D: DB + for<'iter> DBIter<'iter> + 'static, + H: StorageHasher + 'static, +{ + fn emit_ibc_event(&mut self, event: IbcEvent) -> Result<(), Error> { + self.wl_storage.write_log.emit_ibc_event(event); + Ok(()) + } + + /// Get IBC events + fn get_ibc_events( + &self, + event_type: impl AsRef, + ) -> Result, Error> { + Ok(self + .wl_storage + .write_log + .get_ibc_events() + .iter() + .filter(|event| event.event_type == event_type.as_ref()) + .cloned() + .collect()) + } + + /// Transfer token + fn transfer_token( + &mut self, + src: &Address, + dest: &Address, + token: &Address, + amount: DenominatedAmount, + ) -> Result<(), Error> { + token::transfer(self, token, src, dest, amount.amount()) + } + + /// Handle masp tx + fn handle_masp_tx( + &mut self, + _shielded: &IbcShieldedTransfer, + ) -> Result<(), Error> { + unimplemented!("No MASP transfer in an IBC protocol transaction") + } + + /// Mint token + fn mint_token( + &mut self, + target: &Address, + token: &Address, + amount: DenominatedAmount, + ) -> Result<(), Error> { + token::credit_tokens(self.wl_storage, token, target, amount.amount())?; + let minter_key = token::minter_key(token); + self.wl_storage + .write(&minter_key, Address::Internal(InternalAddress::Ibc)) + } + + /// Burn token + fn burn_token( + &mut self, + target: &Address, + token: &Address, + amount: DenominatedAmount, + ) -> Result<(), Error> { + token::burn(self.wl_storage, token, target, amount.amount()) + } + + fn log_string(&self, message: String) { + tracing::trace!(message); + } +} + +impl IbcCommonContext for IbcProtocolContext<'_, D, H> +where + D: DB + for<'iter> DBIter<'iter> + 'static, + H: StorageHasher + 'static, +{ +} + +/// Transfer tokens over IBC +pub fn transfer_over_ibc( + wl_storage: &mut WlStorage, + token: &Address, + source: &Address, + target: &PGFIbcTarget, +) -> storage_api::Result<()> +where + D: DB + for<'iter> DBIter<'iter> + 'static, + H: StorageHasher + 'static, +{ + let token = PrefixedCoin { + denom: token.to_string().parse().expect("invalid token"), + amount: target.amount.into(), + }; + let packet_data = PacketData { + token, + sender: source.to_string().into(), + receiver: target.target.clone().into(), + memo: String::default().into(), + }; + let timeout_timestamp = DateTimeUtc::now() + + read_epoch_duration_parameter(wl_storage)?.min_duration; + let timeout_timestamp = + TmTime::try_from(timeout_timestamp).into_storage_result()?; + let ibc_message = MsgTransfer { + port_id_on_a: target.port_id.clone(), + chan_id_on_a: target.channel_id.clone(), + packet_data, + timeout_height_on_b: TimeoutHeight::Never, + timeout_timestamp_on_b: timeout_timestamp.into(), + }; + let any_msg = ibc_message.to_any(); + let mut data = vec![]; + prost::Message::encode(&any_msg, &mut data).into_storage_result()?; + + let ctx = IbcProtocolContext { wl_storage }; + let mut actions = IbcActions::new(Rc::new(RefCell::new(ctx))); + actions.execute(&data).into_storage_result() +} diff --git a/core/src/ledger/storage_api/mod.rs b/core/src/ledger/storage_api/mod.rs index ad239b2f4c..473a2bfcf6 100644 --- a/core/src/ledger/storage_api/mod.rs +++ b/core/src/ledger/storage_api/mod.rs @@ -5,6 +5,7 @@ pub mod account; pub mod collections; mod error; pub mod governance; +pub mod ibc; pub mod key; pub mod pgf; pub mod token; diff --git a/core/src/types/token.rs b/core/src/types/token.rs index d738086e7b..8774414526 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -962,9 +962,15 @@ impl MaspDenom { } } +impl From for IbcAmount { + fn from(amount: Amount) -> Self { + primitive_types::U256(amount.raw.0).into() + } +} + impl From for IbcAmount { fn from(amount: DenominatedAmount) -> Self { - primitive_types::U256(amount.canonical().amount.raw.0).into() + amount.canonical().amount.into() } } diff --git a/core/src/types/transaction/governance.rs b/core/src/types/transaction/governance.rs index 8e43b488e3..5876d8de60 100644 --- a/core/src/types/transaction/governance.rs +++ b/core/src/types/transaction/governance.rs @@ -8,7 +8,7 @@ use crate::ledger::governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, }; use crate::ledger::governance::storage::proposal::{ - AddRemove, PGFAction, PGFTarget, ProposalType, + AddRemove, PGFAction, ProposalType, }; use crate::ledger::governance::storage::vote::StorageProposalVote; use crate::types::address::Address; @@ -124,12 +124,8 @@ impl TryFrom for InitProposalData { .continuous .iter() .cloned() - .map(|funding| { - let target = PGFTarget { - target: funding.address, - amount: funding.amount, - }; - if funding.amount.is_zero() { + .map(|target| { + if target.amount().is_zero() { PGFAction::Continuous(AddRemove::Remove(target)) } else { PGFAction::Continuous(AddRemove::Add(target)) @@ -142,13 +138,7 @@ impl TryFrom for InitProposalData { .retro .iter() .cloned() - .map(|funding| { - let target = PGFTarget { - target: funding.address, - amount: funding.amount, - }; - PGFAction::Retro(target) - }) + .map(PGFAction::Retro) .collect::>(); let extra_data = [continuous_fundings, retro_fundings].concat(); diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index 0e33d9cc87..6d75ea2829 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -1,6 +1,8 @@ //! E2E test helpers +use std::fs::File; use std::future::Future; +use std::io::Write; use std::path::Path; use std::process::Command; use std::str::FromStr; @@ -19,6 +21,7 @@ use namada::types::address::Address; use namada::types::key::*; use namada::types::storage::Epoch; use namada::types::token; +use namada_apps::cli::context::ENV_VAR_CHAIN_ID; use namada_apps::config::genesis::chain::DeriveEstablishedAddress; use namada_apps::config::genesis::templates; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; @@ -26,6 +29,7 @@ use namada_apps::config::{Config, TendermintMode}; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_sdk::wallet::fs::FsWalletUtils; use namada_sdk::wallet::Wallet; +use toml::Value; use super::setup::{ self, sleep, NamadaBgCmd, NamadaCmd, Test, ENV_VAR_DEBUG, @@ -504,3 +508,109 @@ pub fn wait_for_wasm_pre_compile(ledger: &mut NamadaCmd) -> Result<()> { pub fn epochs_per_year_from_min_duration(min_duration: u64) -> u64 { 60 * 60 * 24 * 365 / min_duration } + +/// Make a Hermes config +pub fn make_hermes_config(test_a: &Test, test_b: &Test) -> Result<()> { + let mut config = toml::map::Map::new(); + + let mut global = toml::map::Map::new(); + global.insert("log_level".to_owned(), Value::String("debug".to_owned())); + config.insert("global".to_owned(), Value::Table(global)); + + let mut mode = toml::map::Map::new(); + let mut clients = toml::map::Map::new(); + clients.insert("enabled".to_owned(), Value::Boolean(true)); + clients.insert("refresh".to_owned(), Value::Boolean(true)); + clients.insert("misbehaviour".to_owned(), Value::Boolean(true)); + mode.insert("clients".to_owned(), Value::Table(clients)); + + let mut connections = toml::map::Map::new(); + connections.insert("enabled".to_owned(), Value::Boolean(false)); + mode.insert("connections".to_owned(), Value::Table(connections)); + + let mut channels = toml::map::Map::new(); + channels.insert("enabled".to_owned(), Value::Boolean(false)); + mode.insert("channels".to_owned(), Value::Table(channels)); + + let mut packets = toml::map::Map::new(); + packets.insert("enabled".to_owned(), Value::Boolean(true)); + packets.insert("clear_interval".to_owned(), Value::Integer(10)); + packets.insert("clear_on_start".to_owned(), Value::Boolean(false)); + packets.insert("tx_confirmation".to_owned(), Value::Boolean(true)); + mode.insert("packets".to_owned(), Value::Table(packets)); + + config.insert("mode".to_owned(), Value::Table(mode)); + + let mut telemetry = toml::map::Map::new(); + telemetry.insert("enabled".to_owned(), Value::Boolean(false)); + telemetry.insert("host".to_owned(), Value::String("127.0.0.1".to_owned())); + telemetry.insert("port".to_owned(), Value::Integer(3001)); + config.insert("telemetry".to_owned(), Value::Table(telemetry)); + + let chains = vec![ + make_hermes_chain_config(test_a), + make_hermes_chain_config(test_b), + ]; + + config.insert("chains".to_owned(), Value::Array(chains)); + + let toml_string = toml::to_string(&Value::Table(config)).unwrap(); + let hermes_dir = test_a.test_dir.as_ref().join("hermes"); + std::fs::create_dir_all(&hermes_dir).unwrap(); + let config_path = hermes_dir.join("config.toml"); + let mut file = File::create(config_path).unwrap(); + file.write_all(toml_string.as_bytes()).map_err(|e| { + eyre!(format!("Writing a Hermes config failed: {}", e,)) + })?; + // One Hermes config.toml is OK, but add one more config.toml to execute + // Hermes from test_b + let hermes_dir = test_b.test_dir.as_ref().join("hermes"); + std::fs::create_dir_all(&hermes_dir).unwrap(); + let config_path = hermes_dir.join("config.toml"); + let mut file = File::create(config_path).unwrap(); + file.write_all(toml_string.as_bytes()).map_err(|e| { + eyre!(format!("Writing a Hermes config failed: {}", e,)) + })?; + + Ok(()) +} + +fn make_hermes_chain_config(test: &Test) -> Value { + let chain_id = test.net.chain_id.as_str(); + let rpc_addr = get_actor_rpc(test, Who::Validator(0)); + + let mut table = toml::map::Map::new(); + table.insert("mode".to_owned(), Value::String("push".to_owned())); + let url = format!("ws://{}/websocket", rpc_addr); + table.insert("url".to_owned(), Value::String(url)); + table.insert("batch_delay".to_owned(), Value::String("500ms".to_owned())); + let event_source = Value::Table(table); + + let mut chain = toml::map::Map::new(); + chain.insert("id".to_owned(), Value::String(chain_id.to_owned())); + chain.insert("type".to_owned(), Value::String("Namada".to_owned())); + chain.insert( + "rpc_addr".to_owned(), + Value::String(format!("http://{rpc_addr}")), + ); + // The grpc isn't used for Namada, but it's required + chain.insert( + "grpc_addr".to_owned(), + Value::String("http://127.0.0.1:9090".to_owned()), + ); + chain.insert("event_source".to_owned(), event_source); + chain.insert("account_prefix".to_owned(), Value::String("".to_owned())); + chain.insert( + "key_name".to_owned(), + Value::String(setup::constants::CHRISTEL_KEY.to_owned()), + ); + chain.insert("store_prefix".to_owned(), Value::String("ibc".to_owned())); + let mut table = toml::map::Map::new(); + table.insert("price".to_owned(), Value::Float(0.001)); + std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); + let nam_addr = find_address(test, setup::constants::NAM).unwrap(); + table.insert("denom".to_owned(), Value::String(nam_addr.to_string())); + chain.insert("gas_price".to_owned(), Value::Table(table)); + + Value::Table(chain) +} diff --git a/tests/src/e2e/ibc_tests.rs b/tests/src/e2e/ibc_tests.rs index d0a3ae4638..d6986cc170 100644 --- a/tests/src/e2e/ibc_tests.rs +++ b/tests/src/e2e/ibc_tests.rs @@ -12,11 +12,15 @@ use core::convert::TryFrom; use core::str::FromStr; use core::time::Duration; -use std::collections::HashMap; -use std::path::PathBuf; +use std::collections::{BTreeSet, HashMap}; +use std::path::{Path, PathBuf}; use color_eyre::eyre::Result; use eyre::eyre; +use namada::core::ledger::governance::cli::onchain::PgfFunding; +use namada::core::ledger::governance::storage::proposal::{ + PGFIbcTarget, PGFTarget, +}; use namada::ibc::apps::transfer::types::VERSION as ICS20_VERSION; use namada::ibc::clients::tendermint::client_state::ClientState as TmClientState; use namada::ibc::clients::tendermint::consensus_state::ConsensusState as TmConsensusState; @@ -55,6 +59,7 @@ use namada::ibc::primitives::{Msg, Signer, Timestamp}; use namada::ledger::events::EventType; use namada::ledger::ibc::storage::*; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; +use namada::ledger::pgf::ADDRESS as PGF_ADDRESS; use namada::ledger::queries::RPC; use namada::ledger::storage::ics23_specs::ibc_proof_specs; use namada::ledger::storage::traits::Sha256Hasher; @@ -62,8 +67,9 @@ use namada::tendermint::abci::Event as AbciEvent; use namada::tendermint::block::Height as TmHeight; use namada::types::address::{Address, InternalAddress}; use namada::types::key::PublicKey; -use namada::types::storage::{BlockHeight, Key}; +use namada::types::storage::{BlockHeight, Epoch, Key}; use namada::types::token::Amount; +use namada_apps::cli::context::ENV_VAR_CHAIN_ID; use namada_apps::client::rpc::{ query_pos_parameters, query_storage_value, query_storage_value_bytes, }; @@ -83,10 +89,13 @@ use tendermint_light_client::components::io::{Io, ProdIo as TmLightClientIo}; use super::setup::set_ethereum_bridge_mode; use crate::e2e::helpers::{ - find_address, get_actor_rpc, get_validator_pk, wait_for_wasm_pre_compile, + epochs_per_year_from_min_duration, find_address, get_actor_rpc, get_epoch, + get_established_addr_from_pregenesis, get_validator_pk, + wait_for_wasm_pre_compile, }; +use crate::e2e::ledger_tests::prepare_proposal_data; use crate::e2e::setup::{ - self, sleep, working_dir, Bin, NamadaCmd, Test, TestDir, Who, + self, run_hermes_cmd, setup_hermes, sleep, Bin, NamadaCmd, Test, Who, }; use crate::strings::{ LEDGER_STARTED, TX_ACCEPTED, TX_APPLIED_SUCCESS, TX_FAILED, VALIDATOR_NODE, @@ -95,51 +104,15 @@ use crate::{run, run_as}; #[test] fn run_ledger_ibc() -> Result<()> { - let (test_a, test_b) = setup_two_single_node_nets()?; - set_ethereum_bridge_mode( - &test_a, - &test_a.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - set_ethereum_bridge_mode( - &test_b, - &test_b.net.chain_id, - Who::Validator(0), - ethereum_bridge::ledger::Mode::Off, - None, - ); - - // Run Chain A - let mut ledger_a = run_as!( - test_a, - Who::Validator(0), - Bin::Node, - &["ledger", "run"], - Some(40) - )?; - ledger_a.exp_string(LEDGER_STARTED)?; - // Run Chain B - let mut ledger_b = run_as!( - test_b, - Who::Validator(0), - Bin::Node, - &["ledger", "run"], - Some(40) - )?; - ledger_b.exp_string(LEDGER_STARTED)?; - ledger_a.exp_string(VALIDATOR_NODE)?; - ledger_b.exp_string(VALIDATOR_NODE)?; - - wait_for_wasm_pre_compile(&mut ledger_a)?; - wait_for_wasm_pre_compile(&mut ledger_b)?; - + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = 31536; + setup::set_validators(1, genesis, base_dir, |_| 0) + }; + let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; let _bg_ledger_a = ledger_a.background(); let _bg_ledger_b = ledger_b.background(); - sleep(5); - let (client_id_a, client_id_b) = create_client(&test_a, &test_b)?; let (conn_id_a, conn_id_b) = @@ -215,56 +188,242 @@ fn run_ledger_ibc() -> Result<()> { Ok(()) } -/// Set up two Namada chains to talk to each other via IBC. -fn setup_two_single_node_nets() -> Result<(Test, Test)> { - const ANOTHER_PROXY_APP: u16 = 27659u16; - const ANOTHER_RPC: u16 = 27660u16; - const ANOTHER_P2P: u16 = 26655u16; - // Download the shielded pool parameters before starting node - let _ = FsShieldedUtils::new(PathBuf::new()); - // epoch per 100 seconds +#[test] +fn run_ledger_ibc_with_hermes() -> Result<()> { let update_genesis = |mut genesis: templates::All, base_dir: &_| { genesis.parameters.parameters.epochs_per_year = 31536; setup::set_validators(1, genesis, base_dir, |_| 0) }; - let test_a = setup::network(update_genesis, None)?; - let test_b = Test { - working_dir: working_dir(), - test_dir: TestDir::new(), - net: test_a.net.clone(), - async_runtime: Default::default(), - }; - for entry in std::fs::read_dir(test_a.test_dir.path()).unwrap() { - let entry = entry.unwrap(); - if entry.path().is_dir() { - copy_dir::copy_dir( - entry.path(), - test_b.test_dir.path().join(entry.file_name()), - ) - .map_err(|e| { - eyre!( - "Failed copying directory from test_a to test_b with {}", - e + let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; + let _bg_ledger_a = ledger_a.background(); + let _bg_ledger_b = ledger_b.background(); + + setup_hermes(&test_a, &test_b)?; + let port_id_a = "transfer".parse().unwrap(); + let port_id_b = "transfer".parse().unwrap(); + let (channel_id_a, channel_id_b) = + create_channel_with_hermes(&test_a, &test_b)?; + + // Start relaying + let hermes = run_hermes(&test_a)?; + let _bg_hermes = hermes.background(); + + // Transfer 100000 from the normal account on Chain A to Chain B + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let receiver = find_address(&test_b, BERTHA)?; + transfer( + &test_a, + ALBERT, + receiver.to_string(), + NAM, + "100000", + ALBERT_KEY, + &port_id_a, + &channel_id_a, + None, + None, + None, + false, + )?; + wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; + check_balances(&port_id_b, &channel_id_b, &test_a, &test_b)?; + + // Transfer 50000 received over IBC on Chain B + let token = format!("{port_id_b}/{channel_id_b}/nam"); + transfer_on_chain(&test_b, BERTHA, ALBERT, token, 50000, BERTHA_KEY)?; + check_balances_after_non_ibc(&port_id_b, &channel_id_b, &test_b)?; + + // Transfer 50000 back from the origin-specific account on Chain B to Chain + // A + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); + let receiver = find_address(&test_a, ALBERT)?; + // Chain A was the source for the sent token + let ibc_denom = format!("{port_id_b}/{channel_id_b}/nam"); + // Send a token from Chain B + transfer( + &test_b, + BERTHA, + receiver.to_string(), + ibc_denom, + "50000", + BERTHA_KEY, + &port_id_b, + &channel_id_b, + None, + None, + None, + false, + )?; + wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; + check_balances_after_back(&port_id_b, &channel_id_b, &test_a, &test_b)?; + + // Transfer a token and it will time out and refund + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let receiver = find_address(&test_b, BERTHA)?; + // Send a token from Chain A + transfer( + &test_a, + ALBERT, + receiver.to_string(), + NAM, + "100000", + ALBERT_KEY, + &port_id_a, + &channel_id_a, + None, + Some(Duration::new(0, 0)), + None, + false, + )?; + // wait for the timeout and the refund + wait_for_packet_relay(&port_id_a, &channel_id_a, &test_a)?; + // The balance should not be changed + check_balances_after_back(&port_id_b, &channel_id_b, &test_a, &test_b)?; + + Ok(()) +} + +#[test] +fn pgf_over_ibc_with_hermes() -> Result<()> { + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(10); + // for the trusting period of IBC client + genesis.parameters.pos_params.pipeline_len = 10; + genesis.parameters.parameters.max_proposal_bytes = + Default::default(); + genesis.parameters.pgf_params.stewards = + BTreeSet::from_iter([get_established_addr_from_pregenesis( + ALBERT_KEY, base_dir, &genesis, ) - })?; - } else { - std::fs::copy( - entry.path(), - test_b.test_dir.path().join(entry.file_name()), - ) - .map_err(|e| { - eyre!("Failed copying file from test_a to test_b with {}", e) - })?; - } + .unwrap()]); + setup::set_validators(1, genesis, base_dir, |_| 0) + }; + let (ledger_a, ledger_b, test_a, test_b) = run_two_nets(update_genesis)?; + let _bg_ledger_a = ledger_a.background(); + let _bg_ledger_b = ledger_b.background(); + + setup_hermes(&test_a, &test_b)?; + let port_id_a = "transfer".parse().unwrap(); + let port_id_b = "transfer".parse().unwrap(); + let (channel_id_a, channel_id_b) = + create_channel_with_hermes(&test_a, &test_b)?; + + // Start relaying + let hermes = run_hermes(&test_a)?; + let _bg_hermes = hermes.background(); + + // Transfer to PGF account + transfer_on_chain( + &test_a, + ALBERT, + PGF_ADDRESS.to_string(), + NAM, + 100, + ALBERT_KEY, + )?; + + // Proposal on Chain A + // Delegate some token + delegate_token(&test_a)?; + let rpc_a = get_actor_rpc(&test_a, Who::Validator(0)); + let mut epoch = get_epoch(&test_a, &rpc_a).unwrap(); + let delegated = epoch + 10u64; + while epoch <= delegated { + sleep(5); + epoch = get_epoch(&test_a, &rpc_a).unwrap(); + } + // funding proposal + let start_epoch = + propose_funding(&test_a, &test_b, &port_id_a, &channel_id_a)?; + let mut epoch = get_epoch(&test_a, &rpc_a).unwrap(); + // Vote + while epoch <= start_epoch { + sleep(5); + epoch = get_epoch(&test_a, &rpc_a).unwrap(); } + submit_votes(&test_a)?; + + // wait for the grace + let grace_epoch = start_epoch + 12u64 + 6u64; + while epoch <= grace_epoch { + sleep(5); + epoch = get_epoch(&test_a, &rpc_a).unwrap(); + } + + // Check balances after funding over IBC + check_funded_balances(&port_id_b, &channel_id_b, &test_b)?; + + Ok(()) +} + +fn run_two_nets( + update_genesis: impl FnMut( + templates::All, + &Path, + ) -> templates::All, +) -> Result<(NamadaCmd, NamadaCmd, Test, Test)> { + let (test_a, test_b) = setup_two_single_node_nets(update_genesis)?; + set_ethereum_bridge_mode( + &test_a, + &test_a.net.chain_id, + Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); + set_ethereum_bridge_mode( + &test_b, + &test_b.net.chain_id, + Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); + + // Run Chain A + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); + let mut ledger_a = + run_as!(test_a, Who::Validator(0), Bin::Node, &["ledger"], Some(40))?; + ledger_a.exp_string(LEDGER_STARTED)?; + ledger_a.exp_string(VALIDATOR_NODE)?; + // Run Chain B + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let mut ledger_b = + run_as!(test_b, Who::Validator(0), Bin::Node, &["ledger"], Some(40))?; + ledger_b.exp_string(LEDGER_STARTED)?; + ledger_b.exp_string(VALIDATOR_NODE)?; + + wait_for_wasm_pre_compile(&mut ledger_a)?; + wait_for_wasm_pre_compile(&mut ledger_b)?; + + sleep(5); + + Ok((ledger_a, ledger_b, test_a, test_b)) +} + +/// Set up two Namada chains to talk to each other via IBC. +fn setup_two_single_node_nets( + mut update_genesis: impl FnMut( + templates::All, + &Path, + ) -> templates::All, +) -> Result<(Test, Test)> { + const ANOTHER_PROXY_APP: u16 = 27659u16; + const ANOTHER_RPC: u16 = 27660u16; + const ANOTHER_P2P: u16 = 26655u16; + // Download the shielded pool parameters before starting node + let _ = FsShieldedUtils::new(PathBuf::new()); + + let test_a = setup::network(&mut update_genesis, None)?; + let test_b = setup::network(update_genesis, None)?; let genesis_b_dir = test_b .test_dir .path() .join(namada_apps::client::utils::NET_ACCOUNTS_DIR) .join("validator-0"); let mut genesis_b = chain::Finalized::read_toml_files( - &genesis_b_dir.join(test_a.net.chain_id.as_str()), + &genesis_b_dir.join(test_b.net.chain_id.as_str()), ) .map_err(|_| eyre!("Could not read genesis files from test b"))?; // chain b's validator needs to listen on a different port than chain a's @@ -298,12 +457,12 @@ fn setup_two_single_node_nets() -> Result<(Test, Test)> { + setup::ANOTHER_CHAIN_PORT_OFFSET; validator_tx.tx.data.net_address.set_port(new_port); genesis_b - .write_toml_files(&genesis_b_dir.join(test_a.net.chain_id.as_str())) + .write_toml_files(&genesis_b_dir.join(test_b.net.chain_id.as_str())) .map_err(|_| eyre!("Could not write genesis toml files for test_b"))?; // modify chain b to use different ports for cometbft let mut config = namada_apps::config::Config::load( &genesis_b_dir, - &test_a.net.chain_id, + &test_b.net.chain_id, Some(TendermintMode::Validator), ); let proxy_app = &mut config.ledger.cometbft.proxy_app; @@ -313,13 +472,95 @@ fn setup_two_single_node_nets() -> Result<(Test, Test)> { let p2p_addr = &mut config.ledger.cometbft.p2p.laddr; set_port(p2p_addr, ANOTHER_P2P); config - .write(&genesis_b_dir, &test_a.net.chain_id, true) + .write(&genesis_b_dir, &test_b.net.chain_id, true) .map_err(|e| { eyre!("Unable to modify chain b's config file due to {}", e) })?; Ok((test_a, test_b)) } +fn create_channel_with_hermes( + test_a: &Test, + test_b: &Test, +) -> Result<(ChannelId, ChannelId)> { + let args = [ + "create", + "channel", + "--a-chain", + &test_a.net.chain_id.to_string(), + "--b-chain", + &test_b.net.chain_id.to_string(), + "--a-port", + "transfer", + "--b-port", + "transfer", + "--new-client-connection", + "--yes", + ]; + + let mut hermes = run_hermes_cmd(test_a, args, Some(120))?; + let (channel_id_a, channel_id_b) = + get_channel_ids_from_hermes_output(&mut hermes)?; + hermes.assert_success(); + + Ok((channel_id_a, channel_id_b)) +} + +fn get_channel_ids_from_hermes_output( + hermes: &mut NamadaCmd, +) -> Result<(ChannelId, ChannelId)> { + let (_, matched) = + hermes.exp_regex("channel handshake already finished .*")?; + + let regex = regex::Regex::new(r"channel-[0-9]+").unwrap(); + let mut iter = regex.find_iter(&matched); + let channel_id_a = iter.next().unwrap().as_str().parse().unwrap(); + let channel_id_b = iter.next().unwrap().as_str().parse().unwrap(); + + Ok((channel_id_a, channel_id_b)) +} + +fn run_hermes(test: &Test) -> Result { + let args = ["start"]; + let mut hermes = run_hermes_cmd(test, args, Some(40))?; + hermes.exp_string("Hermes has started")?; + Ok(hermes) +} + +fn wait_for_packet_relay( + port_id: &PortId, + channel_id: &ChannelId, + test: &Test, +) -> Result<()> { + let args = [ + "--json", + "query", + "packet", + "pending", + "--chain", + test.net.chain_id.as_str(), + "--port", + port_id.as_str(), + "--channel", + channel_id.as_str(), + ]; + for _ in 0..10 { + sleep(10); + let mut hermes = run_hermes_cmd(test, args, Some(40))?; + // Check no pending packet + if hermes + .exp_string( + "\"dst\":{\"unreceived_acks\":[],\"unreceived_packets\":[]},\"\ + src\":{\"unreceived_acks\":[],\"unreceived_packets\":[]}", + ) + .is_ok() + { + return Ok(()); + } + } + Err(eyre!("Pending packet is still left")) +} + fn create_client(test_a: &Test, test_b: &Test) -> Result<(ClientId, ClientId)> { let height = query_height(test_b)?; let client_state = make_client_state(test_b, height); @@ -745,6 +986,7 @@ fn transfer_token( channel_id_a: &ChannelId, ) -> Result<()> { // Send a token from Chain A + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let receiver = find_address(test_b, BERTHA)?; let height = transfer( test_a, @@ -810,6 +1052,7 @@ fn try_invalid_transfers( port_id_a: &PortId, channel_id_a: &ChannelId, ) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let receiver = find_address(test_b, BERTHA)?; // invalid amount @@ -871,6 +1114,7 @@ fn transfer_on_chain( amount: u64, signer: impl AsRef, ) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); let rpc = get_actor_rpc(test, Who::Validator(0)); let tx_args = [ "transfer", @@ -904,6 +1148,7 @@ fn transfer_back( port_id_b: &PortId, channel_id_b: &ChannelId, ) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); let receiver = find_address(test_a, ALBERT)?; // Chain A was the source for the sent token @@ -967,6 +1212,7 @@ fn transfer_timeout( port_id_a: &PortId, channel_id_a: &ChannelId, ) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let receiver = find_address(test_b, BERTHA)?; // Send a token from Chain A @@ -1022,6 +1268,8 @@ fn shielded_transfer( // Get masp proof for the following IBC transfer from the destination chain // It will send 10 BTC from Chain A to PA(B) on Chain B let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); + // Chain B will receive Chain A's BTC + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); let output_folder = test_b.test_dir.path().to_string_lossy(); // PA(B) on Chain B will receive BTC on chain A let token_addr = find_address(test_a, BTC)?; @@ -1043,6 +1291,7 @@ fn shielded_transfer( "--node", &rpc_b, ]; + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let mut client = run!(test_b, Bin::Client, args, Some(120))?; let file_path = get_shielded_transfer_path(&mut client)?; client.assert_success(); @@ -1174,6 +1423,7 @@ fn submit_ibc_tx( signer: &str, wait_reveal_pk: bool, ) -> Result { + std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); let data_path = test.test_dir.path().join("tx.data"); let data = make_ibc_data(message); std::fs::write(&data_path, data).expect("writing data failed"); @@ -1222,6 +1472,7 @@ fn transfer( expected_err: Option<&str>, wait_reveal_pk: bool, ) -> Result { + std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); let rpc = get_actor_rpc(test, Who::Validator(0)); let channel_id = channel_id.to_string(); @@ -1274,6 +1525,121 @@ fn transfer( } } +fn delegate_token(test: &Test) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); + let rpc = get_actor_rpc(test, Who::Validator(0)); + let tx_args = vec![ + "bond", + "--validator", + "validator-0", + "--source", + BERTHA, + "--amount", + "900", + "--node", + &rpc, + ]; + let mut client = run!(test, Bin::Client, tx_args, Some(40))?; + client.exp_string(TX_ACCEPTED)?; + client.exp_string(TX_APPLIED_SUCCESS)?; + client.assert_success(); + Ok(()) +} + +fn propose_funding( + test_a: &Test, + test_b: &Test, + src_port_id: &PortId, + src_channel_id: &ChannelId, +) -> Result { + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let bertha = find_address(test_b, BERTHA)?; + let christel = find_address(test_b, CHRISTEL)?; + + let pgf_funding = PgfFunding { + continuous: vec![PGFTarget::Ibc(PGFIbcTarget { + amount: Amount::from_u64(10), + target: bertha.to_string(), + port_id: src_port_id.clone(), + channel_id: src_channel_id.clone(), + })], + retro: vec![PGFTarget::Ibc(PGFIbcTarget { + amount: Amount::from_u64(5), + target: christel.to_string(), + port_id: src_port_id.clone(), + channel_id: src_channel_id.clone(), + })], + }; + + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); + let albert = find_address(test_a, ALBERT)?; + let rpc_a = get_actor_rpc(test_a, Who::Validator(0)); + let epoch = get_epoch(test_a, &rpc_a)?; + let start_epoch = (epoch.0 + 3) / 3 * 3; + let proposal_json_path = + prepare_proposal_data(test_a, albert, pgf_funding, start_epoch); + + let submit_proposal_args = vec![ + "init-proposal", + "--pgf-funding", + "--data-path", + proposal_json_path.to_str().unwrap(), + "--node", + &rpc_a, + ]; + let mut client = run!(test_a, Bin::Client, submit_proposal_args, Some(40))?; + client.exp_string(TX_ACCEPTED)?; + client.exp_string(TX_APPLIED_SUCCESS)?; + client.assert_success(); + Ok(start_epoch.into()) +} + +fn submit_votes(test: &Test) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test.net.chain_id.to_string()); + let rpc = get_actor_rpc(test, Who::Validator(0)); + + let submit_proposal_vote = vec![ + "vote-proposal", + "--proposal-id", + "0", + "--vote", + "yay", + "--address", + "validator-0", + "--node", + &rpc, + ]; + let mut client = run_as!( + test, + Who::Validator(0), + Bin::Client, + submit_proposal_vote, + Some(40) + )?; + client.exp_string(TX_ACCEPTED)?; + client.exp_string(TX_APPLIED_SUCCESS)?; + client.assert_success(); + + // Send different yay vote from delegator to check majority on 1/3 + let submit_proposal_vote_delagator = vec![ + "vote-proposal", + "--proposal-id", + "0", + "--vote", + "yay", + "--address", + BERTHA, + "--node", + &rpc, + ]; + let mut client = + run!(test, Bin::Client, submit_proposal_vote_delagator, Some(40))?; + client.exp_string(TX_ACCEPTED)?; + client.exp_string(TX_APPLIED_SUCCESS)?; + client.assert_success(); + Ok(()) +} + fn check_tx_height(test: &Test, client: &mut NamadaCmd) -> Result { let (_unread, matched) = client.exp_regex(r"height .*")?; // Expecting e.g. "height 1337." @@ -1418,6 +1784,7 @@ fn check_balances( test_b: &Test, ) -> Result<()> { // Check the balances on Chain A + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); let rpc_a = get_actor_rpc(test_a, Who::Validator(0)); // Check the escrowed balance let escrow = Address::Internal(InternalAddress::Ibc).to_string(); @@ -1436,12 +1803,13 @@ fn check_balances( client.assert_success(); // Check the balance on Chain B - let trace_path = format!("{}/{}", &dest_port_id, &dest_channel_id); + let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/nam"); + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); let query_args = vec![ - "balance", "--owner", BERTHA, "--token", NAM, "--node", &rpc_b, + "balance", "--owner", BERTHA, "--token", &ibc_denom, "--node", &rpc_b, ]; - let expected = format!("{}/nam: 100000", trace_path); + let expected = format!("{ibc_denom}: 100000"); let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; client.exp_string(&expected)?; client.assert_success(); @@ -1452,25 +1820,27 @@ fn check_balances( fn check_balances_after_non_ibc( port_id: &PortId, channel_id: &ChannelId, - test: &Test, + test_b: &Test, ) -> Result<()> { + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); // Check the balance on Chain B - let trace_path = format!("{}/{}", port_id, channel_id); - + let ibc_denom = format!("{port_id}/{channel_id}/nam"); // Check the source - let rpc = get_actor_rpc(test, Who::Validator(0)); - let query_args = - vec!["balance", "--owner", BERTHA, "--token", NAM, "--node", &rpc]; - let expected = format!("{}/nam: 50000", trace_path); - let mut client = run!(test, Bin::Client, query_args, Some(40))?; + let rpc = get_actor_rpc(test_b, Who::Validator(0)); + let query_args = vec![ + "balance", "--owner", BERTHA, "--token", &ibc_denom, "--node", &rpc, + ]; + let expected = format!("{ibc_denom}: 50000"); + let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; client.exp_string(&expected)?; client.assert_success(); - // Check the target - let query_args = - vec!["balance", "--owner", ALBERT, "--token", NAM, "--node", &rpc]; - let expected = format!("{}/nam: 50000", trace_path); - let mut client = run!(test, Bin::Client, query_args, Some(40))?; + // Check the traget + let query_args = vec![ + "balance", "--owner", ALBERT, "--token", &ibc_denom, "--node", &rpc, + ]; + let expected = format!("{ibc_denom}: 50000"); + let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; client.exp_string(&expected)?; client.assert_success(); @@ -1485,6 +1855,7 @@ fn check_balances_after_back( test_b: &Test, ) -> Result<()> { // Check the balances on Chain A + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); let rpc_a = get_actor_rpc(test_a, Who::Validator(0)); // Check the escrowed balance let escrow = Address::Internal(InternalAddress::Ibc).to_string(); @@ -1503,12 +1874,13 @@ fn check_balances_after_back( client.assert_success(); // Check the balance on Chain B - let trace_path = format!("{}/{}", dest_port_id, dest_channel_id); + let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/nam"); + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); let query_args = vec![ - "balance", "--owner", BERTHA, "--token", NAM, "--node", &rpc_b, + "balance", "--owner", BERTHA, "--token", &ibc_denom, "--node", &rpc_b, ]; - let expected = format!("{}/nam: 0", trace_path); + let expected = format!("{ibc_denom}: 0"); let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; client.exp_string(&expected)?; client.assert_success(); @@ -1523,9 +1895,12 @@ fn check_shielded_balances( test_b: &Test, ) -> Result<()> { // Check the balance on Chain B - let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); + std::env::set_var(ENV_VAR_CHAIN_ID, test_a.net.chain_id.to_string()); // PA(B) on Chain B has received BTC on chain A let token_addr = find_address(test_a, BTC)?.to_string(); + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); + let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/btc"); let query_args = vec![ "balance", "--owner", @@ -1536,10 +1911,38 @@ fn check_shielded_balances( "--node", &rpc_b, ]; - let expected = format!("{}/{}/btc: 10", dest_port_id, dest_channel_id); + let expected = format!("{ibc_denom}: 10"); + let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; + client.exp_string(&expected)?; + client.assert_success(); + Ok(()) +} + +fn check_funded_balances( + dest_port_id: &PortId, + dest_channel_id: &ChannelId, + test_b: &Test, +) -> Result<()> { + // Check the balance on Chain B + std::env::set_var(ENV_VAR_CHAIN_ID, test_b.net.chain_id.to_string()); + let ibc_denom = format!("{dest_port_id}/{dest_channel_id}/nam"); + let rpc_b = get_actor_rpc(test_b, Who::Validator(0)); + let query_args = vec![ + "balance", "--owner", BERTHA, "--token", &ibc_denom, "--node", &rpc_b, + ]; + let expected = format!("{ibc_denom}: 10"); let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; client.exp_string(&expected)?; client.assert_success(); + + let query_args = vec![ + "balance", "--owner", CHRISTEL, "--token", &ibc_denom, "--node", &rpc_b, + ]; + let expected = format!("{ibc_denom}: 5"); + let mut client = run!(test_b, Bin::Client, query_args, Some(40))?; + client.exp_string(&expected)?; + client.assert_success(); + Ok(()) } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index ca123601a1..299de13e96 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -27,7 +27,10 @@ use namada_apps::config::ethereum_bridge; use namada_apps::config::utils::convert_tm_addr_to_socket_addr; use namada_apps::facade::tendermint_config::net::Address as TendermintAddress; use namada_core::ledger::governance::cli::onchain::{ - PgfFunding, PgfFundingTarget, StewardsUpdate, + PgfFunding, StewardsUpdate, +}; +use namada_core::ledger::governance::storage::proposal::{ + PGFInternalTarget, PGFTarget, }; use namada_core::types::token::NATIVE_MAX_DECIMAL_PLACES; use namada_sdk::masp::fs::FsShieldedUtils; @@ -2317,14 +2320,14 @@ fn pgf_governance_proposal() -> Result<()> { let christel = find_address(&test, CHRISTEL)?; let pgf_funding = PgfFunding { - continuous: vec![PgfFundingTarget { + continuous: vec![PGFTarget::Internal(PGFInternalTarget { amount: token::Amount::from_u64(10), - address: bertha.clone(), - }], - retro: vec![PgfFundingTarget { + target: bertha.clone(), + })], + retro: vec![PGFTarget::Internal(PGFInternalTarget { amount: token::Amount::from_u64(5), - address: christel, - }], + target: christel, + })], }; let valid_proposal_json_path = @@ -3038,7 +3041,7 @@ fn test_epoch_sleep() -> Result<()> { /// 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( +pub fn prepare_proposal_data( test: &setup::Test, source: Address, data: impl serde::Serialize, diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index 1db62a2fe1..a29b09f4cc 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -39,7 +39,7 @@ use rand::Rng; use serde_json; use tempfile::{tempdir, tempdir_in, TempDir}; -use crate::e2e::helpers::generate_bin_command; +use crate::e2e::helpers::{generate_bin_command, make_hermes_config}; /// For `color_eyre::install`, which fails if called more than once in the same /// process @@ -1141,6 +1141,93 @@ pub fn sleep(seconds: u64) { thread::sleep(time::Duration::from_secs(seconds)); } +pub fn setup_hermes(test_a: &Test, test_b: &Test) -> Result<()> { + println!("\n{}", "Setting up Hermes".underline().green(),); + + make_hermes_config(test_a, test_b)?; + + for test in [test_a, test_b] { + let chain_id = test.net.chain_id.as_str(); + let chain_dir = test.test_dir.as_ref().join(chain_id); + let wallet = wallet::wallet_file(chain_dir); + let args = [ + "keys", + "add", + "--chain", + chain_id, + "--key-file", + &wallet.to_string_lossy(), + ]; + let mut hermes = run_hermes_cmd(test, args, Some(10))?; + hermes.assert_success(); + } + + Ok(()) +} + +pub fn run_hermes_cmd( + test: &Test, + args: I, + timeout_sec: Option, +) -> Result +where + I: IntoIterator, + S: AsRef, +{ + let mut run_cmd = Command::new("hermes"); + let hermes_dir = test.test_dir.as_ref().join("hermes"); + run_cmd.current_dir(hermes_dir.clone()); + let config_path = hermes_dir.join("config.toml"); + run_cmd.args(["--config", &config_path.to_string_lossy()]); + run_cmd.args(args); + + let args: String = + run_cmd.get_args().map(|s| s.to_string_lossy()).join(" "); + let cmd_str = + format!("{} {}", run_cmd.get_program().to_string_lossy(), args); + + let session = Session::spawn(run_cmd).map_err(|e| { + eyre!( + "\n\n{}: {}\n{}: {}", + "Failed to run Hermes".underline().red(), + cmd_str, + "Error".underline().red(), + e + ) + })?; + + let log_path = { + let mut rng = rand::thread_rng(); + let log_dir = test.get_base_dir(Who::NonValidator).join("logs"); + std::fs::create_dir_all(&log_dir)?; + log_dir.join(format!( + "{}-hermes-{}.log", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_micros(), + rng.gen::() + )) + }; + let logger = OpenOptions::new() + .write(true) + .create_new(true) + .open(&log_path)?; + let mut session = expectrl::session::log(session, logger).unwrap(); + + session.set_expect_timeout(timeout_sec.map(std::time::Duration::from_secs)); + + let cmd_process = NamadaCmd { + session, + cmd_str, + log_path, + }; + + println!("{}:\n{}", "> Running".underline().green(), &cmd_process); + + Ok(cmd_process) +} + #[allow(dead_code)] pub mod constants { // Paths to the WASMs used for tests