use core::time::Duration;
use std::ops::Add;
use bytes::BufMut;
use flex_error::define_error;
use tendermint::Hash as TxHash;
use ibc_proto::cosmos::gov::v1::MsgSubmitProposal;
use ibc_proto::cosmos::gov::v1beta1::MsgSubmitProposal as LegacyMsgSubmitProposal;
use ibc_proto::cosmos::upgrade::v1beta1::Plan;
use ibc_proto::google::protobuf::Any;
use ibc_proto::ibc::core::client::v1::{MsgIbcSoftwareUpgrade, UpgradeProposal};
use ibc_relayer_types::clients::ics07_tendermint::client_state::UpgradeOptions;
use ibc_relayer_types::core::ics02_client::client_state::UpgradableClientState;
use ibc_relayer_types::core::ics24_host::identifier::{ChainId, ClientId};
use ibc_relayer_types::{downcast, Height};
use tracing::warn;
use crate::chain::handle::ChainHandle;
use crate::chain::requests::{IncludeProof, QueryClientStateRequest, QueryHeight};
use crate::chain::tracking::TrackedMsgs;
use crate::chain::version::Specs;
use crate::client_state::AnyClientState;
use crate::error::Error;
define_error! {
UpgradeChainError {
Query
[ Error ]
|_| { "error during a query" },
Key
[ Error ]
|_| { "key error" },
Submit
{ chain_id: ChainId }
[ Error ]
|e| {
format!("failed while submitting the Transfer message to chain {0}", e.chain_id)
},
TxResponse
{ event: String }
|e| {
format!("tx response event consists of an error: {}", e.event)
},
TendermintOnly
|_| { "only Tendermint clients can be upgraded" },
UpgradeHeightRevision
{ revision: u64 }
|r| {
format!("invalid upgrade height revision: {r}")
}
}
}
#[derive(Clone, Debug)]
pub struct UpgradePlanOptions {
pub src_client_id: ClientId,
pub amount: u64,
pub denom: String,
pub height_offset: u64,
pub upgraded_chain_id: ChainId,
pub upgraded_unbonding_period: Option<Duration>,
pub upgrade_plan_name: String,
pub gov_account: String,
}
pub fn build_and_send_ibc_upgrade_proposal(
dst_chain: impl ChainHandle, src_chain: impl ChainHandle, opts: &UpgradePlanOptions,
) -> Result<TxHash, UpgradeChainError> {
let any_msg = if requires_legacy_upgrade_proposal(dst_chain.clone())? {
build_legacy_upgrade_proposal(dst_chain.clone(), src_chain, opts)
} else {
build_upgrade_proposal(dst_chain.clone(), src_chain, opts)
}?;
let responses = dst_chain
.send_messages_and_wait_check_tx(TrackedMsgs::new_single(any_msg, "upgrade"))
.map_err(|e| UpgradeChainError::submit(dst_chain.id(), e))?;
Ok(responses[0].hash)
}
pub fn requires_legacy_upgrade_proposal(
dst_chain: impl ChainHandle,
) -> Result<bool, UpgradeChainError> {
let Ok(version_specs) = dst_chain.version_specs() else {
warn!("failed to get version specs, assuming legacy upgrade proposal is required");
return Ok(true);
};
let version_specs = match version_specs {
Specs::Cosmos(v) => v,
Specs::Penumbra(_) => {
return Err(UpgradeChainError::submit(
dst_chain.id(),
crate::chain::namada::error::Error::upgrade().into(),
))
}
Specs::Namada(_) => {
return Err(UpgradeChainError::submit(
dst_chain.id(),
crate::chain::namada::error::Error::upgrade().into(),
))
}
};
let sdk_before_50 = version_specs
.cosmos_sdk
.as_ref()
.map(|s| s.minor < 50)
.unwrap_or(true);
Ok(match version_specs.ibc_go {
None => sdk_before_50,
Some(ibc_version) => {
if ibc_version.major < 4 {
sdk_before_50
} else {
ibc_version.major < 8
}
}
})
}
fn build_legacy_upgrade_proposal(
dst_chain: impl ChainHandle, src_chain: impl ChainHandle, opts: &UpgradePlanOptions,
) -> Result<Any, UpgradeChainError> {
let plan_height = dst_chain
.query_latest_height() .map_err(UpgradeChainError::query)?
.add(opts.height_offset);
let upgraded_client_latest_height =
if dst_chain.id().version() == opts.upgraded_chain_id.version() {
plan_height.increment()
} else {
Height::new(opts.upgraded_chain_id.version(), 1).map_err(|_| {
UpgradeChainError::upgrade_height_revision(opts.upgraded_chain_id.version())
})?
};
let (client_state, _) = src_chain
.query_client_state(
QueryClientStateRequest {
client_id: opts.src_client_id.clone(),
height: QueryHeight::Latest,
},
IncludeProof::No,
)
.map_err(UpgradeChainError::query)?;
let mut client_state = downcast!(client_state => AnyClientState::Tendermint)
.ok_or_else(UpgradeChainError::tendermint_only)?;
let upgrade_options = UpgradeOptions {
unbonding_period: opts
.upgraded_unbonding_period
.unwrap_or(client_state.unbonding_period),
};
client_state.upgrade(
upgraded_client_latest_height,
upgrade_options,
opts.upgraded_chain_id.clone(),
);
let proposal = UpgradeProposal {
title: "proposal 0".to_string(),
description: "upgrade the chain software and unbonding period".to_string(),
upgraded_client_state: Some(Any::from(AnyClientState::from(client_state))),
plan: Some(Plan {
name: opts.upgrade_plan_name.clone(),
height: plan_height.revision_height() as i64,
info: "".to_string(),
..Default::default() }),
};
let proposal = Proposal::Legacy(proposal);
let mut buf_proposal = Vec::new();
proposal.encode(&mut buf_proposal);
let any_proposal = Any {
type_url: proposal.type_url(),
value: buf_proposal,
};
let proposer = dst_chain.get_signer().map_err(UpgradeChainError::key)?;
let coins = ibc_proto::cosmos::base::v1beta1::Coin {
denom: opts.denom.clone(),
amount: opts.amount.to_string(),
};
let msg = LegacyMsgSubmitProposal {
content: Some(any_proposal),
initial_deposit: vec![coins],
proposer: proposer.to_string(),
};
let mut buf_msg = Vec::new();
prost::Message::encode(&msg, &mut buf_msg).unwrap();
Ok(Any {
type_url: "/cosmos.gov.v1beta1.MsgSubmitProposal".to_string(),
value: buf_msg,
})
}
fn build_upgrade_proposal(
dst_chain: impl ChainHandle, src_chain: impl ChainHandle, opts: &UpgradePlanOptions,
) -> Result<Any, UpgradeChainError> {
let plan_height = dst_chain
.query_latest_height() .map_err(UpgradeChainError::query)?
.add(opts.height_offset);
let upgraded_client_latest_height =
if dst_chain.id().version() == opts.upgraded_chain_id.version() {
plan_height.increment()
} else {
Height::new(opts.upgraded_chain_id.version(), 1).map_err(|_| {
UpgradeChainError::upgrade_height_revision(opts.upgraded_chain_id.version())
})?
};
let (client_state, _) = src_chain
.query_client_state(
QueryClientStateRequest {
client_id: opts.src_client_id.clone(),
height: QueryHeight::Latest,
},
IncludeProof::No,
)
.map_err(UpgradeChainError::query)?;
let mut client_state = downcast!(client_state => AnyClientState::Tendermint)
.ok_or_else(UpgradeChainError::tendermint_only)?;
let upgrade_options = UpgradeOptions {
unbonding_period: opts
.upgraded_unbonding_period
.unwrap_or(client_state.unbonding_period),
};
client_state.upgrade(
upgraded_client_latest_height,
upgrade_options,
opts.upgraded_chain_id.clone(),
);
let proposal = MsgIbcSoftwareUpgrade {
plan: Some(Plan {
name: opts.upgrade_plan_name.clone(),
height: plan_height.revision_height() as i64,
info: "".to_string(),
..Default::default() }),
upgraded_client_state: Some(Any::from(AnyClientState::from(client_state))),
signer: opts.gov_account.clone(),
};
let proposal = Proposal::Default(proposal);
let mut buf_proposal = Vec::new();
proposal.encode(&mut buf_proposal);
let any_proposal = Any {
type_url: proposal.type_url(),
value: buf_proposal,
};
let proposer = dst_chain.get_signer().map_err(UpgradeChainError::key)?;
let coins = ibc_proto::cosmos::base::v1beta1::Coin {
denom: opts.denom.clone(),
amount: opts.amount.to_string(),
};
let msg = MsgSubmitProposal {
messages: vec![any_proposal],
initial_deposit: vec![coins],
proposer: proposer.to_string(),
metadata: "".to_string(),
title: "proposal 0".to_string(),
summary: "upgrade the chain software and unbonding period".to_string(),
expedited: false,
};
let mut buf_msg = Vec::new();
prost::Message::encode(&msg, &mut buf_msg).unwrap();
Ok(Any {
type_url: "/cosmos.gov.v1.MsgSubmitProposal".to_string(),
value: buf_msg,
})
}
enum Proposal {
Default(MsgIbcSoftwareUpgrade),
Legacy(UpgradeProposal),
}
impl Proposal {
fn encode(&self, buf: &mut impl BufMut) {
match self {
Proposal::Default(p) => prost::Message::encode(p, buf),
Proposal::Legacy(p) => prost::Message::encode(p, buf),
}
.unwrap()
}
fn type_url(&self) -> String {
match self {
Proposal::Default(_) => "/ibc.core.client.v1.MsgIBCSoftwareUpgrade",
Proposal::Legacy(_) => "/ibc.core.client.v1.UpgradeProposal",
}
.to_owned()
}
}