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

feat(sequencer): implement ICS20 withdrawals #609

Merged
merged 46 commits into from
Dec 13, 2023
Merged

Conversation

noot
Copy link
Collaborator

@noot noot commented Nov 27, 2023

Summary

Counterpart to #579 which supports incoming ICS20 transfers, this PR implements outgoing ICS20 transfers.

Background

required for IBC token transfer support.

See https://github.com/cosmos/ibc/blob/fe150abb629de5c1a598e8c7896a7568f2083681/spec/app/ics-020-fungible-token-transfer/README.md sendFungibleTokens for spec.

Changes

  • add Ics20Withdrawal proto and native type which is an action (maybe rename to Ics20WithdrawalAction?)
  • implement check logic (apart from IBC packet check, check the sender has enough balance)
  • implement execution logic (escrow tokens if native, or burn if not native)

Testing

not really - same issue as #579

Related Issues

closes #597

Base automatically changed from noot/ibc to main November 28, 2023 17:40
}
}

impl From<raw::IbcHeight> for IbcHeight {
Copy link
Member

Choose a reason for hiding this comment

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

We have a Protobuf trait for conversions on foreign types. Can you impl it instead of From? Use type Error = ::std::convert::Infallible

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I implemented From because the conversion never fails, so I left this for now, but I can change to a from_raw or something instead?

Copy link
Member

@SuperFluffy SuperFluffy Dec 13, 2023

Choose a reason for hiding this comment

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

Your logic for implementing From is sound. The reason I don't like From here is consistency. The rest of the crate is has from_raw and try_from_raw so readers will expect to find it.

I intend to expand the Protobuf trait to all our proto types to document all (raw, native) pairs.

}
}

impl Error for Ics20WithdrawalError {}
Copy link
Member

Choose a reason for hiding this comment

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

This impl would swallow all sources. This should implement the fn source(&self) -> Option<&dyn std::error::Error> that returns the sources for those variants that are actually wrapping errors.

You can avoid some boiler plate by using thiserror which I will probably use to migrate all errors over.

I would also like to make use of an extra indirection like below to hide the actual errors behind an opaque facade (see how the top level is pub but the variants are priv):

pub struct Ics20WithdrawalError {
    kind: Ics20WithdrawalErrorKind,
}

enum Ics20WithdrawalErrorKind {
    TheOtherVariants,
}

Copy link
Member

Choose a reason for hiding this comment

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

Note on this: in #646 I have reimplemented all errors in terms of thiserror using something like this:

#[derive(thiserror::Error)]
#[error(transparent)]
struct PublicError(PrivateError);

#[derive(thiserror::Error)]
enum PrivateError{}

@@ -563,6 +580,7 @@ enum ActionErrorKind {
SudoAddressChange(SudoAddressChangeActionError),
Mint(MintActionError),
Ibc(Box<dyn Error + Send + Sync>),
Ics20Withdrawal(Box<dyn Error + Send + Sync>),
Copy link
Member

@SuperFluffy SuperFluffy Dec 7, 2023

Choose a reason for hiding this comment

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

I think this is not necessary, you can toss the actual error type in here. We used the boxed error trait object because I don't want anyhow in here. :)

pub source_channel: ChannelId,
}

