clementine_core/rpc/parser/
mod.rs

1use super::clementine::{
2    self, DepositParams, FeeType, Outpoint, RawSignedTx, RbfSigningInfoRpc, SchnorrSig,
3    TransactionRequest, WinternitzPubkey,
4};
5use super::error;
6use crate::builder::transaction::sign::TransactionRequestData;
7use crate::builder::transaction::TransactionType;
8use crate::constants::{MAX_BYTES_PER_WINTERNITZ_KEY, MAX_WINTERNITZ_DIGITS_PER_KEY};
9use crate::deposit::{
10    Actors, BaseDepositData, DepositData, DepositInfo, DepositType, ReplacementDepositData,
11    SecurityCouncil,
12};
13use crate::errors::BridgeError;
14use crate::operator::RoundIndex;
15use crate::rpc::clementine::{SignedTxWithType, SignedTxsWithType};
16use crate::utils::{FeePayingType, RbfSigningInfo};
17use bitcoin::hashes::{sha256d, FromSliceError, Hash};
18use bitcoin::secp256k1::schnorr::Signature;
19use bitcoin::{OutPoint, TapNodeHash, Transaction, Txid, XOnlyPublicKey};
20use bitvm::signatures::winternitz;
21use eyre::Context;
22use std::fmt::{Debug, Display};
23use std::num::TryFromIntError;
24use tonic::Status;
25
26pub mod operator;
27pub mod verifier;
28
29#[derive(Debug, Clone, thiserror::Error)]
30pub enum ParserError {
31    // RPC errors
32    #[error("RPC function field {0} is required")]
33    RPCRequiredParam(&'static str),
34    #[error("RPC function parameter {0} is malformed")]
35    RPCParamMalformed(String),
36    #[error("RPC function parameter {0} is oversized: {1}")]
37    RPCParamOversized(String, usize),
38}
39
40impl From<ParserError> for tonic::Status {
41    fn from(value: ParserError) -> Self {
42        match value {
43            ParserError::RPCRequiredParam(field) => {
44                Status::invalid_argument(format!("RPC function field {field} is required."))
45            }
46            ParserError::RPCParamMalformed(field) => {
47                Status::invalid_argument(format!("RPC function parameter {field} is malformed."))
48            }
49            ParserError::RPCParamOversized(field, size) => Status::invalid_argument(format!(
50                "RPC function parameter {field} is oversized: {size}",
51            )),
52        }
53    }
54}
55
56#[allow(dead_code)]
57#[allow(clippy::result_large_err)]
58/// Converts an integer type in to another integer type. This is needed because
59/// tonic defaults to wrong integer types for some parameters.
60pub fn convert_int_to_another<SOURCE, TARGET>(
61    field_name: &str,
62    value: SOURCE,
63    try_from: fn(SOURCE) -> Result<TARGET, TryFromIntError>,
64) -> Result<TARGET, Status>
65where
66    SOURCE: Copy + Debug + Display,
67{
68    try_from(value)
69        .map_err(|e| error::invalid_argument(field_name, "Given number is out of bounds")(e))
70}
71
72/// Fetches the next message from a stream which is unwrapped and encapsulated
73/// by a [`Result`].
74///
75/// # Parameters
76///
77/// - stream: [`tonic::Streaming`] typed input stream
78/// - field: Input field ident (struct member) to look in the next message
79///
80/// # Returns
81///
82/// A [`Result`] containing the next message. Will return an [`Err`] variant if
83/// stream has exhausted.
84#[macro_export]
85macro_rules! fetch_next_message_from_stream {
86    ($stream:expr, $field:ident) => {
87        $crate::fetch_next_optional_message_from_stream!($stream, $field).ok_or(
88            $crate::rpc::error::expected_msg_got_none(stringify!($field))(),
89        )
90    };
91}
92
93/// Fetches next message from a stream.
94///
95/// # Parameters
96///
97/// - stream: [`tonic::Streaming`] typed input stream
98/// - field: Input field ident (struct member) to look in the next message
99///
100/// # Returns
101///
102/// An [`Option`] containing the next message. Will return a [`None`] variant if
103/// stream has exhausted.
104#[macro_export]
105macro_rules! fetch_next_optional_message_from_stream {
106    ($stream:expr, $field:ident) => {
107        $stream.message().await?.and_then(|msg| msg.$field)
108    };
109}
110
111impl From<RbfSigningInfo> for RbfSigningInfoRpc {
112    fn from(value: RbfSigningInfo) -> Self {
113        RbfSigningInfoRpc {
114            merkle_root: value
115                .tweak_merkle_root
116                .map_or(vec![], |root| root.to_byte_array().to_vec()),
117            vout: value.vout,
118        }
119    }
120}
121
122impl TryFrom<RbfSigningInfoRpc> for RbfSigningInfo {
123    type Error = BridgeError;
124
125    fn try_from(value: RbfSigningInfoRpc) -> Result<Self, Self::Error> {
126        Ok(RbfSigningInfo {
127            tweak_merkle_root: if value.merkle_root.is_empty() {
128                None
129            } else {
130                Some(
131                    TapNodeHash::from_slice(&value.merkle_root).wrap_err(eyre::eyre!(
132                        "Failed to convert merkle root bytes from rpc to TapNodeHash"
133                    ))?,
134                )
135            },
136            vout: value.vout,
137            #[cfg(test)]
138            annex: None,
139            #[cfg(test)]
140            additional_taproot_output_count: None,
141        })
142    }
143}
144
145impl TryFrom<Outpoint> for OutPoint {
146    type Error = BridgeError;
147
148    fn try_from(value: Outpoint) -> Result<Self, Self::Error> {
149        let hash = match Hash::from_slice(
150            &value
151                .txid
152                .ok_or(eyre::eyre!("Can't convert empty txid"))?
153                .txid,
154        ) {
155            Ok(h) => h,
156            Err(e) => return Err(BridgeError::FromSliceError(e)),
157        };
158
159        Ok(OutPoint {
160            txid: Txid::from_raw_hash(hash),
161            vout: value.vout,
162        })
163    }
164}
165impl From<OutPoint> for Outpoint {
166    fn from(value: OutPoint) -> Self {
167        Outpoint {
168            txid: Some(value.txid.into()),
169            vout: value.vout,
170        }
171    }
172}
173
174impl TryFrom<WinternitzPubkey> for winternitz::PublicKey {
175    type Error = BridgeError;
176
177    fn try_from(value: WinternitzPubkey) -> Result<Self, Self::Error> {
178        let inner = value.digit_pubkey;
179
180        // Add reasonable size limit per key
181        if inner.len() > MAX_WINTERNITZ_DIGITS_PER_KEY {
182            return Err(BridgeError::Parser(ParserError::RPCParamOversized(
183                "digit_pubkey".to_string(),
184                inner.len(),
185            )));
186        }
187
188        // Add total memory limit check
189        let total_bytes = inner.len() * 20;
190        if total_bytes > MAX_BYTES_PER_WINTERNITZ_KEY {
191            return Err(BridgeError::Parser(ParserError::RPCParamOversized(
192                "digit_pubkey".to_string(),
193                inner.len(),
194            )));
195        }
196
197        inner
198            .into_iter()
199            .enumerate()
200            .map(|(i, inner_vec)| {
201                inner_vec
202                    .try_into()
203                    .map_err(|e: Vec<_>| eyre::eyre!("Incorrect length {:?}, expected 20", e.len()))
204                    .wrap_err_with(|| ParserError::RPCParamMalformed(format!("digit_pubkey.[{i}]")))
205            })
206            .collect::<Result<Vec<[u8; 20]>, eyre::Report>>()
207            .map_err(Into::into)
208    }
209}
210
211impl From<FeePayingType> for FeeType {
212    fn from(value: FeePayingType) -> Self {
213        match value {
214            FeePayingType::CPFP => FeeType::Cpfp,
215            FeePayingType::RBF => FeeType::Rbf,
216            FeePayingType::NoFunding => FeeType::NoFunding,
217        }
218    }
219}
220
221impl TryFrom<FeeType> for FeePayingType {
222    type Error = Status;
223
224    fn try_from(value: FeeType) -> Result<Self, Self::Error> {
225        match value {
226            FeeType::Cpfp => Ok(FeePayingType::CPFP),
227            FeeType::Rbf => Ok(FeePayingType::RBF),
228            FeeType::NoFunding => Ok(FeePayingType::NoFunding),
229            _ => Err(Status::invalid_argument("Invalid FeeType variant")),
230        }
231    }
232}
233
234impl TryFrom<SchnorrSig> for Signature {
235    type Error = BridgeError;
236
237    fn try_from(value: SchnorrSig) -> Result<Self, Self::Error> {
238        Signature::from_slice(&value.schnorr_sig)
239            .wrap_err("Failed to parse schnorr signature")
240            .wrap_err_with(|| ParserError::RPCParamMalformed("schnorr_sig".to_string()))
241            .map_err(Into::into)
242    }
243}
244impl From<winternitz::PublicKey> for WinternitzPubkey {
245    fn from(value: winternitz::PublicKey) -> Self {
246        {
247            let digit_pubkey = value.into_iter().map(|inner| inner.to_vec()).collect();
248
249            WinternitzPubkey { digit_pubkey }
250        }
251    }
252}
253
254impl From<DepositInfo> for clementine::Deposit {
255    fn from(value: DepositInfo) -> Self {
256        clementine::Deposit {
257            deposit_outpoint: Some(value.deposit_outpoint.into()),
258            deposit_data: Some(value.deposit_type.into()),
259        }
260    }
261}
262
263impl TryFrom<clementine::Deposit> for DepositInfo {
264    type Error = Status;
265
266    fn try_from(value: clementine::Deposit) -> Result<Self, Self::Error> {
267        let deposit_outpoint: OutPoint = value
268            .deposit_outpoint
269            .ok_or_else(|| Status::invalid_argument("No deposit outpoint received"))?
270            .try_into()?;
271
272        let deposit_type = value
273            .deposit_data
274            .ok_or_else(|| Status::invalid_argument("No deposit data received"))?
275            .try_into()?;
276
277        Ok(DepositInfo {
278            deposit_outpoint,
279            deposit_type,
280        })
281    }
282}
283
284impl From<DepositData> for DepositParams {
285    fn from(value: DepositData) -> Self {
286        let actors: clementine::Actors = value.actors.into();
287        let security_council: clementine::SecurityCouncil = value.security_council.into();
288        let deposit: clementine::Deposit = value.deposit.into();
289
290        DepositParams {
291            deposit: Some(deposit),
292            actors: Some(actors),
293            security_council: Some(security_council),
294        }
295    }
296}
297
298impl TryFrom<DepositParams> for DepositData {
299    type Error = Status;
300
301    fn try_from(value: DepositParams) -> Result<Self, Self::Error> {
302        let deposit: DepositInfo = value
303            .deposit
304            .ok_or(Status::invalid_argument("No deposit received"))?
305            .try_into()?;
306        let actors: Actors = value
307            .actors
308            .ok_or(Status::invalid_argument("No actors received"))?
309            .try_into()?;
310
311        let security_council: SecurityCouncil = value
312            .security_council
313            .ok_or(Status::invalid_argument("No security council received"))?
314            .try_into()?;
315
316        Ok(DepositData {
317            nofn_xonly_pk: None,
318            deposit,
319            actors,
320            security_council,
321        })
322    }
323}
324
325impl TryFrom<clementine::deposit::DepositData> for DepositType {
326    type Error = Status;
327
328    fn try_from(value: clementine::deposit::DepositData) -> Result<Self, Self::Error> {
329        match value {
330            clementine::deposit::DepositData::BaseDeposit(data) => {
331                Ok(DepositType::BaseDeposit(BaseDepositData {
332                    evm_address: data.evm_address.try_into().map_err(|e| {
333                        Status::invalid_argument(format!(
334                            "Failed to convert evm_address to EVMAddress: {e}",
335                        ))
336                    })?,
337                    recovery_taproot_address: data
338                        .recovery_taproot_address
339                        .parse::<bitcoin::Address<_>>()
340                        .map_err(|e| Status::internal(e.to_string()))?,
341                }))
342            }
343            clementine::deposit::DepositData::ReplacementDeposit(data) => {
344                Ok(DepositType::ReplacementDeposit(ReplacementDepositData {
345                    old_move_txid: data
346                        .old_move_txid
347                        .ok_or(Status::invalid_argument("No move_txid received"))?
348                        .try_into().map_err(|e| {
349                            Status::invalid_argument(format!(
350                                "Failed to convert replacement deposit move_txid to bitcoin::Txid: {e}",
351                            ))
352                        })?,
353                }))
354            }
355        }
356    }
357}
358
359impl From<DepositType> for clementine::deposit::DepositData {
360    fn from(value: DepositType) -> Self {
361        match value {
362            DepositType::BaseDeposit(data) => {
363                clementine::deposit::DepositData::BaseDeposit(clementine::BaseDeposit {
364                    evm_address: data.evm_address.0.to_vec(),
365                    recovery_taproot_address: data
366                        .recovery_taproot_address
367                        .assume_checked()
368                        .to_string(),
369                })
370            }
371            DepositType::ReplacementDeposit(data) => {
372                clementine::deposit::DepositData::ReplacementDeposit(
373                    clementine::ReplacementDeposit {
374                        old_move_txid: Some(data.old_move_txid.into()),
375                    },
376                )
377            }
378        }
379    }
380}
381
382impl TryFrom<clementine::XOnlyPublicKeys> for Vec<XOnlyPublicKey> {
383    type Error = Status;
384
385    fn try_from(value: clementine::XOnlyPublicKeys) -> Result<Self, Self::Error> {
386        value
387            .xonly_public_keys
388            .iter()
389            .map(|pk| {
390                XOnlyPublicKey::from_slice(pk).map_err(|e| {
391                    Status::invalid_argument(format!("Failed to parse xonly public key: {e}"))
392                })
393            })
394            .collect::<Result<Vec<_>, _>>()
395    }
396}
397
398impl From<Vec<XOnlyPublicKey>> for clementine::XOnlyPublicKeys {
399    fn from(value: Vec<XOnlyPublicKey>) -> Self {
400        clementine::XOnlyPublicKeys {
401            xonly_public_keys: value.iter().map(|pk| pk.serialize().to_vec()).collect(),
402        }
403    }
404}
405
406impl TryFrom<clementine::Actors> for Actors {
407    type Error = Status;
408
409    fn try_from(value: clementine::Actors) -> Result<Self, Self::Error> {
410        let verifiers = value
411            .verifiers
412            .ok_or(Status::invalid_argument("No verifiers received"))?
413            .try_into()?;
414        let watchtowers = value
415            .watchtowers
416            .ok_or(Status::invalid_argument("No watchtowers received"))?
417            .try_into()?;
418        let operators = value
419            .operators
420            .ok_or(Status::invalid_argument("No operators received"))?
421            .try_into()?;
422
423        Ok(Actors {
424            verifiers,
425            watchtowers,
426            operators,
427        })
428    }
429}
430
431impl From<Actors> for clementine::Actors {
432    fn from(value: Actors) -> Self {
433        clementine::Actors {
434            verifiers: Some(value.verifiers.into()),
435            watchtowers: Some(value.watchtowers.into()),
436            operators: Some(value.operators.into()),
437        }
438    }
439}
440
441impl From<SecurityCouncil> for clementine::SecurityCouncil {
442    fn from(value: SecurityCouncil) -> Self {
443        clementine::SecurityCouncil {
444            pks: value
445                .pks
446                .into_iter()
447                .map(|pk| pk.serialize().to_vec())
448                .collect(),
449            threshold: value.threshold,
450        }
451    }
452}
453
454impl TryFrom<clementine::SecurityCouncil> for SecurityCouncil {
455    type Error = Status;
456
457    fn try_from(value: clementine::SecurityCouncil) -> Result<Self, Self::Error> {
458        let pks = value
459            .pks
460            .into_iter()
461            .map(|pk| {
462                XOnlyPublicKey::from_slice(&pk).map_err(|e| {
463                    Status::invalid_argument(format!("Failed to parse xonly public key: {e}"))
464                })
465            })
466            .collect::<Result<Vec<_>, _>>()?;
467
468        Ok(SecurityCouncil {
469            pks,
470            threshold: value.threshold,
471        })
472    }
473}
474
475impl TryFrom<RawSignedTx> for bitcoin::Transaction {
476    type Error = Status;
477
478    fn try_from(value: RawSignedTx) -> Result<Self, Self::Error> {
479        bitcoin::consensus::encode::deserialize(&value.raw_tx)
480            .map_err(|e| Status::invalid_argument(format!("Failed to parse raw signed tx: {e}")))
481    }
482}
483
484impl From<&bitcoin::Transaction> for RawSignedTx {
485    fn from(value: &bitcoin::Transaction) -> Self {
486        RawSignedTx {
487            raw_tx: bitcoin::consensus::encode::serialize(value),
488        }
489    }
490}
491
492impl From<Txid> for clementine::Txid {
493    fn from(value: Txid) -> Self {
494        clementine::Txid {
495            txid: value.to_byte_array().to_vec(),
496        }
497    }
498}
499impl TryFrom<clementine::Txid> for Txid {
500    type Error = FromSliceError;
501
502    fn try_from(value: clementine::Txid) -> Result<Self, Self::Error> {
503        Ok(Txid::from_raw_hash(sha256d::Hash::from_slice(&value.txid)?))
504    }
505}
506
507#[allow(clippy::result_large_err)]
508impl TryFrom<TransactionRequest> for TransactionRequestData {
509    type Error = Status;
510
511    fn try_from(request: TransactionRequest) -> Result<Self, Self::Error> {
512        let deposit_outpoint: OutPoint = request
513            .deposit_outpoint
514            .ok_or(Status::invalid_argument("No deposit params received"))?
515            .try_into()?;
516
517        let kickoff_id = request
518            .kickoff_id
519            .ok_or(Status::invalid_argument("No kickoff params received"))?;
520
521        Ok(TransactionRequestData {
522            deposit_outpoint,
523            kickoff_data: kickoff_id.try_into()?,
524        })
525    }
526}
527
528impl From<TransactionRequestData> for TransactionRequest {
529    fn from(value: TransactionRequestData) -> Self {
530        TransactionRequest {
531            deposit_outpoint: Some(value.deposit_outpoint.into()),
532            kickoff_id: Some(value.kickoff_data.into()),
533        }
534    }
535}
536
537impl TryFrom<clementine::KickoffId> for crate::deposit::KickoffData {
538    type Error = Status;
539
540    fn try_from(value: clementine::KickoffId) -> Result<Self, Self::Error> {
541        let operator_xonly_pk =
542            XOnlyPublicKey::from_slice(&value.operator_xonly_pk).map_err(|e| {
543                Status::invalid_argument(format!("Failed to parse operator_xonly_pk: {e}"))
544            })?;
545
546        Ok(crate::deposit::KickoffData {
547            operator_xonly_pk,
548            round_idx: RoundIndex::from_index(value.round_idx as usize),
549            kickoff_idx: value.kickoff_idx,
550        })
551    }
552}
553
554impl From<crate::deposit::KickoffData> for clementine::KickoffId {
555    fn from(value: crate::deposit::KickoffData) -> Self {
556        clementine::KickoffId {
557            operator_xonly_pk: value.operator_xonly_pk.serialize().to_vec(),
558            round_idx: value.round_idx.to_index() as u32,
559            kickoff_idx: value.kickoff_idx,
560        }
561    }
562}
563
564impl From<Vec<(TransactionType, Transaction)>> for SignedTxsWithType {
565    fn from(value: Vec<(TransactionType, Transaction)>) -> Self {
566        SignedTxsWithType {
567            signed_txs: value
568                .into_iter()
569                .map(|(tx_type, signed_tx)| SignedTxWithType {
570                    transaction_type: Some(tx_type.into()),
571                    raw_tx: bitcoin::consensus::serialize(&signed_tx),
572                })
573                .collect(),
574        }
575    }
576}
577
578impl TryFrom<SignedTxWithType> for (TransactionType, Transaction) {
579    type Error = Status;
580
581    fn try_from(value: SignedTxWithType) -> Result<Self, Self::Error> {
582        Ok((
583            value
584                .transaction_type
585                .ok_or(Status::invalid_argument("No transaction type received"))?
586                .try_into()
587                .map_err(|e| {
588                    Status::invalid_argument(format!("Failed to parse transaction type: {e}"))
589                })?,
590            bitcoin::consensus::encode::deserialize(&value.raw_tx).map_err(|e| {
591                Status::invalid_argument(format!("Failed to parse raw signed tx: {e}"))
592            })?,
593        ))
594    }
595}
596
597impl TryFrom<clementine::SignedTxsWithType> for Vec<(TransactionType, Transaction)> {
598    type Error = Status;
599
600    fn try_from(value: clementine::SignedTxsWithType) -> Result<Self, Self::Error> {
601        value
602            .signed_txs
603            .into_iter()
604            .map(|signed_tx| signed_tx.try_into())
605            .collect::<Result<Vec<_>, _>>()
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use crate::rpc::clementine::{self, Outpoint, WinternitzPubkey};
612    use bitcoin::{hashes::Hash, OutPoint, Txid};
613    use bitvm::signatures::winternitz;
614
615    #[test]
616    fn from_bitcoin_outpoint_to_proto_outpoint() {
617        let og_outpoint = OutPoint {
618            txid: Txid::from_raw_hash(Hash::from_slice(&[0x1F; 32]).unwrap()),
619            vout: 0x45,
620        };
621
622        let proto_outpoint: Outpoint = og_outpoint.into();
623        let bitcoin_outpoint: OutPoint = proto_outpoint.try_into().unwrap();
624        assert_eq!(og_outpoint, bitcoin_outpoint);
625
626        let proto_outpoint = Outpoint {
627            txid: Some(clementine::Txid {
628                txid: vec![0x1F; 32],
629            }),
630            vout: 0x45,
631        };
632        let bitcoin_outpoint: OutPoint = proto_outpoint.try_into().unwrap();
633        assert_eq!(og_outpoint, bitcoin_outpoint);
634    }
635
636    #[test]
637    fn from_proto_outpoint_to_bitcoin_outpoint() {
638        let og_outpoint = Outpoint {
639            txid: Some(clementine::Txid {
640                txid: vec![0x1F; 32],
641            }),
642            vout: 0x45,
643        };
644
645        let bitcoin_outpoint: OutPoint = og_outpoint.clone().try_into().unwrap();
646        let proto_outpoint: Outpoint = bitcoin_outpoint.into();
647        assert_eq!(og_outpoint, proto_outpoint);
648
649        let bitcoin_outpoint = OutPoint {
650            txid: Txid::from_raw_hash(Hash::from_slice(&[0x1F; 32]).unwrap()),
651            vout: 0x45,
652        };
653        let proto_outpoint: Outpoint = bitcoin_outpoint.into();
654        assert_eq!(og_outpoint, proto_outpoint);
655    }
656
657    #[test]
658    fn from_proto_winternitz_public_key_to_bitvm() {
659        let og_wpk = vec![[0x45u8; 20]];
660
661        let rpc_wpk: WinternitzPubkey = og_wpk.clone().into();
662        let rpc_converted_wpk: winternitz::PublicKey =
663            rpc_wpk.try_into().expect("encoded wpk has to be valid");
664        assert_eq!(og_wpk, rpc_converted_wpk);
665    }
666
667    #[test]
668    fn from_txid_to_proto_txid() {
669        let og_txid = Txid::from_raw_hash(Hash::from_slice(&[0x1F; 32]).unwrap());
670
671        let rpc_txid: clementine::Txid = og_txid.into();
672        let rpc_converted_txid: Txid = rpc_txid.try_into().unwrap();
673        assert_eq!(og_txid, rpc_converted_txid);
674    }
675}