clementine_core/database/
wrapper.rs

1//! # Type Wrappers for Parsing
2//!
3//! This module includes wrappers for easy parsing of the foreign types.
4
5use bitcoin::{
6    address::NetworkUnchecked,
7    block,
8    consensus::{deserialize, serialize, Decodable, Encodable},
9    hashes::Hash,
10    hex::DisplayHex,
11    secp256k1::{schnorr, Message, PublicKey},
12    Address, OutPoint, ScriptBuf, TxOut, Txid, XOnlyPublicKey,
13};
14use clementine_primitives::EVMAddress;
15use eyre::eyre;
16use prost::Message as _;
17use risc0_zkvm::Receipt;
18use secp256k1::musig;
19use serde::{Deserialize, Serialize};
20use sqlx::{
21    error::BoxDynError,
22    postgres::{PgArgumentBuffer, PgValueRef},
23    Decode, Encode, Postgres,
24};
25use std::str::FromStr;
26
27/// Macro to reduce boilerplate for [`impl_text_wrapper_custom`].
28///
29/// Implements the Type, Encode and Decode traits for a wrapper type.
30/// Assumes the type is declared.
31macro_rules! impl_text_wrapper_base {
32    ($wrapper:ident, $inner:ty, $encode:expr, $decode:expr) => {
33        impl sqlx::Type<sqlx::Postgres> for $wrapper {
34            fn type_info() -> sqlx::postgres::PgTypeInfo {
35                sqlx::postgres::PgTypeInfo::with_name("TEXT")
36            }
37        }
38
39        impl Encode<'_, Postgres> for $wrapper {
40            fn encode_by_ref(
41                &self,
42                buf: &mut PgArgumentBuffer,
43            ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
44                let s = $encode(&self.0);
45                <&str as Encode<Postgres>>::encode_by_ref(&s.as_str(), buf)
46            }
47        }
48
49        impl<'r> Decode<'r, Postgres> for $wrapper {
50            fn decode(value: PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
51                let s = <&str as Decode<Postgres>>::decode(value)?;
52                Ok(Self($decode(s)?))
53            }
54        }
55    };
56}
57
58/// Macro for implementing text-based SQL wrapper types with custom encoding/decoding
59///
60/// # Parameters
61/// - `$wrapper`: The name of the wrapper type to create
62/// - `$inner`: The inner type being wrapped
63/// - `$encode`: Expression for converting inner type to string
64/// - `$decode`: Expression for converting string back to inner type
65///
66/// The macro creates a new type that wraps the inner type and implements:
67/// - SQLx Type trait to indicate TEXT column type
68/// - SQLx Encode trait for converting to database format
69/// - SQLx Decode trait for converting from database format
70macro_rules! impl_text_wrapper_custom {
71    // Default case (include serde)
72    ($wrapper:ident, $inner:ty, $encode:expr, $decode:expr) => {
73        impl_text_wrapper_custom!($wrapper, $inner, $encode, $decode, true);
74    };
75
76    // true case - with serde
77    ($wrapper:ident, $inner:ty, $encode:expr, $decode:expr, true) => {
78        #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, PartialEq)]
79        pub struct $wrapper(pub $inner);
80
81        impl_text_wrapper_base!($wrapper, $inner, $encode, $decode);
82    };
83
84    // false case - without serde
85    ($wrapper:ident, $inner:ty, $encode:expr, $decode:expr, false) => {
86        #[derive(sqlx::FromRow, Debug, Clone, PartialEq)]
87        pub struct $wrapper(pub $inner);
88
89        impl_text_wrapper_base!($wrapper, $inner, $encode, $decode);
90    };
91}
92
93/// Macro for implementing BYTEA-based SQL wrapper types with custom encoding/decoding
94///
95/// # Parameters
96/// - `$wrapper`: The name of the wrapper type to create
97/// - `$inner`: The inner type being wrapped
98/// - `$encode`: Expression for converting inner type to bytes
99/// - `$decode`: Expression for converting bytes back to inner type
100///
101/// The macro creates a new type that wraps the inner type and implements:
102/// - SQLx Type trait to indicate BYTEA column type
103/// - SQLx Encode trait for converting to database format
104/// - SQLx Decode trait for converting from database format
105macro_rules! impl_bytea_wrapper_custom {
106    ($wrapper:ident, $inner:ty, $encode:expr, $decode:expr) => {
107        #[derive(sqlx::FromRow, Debug, Clone, PartialEq)]
108        pub struct $wrapper(pub $inner);
109
110        impl sqlx::Type<sqlx::Postgres> for $wrapper {
111            fn type_info() -> sqlx::postgres::PgTypeInfo {
112                sqlx::postgres::PgTypeInfo::with_name("BYTEA")
113            }
114        }
115
116        impl Encode<'_, Postgres> for $wrapper {
117            fn encode_by_ref(
118                &self,
119                buf: &mut PgArgumentBuffer,
120            ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
121                let bytes = $encode(&self.0);
122                <&[u8] as Encode<Postgres>>::encode(bytes.as_ref(), buf)
123            }
124        }
125
126        impl<'r> Decode<'r, Postgres> for $wrapper {
127            fn decode(value: PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
128                let bytes = <Vec<u8> as Decode<Postgres>>::decode(value)?;
129                Ok(Self($decode(&bytes)?))
130            }
131        }
132    };
133}
134
135/// Same as `impl_bytea_wrapper_custom` but with an encode function that returns a Result
136macro_rules! impl_bytea_wrapper_custom_with_error {
137    ($wrapper:ident, $inner:ty, $encode:expr, $decode:expr) => {
138        #[derive(sqlx::FromRow, Debug, Clone)]
139        pub struct $wrapper(pub $inner);
140
141        impl sqlx::Type<sqlx::Postgres> for $wrapper {
142            fn type_info() -> sqlx::postgres::PgTypeInfo {
143                sqlx::postgres::PgTypeInfo::with_name("BYTEA")
144            }
145        }
146
147        impl Encode<'_, Postgres> for $wrapper {
148            fn encode_by_ref(
149                &self,
150                buf: &mut PgArgumentBuffer,
151            ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
152                let bytes = $encode(&self.0)?;
153                <&[u8] as Encode<Postgres>>::encode(bytes.as_ref(), buf)
154            }
155        }
156
157        impl<'r> Decode<'r, Postgres> for $wrapper {
158            fn decode(value: PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
159                let bytes = <Vec<u8> as Decode<Postgres>>::decode(value)?;
160                Ok(Self($decode(&bytes)?))
161            }
162        }
163    };
164}
165
166/// Macro for implementing BYTEA-based SQL wrapper types using standard serialization
167///
168/// This macro creates a wrapper type that uses the inner type's default serialization
169/// methods (`serialize()` and `from_slice()`) for encoding/decoding to/from BYTEA columns.
170///
171/// # Parameters
172/// - `$wrapper`: The name of the wrapper type to create
173/// - `$inner`: The inner type being wrapped
174///
175/// The macro creates a new type that wraps the inner type and implements:
176/// - SQLx Type trait to indicate BYTEA column type
177/// - SQLx Encode trait for converting to database format
178/// - SQLx Decode trait for converting from database format
179macro_rules! impl_bytea_wrapper_default {
180    ($wrapper:ident, $inner:ty) => {
181        impl_bytea_wrapper_custom!(
182            $wrapper,
183            $inner,
184            |x: &$inner| x.serialize(),
185            |x: &[u8]| -> Result<$inner, BoxDynError> {
186                <$inner>::from_slice(x).map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
187            }
188        );
189    };
190}
191
192/// Macro for implementing text-based SQL wrapper types using standard string conversion
193///
194/// This macro creates a wrapper type that uses the inner type's default string conversion
195/// methods (`to_string()` and `from_str()`) for encoding/decoding to/from TEXT columns.
196///
197/// # Parameters
198/// - `$wrapper`: The name of the wrapper type to create
199/// - `$inner`: The inner type being wrapped
200///
201/// The macro creates a new type that wraps the inner type and implements:
202/// - SQLx Type trait to indicate TEXT column type
203/// - SQLx Encode trait for converting to database format
204/// - SQLx Decode trait for converting from database format
205macro_rules! impl_text_wrapper_default {
206    ($wrapper:ident, $inner:ty) => {
207        impl_text_wrapper_custom!(
208            $wrapper,
209            $inner,
210            <$inner as ToString>::to_string,
211            <$inner as FromStr>::from_str
212        );
213    };
214}
215
216impl_text_wrapper_default!(OutPointDB, OutPoint);
217impl_text_wrapper_default!(BlockHashDB, block::BlockHash);
218impl_text_wrapper_default!(PublicKeyDB, PublicKey);
219impl_text_wrapper_default!(XOnlyPublicKeyDB, XOnlyPublicKey);
220
221impl_bytea_wrapper_default!(SignatureDB, schnorr::Signature);
222
223impl_bytea_wrapper_custom!(
224    MusigPubNonceDB,
225    musig::PublicNonce,
226    |pub_nonce: &musig::PublicNonce| pub_nonce.serialize(),
227    |x: &[u8]| -> Result<musig::PublicNonce, BoxDynError> {
228        let arr: &[u8; 66] = x
229            .try_into()
230            .map_err(|_| eyre!("Expected 66 bytes for PublicNonce"))?;
231        Ok(musig::PublicNonce::from_byte_array(arr)?)
232    }
233);
234
235impl_bytea_wrapper_custom!(
236    MusigAggNonceDB,
237    musig::AggregatedNonce,
238    |pub_nonce: &musig::AggregatedNonce| pub_nonce.serialize(),
239    |x: &[u8]| -> Result<musig::AggregatedNonce, BoxDynError> {
240        let arr: &[u8; 66] = x
241            .try_into()
242            .map_err(|_| eyre!("Expected 66 bytes for AggregatedNonce"))?;
243        Ok(musig::AggregatedNonce::from_byte_array(arr)?)
244    }
245);
246
247impl_bytea_wrapper_custom_with_error!(
248    ReceiptDB,
249    Receipt,
250    |lcp: &Receipt| -> Result<Vec<u8>, BoxDynError> { borsh::to_vec(lcp).map_err(Into::into) },
251    |x: &[u8]| -> Result<Receipt, BoxDynError> { borsh::from_slice(x).map_err(Into::into) }
252);
253
254impl_text_wrapper_custom!(
255    AddressDB,
256    Address<NetworkUnchecked>,
257    |addr: &Address<NetworkUnchecked>| addr.clone().assume_checked().to_string(),
258    |s: &str| Address::from_str(s)
259);
260
261impl_text_wrapper_custom!(
262    EVMAddressDB,
263    EVMAddress,
264    |addr: &EVMAddress| hex::encode(addr.0),
265    |s: &str| -> Result<EVMAddress, BoxDynError> {
266        let bytes = hex::decode(s).map_err(Box::new)?;
267
268        Ok(EVMAddress(bytes.try_into().map_err(|arr: Vec<u8>| {
269            eyre!("Failed to deserialize EVMAddress from {:?}", arr)
270        })?))
271    }
272);
273
274impl_bytea_wrapper_custom!(
275    TxidDB,
276    Txid,
277    |txid: &Txid| *txid, // Txid is Copy, which requires this hack
278    |x: &[u8]| -> Result<Txid, BoxDynError> { Ok(Txid::from_slice(x)?) }
279);
280
281// Enable binding Vec<TxidDB> as bytea[] in queries (e.g., WHERE txid = ANY($1))
282impl sqlx::postgres::PgHasArrayType for TxidDB {
283    fn array_type_info() -> sqlx::postgres::PgTypeInfo {
284        sqlx::postgres::PgTypeInfo::with_name("_bytea") // PostgreSQL array type for bytea
285    }
286}
287
288impl_bytea_wrapper_custom!(
289    MessageDB,
290    Message,
291    |msg: &Message| *msg, // Message is Copy, which requires this hack
292    |x: &[u8]| -> Result<Message, BoxDynError> { Ok(Message::from_digest(x.try_into()?)) }
293);
294
295use crate::rpc::clementine::DepositSignatures;
296impl_bytea_wrapper_custom!(
297    SignaturesDB,
298    DepositSignatures,
299    |signatures: &DepositSignatures| { signatures.encode_to_vec() },
300    |x: &[u8]| -> Result<DepositSignatures, BoxDynError> {
301        DepositSignatures::decode(x).map_err(Into::into)
302    }
303);
304
305use crate::rpc::clementine::DepositParams;
306impl_bytea_wrapper_custom!(
307    DepositParamsDB,
308    DepositParams,
309    |deposit_params: &DepositParams| { deposit_params.encode_to_vec() },
310    |x: &[u8]| -> Result<DepositParams, BoxDynError> {
311        DepositParams::decode(x).map_err(Into::into)
312    }
313);
314
315impl_bytea_wrapper_custom!(
316    ScriptBufDB,
317    ScriptBuf,
318    |script: &ScriptBuf| serialize(script),
319    |x: &[u8]| -> Result<ScriptBuf, BoxDynError> { deserialize(x).map_err(Into::into) }
320);
321
322impl_text_wrapper_custom!(
323    BlockHeaderDB,
324    block::Header,
325    |header: &block::Header| {
326        let mut bytes = Vec::new();
327        header
328            .consensus_encode(&mut bytes)
329            .expect("exceeded max Vec size or ran out of memory");
330        bytes.to_hex_string(bitcoin::hex::Case::Lower)
331    },
332    |s: &str| -> Result<block::Header, BoxDynError> {
333        let bytes = hex::decode(s)?;
334        block::Header::consensus_decode(&mut bytes.as_slice()).map_err(Into::into)
335    }
336);
337
338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow)]
339pub struct UtxoDB {
340    pub outpoint_db: OutPointDB,
341    pub txout_db: TxOutDB,
342}
343
344impl_text_wrapper_custom!(
345    TxOutDB,
346    TxOut,
347    |txout: &TxOut| bitcoin::consensus::encode::serialize_hex(&txout),
348    |s: &str| -> Result<TxOut, BoxDynError> {
349        bitcoin::consensus::encode::deserialize_hex(s)
350            .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
351    }
352);
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use crate::{
358        bitvm_client::{self, SECP},
359        database::Database,
360        musig2,
361        rpc::clementine::TaggedSignature,
362        test::common::*,
363    };
364    use bitcoin::{
365        block::{self, Version},
366        hashes::Hash,
367        key::Keypair,
368        secp256k1::{schnorr::Signature, SecretKey},
369        Amount, BlockHash, CompactTarget, OutPoint, ScriptBuf, TxMerkleNode, TxOut, Txid,
370    };
371    use clementine_primitives::EVMAddress;
372    use secp256k1::{musig::AggregatedNonce, SECP256K1};
373    use sqlx::{Executor, Type};
374
375    macro_rules! test_encode_decode_invariant {
376        ($db_type:ty, $inner:ty, $db_wrapper:expr, $table_name:expr, $column_type:expr) => {
377            let db_wrapper = $db_wrapper;
378
379            let config = create_test_config_with_thread_name().await;
380            let database = Database::new(&config).await.unwrap();
381
382            // Create table if it doesn't exist
383            database
384                .connection
385                .execute(sqlx::query(&format!(
386                    "CREATE TABLE IF NOT EXISTS {} ({} {} PRIMARY KEY)",
387                    $table_name, $table_name, $column_type
388                )))
389                .await
390                .unwrap();
391
392            // Insert the value
393            database
394                .connection
395                .execute(
396                    sqlx::query(&format!(
397                        "INSERT INTO {} ({}) VALUES ($1)",
398                        $table_name, $table_name
399                    ))
400                    .bind(db_wrapper.clone()),
401                )
402                .await
403                .unwrap();
404
405            // Retrieve the value
406            let retrieved: $db_type = sqlx::query_scalar(&format!(
407                "SELECT {} FROM {} WHERE {} = $1",
408                $table_name, $table_name, $table_name
409            ))
410            .bind(db_wrapper.clone())
411            .fetch_one(&database.connection)
412            .await
413            .unwrap();
414
415            // Verify the retrieved value matches the original
416            assert_eq!(retrieved, db_wrapper);
417
418            // Clean up
419            database
420                .connection
421                .execute(sqlx::query(&format!("DROP TABLE {}", $table_name)))
422                .await
423                .unwrap();
424        };
425    }
426    #[tokio::test]
427    async fn outpoint_encode_decode_invariant() {
428        assert_eq!(
429            OutPointDB::type_info(),
430            sqlx::postgres::PgTypeInfo::with_name("TEXT")
431        );
432
433        test_encode_decode_invariant!(
434            OutPointDB,
435            OutPoint,
436            OutPointDB(OutPoint {
437                txid: Txid::all_zeros(),
438                vout: 0x45
439            }),
440            "outpoint",
441            "TEXT"
442        );
443    }
444
445    #[tokio::test]
446    async fn txoutdb_encode_decode_invariant() {
447        assert_eq!(
448            TxOutDB::type_info(),
449            sqlx::postgres::PgTypeInfo::with_name("TEXT")
450        );
451
452        test_encode_decode_invariant!(
453            TxOutDB,
454            TxOut,
455            TxOutDB(TxOut {
456                value: Amount::from_sat(0x45),
457                script_pubkey: ScriptBuf::new(),
458            }),
459            "txout",
460            "TEXT"
461        );
462    }
463
464    #[tokio::test]
465    async fn addressdb_encode_decode_invariant() {
466        assert_eq!(
467            AddressDB::type_info(),
468            sqlx::postgres::PgTypeInfo::with_name("TEXT")
469        );
470
471        let address = bitcoin::Address::p2tr(
472            &SECP,
473            *bitvm_client::UNSPENDABLE_XONLY_PUBKEY,
474            None,
475            bitcoin::Network::Regtest,
476        );
477        let address = AddressDB(address.as_unchecked().clone());
478
479        test_encode_decode_invariant!(
480            AddressDB,
481            Address<NetworkUnchecked>,
482            address,
483            "address",
484            "TEXT"
485        );
486    }
487
488    #[tokio::test]
489    async fn evmaddressdb_encode_decode_invariant() {
490        assert_eq!(
491            EVMAddressDB::type_info(),
492            sqlx::postgres::PgTypeInfo::with_name("TEXT")
493        );
494
495        let evmaddress = EVMAddressDB(EVMAddress([0x45u8; 20]));
496        test_encode_decode_invariant!(EVMAddressDB, EVMAddress, evmaddress, "evmaddress", "TEXT");
497    }
498
499    #[tokio::test]
500    async fn txiddb_encode_decode_invariant() {
501        assert_eq!(
502            TxidDB::type_info(),
503            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
504        );
505
506        let txid = TxidDB(Txid::all_zeros());
507        test_encode_decode_invariant!(TxidDB, Txid, txid, "txid", "BYTEA");
508    }
509
510    #[tokio::test]
511    async fn signaturedb_encode_decode_invariant() {
512        assert_eq!(
513            SignatureDB::type_info(),
514            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
515        );
516
517        let signature = SignatureDB(Signature::from_slice(&[0u8; 64]).unwrap());
518        test_encode_decode_invariant!(SignatureDB, Signature, signature, "signature", "BYTEA");
519    }
520
521    #[tokio::test]
522    async fn signaturesdb_encode_decode_invariant() {
523        assert_eq!(
524            SignaturesDB::type_info(),
525            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
526        );
527
528        use crate::rpc::clementine::{
529            DepositSignatures, NormalSignatureKind, NumberedSignatureKind,
530        };
531        let signatures = DepositSignatures {
532            signatures: vec![
533                TaggedSignature {
534                    signature: vec![0x1Fu8; 64],
535                    signature_id: Some(NormalSignatureKind::NotStored.into()),
536                },
537                TaggedSignature {
538                    signature: vec![0x45u8; 64],
539                    signature_id: Some((NumberedSignatureKind::NumberedNotStored, 1).into()),
540                },
541            ],
542        };
543        test_encode_decode_invariant!(
544            SignaturesDB,
545            DepositSignatures,
546            SignaturesDB(signatures),
547            "signatures",
548            "BYTEA"
549        );
550    }
551
552    #[tokio::test]
553    async fn utxodb_json_encode_decode_invariant() {
554        use sqlx::types::Json;
555
556        assert_eq!(
557            Json::<UtxoDB>::type_info(),
558            sqlx::postgres::PgTypeInfo::with_name("JSONB")
559        );
560
561        let utxodb = UtxoDB {
562            outpoint_db: OutPointDB(OutPoint {
563                txid: Txid::all_zeros(),
564                vout: 0x45,
565            }),
566            txout_db: TxOutDB(TxOut {
567                value: Amount::from_sat(0x45),
568                script_pubkey: ScriptBuf::new(),
569            }),
570        };
571
572        test_encode_decode_invariant!(Json<UtxoDB>, Utxodb, Json(utxodb), "utxodb", "JSONB");
573    }
574
575    #[tokio::test]
576    async fn blockhashdb_encode_decode_invariant() {
577        assert_eq!(
578            OutPointDB::type_info(),
579            sqlx::postgres::PgTypeInfo::with_name("TEXT")
580        );
581
582        let blockhash = BlockHashDB(BlockHash::all_zeros());
583        test_encode_decode_invariant!(BlockHashDB, BlockHash, blockhash, "blockhash", "TEXT");
584    }
585
586    #[tokio::test]
587    async fn blockheaderdb_encode_decode_invariant() {
588        assert_eq!(
589            OutPointDB::type_info(),
590            sqlx::postgres::PgTypeInfo::with_name("TEXT")
591        );
592
593        let blockheader = BlockHeaderDB(block::Header {
594            version: Version::TWO,
595            prev_blockhash: BlockHash::all_zeros(),
596            merkle_root: TxMerkleNode::all_zeros(),
597            time: 0,
598            bits: CompactTarget::default(),
599            nonce: 0,
600        });
601        test_encode_decode_invariant!(
602            BlockHeaderDB,
603            block::Header,
604            blockheader,
605            "blockheader",
606            "TEXT"
607        );
608    }
609
610    #[tokio::test]
611    async fn musigpubnoncedb_encode_decode_invariant() {
612        assert_eq!(
613            MusigPubNonceDB::type_info(),
614            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
615        );
616
617        let kp = Keypair::from_secret_key(&SECP, &SecretKey::from_slice(&[1u8; 32]).unwrap());
618        let (_sec_nonce, pub_nonce) = musig2::nonce_pair(&kp).unwrap();
619        let public_nonce = MusigPubNonceDB(pub_nonce);
620        test_encode_decode_invariant!(
621            MusigPubNonceDB,
622            PublicNonce,
623            public_nonce,
624            "public_nonce",
625            "BYTEA"
626        );
627    }
628
629    #[tokio::test]
630    async fn musigaggnoncedb_encode_decode_invariant() {
631        assert_eq!(
632            MusigAggNonceDB::type_info(),
633            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
634        );
635
636        let kp = Keypair::from_secret_key(&SECP, &SecretKey::from_slice(&[1u8; 32]).unwrap());
637        let (_sec_nonce, pub_nonce) = musig2::nonce_pair(&kp).unwrap();
638        let aggregated_nonce = MusigAggNonceDB(AggregatedNonce::new(SECP256K1, &[&pub_nonce]));
639        test_encode_decode_invariant!(
640            MusigAggNonceDB,
641            AggregatedNonce,
642            aggregated_nonce,
643            "aggregated_nonce",
644            "BYTEA"
645        );
646    }
647}