Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(core, sequencer): move stateless checks to domain type parsing #1770

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/astria-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ penumbra-proto = { workspace = true }
prost = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = { workspace = true }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this change force us to always activate serde for all of astria-core? After all, the memo serde impls are feature gated I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to address this in 84b00ef. Let me know if this is what you had in mind!

sha2 = { workspace = true }
tendermint = { workspace = true }
tendermint-proto = { workspace = true }
Expand Down
187 changes: 164 additions & 23 deletions crates/astria-core/src/protocol/transaction/v1/action/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,25 @@ use crate::{
IncorrectRollupIdLength,
RollupId,
},
protocol::fees::v1::{
BridgeLockFeeComponents,
BridgeSudoChangeFeeComponents,
BridgeUnlockFeeComponents,
FeeAssetChangeFeeComponents,
FeeChangeFeeComponents,
FeeComponentError,
IbcRelayFeeComponents,
IbcRelayerChangeFeeComponents,
IbcSudoChangeFeeComponents,
Ics20WithdrawalFeeComponents,
InitBridgeAccountFeeComponents,
RollupDataSubmissionFeeComponents,
SudoAddressChangeFeeComponents,
TransferFeeComponents,
ValidatorUpdateFeeComponents,
protocol::{
fees::v1::{
BridgeLockFeeComponents,
BridgeSudoChangeFeeComponents,
BridgeUnlockFeeComponents,
FeeAssetChangeFeeComponents,
FeeChangeFeeComponents,
FeeComponentError,
IbcRelayFeeComponents,
IbcRelayerChangeFeeComponents,
IbcSudoChangeFeeComponents,
Ics20WithdrawalFeeComponents,
InitBridgeAccountFeeComponents,
RollupDataSubmissionFeeComponents,
SudoAddressChangeFeeComponents,
TransferFeeComponents,
ValidatorUpdateFeeComponents,
},
memos::v1::Ics20WithdrawalFromRollup,
},
Protobuf,
};
Expand Down Expand Up @@ -1032,12 +1035,19 @@ impl Protobuf for Ics20Withdrawal {
bridge_address,
use_compat_address,
} = proto;
let amount = amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?;
let amount = u128::from(amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?);
ethanoroshiba marked this conversation as resolved.
Show resolved Hide resolved
let return_address = Address::try_from_raw(
&return_address.ok_or(Ics20WithdrawalError::field_not_set("return_address"))?,
)
.map_err(Ics20WithdrawalError::return_address)?;

if timeout_time == 0 {
return Err(Ics20WithdrawalError::invalid_timeout_time());
}
if amount == 0 {
ethanoroshiba marked this conversation as resolved.
Show resolved Hide resolved
return Err(Ics20WithdrawalError::invalid_amount());
}

let timeout_height = timeout_height
.ok_or(Ics20WithdrawalError::field_not_set("timeout_height"))?
.into();
Expand All @@ -1047,8 +1057,20 @@ impl Protobuf for Ics20Withdrawal {
.transpose()
.map_err(Ics20WithdrawalError::invalid_bridge_address)?;

if bridge_address.is_some() {
let parsed_withdrawal: Ics20WithdrawalFromRollup =
serde_json::from_str(&memo).map_err(Ics20WithdrawalError::parse_memo)?;
Comment on lines +1123 to +1124
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we edit our domain type Ics20Withdrawal to then include the parsed withdrawal as well? Then, we don't have to parse again during check_and_execute

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, absolutely. Easiest is probably to just make Ics20Withdrawal an enum over 2 variants (one where bridge_address is set and contains the parsed payload, and one where it's not set and just contains the passed through String memo).

You should also lean on a serde(try_from = "") to a) first read into some UnverifiedIcs20WithdrawalFromRollup, and then b) via a TryFrom<UnverifiedIcs20WithdrawalFromRollup> Ics20WithdrawalFromRollup come up with the final type.

Copy link
Contributor Author

@ethanoroshiba ethanoroshiba Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split into enum in 84b00ef. I wanted to ask further before continuing with your second suggestion: in order to do this we would need to implement a new UnverifiedIcs20WithdrawalFromRollup type and then the TryFrom trait as well for it. While this may be fine, it would be a little bit backwards from the way we currently do things.

Ics20WithdrawalFromRollup is the raw protobuf type, which usually represents the "unverified" version as opposed to the verified one. We could rename it to reflect that it is unverified, and then create a new domain type for the verified version, but I'm not sure if the name change would be proto breaking. In addition to this, it seems like it would be many more lines for questionable benefit to just performing these verifications in Ics20Withdrawal::try_from_raw. Do you still think it would be worth it to do this?

validate_rollup_return_address(&parsed_withdrawal.rollup_return_address)
.map_err(Ics20WithdrawalError::rollup_withdrawal)?;
validate_withdrawal_event_id_and_block_number(
&parsed_withdrawal.rollup_withdrawal_event_id,
parsed_withdrawal.rollup_block_number,
)
.map_err(Ics20WithdrawalError::rollup_withdrawal)?;
}

