// Copyright (C) 2024-2026 Whiterun LLC // // This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later. // A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html //! BLOB storage helpers for efficient database storage of hash types. //! //! Converts 32-byte hashes from TEXT (64-char hex) to BLOB (32 bytes) for ~50% storage savings. use anyhow::{Context, Result}; use bitcoin_hashes::Hash; use bitcoincash::{BlockHash, PubkeyHash, TokenID, Txid}; use riftenlabs_defi::chainutil::OutPointHash; use crate::def::PoolID; /// Trait for converting hash types to BLOB bytes for database storage. pub trait ToBlob { fn to_blob(&self) -> Vec; } /// Trait for converting BLOB bytes back to hash types. pub trait FromBlob: Sized { fn from_blob(bytes: &[u8]) -> Result; } // Implement ToBlob for 32-byte hash types impl ToBlob for Txid { fn to_blob(&self) -> Vec { self.into_inner().to_vec() } } impl ToBlob for BlockHash { fn to_blob(&self) -> Vec { self.into_inner().to_vec() } } impl ToBlob for TokenID { fn to_blob(&self) -> Vec { self.into_inner().to_vec() } } impl ToBlob for OutPointHash { fn to_blob(&self) -> Vec { self.into_inner().to_vec() } } impl ToBlob for PoolID { fn to_blob(&self) -> Vec { self.into_inner().to_vec() } } // Implement ToBlob for 20-byte PubkeyHash impl ToBlob for PubkeyHash { fn to_blob(&self) -> Vec { self.into_inner().to_vec() } } // Implement FromBlob for 32-byte hash types impl FromBlob for Txid { fn from_blob(bytes: &[u8]) -> Result { let arr: [u8; 32] = bytes.try_into().context("Txid blob must be 32 bytes")?; Ok(Txid::from_inner(arr)) } } impl FromBlob for BlockHash { fn from_blob(bytes: &[u8]) -> Result { let arr: [u8; 32] = bytes .try_into() .context("BlockHash blob must be 32 bytes")?; Ok(BlockHash::from_inner(arr)) } } impl FromBlob for TokenID { fn from_blob(bytes: &[u8]) -> Result { let arr: [u8; 32] = bytes.try_into().context("TokenID blob must be 32 bytes")?; Ok(TokenID::from_inner(arr)) } } impl FromBlob for OutPointHash { fn from_blob(bytes: &[u8]) -> Result { let arr: [u8; 32] = bytes .try_into() .context("OutPointHash blob must be 32 bytes")?; Ok(OutPointHash::from_inner(arr)) } } impl FromBlob for PoolID { fn from_blob(bytes: &[u8]) -> Result { let arr: [u8; 32] = bytes.try_into().context("PoolID blob must be 32 bytes")?; Ok(PoolID::from_inner(arr)) } } // Implement FromBlob for 20-byte PubkeyHash impl FromBlob for PubkeyHash { fn from_blob(bytes: &[u8]) -> Result { let arr: [u8; 20] = bytes .try_into() .context("PubkeyHash blob must be 20 bytes")?; Ok(PubkeyHash::from_inner(arr)) } } #[cfg(test)] mod tests { use super::*; use bitcoin_hashes::hex::{FromHex, ToHex}; #[test] fn test_txid_roundtrip() { let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; let txid = Txid::from_hex(hex).unwrap(); let blob = txid.to_blob(); assert_eq!(blob.len(), 32); let recovered = Txid::from_blob(&blob).unwrap(); assert_eq!(txid, recovered); assert_eq!(recovered.to_hex(), hex); } #[test] fn test_blockhash_roundtrip() { let hex = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; let hash = BlockHash::from_hex(hex).unwrap(); let blob = hash.to_blob(); assert_eq!(blob.len(), 32); let recovered = BlockHash::from_blob(&blob).unwrap(); assert_eq!(hash, recovered); } #[test] fn test_token_id_roundtrip() { let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; let token = TokenID::from_hex(hex).unwrap(); let blob = token.to_blob(); assert_eq!(blob.len(), 32); let recovered = TokenID::from_blob(&blob).unwrap(); assert_eq!(token, recovered); } #[test] fn test_outpointhash_roundtrip() { let hex = "1111111111111111111111111111111111111111111111111111111111111111"; let hash = OutPointHash::from_hex(hex).unwrap(); let blob = hash.to_blob(); assert_eq!(blob.len(), 32); let recovered = OutPointHash::from_blob(&blob).unwrap(); assert_eq!(hash, recovered); } #[test] fn test_pubkeyhash_roundtrip() { let hex = "0123456789abcdef01230123456789abcdef0123"; let pkh = PubkeyHash::from_hex(hex).unwrap(); let blob = pkh.to_blob(); assert_eq!(blob.len(), 20); let recovered = PubkeyHash::from_blob(&blob).unwrap(); assert_eq!(pkh, recovered); } #[test] fn test_invalid_blob_size() { let short_blob = vec![0u8; 16]; assert!(Txid::from_blob(&short_blob).is_err()); assert!(BlockHash::from_blob(&short_blob).is_err()); assert!(TokenID::from_blob(&short_blob).is_err()); assert!(PubkeyHash::from_blob(&short_blob).is_err()); } }