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}