Ok(Self {
amount: amount.into(),
amount,
denom: denom.parse().map_err(Ics20WithdrawalError::invalid_denom)?,
destination_chain_address,
return_address,
Expand Down Expand Up @@ -1089,14 +1111,21 @@ impl Protobuf for Ics20Withdrawal {
bridge_address,
use_compat_address,
} = proto;
let amount = amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?;
let amount = u128::from(amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?);
let return_address = Address::try_from_raw(
return_address
.as_ref()
.ok_or(Ics20WithdrawalError::field_not_set("return_address"))?,
)
.map_err(Ics20WithdrawalError::return_address)?;

if *timeout_time == 0 {
return Err(Ics20WithdrawalError::invalid_timeout_time());
}
if amount == 0 {
return Err(Ics20WithdrawalError::invalid_amount());
}

let timeout_height = timeout_height
.clone()
.ok_or(Ics20WithdrawalError::field_not_set("timeout_height"))?
Expand All @@ -1107,8 +1136,20 @@ impl Protobuf for Ics20Withdrawal {
.transpose()
.map_err(Ics20WithdrawalError::invalid_bridge_address)?;

if bridge_address.is_some() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty unhappy that we now have a JSON formatted payload in here. I wonder if we can make a breaking change to these transactions and add another variant to the protobuf that makes this a protobuf.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering this would be network breaking, I'm not sure what we can really do about it now, barring a hard fork :/

We could make a new action which uses a protobuf-formatted memo instead and follow the same upgrade path as the new ValidatorUpdateV2 action, but I'm not sure how worth it that would be.

let parsed_withdrawal: Ics20WithdrawalFromRollup =
serde_json::from_str(memo).map_err(Ics20WithdrawalError::parse_memo)?;
validate_rollup_return_address(&parsed_withdrawal.rollup_return_address)
.map_err(Ics20WithdrawalError::rollup_withdrawal)?;
validate_withdrawal_event_id_and_block_number(
&parsed_withdrawal.rollup_withdrawal_event_id,
parsed_withdrawal.rollup_block_number,
)
.map_err(Ics20WithdrawalError::rollup_withdrawal)?;
}

Ok(Self {
amount: amount.into(),
amount,
denom: denom.parse().map_err(Ics20WithdrawalError::invalid_denom)?,
destination_chain_address: destination_chain_address.clone(),
return_address,
Expand Down Expand Up @@ -1189,11 +1230,32 @@ impl Ics20WithdrawalError {
Self(Ics20WithdrawalErrorKind::InvalidBridgeAddress(err))
}

#[must_use]
fn invalid_denom(source: asset::ParseDenomError) -> Self {
Self(Ics20WithdrawalErrorKind::InvalidDenom {
source,
})
}

#[must_use]
fn invalid_timeout_time() -> Self {
Self(Ics20WithdrawalErrorKind::InvalidTimeoutTime)
}

#[must_use]
fn invalid_amount() -> Self {
Self(Ics20WithdrawalErrorKind::InvalidAmount)
}

#[must_use]
fn parse_memo(err: serde_json::Error) -> Self {
Self(Ics20WithdrawalErrorKind::ParseMemo(err))
}

#[must_use]
fn rollup_withdrawal(err: RollupWithdrawalError) -> Self {
Self(Ics20WithdrawalErrorKind::RollupWithdrawal(err))
}
}

#[derive(Debug, thiserror::Error)]
Expand All @@ -1210,6 +1272,14 @@ enum Ics20WithdrawalErrorKind {
InvalidBridgeAddress(#[source] AddressError),
#[error("`denom` field was invalid")]
InvalidDenom { source: asset::ParseDenomError },
#[error("`timeout_time` must be non-zero")]
InvalidTimeoutTime,
ethanoroshiba marked this conversation as resolved.
Show resolved Hide resolved
#[error("`amount` must be greater than zero")]
InvalidAmount,
ethanoroshiba marked this conversation as resolved.
Show resolved Hide resolved
#[error("failed to parse memo for ICS bound bridge withdrawal")]
ParseMemo(#[source] serde_json::Error),
#[error("rollup withdrawal information was invalid")]
RollupWithdrawal(#[source] RollupWithdrawalError),
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -1705,18 +1775,30 @@ impl Protobuf for BridgeUnlock {
let to = to
.ok_or_else(|| BridgeUnlockError::field_not_set("to"))
.and_then(|to| Address::try_from_raw(&to).map_err(BridgeUnlockError::address))?;
let amount = amount.ok_or_else(|| BridgeUnlockError::field_not_set("amount"))?;
let amount = u128::from(amount.ok_or_else(|| BridgeUnlockError::field_not_set("amount"))?);
if amount == 0 {
return Err(BridgeUnlockError::invalid_amount());
}
if memo.len() > 64 {
return Err(BridgeUnlockError::invalid_memo());
}
let fee_asset = fee_asset.parse().map_err(BridgeUnlockError::fee_asset)?;

validate_withdrawal_event_id_and_block_number(
&rollup_withdrawal_event_id,
rollup_block_number,
)
.map_err(BridgeUnlockError::rollup_withdrawal)?;

let bridge_address = bridge_address
.ok_or_else(|| BridgeUnlockError::field_not_set("bridge_address"))
.and_then(|to| Address::try_from_raw(&to).map_err(BridgeUnlockError::bridge_address))?;
Ok(Self {
to,
amount: amount.into(),
amount,
fee_asset,
memo,
bridge_address,
memo,
rollup_block_number,
rollup_withdrawal_event_id,
})
Expand Down Expand Up @@ -1766,6 +1848,21 @@ impl BridgeUnlockError {
source,
})
}

#[must_use]
fn invalid_amount() -> Self {
Self(BridgeUnlockErrorKind::InvalidAmount)
}

#[must_use]
fn invalid_memo() -> Self {
Self(BridgeUnlockErrorKind::InvalidMemo)
}

#[must_use]
fn rollup_withdrawal(err: RollupWithdrawalError) -> Self {
Self(BridgeUnlockErrorKind::RollupWithdrawal(err))
}
}

#[derive(Debug, thiserror::Error)]
Expand All @@ -1778,6 +1875,12 @@ enum BridgeUnlockErrorKind {
FeeAsset { source: asset::ParseDenomError },
#[error("the `bridge_address` field was invalid")]
BridgeAddress { source: AddressError },
#[error("`amount` must be greater than zero")]
InvalidAmount,
#[error("`memo` muse be no longer than 64 bytes")]
InvalidMemo,
#[error("rollup withdrawal information was invalid")]
RollupWithdrawal(#[source] RollupWithdrawalError),
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -2076,3 +2179,41 @@ impl Protobuf for FeeChange {
})
}
}

#[derive(Debug, thiserror::Error)]
enum RollupWithdrawalError {
#[error("rollup return address must be non-empty")]
EmptyRollupReturnAddress,
#[error("rollup return address must be no more than 256 bytes")]
InvalidRollupReturnAddress,
#[error("rollup withdrawal event id must be non-empty")]
EmptyRollupEventId,
#[error("rollup withdrawal event id must be no more than 256 bytes")]
InvalidRollupEventId,
#[error("rollup block number must be non-zero")]
InvalidRollupBlockNumber,
}

fn validate_rollup_return_address(address: &str) -> Result<(), RollupWithdrawalError> {
if address.is_empty() {
return Err(RollupWithdrawalError::EmptyRollupReturnAddress);
} else if address.len() > 256 {
return Err(RollupWithdrawalError::InvalidRollupReturnAddress);
}
Ok(())
}

fn validate_withdrawal_event_id_and_block_number(
id: &str,
block: u64,
) -> Result<(), RollupWithdrawalError> {
if id.is_empty() {
return Err(RollupWithdrawalError::EmptyRollupEventId);
} else if id.len() > 256 {
return Err(RollupWithdrawalError::InvalidRollupEventId);
}
if block == 0 {
return Err(RollupWithdrawalError::InvalidRollupBlockNumber);
}
Ok(())
}
1 change: 1 addition & 0 deletions crates/astria-sequencer-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ serde_json = { workspace = true }
tokio = { workspace = true }
tokio-test = { workspace = true }
wiremock = { workspace = true }
astria-core = { path = "../astria-core", features = ["serde"] }
15 changes: 0 additions & 15 deletions crates/astria-sequencer/src/bridge/bridge_unlock_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,7 @@ use crate::{

#[async_trait::async_trait]
impl ActionHandler for BridgeUnlock {
// TODO(https://github.com/astriaorg/astria/issues/1430): move checks to the `BridgeUnlock` parsing.
async fn check_stateless(&self) -> Result<()> {
ensure!(self.amount > 0, "amount must be greater than zero",);
ensure!(self.memo.len() <= 64, "memo must not be more than 64 bytes");
ensure!(
!self.rollup_withdrawal_event_id.is_empty(),
"rollup withdrawal event id must be non-empty",
);
ensure!(
self.rollup_withdrawal_event_id.len() <= 256,
"rollup withdrawal event id must not be more than 256 bytes",
);
ensure!(
self.rollup_block_number > 0,
"rollup block number must be greater than zero",
);
Ok(())
}

Expand Down
29 changes: 0 additions & 29 deletions crates/astria-sequencer/src/ibc/ics20_withdrawal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,36 +143,7 @@ async fn establish_withdrawal_target<'a, S: StateRead>(

#[async_trait::async_trait]
impl ActionHandler for action::Ics20Withdrawal {
// TODO(https://github.com/astriaorg/astria/issues/1430): move checks to the `Ics20Withdrawal` parsing.
async fn check_stateless(&self) -> Result<()> {
ensure!(self.timeout_time() != 0, "timeout time must be non-zero",);
ensure!(self.amount() > 0, "amount must be greater than zero",);
if self.bridge_address.is_some() {
let parsed_bridge_memo: Ics20WithdrawalFromRollup = serde_json::from_str(&self.memo)
.context("failed to parse memo for ICS bound bridge withdrawal")?;

ensure!(
!parsed_bridge_memo.rollup_return_address.is_empty(),
"rollup return address must be non-empty",
);
ensure!(
parsed_bridge_memo.rollup_return_address.len() <= 256,
"rollup return address must be no more than 256 bytes",
);
ensure!(
!parsed_bridge_memo.rollup_withdrawal_event_id.is_empty(),
"rollup withdrawal event id must be non-empty",
);
ensure!(
parsed_bridge_memo.rollup_withdrawal_event_id.len() <= 256,
"rollup withdrawal event id must be no more than 256 bytes",
);
ensure!(
parsed_bridge_memo.rollup_block_number != 0,
"rollup block number must be non-zero",
);
}

// NOTE (from penumbra): we could validate the destination chain address as bech32 to
// prevent mistyped addresses, but this would preclude sending to chains that don't
// use bech32 addresses.
Expand Down
Loading