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 crate::EVMAddress;
6use bitcoin::{
7    address::NetworkUnchecked,
8    block,
9    consensus::{deserialize, serialize, Decodable, Encodable},
10    hashes::Hash,
11    hex::DisplayHex,
12    secp256k1::{schnorr, Message, PublicKey},
13    Address, OutPoint, ScriptBuf, TxOut, Txid, XOnlyPublicKey,
14};
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
281impl_bytea_wrapper_custom!(
282    MessageDB,
283    Message,
284    |msg: &Message| *msg, // Message is Copy, which requires this hack
285    |x: &[u8]| -> Result<Message, BoxDynError> { Ok(Message::from_digest(x.try_into()?)) }
286);
287
288use crate::rpc::clementine::DepositSignatures;
289impl_bytea_wrapper_custom!(
290    SignaturesDB,
291    DepositSignatures,
292    |signatures: &DepositSignatures| { signatures.encode_to_vec() },
293    |x: &[u8]| -> Result<DepositSignatures, BoxDynError> {
294        DepositSignatures::decode(x).map_err(Into::into)
295    }
296);
297
298use crate::rpc::clementine::DepositParams;
299impl_bytea_wrapper_custom!(
300    DepositParamsDB,
301    DepositParams,
302    |deposit_params: &DepositParams| { deposit_params.encode_to_vec() },
303    |x: &[u8]| -> Result<DepositParams, BoxDynError> {
304        DepositParams::decode(x).map_err(Into::into)
305    }
306);
307
308impl_bytea_wrapper_custom!(
309    ScriptBufDB,
310    ScriptBuf,
311    |script: &ScriptBuf| serialize(script),
312    |x: &[u8]| -> Result<ScriptBuf, BoxDynError> { deserialize(x).map_err(Into::into) }
313);
314
315impl_text_wrapper_custom!(
316    BlockHeaderDB,
317    block::Header,
318    |header: &block::Header| {
319        let mut bytes = Vec::new();
320        header
321            .consensus_encode(&mut bytes)
322            .expect("exceeded max Vec size or ran out of memory");
323        bytes.to_hex_string(bitcoin::hex::Case::Lower)
324    },
325    |s: &str| -> Result<block::Header, BoxDynError> {
326        let bytes = hex::decode(s)?;
327        block::Header::consensus_decode(&mut bytes.as_slice()).map_err(Into::into)
328    }
329);
330
331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow)]
332pub struct UtxoDB {
333    pub outpoint_db: OutPointDB,
334    pub txout_db: TxOutDB,
335}
336
337impl_text_wrapper_custom!(
338    TxOutDB,
339    TxOut,
340    |txout: &TxOut| bitcoin::consensus::encode::serialize_hex(&txout),
341    |s: &str| -> Result<TxOut, BoxDynError> {
342        bitcoin::consensus::encode::deserialize_hex(s)
343            .map_err(|e| Box::new(e) as sqlx::error::BoxDynError)
344    }
345);
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::{
351        bitvm_client::{self, SECP},
352        database::Database,
353        musig2,
354        rpc::clementine::TaggedSignature,
355        test::common::*,
356        EVMAddress,
357    };
358    use bitcoin::{
359        block::{self, Version},
360        hashes::Hash,
361        key::Keypair,
362        secp256k1::{schnorr::Signature, SecretKey},
363        Amount, BlockHash, CompactTarget, OutPoint, ScriptBuf, TxMerkleNode, TxOut, Txid,
364    };
365    use secp256k1::{musig::AggregatedNonce, SECP256K1};
366    use sqlx::{Executor, Type};
367
368    macro_rules! test_encode_decode_invariant {
369        ($db_type:ty, $inner:ty, $db_wrapper:expr, $table_name:expr, $column_type:expr) => {
370            let db_wrapper = $db_wrapper;
371
372            let config = create_test_config_with_thread_name().await;
373            let database = Database::new(&config).await.unwrap();
374
375            // Create table if it doesn't exist
376            database
377                .connection
378                .execute(sqlx::query(&format!(
379                    "CREATE TABLE IF NOT EXISTS {} ({} {} PRIMARY KEY)",
380                    $table_name, $table_name, $column_type
381                )))
382                .await
383                .unwrap();
384
385            // Insert the value
386            database
387                .connection
388                .execute(
389                    sqlx::query(&format!(
390                        "INSERT INTO {} ({}) VALUES ($1)",
391                        $table_name, $table_name
392                    ))
393                    .bind(db_wrapper.clone()),
394                )
395                .await
396                .unwrap();
397
398            // Retrieve the value
399            let retrieved: $db_type = sqlx::query_scalar(&format!(
400                "SELECT {} FROM {} WHERE {} = $1",
401                $table_name, $table_name, $table_name
402            ))
403            .bind(db_wrapper.clone())
404            .fetch_one(&database.connection)
405            .await
406            .unwrap();
407
408            // Verify the retrieved value matches the original
409            assert_eq!(retrieved, db_wrapper);
410
411            // Clean up
412            database
413                .connection
414                .execute(sqlx::query(&format!("DROP TABLE {}", $table_name)))
415                .await
416                .unwrap();
417        };
418    }
419    #[tokio::test]
420    async fn outpoint_encode_decode_invariant() {
421        assert_eq!(
422            OutPointDB::type_info(),
423            sqlx::postgres::PgTypeInfo::with_name("TEXT")
424        );
425
426        test_encode_decode_invariant!(
427            OutPointDB,
428            OutPoint,
429            OutPointDB(OutPoint {
430                txid: Txid::all_zeros(),
431                vout: 0x45
432            }),
433            "outpoint",
434            "TEXT"
435        );
436    }
437
438    #[tokio::test]
439    async fn txoutdb_encode_decode_invariant() {
440        assert_eq!(
441            TxOutDB::type_info(),
442            sqlx::postgres::PgTypeInfo::with_name("TEXT")
443        );
444
445        test_encode_decode_invariant!(
446            TxOutDB,
447            TxOut,
448            TxOutDB(TxOut {
449                value: Amount::from_sat(0x45),
450                script_pubkey: ScriptBuf::new(),
451            }),
452            "txout",
453            "TEXT"
454        );
455    }
456
457    #[tokio::test]
458    async fn addressdb_encode_decode_invariant() {
459        assert_eq!(
460            AddressDB::type_info(),
461            sqlx::postgres::PgTypeInfo::with_name("TEXT")
462        );
463
464        let address = bitcoin::Address::p2tr(
465            &SECP,
466            *bitvm_client::UNSPENDABLE_XONLY_PUBKEY,
467            None,
468            bitcoin::Network::Regtest,
469        );
470        let address = AddressDB(address.as_unchecked().clone());
471
472        test_encode_decode_invariant!(
473            AddressDB,
474            Address<NetworkUnchecked>,
475            address,
476            "address",
477            "TEXT"
478        );
479    }
480
481    #[tokio::test]
482    async fn evmaddressdb_encode_decode_invariant() {
483        assert_eq!(
484            EVMAddressDB::type_info(),
485            sqlx::postgres::PgTypeInfo::with_name("TEXT")
486        );
487
488        let evmaddress = EVMAddressDB(EVMAddress([0x45u8; 20]));
489        test_encode_decode_invariant!(EVMAddressDB, EVMAddress, evmaddress, "evmaddress", "TEXT");
490    }
491
492    #[tokio::test]
493    async fn txiddb_encode_decode_invariant() {
494        assert_eq!(
495            TxidDB::type_info(),
496            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
497        );
498
499        let txid = TxidDB(Txid::all_zeros());
500        test_encode_decode_invariant!(TxidDB, Txid, txid, "txid", "BYTEA");
501    }
502
503    #[tokio::test]
504    async fn signaturedb_encode_decode_invariant() {
505        assert_eq!(
506            SignatureDB::type_info(),
507            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
508        );
509
510        let signature = SignatureDB(Signature::from_slice(&[0u8; 64]).unwrap());
511        test_encode_decode_invariant!(SignatureDB, Signature, signature, "signature", "BYTEA");
512    }
513
514    #[tokio::test]
515    async fn signaturesdb_encode_decode_invariant() {
516        assert_eq!(
517            SignaturesDB::type_info(),
518            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
519        );
520
521        use crate::rpc::clementine::{
522            DepositSignatures, NormalSignatureKind, NumberedSignatureKind,
523        };
524        let signatures = DepositSignatures {
525            signatures: vec![
526                TaggedSignature {
527                    signature: vec![0x1Fu8; 64],
528                    signature_id: Some(NormalSignatureKind::NotStored.into()),
529                },
530                TaggedSignature {
531                    signature: vec![0x45u8; 64],
532                    signature_id: Some((NumberedSignatureKind::NumberedNotStored, 1).into()),
533                },
534            ],
535        };
536        test_encode_decode_invariant!(
537            SignaturesDB,
538            DepositSignatures,
539            SignaturesDB(signatures),
540            "signatures",
541            "BYTEA"
542        );
543    }
544
545    #[tokio::test]
546    async fn utxodb_json_encode_decode_invariant() {
547        use sqlx::types::Json;
548
549        assert_eq!(
550            Json::<UtxoDB>::type_info(),
551            sqlx::postgres::PgTypeInfo::with_name("JSONB")
552        );
553
554        let utxodb = UtxoDB {
555            outpoint_db: OutPointDB(OutPoint {
556                txid: Txid::all_zeros(),
557                vout: 0x45,
558            }),
559            txout_db: TxOutDB(TxOut {
560                value: Amount::from_sat(0x45),
561                script_pubkey: ScriptBuf::new(),
562            }),
563        };
564
565        test_encode_decode_invariant!(Json<UtxoDB>, Utxodb, Json(utxodb), "utxodb", "JSONB");
566    }
567
568    #[tokio::test]
569    async fn blockhashdb_encode_decode_invariant() {
570        assert_eq!(
571            OutPointDB::type_info(),
572            sqlx::postgres::PgTypeInfo::with_name("TEXT")
573        );
574
575        let blockhash = BlockHashDB(BlockHash::all_zeros());
576        test_encode_decode_invariant!(BlockHashDB, BlockHash, blockhash, "blockhash", "TEXT");
577    }
578
579    #[tokio::test]
580    async fn blockheaderdb_encode_decode_invariant() {
581        assert_eq!(
582            OutPointDB::type_info(),
583            sqlx::postgres::PgTypeInfo::with_name("TEXT")
584        );
585
586        let blockheader = BlockHeaderDB(block::Header {
587            version: Version::TWO,
588            prev_blockhash: BlockHash::all_zeros(),
589            merkle_root: TxMerkleNode::all_zeros(),
590            time: 0,
591            bits: CompactTarget::default(),
592            nonce: 0,
593        });
594        test_encode_decode_invariant!(
595            BlockHeaderDB,
596            block::Header,
597            blockheader,
598            "blockheader",
599            "TEXT"
600        );
601    }
602
603    #[tokio::test]
604    async fn musigpubnoncedb_encode_decode_invariant() {
605        assert_eq!(
606            MusigPubNonceDB::type_info(),
607            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
608        );
609
610        let kp = Keypair::from_secret_key(&SECP, &SecretKey::from_slice(&[1u8; 32]).unwrap());
611        let (_sec_nonce, pub_nonce) = musig2::nonce_pair(&kp).unwrap();
612        let public_nonce = MusigPubNonceDB(pub_nonce);
613        test_encode_decode_invariant!(
614            MusigPubNonceDB,
615            PublicNonce,
616            public_nonce,
617            "public_nonce",
618            "BYTEA"
619        );
620    }
621
622    #[tokio::test]
623    async fn musigaggnoncedb_encode_decode_invariant() {
624        assert_eq!(
625            MusigAggNonceDB::type_info(),
626            sqlx::postgres::PgTypeInfo::with_name("BYTEA")
627        );
628
629        let kp = Keypair::from_secret_key(&SECP, &SecretKey::from_slice(&[1u8; 32]).unwrap());
630        let (_sec_nonce, pub_nonce) = musig2::nonce_pair(&kp).unwrap();
631        let aggregated_nonce = MusigAggNonceDB(AggregatedNonce::new(SECP256K1, &[&pub_nonce]));
632        test_encode_decode_invariant!(
633            MusigAggNonceDB,
634            AggregatedNonce,
635            aggregated_nonce,
636            "aggregated_nonce",
637            "BYTEA"
638        );
639    }
640}