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

Outbound queue v2 #4

Open
wants to merge 24 commits into
base: snowbridge-v2
Choose a base branch
from
Open

Outbound queue v2 #4

wants to merge 24 commits into from

Conversation

yrong
Copy link
Owner

@yrong yrong commented Oct 18, 2024

Resolves: https://linear.app/snowfork/issue/SNO-1205

@yrong
Copy link
Owner Author

yrong commented Oct 21, 2024

Workflow

  1. on AH we add a custom exporter returns a lower fee(without the cost on Ethereum) charged from the user when exporting to Ethereum, essentially predict the route by content of XCM, more details in Predicate route table by Xcm paritytech/polkadot-sdk#6074, since there is no XCMV5 branch ready for tests yet, as a workaround we can execute raw xcm with some custom instruction on AH to differ V2 with V1.

  2. on BH we route the new XCM(with V2 specific instructions) to the new pallet OutboundQueueV2, in which we convert the XCM to Message structure as following:

pub struct Message {
    /// Origin
    pub origin: H256,
    /// ID
    pub id: H256,
    /// Fee
    pub fee: u128,
    /// Commands
    pub commands: BoundedVec<Command, ConstU32<5>>,
}

which means we can execute multiple commands on Ethereum in one message.

  1. Then we abi encode the message and store it into a merkle tree as before, meanwhile add the pending fee into LockedFee storage and increase the nonce. more detais here

  2. add a new extrinsic submit_delivery_proof in which we verify the event log is truely happened on Ethereum side, and add the fee to RewardLeger.

