babylon_apis/
validate.rs

1use babylon_bitcoin::{deserialize, Transaction};
2
3use cosmwasm_std::StdError;
4
5use crate::btc_staking_api::{
6    ActiveBtcDelegation, FinalityProviderDescription, NewFinalityProvider, ProofOfPossessionBtc,
7    SlashedBtcDelegation, UnbondedBtcDelegation, HASH_SIZE,
8};
9use crate::error::StakingApiError;
10
11/// Trait for validating the API structs / input.
12pub trait Validate {
13    fn validate(&self) -> Result<(), StakingApiError>;
14}
15
16impl Validate for NewFinalityProvider {
17    fn validate(&self) -> Result<(), StakingApiError> {
18        self.description
19            .as_ref()
20            .map(FinalityProviderDescription::validate)
21            .transpose()?;
22
23        if self.btc_pk_hex.is_empty() {
24            return Err(StakingApiError::EmptyBtcPk);
25        }
26
27        let _btc_pk = hex::decode(&self.btc_pk_hex)?;
28
29        // TODO: Validate BTC public key (requires valid BTC PK test data) (#7 extra)
30        // PublicKey::from_slice(&btc_pk)
31        //     .map_err(|_| StakingApiError::InvalidBtcPk(self.btc_pk_hex.clone()))?;
32
33        match self.pop {
34            Some(ref pop) => pop.validate()?,
35            None => return Err(StakingApiError::MissingPop),
36        }
37
38        // Validate consumer_id
39        if self.consumer_id.is_empty() {
40            return Err(StakingApiError::EmptyChainId);
41        }
42
43        Ok(())
44    }
45}
46
47impl Validate for FinalityProviderDescription {
48    fn validate(&self) -> Result<(), StakingApiError> {
49        if self.moniker.is_empty() {
50            return Err(StakingApiError::description_err("Moniker cannot be empty"));
51        }
52        if self.moniker.len() > FinalityProviderDescription::MAX_MONIKER_LENGTH {
53            return Err(StakingApiError::description_err(format!(
54                "Invalid moniker length; got: {}, max: {}",
55                self.moniker.len(),
56                FinalityProviderDescription::MAX_MONIKER_LENGTH
57            )));
58        }
59
60        if self.identity.len() > FinalityProviderDescription::MAX_IDENTITY_LENGTH {
61            return Err(StakingApiError::from(StdError::generic_err(format!(
62                "Invalid identity length; got: {}, max: {}",
63                self.identity.len(),
64                FinalityProviderDescription::MAX_IDENTITY_LENGTH
65            ))));
66        }
67
68        if self.website.len() > FinalityProviderDescription::MAX_WEBSITE_LENGTH {
69            return Err(StakingApiError::from(StdError::generic_err(format!(
70                "Invalid website length; got: {}, max: {}",
71                self.website.len(),
72                FinalityProviderDescription::MAX_WEBSITE_LENGTH
73            ))));
74        }
75
76        if self.security_contact.len() > FinalityProviderDescription::MAX_SECURITY_CONTACT_LENGTH {
77            return Err(StakingApiError::from(StdError::generic_err(format!(
78                "Invalid security contact length; got: {}, max: {}",
79                self.security_contact.len(),
80                FinalityProviderDescription::MAX_SECURITY_CONTACT_LENGTH
81            ))));
82        }
83
84        if self.details.len() > FinalityProviderDescription::MAX_DETAILS_LENGTH {
85            return Err(StakingApiError::from(StdError::generic_err(format!(
86                "Invalid details length; got: {}, max: {}",
87                self.details.len(),
88                FinalityProviderDescription::MAX_DETAILS_LENGTH
89            ))));
90        }
91
92        Ok(())
93    }
94}
95
96impl Validate for ProofOfPossessionBtc {
97    // TODO: Validate proof of possession (#7.0)
98    fn validate(&self) -> Result<(), StakingApiError> {
99        Ok(())
100    }
101}
102
103impl Validate for ActiveBtcDelegation {
104    fn validate(&self) -> Result<(), StakingApiError> {
105        fn check_duplicated_fps(del: &ActiveBtcDelegation) -> Result<(), StakingApiError> {
106            let mut fp_btc_pk_set = std::collections::HashSet::new();
107            for fp_btc_pk in &del.fp_btc_pk_list {
108                if !fp_btc_pk_set.insert(fp_btc_pk) {
109                    return Err(StakingApiError::DuplicatedBtcPk(fp_btc_pk.clone()));
110                }
111            }
112            Ok(())
113        }
114
115        if self.btc_pk_hex.is_empty() {
116            return Err(StakingApiError::EmptyBtcPk);
117        }
118        if self.staking_tx.is_empty() {
119            return Err(StakingApiError::EmptyStakingTx);
120        }
121        if self.slashing_tx.is_empty() {
122            return Err(StakingApiError::EmptySlashingTx);
123        }
124        let _: Transaction = deserialize(&self.slashing_tx)
125            .map_err(|_| StakingApiError::InvalidBtcTx(hex::encode(&self.slashing_tx)))?;
126
127        // TODO: Verify delegator slashing Schnorr signature (#7.2)
128
129        // Ensure the list of finality provider BTC PKs is not empty
130        if self.fp_btc_pk_list.is_empty() {
131            return Err(StakingApiError::EmptyBtcPkList);
132        }
133        // Ensure the list of finality provider BTC PKs is not duplicated
134        check_duplicated_fps(self)?;
135
136        // TODO: Verifications about undelegation info / on-demand unbonding (#7.2)
137        // Check unbonding time is lower than max uint16
138        if self.unbonding_time > u16::MAX as u32 {
139            return Err(StakingApiError::ErrInvalidUnbondingTime(
140                self.unbonding_time,
141                u16::MAX as u32,
142            ));
143        }
144
145        let undelegation_info = &self.undelegation_info;
146        // Check that the unbonding tx is there
147        if undelegation_info.unbonding_tx.is_empty() {
148            return Err(StakingApiError::EmptyUnbondingTx);
149        }
150
151        // Check that the unbonding slashing tx is there
152        if undelegation_info.slashing_tx.is_empty() {
153            return Err(StakingApiError::EmptySlashingTx);
154        }
155
156        // Check that the delegator slashing signature is there
157        if undelegation_info.delegator_slashing_sig.is_empty() {
158            return Err(StakingApiError::EmptySignature);
159        }
160
161        Ok(())
162    }
163}
164
165impl Validate for UnbondedBtcDelegation {
166    fn validate(&self) -> Result<(), StakingApiError> {
167        if self.staking_tx_hash.len() != HASH_SIZE * 2 {
168            return Err(StakingApiError::InvalidStakingTxHash(HASH_SIZE * 2));
169        }
170
171        if self.unbonding_tx_sig.is_empty() {
172            return Err(StakingApiError::EmptySignature);
173        }
174
175        // TODO: Verify delegator unbonding Schnorr signature (#7.3)
176
177        Ok(())
178    }
179}
180
181impl Validate for SlashedBtcDelegation {
182    fn validate(&self) -> Result<(), StakingApiError> {
183        if self.staking_tx_hash.len() != HASH_SIZE * 2 {
184            return Err(StakingApiError::InvalidStakingTxHash(HASH_SIZE * 2));
185        }
186
187        // if self.recovered_fp_btc_sk.is_empty() {
188        //     return Err(StakingApiError::EmptyBtcSk);
189        // }
190
191        Ok(())
192    }
193}