clementine_core/rpc/parser/
operator.rs

1use crate::{
2    citrea::CitreaClientT,
3    errors::BridgeError,
4    fetch_next_message_from_stream,
5    operator::Operator,
6    rpc::{
7        clementine::{
8            operator_params, DepositParams, DepositSignSession, OperatorConfig, OperatorParams,
9            Outpoint, SchnorrSig, WithdrawParams, XOnlyPublicKeyRpc,
10        },
11        error::{self, expected_msg_got_none},
12    },
13};
14use bitcoin::{
15    address::NetworkUnchecked, secp256k1::schnorr::Signature, taproot, Address, Amount, OutPoint,
16    ScriptBuf, TapSighashType, XOnlyPublicKey,
17};
18use bitvm::signatures::winternitz;
19use eyre::Context;
20use std::str::FromStr;
21use tonic::Status;
22
23impl<C> From<Operator<C>> for OperatorParams
24where
25    C: CitreaClientT,
26{
27    fn from(operator: Operator<C>) -> Self {
28        let operator_config = OperatorConfig {
29            collateral_funding_outpoint: Some(Outpoint {
30                txid: Some(operator.collateral_funding_outpoint.txid.into()),
31                vout: operator.collateral_funding_outpoint.vout,
32            }),
33            xonly_pk: operator.signer.xonly_public_key.to_string(),
34            wallet_reimburse_address: operator.reimburse_addr.to_string(),
35        };
36
37        OperatorParams {
38            response: Some(operator_params::Response::OperatorDetails(operator_config)),
39        }
40    }
41}
42
43impl From<winternitz::PublicKey> for OperatorParams {
44    fn from(winternitz_pubkey: winternitz::PublicKey) -> Self {
45        OperatorParams {
46            response: Some(operator_params::Response::WinternitzPubkeys(
47                winternitz_pubkey.into(),
48            )),
49        }
50    }
51}
52
53impl From<Signature> for OperatorParams {
54    fn from(sig: Signature) -> Self {
55        OperatorParams {
56            response: Some(operator_params::Response::UnspentKickoffSig(SchnorrSig {
57                schnorr_sig: sig.serialize().to_vec(),
58            })),
59        }
60    }
61}
62
63impl TryFrom<DepositSignSession> for DepositParams {
64    type Error = Status;
65
66    fn try_from(deposit_sign_session: DepositSignSession) -> Result<Self, Self::Error> {
67        match deposit_sign_session.deposit_params {
68            Some(deposit_params) => Ok(deposit_params),
69            None => Err(expected_msg_got_none("Deposit Params")()),
70        }
71    }
72}
73
74impl From<XOnlyPublicKey> for XOnlyPublicKeyRpc {
75    fn from(xonly_public_key: XOnlyPublicKey) -> Self {
76        XOnlyPublicKeyRpc {
77            xonly_public_key: xonly_public_key.serialize().to_vec(),
78        }
79    }
80}
81
82impl TryFrom<XOnlyPublicKeyRpc> for XOnlyPublicKey {
83    type Error = BridgeError;
84
85    fn try_from(xonly_public_key_rpc: XOnlyPublicKeyRpc) -> Result<Self, Self::Error> {
86        Ok(
87            XOnlyPublicKey::from_slice(&xonly_public_key_rpc.xonly_public_key)
88                .wrap_err("Failed to parse XOnlyPublicKey")?,
89        )
90    }
91}
92
93/// Parses operator configuration from a given stream.
94///
95/// # Returns
96///
97/// A tuple, containing:
98///
99/// - Operator index
100/// - Collateral Funding txid
101/// - Operator's X-only public key
102/// - Wallet reimburse address
103pub async fn parse_details(
104    stream: &mut tonic::Streaming<OperatorParams>,
105) -> Result<(OutPoint, XOnlyPublicKey, Address<NetworkUnchecked>), Status> {
106    let operator_param = fetch_next_message_from_stream!(stream, response)?;
107
108    let operator_config =
109        if let operator_params::Response::OperatorDetails(operator_config) = operator_param {
110            operator_config
111        } else {
112            return Err(expected_msg_got_none("OperatorDetails")());
113        };
114
115    let operator_xonly_pk = XOnlyPublicKey::from_str(&operator_config.xonly_pk)
116        .map_err(|_| Status::invalid_argument("Invalid operator xonly public key".to_string()))?;
117
118    let collateral_funding_outpoint = operator_config
119        .collateral_funding_outpoint
120        .ok_or(Status::invalid_argument(
121            "Collateral funding outpoint not provided".to_string(),
122        ))?
123        .try_into()?;
124
125    let wallet_reimburse_address = Address::from_str(&operator_config.wallet_reimburse_address)
126        .map_err(|e| {
127            Status::invalid_argument(format!("Failed to parse wallet reimburse address: {e:?}"))
128        })?;
129
130    Ok((
131        collateral_funding_outpoint,
132        operator_xonly_pk,
133        wallet_reimburse_address,
134    ))
135}
136
137pub async fn parse_winternitz_public_keys(
138    stream: &mut tonic::Streaming<OperatorParams>,
139) -> Result<winternitz::PublicKey, Status> {
140    let operator_param = fetch_next_message_from_stream!(stream, response)?;
141
142    if let operator_params::Response::WinternitzPubkeys(wpk) = operator_param {
143        Ok(wpk.try_into()?)
144    } else {
145        Err(expected_msg_got_none("WinternitzPubkeys")())
146    }
147}
148
149pub async fn parse_schnorr_sig(
150    stream: &mut tonic::Streaming<OperatorParams>,
151) -> Result<Signature, Status> {
152    let operator_param = fetch_next_message_from_stream!(stream, response)?;
153
154    if let operator_params::Response::UnspentKickoffSig(wpk) = operator_param {
155        Ok(wpk.try_into()?)
156    } else {
157        Err(expected_msg_got_none("UnspentKickoffSig")())
158    }
159}
160
161#[allow(clippy::result_large_err)]
162pub fn parse_withdrawal_sig_params(
163    params: WithdrawParams,
164) -> Result<(u32, taproot::Signature, OutPoint, ScriptBuf, Amount), Status> {
165    let mut input_signature =
166        taproot::Signature::from_slice(&params.input_signature).map_err(|e| {
167            Status::invalid_argument(format!("Can't convert input to taproot Signature - {e}"))
168        })?;
169
170    // If the Taproot sighash type is Default (no explicit type attached; i.e. a 64-byte
171    // signature without a sighash flag), normalize it to SinglePlusAnyoneCanPay.
172    // Prior to v0.5 this was Clementine's implicit behavior; we retain it here for
173    // backwards compatibility when a 64-byte signature is provided.
174    if input_signature.sighash_type == TapSighashType::Default {
175        tracing::warn!(
176            "Input signature for withdrawal {} has sighash type default, setting to SinglePlusAnyoneCanPay", params.withdrawal_id,
177        );
178        input_signature.sighash_type = TapSighashType::SinglePlusAnyoneCanPay;
179    }
180
181    // enforce sighash type here
182    if input_signature.sighash_type != TapSighashType::SinglePlusAnyoneCanPay {
183        return Err(Status::invalid_argument(format!(
184            "Input signature has wrong sighash type, SinglePlusAnyoneCanPay expected, got {}",
185            input_signature.sighash_type
186        )));
187    }
188
189    let input_outpoint: OutPoint = params
190        .input_outpoint
191        .ok_or_else(error::input_ended_prematurely)?
192        .try_into()?;
193
194    let users_intent_script_pubkey = ScriptBuf::from_bytes(params.output_script_pubkey);
195
196    Ok((
197        params.withdrawal_id,
198        input_signature,
199        input_outpoint,
200        users_intent_script_pubkey,
201        Amount::from_sat(params.output_amount),
202    ))
203}