pallet_address_associations/
lib.rs

1//! Pallet storing associations from main chain public key to partner chain address.
2//!
3//! ## Purpose of this pallet
4//!
5//! This pallet establishes a many-to-one mapping from Cardano staking keys to Partner Chain addresses.
6//! The purpose of this mapping is primarily to indicate the local PC address to be the recipient of any
7//! block production rewards or cross-chain token transfers credited to a Cardano key holders on a Partner
8//! Chain. Some intended scenarios include:
9//! 1. ADA delegators become eligible for block rewards due to their stake pool's operator participating
10//!    in a Partner Chain network. The on-chain payout mechanism uses data provided by this pallet to
11//!    identify each delegator's Partner Chain address based on their Cardano staking key.
12//! 2. A Partner Chain develops its own cross-chain bridge from Cardano. A Cardano user associates their
13//!    Cardano public key with a Partner Chain address that they control. The user then uses the bridge
14//!    to send some tokens to themselves. The receiving logic in the Partner Chain ledger then uses this
15//!    pallet's data to identify the user's PC account and complete the transfer.
16//!
17//! ## Usage - PC Builder
18//!
19//! This pallet is self-contained and doesn't need any node support.
20//! To include the pallet in the runtime, one must only provide its configuration.
21//! Consult documentation of [pallet::Config] for explanation of all configuration fields.
22//!
23//! For the pallet to verify the validity of address associations submitted by Users, it requires a signature
24//! created using the Cardano private key corresponding to the public key being associated. A dedicated
25//! CLI command for creating the signature is provided by `cli_commands` crate. Consult the crate's
26//! documentation for more information.
27//!
28//! ## Usage - PC User
29//!
30//! This pallet exposes a single extrinsic `associate_address` accepting the Cardano public key
31//! and Partner Chain address to be associated together with a signature confirming that the submitter is
32//! the owner of the associated Cardano public key.
33//!
34//! To obtain the signature, the User should use the dedicated signing command wired into the Partner Chain
35//! node executable.
36//!
37//! *Important*: For compliance reasons, all address associations are final and can not be changed.
38//!
39
40#![cfg_attr(not(feature = "std"), no_std)]
41#![deny(missing_docs)]
42
43extern crate alloc;
44
45pub use pallet::*;
46
47mod benchmarking;
48pub mod weights;
49
50#[cfg(test)]
51mod mock;
52
53#[cfg(test)]
54mod tests;
55
56use parity_scale_codec::Encode;
57use sidechain_domain::MainchainKeyHash;
58use sidechain_domain::{StakePublicKey, UtxoId};
59
60/// Schema of the message signed by a User to verify validity of submitted address association
61#[derive(Debug, Clone, Encode)]
62pub struct AddressAssociationSignedMessage<PartnerChainAddress> {
63	/// Cardano stake public key to be associated
64	pub stake_public_key: StakePublicKey,
65	/// Partner Chain address to be associated
66	pub partnerchain_address: PartnerChainAddress,
67	/// Genesis UTXO of the Partner Chain on which the association is to be registered
68	pub genesis_utxo: UtxoId,
69}
70
71/// Handler for new associations
72pub trait OnNewAssociation<PartnerChainAddress> {
73	/// Function called every time a new address association is created
74	fn on_new_association(
75		partner_chain_address: PartnerChainAddress,
76		main_chain_key_hash: MainchainKeyHash,
77	);
78}
79
80impl<PartnerChainAddress> OnNewAssociation<PartnerChainAddress> for () {
81	fn on_new_association(
82		_partner_chain_address: PartnerChainAddress,
83		_main_chain_key_hash: MainchainKeyHash,
84	) {
85	}
86}
87#[frame_support::pallet]
88pub mod pallet {
89	use super::*;
90	use crate::weights::WeightInfo;
91	use frame_support::pallet_prelude::*;
92	use frame_support::traits::fungible::{Inspect, Mutate};
93	use frame_support::traits::tokens::{Fortitude, Precision, Preservation};
94	use frame_system::{ensure_signed, pallet_prelude::OriginFor};
95	use sidechain_domain::{MainchainKeyHash, StakeKeySignature, StakePublicKey, UtxoId};
96
97	/// Current version of the pallet
98	pub const PALLET_VERSION: u32 = 2;
99
100	#[pallet::pallet]
101	pub struct Pallet<T>(_);
102
103	#[pallet::config]
104	pub trait Config: frame_system::Config {
105		/// Weight information on extrinsic in the pallet. For convenience weights in [weights] module can be used.
106		type WeightInfo: crate::weights::WeightInfo;
107
108		/// Type representing a local PC address. This can be a standard Substrate address, an
109		/// account ID, or some address type specific to the Partner Chain.
110		type PartnerChainAddress: Member + Parameter + MaxEncodedLen;
111
112		/// The currency used for burning tokens when an address association is made
113		type Currency: Mutate<Self::AccountId>;
114
115		/// The amount of tokens to burn when upserting metadata
116		#[pallet::constant]
117		type BurnAmount: Get<<Self::Currency as Inspect<Self::AccountId>>::Balance>;
118
119		/// Function returning the genesis UTXO of the Partner Chain.
120		/// This typically should be wired with the `genesis_utxo` function exposed by `pallet_sidechain`.
121		fn genesis_utxo() -> UtxoId;
122
123		/// Handler that is called for each new address association.
124		///
125		/// If no handling logic is needed, [()] can be used for a no-op implementation.
126		type OnNewAssociation: OnNewAssociation<Self::PartnerChainAddress>;
127	}
128
129	/// Storage of address association
130	#[pallet::storage]
131	pub type AddressAssociations<T: Config> = StorageMap<
132		Hasher = Blake2_128Concat,
133		Key = MainchainKeyHash,
134		Value = T::PartnerChainAddress,
135		QueryKind = OptionQuery,
136	>;
137
138	/// Error type returned by the pallet's extrinsic
139	#[pallet::error]
140	pub enum Error<T> {
141		/// Signals that the Cardano key is already associated
142		MainchainKeyAlreadyAssociated,
143		/// Signals an invalid Cardano key signature
144		InvalidMainchainSignature,
145		/// Could not burn additional fee for occupying space
146		InsufficientBalance,
147	}
148
149	#[pallet::call]
150	impl<T: Config> Pallet<T> {
151		/// Extrinsic creating a new address association.
152		///
153		/// `signature` is expected to be a signature of the Cardano private key corresponding to `stake_public_key`
154		/// of [AddressAssociationSignedMessage] created using the associated public key, address and the genesis UTXO
155		/// of the particular Partner Chain it is being submitted to.
156		#[pallet::call_index(0)]
157		#[pallet::weight(T::WeightInfo::associate_address())]
158		pub fn associate_address(
159			origin: OriginFor<T>,
160			partnerchain_address: T::PartnerChainAddress,
161			signature: StakeKeySignature,
162			stake_public_key: StakePublicKey,
163		) -> DispatchResult {
164			let origin_account_id = ensure_signed(origin)?;
165			let genesis_utxo = T::genesis_utxo();
166
167			let stake_key_hash = stake_public_key.hash();
168
169			ensure!(
170				!AddressAssociations::<T>::contains_key(&stake_key_hash),
171				Error::<T>::MainchainKeyAlreadyAssociated
172			);
173			T::Currency::burn_from(
174				&origin_account_id,
175				T::BurnAmount::get(),
176				Preservation::Preserve,
177				Precision::Exact,
178				Fortitude::Force,
179			)
180			.map_err(|_| Error::<T>::InsufficientBalance)?;
181
182			let address_association_message = AddressAssociationSignedMessage {
183				stake_public_key: stake_public_key.clone(),
184				partnerchain_address: partnerchain_address.clone(),
185				genesis_utxo,
186			};
187
188			let is_valid_signature =
189				signature.verify(&stake_public_key, &address_association_message.encode());
190
191			ensure!(is_valid_signature, Error::<T>::InvalidMainchainSignature);
192
193			AddressAssociations::<T>::insert(stake_key_hash, partnerchain_address.clone());
194
195			T::OnNewAssociation::on_new_association(partnerchain_address, stake_key_hash);
196
197			Ok(())
198		}
199	}
200
201	impl<T: Config> Pallet<T> {
202		/// Returns the current pallet version.
203		pub fn get_version() -> u32 {
204			PALLET_VERSION
205		}
206
207		/// Retrieves all main chain - partner chain address associations from the runtime storage.
208		pub fn get_all_address_associations()
209		-> impl Iterator<Item = (MainchainKeyHash, T::PartnerChainAddress)> {
210			AddressAssociations::<T>::iter()
211		}
212
213		/// Retrieves the partner chain address for a given main chain public key if the association for it exists.
214		pub fn get_partner_chain_address_for(
215			stake_public_key: &StakePublicKey,
216		) -> Option<T::PartnerChainAddress> {
217			AddressAssociations::<T>::get(stake_public_key.hash())
218		}
219	}
220}