sp_partner_chains_bridge/lib.rs
1//! # Partner Chain Token Bridge
2//!
3//! This crate defines common primitive types, the inherent data provider and data source API
4//! for the token bridge feature of Partner Chains Toolkit.
5//!
6//! ## Overview
7//!
8//! The bridge feature of the Partner Chains Toolkit allows sending the native token from
9//! Cardano to its Partner Chain in a trustless manner, making use of direct observation by
10//! the Partner Chain nodes of the Cardano ledger.
11//!
12//! Each token *transfer* is made by creating a UTXO on a specific *illiquid circulating
13//! supply* (ICS) address in which the tokens are locked so they can be released on the
14//! Partner Chain. The feature distinguishes two types of a transfer:
15//!
16//! 1. *User transfers*, which are sent by ordinary users and addressed to a recipient that
17//! is indicated in the datum attached to the transfer's UTXO.
18//! 2. *Reserve transfers*, which are sent as part of the Partner Chain's operation, and
19//! allow the chain to gradually move its token reserve (ie. the reserve of tokens used
20//! to pay block producer rewards) from Cardano to its own ledger.
21//!
22//! Newly made transfers are picked up by the observability layer once their blocks become
23//! stable, and are made available to the runtime as inherent data. The bridge pallet in turn
24//! makes this data available to the Partner Chains ledger by passing it to the transfer handler,
25//! which is left for each Partner Chain's builders to implement according to their particular
26//! requirements.
27//!
28//! ## Usage
29//!
30//! ### Prerequisites
31//!
32//! #### Pallet and runtime API
33//!
34//! Consult the documentation of `pallet_partner_chains_bridge` for instructions on how to add
35//! the bridge pallet to the runtime and implement runtime APIs. The inherent data provider
36//! defined in this crate requires [TokenBridgeIDPRuntimeApi] to be implemented in the runtime.
37//!
38//! #### Data source
39//!
40//! A data source implementing [TokenBridgeDataSource] is required to be present in the node.
41//! The `partner_chains_db_sync_data_sources` crate provides a production ready Db-Sync-based
42//! implementation, and a mocked implementation is provided by `partner-chains-mock-data-sources`.
43//! See the documentation of those crates for instructions on how to add these data sources to
44//! your node.
45//!
46//! #### Recipient type
47//!
48//! All user transfers sent through the bridge are addressed to some recipient identified by a
49//! chain-specific type, usually an address or public key. Because the toolkit makes no assumptions
50//! about the ledger structure, this type must be provided in various places as a type parameter.
51//! This type can be arbitrary, as long as it conforms to the trait bounds required by the
52//! inherent data provider, data source and the pallet. For simple Substrate chains, the account ID
53//! type used by their ledgers is a good choice.
54//!
55//! ### Adding the inherent data provider
56//!
57//! Include [TokenBridgeInherentDataProvider] in the list of `InherentDataProviders` of your
58//! implementation of [CreateInherentDataProviders]:
59//!
60//! ```rust
61//! use sp_partner_chains_bridge::TokenBridgeInherentDataProvider;
62//! struct AccountId;
63//! type InherentDataProviders = (
64//! // sp_timestamp::InherentDataProvider,
65//! // ...
66//! TokenBridgeInherentDataProvider<AccountId>,
67//! );
68//! ```
69//!
70//! The IDP is created the same way when proposing and validating a block:
71//! ```rust
72//! # use sp_partner_chains_bridge::*;
73//! # use sidechain_domain::*;
74//! # #[derive(sp_core::Encode)]
75//! # struct AccountId;
76//! type InherentDataProviders = ( /* other IDPs */ TokenBridgeInherentDataProvider<AccountId>);
77//!
78//! async fn create_inherent_data_providers<T, Block>(
79//! client: &T,
80//! bridge_data_source: &(impl TokenBridgeDataSource<AccountId> + Send + Sync),
81//! parent_hash: Block::Hash,
82//! ) -> Result<InherentDataProviders, Box<dyn std::error::Error + Send + Sync>>
83//! where
84//! Block: sp_runtime::traits::Block,
85//! T: sp_api::ProvideRuntimeApi<Block> + Send + Sync,
86//! T::Api: TokenBridgeIDPRuntimeApi<Block> + Send + Sync
87//! {
88//! /*
89//! Create other IDPs
90//! */
91//! let mc_hash: McBlockHash = todo!("provided by the MC Hash IDP from `sidechain_mc_hash` crate");
92//!
93//! let bridge = TokenBridgeInherentDataProvider::new(
94//! client,
95//! parent_hash,
96//! mc_hash,
97//! bridge_data_source,
98//! )
99//! .await?;
100//!
101//! Ok((/* other IDPs */ bridge))
102//! }
103//! ```
104//!
105//! ### Adding to a running chain
106//!
107//! [TokenBridgeInherentDataProvider] is version-aware and will stay inactive until the pallet
108//! is added and fully configured along with [TokenBridgeIDPRuntimeApi]. Thus, the correct order
109//! of steps when adding the feature to an already running chain will be:
110//!
111//! 0. Initialize the bridge smart contracts on Cardano using the offchain provided by the
112//! `partner-chains-cardano-offchain` crate. This step can be performed independently from the
113//! order of other steps.
114//! 1. Release a new version of the chain's node, with added data source and inhrent data provider.
115//! 2. Distribute the new version and wait until most of the network's nodes have been updated.
116//! 3. Perform a runtime upgrade to a runtime version containing the pallet.
117//! 4. Complete the configuration of the pallet by setting the correct main chain script and data
118//! checkpoint via an extrinsic. This step requires the governance authority to know the main
119//! chain script values, which it should obtain using the offchain.
120//!
121#![cfg_attr(not(feature = "std"), no_std)]
122#![deny(missing_docs)]
123
124extern crate alloc;
125
126use alloc::vec::*;
127use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
128use scale_info::TypeInfo;
129use serde::{Deserialize, Serialize};
130use sidechain_domain::{
131 AssetId, AssetName, MainchainAddress, McBlockHash, McBlockNumber, PolicyId, UtxoId,
132};
133use sp_inherents::*;
134
135#[cfg(feature = "std")]
136use {sp_api::ApiExt, sp_api::ProvideRuntimeApi, sp_runtime::traits::Block as BlockT};
137
138/// Smart contract hashes and addresses used by the token bridge on Cardano.
139#[derive(
140 Default,
141 Debug,
142 Clone,
143 PartialEq,
144 Eq,
145 TypeInfo,
146 Encode,
147 Decode,
148 DecodeWithMemTracking,
149 MaxEncodedLen,
150 Serialize,
151 Deserialize,
152)]
153pub struct MainChainScripts {
154 /// Minting policy ID of the native token
155 pub token_policy_id: PolicyId,
156 /// Asset name of the native token
157 pub token_asset_name: AssetName,
158 /// Address of the illiquid supply validator.
159 ///
160 /// All tokens sent to that address are effectively locked and considered "sent" to the Partner Chain.
161 pub illiquid_circulation_supply_validator_address: MainchainAddress,
162}
163
164impl MainChainScripts {
165 /// Return full asset ID fo the bridged token (minting policy ID and asset name)
166 pub fn asset_id(&self) -> AssetId {
167 AssetId {
168 policy_id: self.token_policy_id.clone(),
169 asset_name: self.token_asset_name.clone(),
170 }
171 }
172}
173
174#[cfg(feature = "std")]
175impl MainChainScripts {
176 /// Reads the main chain script values from environment
177 ///
178 /// It expects the following variables to be set:
179 /// - `BRIDGE_TOKEN_POLICY_ID`
180 /// - `BRIDGE_TOKEN_ASSET_NAME`
181 /// - `ILLIQUID_CIRCULATION_SUPPLY_VALIDATOR_ADDRESS`
182 pub fn read_from_env() -> Result<Self, envy::Error> {
183 #[derive(serde::Serialize, serde::Deserialize)]
184 pub struct MainChainScriptsEnvConfig {
185 pub bridge_token_policy_id: PolicyId,
186 pub bridge_token_asset_name: AssetName,
187 pub illiquid_circulation_supply_validator_address: MainchainAddress,
188 }
189
190 let MainChainScriptsEnvConfig {
191 bridge_token_policy_id,
192 bridge_token_asset_name,
193 illiquid_circulation_supply_validator_address,
194 } = envy::from_env::<MainChainScriptsEnvConfig>()?;
195
196 Ok(Self {
197 token_policy_id: bridge_token_policy_id,
198 token_asset_name: bridge_token_asset_name,
199 illiquid_circulation_supply_validator_address,
200 })
201 }
202}
203
204/// Type containing all information needed to process a single transfer incoming from
205/// main chain, corresponding to a single UTXO on Cardano
206#[derive(
207 Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq, MaxEncodedLen,
208)]
209pub enum BridgeTransferV1<RecipientAddress> {
210 /// Token transfer initiated by a user on Cardano
211 UserTransfer {
212 /// Amount of tokens tranfered
213 token_amount: u64,
214 /// Transfer recipient on the Partner Chain
215 recipient: RecipientAddress,
216 },
217 /// Token transfer carrying reserve funds being moved from Cardano to the Partner Chain
218 ReserveTransfer {
219 /// Amount of tokens tranfered
220 token_amount: u64,
221 },
222 /// Invalid transfer coming from a UTXO on Cardano that does not contain a datum that can be
223 /// correctly interpreted. These transfers can either be ignored and considered lost or recovered
224 /// through some custom mechanism.
225 InvalidTransfer {
226 /// Amount of tokens tranfered
227 token_amount: u64,
228 /// ID of the UTXO containing an invalid transfer
229 utxo_id: sidechain_domain::UtxoId,
230 },
231}
232
233/// Structure representing all token bridge transfers incoming from Cardano that are to be
234/// handled in one Partner Chain block.
235#[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq)]
236pub struct TokenBridgeTransfersV1<RecipientAddress> {
237 /// Transfers to be handled in one Partner Chain block
238 pub transfers: Vec<BridgeTransferV1<RecipientAddress>>,
239 /// Pointer to last data processed, used as an idempotency key by the data layer
240 pub data_checkpoint: BridgeDataCheckpoint,
241}
242
243/// Inherent identifier used by the Partner Chains token bridge
244pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"pctokbrg";
245
246/// Error type returned by failing calls of the bridge inherent
247#[derive(Debug, Encode, Decode, thiserror::Error, PartialEq)]
248pub enum InherentError {
249 /// Inherent was not produced when expected
250 #[error("Bridge inherent data was present but an inherent was not provided")]
251 InherentRequired,
252 /// Inherent produced when inherent data not present
253 #[error("Bridge inherent produced when no data present")]
254 InherentNotExpected,
255 /// Inherent produced does not match inherent data
256 #[error("Inherent produced does not match inherent data")]
257 IncorrectInherent,
258}
259
260impl IsFatalError for InherentError {
261 fn is_fatal_error(&self) -> bool {
262 true
263 }
264}
265
266/// Inherent data provider that provides data on token bridge transfers and special token transfers
267#[cfg(feature = "std")]
268#[derive(Debug)]
269pub enum TokenBridgeInherentDataProvider<RecipientAddress> {
270 /// Inert IDP. Will not provide inherent data and will never fail.
271 Inert,
272 /// Version 1
273 ActiveV1 {
274 /// Token bridge transfer data
275 data: TokenBridgeTransfersV1<RecipientAddress>,
276 },
277}
278
279/// Value specifying the point in time up to which bridge transfers have been processed
280///
281/// This type is an enum wrapping either a block number or a utxo to handle both a case
282/// where all transfers up to a block have been handled and a case where there were more
283/// transfers than the limit allows and observability needs to pick up after the last
284/// utxo that could be observed
285#[derive(
286 Clone, Debug, Encode, Decode, DecodeWithMemTracking, TypeInfo, PartialEq, Eq, MaxEncodedLen,
287)]
288pub enum BridgeDataCheckpoint {
289 /// Last transfer utxo that has been processed
290 Utxo(UtxoId),
291 /// Cardano block up to which data has been processed
292 Block(McBlockNumber),
293}
294
295/// Interface for data sources that can be used by [TokenBridgeInherentDataProvider]
296#[cfg(feature = "std")]
297#[async_trait::async_trait]
298pub trait TokenBridgeDataSource<RecipientAddress>: Send + Sync {
299 /// Fetches at most `max_transfers` of token bridge transfers after `data_checkpoint` up to `current_mc_block`
300 async fn get_transfers(
301 &self,
302 main_chain_scripts: MainChainScripts,
303 data_checkpoint: BridgeDataCheckpoint,
304 max_transfers: u32,
305 current_mc_block: McBlockHash,
306 ) -> Result<
307 (Vec<BridgeTransferV1<RecipientAddress>>, BridgeDataCheckpoint),
308 Box<dyn std::error::Error + Send + Sync>,
309 >;
310}
311
312/// Error type returned when creating [TokenBridgeInherentDataProvider] fails
313#[cfg(feature = "std")]
314#[derive(Debug, thiserror::Error)]
315pub enum InherentDataCreationError {
316 /// Signals that a runtime API call failed
317 #[error("Runtime API call failed: {0}")]
318 ApiError(#[from] sp_api::ApiError),
319 /// Signals that the data source returned an error
320 #[error("Data source call failed: {0}")]
321 DataSourceError(Box<dyn std::error::Error + Send + Sync>),
322 /// Signals that the current pallet version on the chain is higher than supported by the node's IDP
323 #[error("Unsupported pallet version {0} (highest supported version: {1})")]
324 UnsupportedPalletVersion(u32, u32),
325}
326
327#[cfg(feature = "std")]
328#[async_trait::async_trait]
329impl<RecipientAddress: Encode + Send + Sync> sp_inherents::InherentDataProvider
330 for TokenBridgeInherentDataProvider<RecipientAddress>
331{
332 async fn provide_inherent_data(
333 &self,
334 inherent_data: &mut InherentData,
335 ) -> Result<(), sp_inherents::Error> {
336 match self {
337 Self::Inert => {},
338 Self::ActiveV1 { data } => {
339 inherent_data.put_data(INHERENT_IDENTIFIER, data)?;
340 },
341 }
342 Ok(())
343 }
344
345 async fn try_handle_error(
346 &self,
347 identifier: &InherentIdentifier,
348 mut error: &[u8],
349 ) -> Option<Result<(), sp_inherents::Error>> {
350 if identifier == &INHERENT_IDENTIFIER {
351 let error = InherentError::decode(&mut error).ok()?;
352 Some(Err(sp_inherents::Error::Application(Box::from(error))))
353 } else {
354 None
355 }
356 }
357}
358
359sp_api::decl_runtime_apis! {
360 /// Runtime API used by [TokenBridgeInherentDataProvider]
361 #[api_version(1)]
362 pub trait TokenBridgeIDPRuntimeApi {
363 /// Returns the current version of the pallet, 1-based.
364 fn get_pallet_version() -> u32;
365 /// Returns the currenlty configured main chain scripts
366 fn get_main_chain_scripts() -> Option<MainChainScripts>;
367 /// Returns the currently configured transfer number limit
368 fn get_max_transfers_per_block() -> u32;
369 /// Returns last data checkpoint saved in the pallet
370 fn get_last_data_checkpoint() -> Option<BridgeDataCheckpoint>;
371 }
372}
373
374#[cfg(feature = "std")]
375impl<RecipientAddress: Encode + Send + Sync> TokenBridgeInherentDataProvider<RecipientAddress> {
376 /// Creates new [TokenBridgeInherentDataProvider]
377 ///
378 /// This function is version-aware and will create [TokenBridgeInherentDataProvider] based on
379 /// the version reported by the pallet through [TokenBridgeIDPRuntimeApi].
380 pub async fn new<Block, T>(
381 client: &T,
382 parent_hash: Block::Hash,
383 current_mc_hash: McBlockHash,
384 data_source: &(dyn TokenBridgeDataSource<RecipientAddress> + Send + Sync),
385 ) -> Result<Self, InherentDataCreationError>
386 where
387 Block: BlockT,
388 T: ProvideRuntimeApi<Block> + Send + Sync,
389 T::Api: TokenBridgeIDPRuntimeApi<Block>,
390 {
391 let api = client.runtime_api();
392
393 let Some(pallet_version) =
394 api.api_version::<dyn TokenBridgeIDPRuntimeApi<Block>>(parent_hash)?
395 else {
396 log::info!(
397 "💤 Skipping token bridge transfer observation. Pallet not detected in the runtime."
398 );
399 return Ok(Self::Inert);
400 };
401
402 match pallet_version {
403 1 => Self::new_v1(api, parent_hash, current_mc_hash, data_source).await,
404 unsupported_version => {
405 Err(InherentDataCreationError::UnsupportedPalletVersion(unsupported_version, 1))
406 },
407 }
408 }
409
410 /// Creates new [TokenBridgeInherentDataProvider::ActiveV1]
411 pub async fn new_v1<'a, Block, Api>(
412 api: sp_api::ApiRef<'a, Api>,
413 parent_hash: Block::Hash,
414 current_mc_hash: McBlockHash,
415 data_source: &dyn TokenBridgeDataSource<RecipientAddress>,
416 ) -> Result<Self, InherentDataCreationError>
417 where
418 Block: BlockT,
419 Api: TokenBridgeIDPRuntimeApi<Block>,
420 {
421 let max_transfers = api.get_max_transfers_per_block(parent_hash)?;
422 let (Some(last_checkpoint), Some(main_chain_scripts)) =
423 (api.get_last_data_checkpoint(parent_hash)?, api.get_main_chain_scripts(parent_hash)?)
424 else {
425 log::info!("💤 Skipping token bridge transfer observation. Pallet not configured.");
426 return Ok(Self::Inert);
427 };
428
429 let (transfers, new_checkpoint) = data_source
430 .get_transfers(main_chain_scripts, last_checkpoint, max_transfers, current_mc_hash)
431 .await
432 .map_err(InherentDataCreationError::DataSourceError)?;
433
434 Ok(Self::ActiveV1 {
435 data: TokenBridgeTransfersV1 { transfers, data_checkpoint: new_checkpoint },
436 })
437 }
438}