bridges/snowbridge/primitives/core/src/merkle.rs Outdated Show resolved Hide resolved
bridges/snowbridge/primitives/core/src/outbound_v2.rs Outdated Show resolved Hide resolved
@@ -172,3 +174,13 @@ impl Default for AssetMetadata {

/// Maximum length of a string field in ERC20 token metada
const METADATA_FIELD_MAX_LEN: u32 = 32;

// Origin for high-priority governance commands
pub fn primary_governance_origin() -> H256 {
Copy link

Choose a reason for hiding this comment

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

This is now a V1-only type, so should go in core/src/v1/mod.rs.

Copy link
Owner Author

@yrong yrong Oct 23, 2024

Choose a reason for hiding this comment

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

850fe96
Actually it's V2 specific.

The usage in

if ticket.origin != primary_governance_origin() {
ensure!(!Self::operating_mode().is_halted(), SendError::Halted);
}
is to allow upgrading even when bridge is halted.

}

// Origin for lower-priority governance commands
pub fn second_governance_origin() -> H256 {
Copy link

Choose a reason for hiding this comment

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

ditto

Copy link
Owner Author

Choose a reason for hiding this comment

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

bridges/snowbridge/pallets/outbound-queue-v2/src/types.rs Outdated Show resolved Hide resolved
bridges/snowbridge/pallets/outbound-queue-v2/src/types.rs Outdated Show resolved Hide resolved
bridges/snowbridge/primitives/core/src/outbound_v2.rs Outdated Show resolved Hide resolved
bridges/snowbridge/primitives/core/src/outbound_v2.rs Outdated Show resolved Hide resolved
bridges/snowbridge/primitives/core/src/outbound_v2.rs Outdated Show resolved Hide resolved
@yrong yrong changed the base branch from v2 to master October 24, 2024 16:55
@yrong yrong changed the base branch from master to xcm-v5 October 29, 2024 02:03
@yrong
Copy link
Owner Author

yrong commented Oct 29, 2024

Check details in emulated tests how to construct transfer message for both ENA&PNA with XCMV5 instructions.

cargo test -p bridge-hub-westend-integration-tests --lib tests::snowbridge_v2  -- --nocapture

@yrong yrong marked this pull request as ready for review October 29, 2024 12:44
@yrong yrong force-pushed the outbound-queue-v2 branch 3 times, most recently from fffd895 to df137c0 Compare November 5, 2024 17:03
@yrong yrong changed the base branch from xcm-v5 to master November 11, 2024 02:33
/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
#[pallet::call_index(11)]
#[pallet::weight(T::WeightInfo::register_token())]
pub fn register_token_v2(
Copy link

Choose a reason for hiding this comment

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

Alternatively can we reimplement the original register_token extrinsic to optionally use V2 protocol, depending on a config field in storage.

When the ethereum contracts have been upgraded to V2, we can configure register_token to use V2.

The benefit is that user-level apps won't need to change any code to register tokens using V2.

Copy link

@vgeddes vgeddes Nov 14, 2024

Choose a reason for hiding this comment

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

Actually, ignore my comment, I realize we need separate register extrinsics for V1 and V2 as they have different fee-taking behavior.

I see now there is also the problem of how to provide WETH as the fees for these governance calls. On BH users can only provide DOT

Copy link

Choose a reason for hiding this comment

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

Maybe we need to make the rewards pallet support both WETH and DOT.

Copy link
Owner Author

@yrong yrong Nov 14, 2024

Choose a reason for hiding this comment

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

how to provide WETH as the fees for these governance calls.

Actually there is no fee charged for governance calls. So it's possible pendingOrder bound with zero fee and no need to reward in this case.

// No fee for governance order
if !order.fee.is_zero() {
T::RewardLedger::deposit(envelope.reward_address, order.fee.into())?;
}

Copy link

Choose a reason for hiding this comment

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

The problem is that then no off-chain relayer will be incentivized to deliver the message. Letting users optionally pay using DOT is a solution we should consider.

Another issue is that when we make register_token_v2 permissionless, there is still no way for many parachains to call the extrinsic since they won't have HRMP channels registered.

So I think we have to force users to route their governance calls through AH and provide DOT. In which case, BH will see message like:

AliasOrigin /Parachain/Hydra
ReceiveTeleportedAsset (DOT, 10)
DepositAsset (DOT, /Parachain/Hydra)
Transact(EthereumSystem.register_token_v2)

Copy link
Owner Author

@yrong yrong Nov 15, 2024

Choose a reason for hiding this comment

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

So I think we have to force users to route their governance calls through AH and provide DOT.

Agree. All calls should be through AH.

But I'd assume in this case the fee could also be swapped to WETH beforehand in AH so that in BH we only use WETH as fee for simplicity. Some todo logic here:

// Todo: Validate fee asset is WETH
let fee_amount = match fee_asset {
Asset { id: _, fun: Fungible(amount) } => Some(*amount),
_ => None,
}
.ok_or(AssetResolutionFailed)?;

Choose a reason for hiding this comment

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

I would also prefer a solution where we only have 1 asset type in the rewards pallet.

Copy link

@vgeddes vgeddes Nov 20, 2024

Choose a reason for hiding this comment

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

Yeah, that would be best.

If we move governance extrinsics such as register-token, create-agent to AH, then that would neatly solve the problem

As we could burn the WETH on AH and then send message to BH with the weth reward for the outbound message

Example flow:

  1. User calls EthereumSystem.register_token extrinsic on AH, supplying WETH for reward
  2. WETH is burnt, and AH sends Transact(EthereumSystem.register_token) XCM to BH
  3. On BH, the low-level EthereumSystem.register_token adds burnt WETH to rewards pallet and queues outbound command

use alloy_sol_types::SolValue;

sol! {
struct InboundMessageWrapper {
Copy link

Choose a reason for hiding this comment

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

Is it possible to use modules as namespaces instead of appending Wrapper to the struct names? Thinking of this pattern:

mod abi {
	sol! {
		struct InboundMessageWrapper { ... }
	}
}

Then we can reference the type using abi::InboundMessage

Copy link
Owner Author

Choose a reason for hiding this comment

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

Copy link

Choose a reason for hiding this comment

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

The point of adding the abi namespace is that we rename InboundMessageWrapper and CommandWrapper to InboundMessage and Command respectively

Copy link
Owner Author

@yrong yrong Nov 15, 2024

Choose a reason for hiding this comment

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

Yeah, it seems a bit cubersome with InboundMessage and InboundMessageWrapper.

The reason is that it's not allowed to add macro #[derive(Encode, Decode, RuntimeDebug, PartialEq,TypeInfo)] into InboundMessageWrapper which is required for storing the data on chain, or error as follows:

--> /Users/yangrong/Projects/polkadot-sdk/bridges/snowbridge/primitives/core/src/outbound/v2.rs:31:12
     |
  31 |         #[derive(Encode, Decode, RuntimeDebug, PartialEq,TypeInfo)]
     |                  ^^^^^^ the trait `WrapperTypeEncode` is not implemented for `alloy_primitives::FixedBytes<32>`, which is required by `alloy_primitives::FixedBytes<32>: Encode`
     |
     = help: the following other types implement trait `WrapperTypeEncode`:
               &T
               &mut T
               Arc<T>
               Box<T>
               Cow<'a, T>
               Rc<T>
               alloy_primitives::bytes::Bytes
               parity_scale_codec::Ref<'a, T, U>
             and 3 others
     = note: required for `alloy_primitives::FixedBytes<32>` to implement `Encode`
     = note: this error originates in the derive macro `Encode` (in Nightly builds, run with -Z macro-backtrace for more info)
  ...

So InboundMessageWrapper is the intermediate data structure only for abi encode, will be converted to InboundMessage here when storing on chain.

bridges/snowbridge/primitives/core/src/outbound/v2.rs Outdated Show resolved Hide resolved
bridges/snowbridge/primitives/core/src/outbound/v2.rs Outdated Show resolved Hide resolved
bridges/snowbridge/primitives/core/src/outbound/v2.rs Outdated Show resolved Hide resolved
})?;

// Inspect AliasOrigin as V2 message
let mut instructions = message.clone().0;
Copy link

Choose a reason for hiding this comment

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

Seems like this logic should be inside XcmConverter

Copy link
Owner Author

@yrong yrong Nov 14, 2024

Choose a reason for hiding this comment

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

It's for compatible with V1, for xcm which does not contain AliasOrigin will fallback to EthereumBlobExporterV1.

type MessageExporter = (
XcmOverBridgeHubRococo,
crate::bridge_to_ethereum_config::SnowbridgeExporterV2,
crate::bridge_to_ethereum_config::SnowbridgeExporter,
);

};
}

pub struct XcmConverter<'a, ConvertAssetId, Call> {
Copy link

Choose a reason for hiding this comment

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

There is going to be a lot more conversion code for v2, so I suggest moving XcmConverter to a new file like outbound/v2/converter.rs.

Copy link
Owner Author

Choose a reason for hiding this comment

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

}

