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}