impl From<Ics20Withdrawal> for FungibleTokenPacketData {
Copy link
Member

Choose a reason for hiding this comment

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

Can you impl this as an inherent method on Ics20Withdrawal? fn to_fungible_token_packet_data(&self)

penumbra-ibc = { workspace = true }
penumbra-proto = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }
serde = { workspace = true, 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.

Note below: please remove

.await
.context("failed getting `from` account balance for transfer")?;
ensure!(
from_transfer_balance > self.amount,
Copy link
Member

Choose a reason for hiding this comment

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

> or >=?

);

state
.put_account_balance(from, self.denom.id(), from_transfer_balance - self.amount)
Copy link
Member

Choose a reason for hiding this comment

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

Please use checked math here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the "insufficient funds for transfer" check above covers this

.put_ibc_channel_balance(
&self.source_channel,
self.denom.id(),
channel_balance + self.amount,
Copy link
Member

Choose a reason for hiding this comment

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

checked math

.context("failed to get channel balance")?;

state
.put_ibc_channel_balance(
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to worry about making the entirety of these steps a transaction similar to how SQL transactions are atomic (if one fails, all fail or are rolled back)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

mentioned this before, but check deliver_tx, it starts an atomic tx for the db that's only committed if the tx succeeds

Copy link
Member

Choose a reason for hiding this comment

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

OK, cool. I have to go over how new entries in the DB are created to understand the flow again.

};

#[derive(Debug, Clone)]
pub struct Ics20Withdrawal {
Copy link
Member

@SuperFluffy SuperFluffy Dec 7, 2023

Choose a reason for hiding this comment

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

I increasingly lean toward abstaining from making all of these private pub by default.

Can you lock it down?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you mean make the fields private? yeah i changed it, made getters instead

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that's what I meant! Sorry, I meant to write public instead of private.

Copy link
Member

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

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

Looks good know.

I have added a few comments that could be addressed before merge.

Can we rebase this on top of the refactors in #644 #646 in before we merge this?

}
}

impl From<IbcHeight> for raw::IbcHeight {
Copy link
Member

Choose a reason for hiding this comment

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

Remove so there is only a single way to do this.

.put_account_balance(
from,
self.denom().id(),
from_transfer_balance - self.amount(),
Copy link
Member

Choose a reason for hiding this comment

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

You can do checked math && the check in one go:

let Some(new_balance) = from_transfer_balance.checked_sub(self.amount()) else {
    bail!("insufficient funds for transfer");
}

This addresses my ask for checked math while also defending against accidental refactor.

.context("failed to get channel balance")?;

state
.put_ibc_channel_balance(
Copy link
Member

Choose a reason for hiding this comment

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

OK, cool. I have to go over how new entries in the DB are created to understand the flow again.

amount: Some(self.amount.into()),
denom: self.denom.to_string(),
destination_chain_address: self.destination_chain_address,
return_address: self.return_address.0.to_vec(),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return_address: self.return_address.0.to_vec(),
return_address: self.return_address.to_vec(),

Address has a to_vec method

}
}

impl crate::native::Protobuf for IbcHeight {
Copy link
Member

Choose a reason for hiding this comment

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

Since all the fields of raw::IbcHeight are Copy you don't have to impl all the trait methods. The trait definition provides default implementations for try_from_raw (in terms of try_from_raw_ref) and into_raw (in terms of to_raw). So if you impl to_raw and try_from_raw_ref you get the other two for free.

try_from_raw and into_raw can be implemented by non-copy types to avoid allocations.


#[allow(clippy::module_name_repetitions)]
#[derive(Debug)]
pub enum Ics20WithdrawalError {
Copy link
Member

Choose a reason for hiding this comment

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

See how this is done in PR #646: derive thiserror::Error for this and split into a public wrapper type that has #[error(transparent)] and a private error type that has all the enums.

@@ -0,0 +1,140 @@
use anyhow::{
ensure,
Context,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Context,
Context as _,

@@ -75,3 +76,18 @@ message MintAction {
bytes to = 1;
astria.primitive.v1.Uint128 amount = 2;
}

message Ics20Withdrawal {
Copy link
Member

@SuperFluffy SuperFluffy Dec 13, 2023

Choose a reason for hiding this comment

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

Can you add comments to this type, what IBC spec it matches, maybe with links. and how this is used?

Please also cover the fields and their constraints, i.e. the expected format of the strings, the number of bytes for things like the return address, units of the time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep, added more description, and added a longer comment + spec link to the rust type

string destination_chain_address = 3;
bytes return_address = 4;
IbcHeight timeout_height = 5;
uint64 timeout_time = 6;
Copy link
Member

@SuperFluffy SuperFluffy Dec 13, 2023

Choose a reason for hiding this comment

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

units? greater 0? Also is this used as a time ("timeout at time T") or as a duration ("timeout in duration D seconds")? This here is probably used a duration, so I would name it thus.

I have seen in other projects that that lead to confusion, so IMO it's worth bikeshedding this naming.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

copied the comments on the rust type here - this is a unix timestamp

astria.primitive.v1.Uint128 amount = 1;
string denom = 2;
string destination_chain_address = 3;
bytes return_address = 4;
Copy link
Member

Choose a reason for hiding this comment

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

Our address type? 20 bytes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah it is, in the rust Ics20Withdrawal type this is an Address

@noot noot merged commit c353cf2 into main Dec 13, 2023
30 checks passed
@noot noot deleted the noot/ics20-withdrawal branch December 13, 2023 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proto pertaining to the Astria Protobuf spec sequencer pertaining to the astria-sequencer crate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

sequencer + IBC: implement ICS20Withdrawal action type
2 participants