pub fn convert(&mut self) -> Result<Message, XcmConverterError> {
let result = match self.jump_to() {
Copy link

Choose a reason for hiding this comment

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

I don't think its safe to just "jump" over instructions, we should validate each instruction in turn to see whether it matches expectations.

Copy link
Owner Author

Choose a reason for hiding this comment

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

}
}

pub fn convert(&mut self) -> Result<Message, XcmConverterError> {
Copy link

Choose a reason for hiding this comment

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

For V2, an InboundMessage can contain many commands. This conversion code looks like it only matches enough instructions to produce a single command?

Also we need support for XCM::Transact, ie convert it to gateway CallContract command.

We should support any combination of UnlockNativeToken, MintForeignToken, CallContract commands.

Copy link
Owner Author

@yrong yrong Nov 14, 2024

Choose a reason for hiding this comment

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

Yeah, for simplicity we start from a single command, will improve it later.

Copy link
Owner Author

@yrong yrong Nov 19, 2024

Choose a reason for hiding this comment

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

af928ba

Btw: Transact support is not included yet. I'd prefer to limit the scope of this PR and implement transact in another one.

@yrong yrong changed the base branch from master to snowbridge-v2 November 15, 2024 06:45
bridges/snowbridge/pallets/outbound-queue-v2/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 277 to 279
ensure!(<PendingOrders<T>>::contains_key(nonce), Error::<T>::PendingNonceNotExist);

let order = <PendingOrders<T>>::get(nonce).ok_or(Error::<T>::PendingNonceNotExist)?;

Choose a reason for hiding this comment

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

Suggested change
ensure!(<PendingOrders<T>>::contains_key(nonce), Error::<T>::PendingNonceNotExist);
let order = <PendingOrders<T>>::get(nonce).ok_or(Error::<T>::PendingNonceNotExist)?;
let order = <PendingOrders<T>>::get(nonce).ok_or(Error::<T>::PendingNonceNotExist)?;

Is the first check necessary? Would it not return an error if the nonce does not exist?

Copy link
Owner Author

Choose a reason for hiding this comment

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


/// A message which can be accepted by implementations of `/[`SendMessage`\]`
#[derive(Encode, Decode, TypeInfo, PartialEq, Clone, RuntimeDebug)]
pub struct Message {

Choose a reason for hiding this comment

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

For both the inbound and outbound pallets, I was wondering if we want to do message versioning at all like we did for v1? Or did we see that if we want to make large changes we'll create a new pallet like we are doing for v2? @vgeddes

Comment on lines +304 to +311
pub fn primary_governance_origin() -> H256 {
hex!("0000000000000000000000000000000000000000000000000000000000000001").into()
}

// Origin for lower-priority governance commands
pub fn second_governance_origin() -> H256 {
hex!("0000000000000000000000000000000000000000000000000000000000000002").into()
}

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Owner Author

Choose a reason for hiding this comment

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

}
},
);
ensure!(result.is_err(), SendError::NotApplicable);

Choose a reason for hiding this comment

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

I don't quite understand this. Why are we asserting that we get an error here?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yeah, seems a bit weird.

Because we're using match_next_inst_while macro to match against AliasOrigin with error return, if not matched, then fallback to V1.

#4 (comment)

Choose a reason for hiding this comment

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

But doesn't this expect the result to always be an error? For example, if result is Ok(), this ensure will still error?

@@ -55,7 +57,7 @@ impl From<AggregateMessageOrigin> for Location {
Sibling(id) => Location::new(1, Junction::Parachain(id.into())),
// NOTE: We don't need this conversion for Snowbridge. However, we have to
// implement it anyway as xcm_builder::ProcessXcmMessage requires it.
Snowbridge(_) => Location::default(),
_ => Location::default(),

Choose a reason for hiding this comment

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

Why not add SnowbridgeV2 here?

Copy link
Owner Author

Choose a reason for hiding this comment

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

We don't need this conversion for Snowbridge. However, we have to implement it anyway as xcm_builder::ProcessXcmMessage requires it.

Just as the comment above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants