// 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. //! //! ## IMPORTANT: Byte Order (Endianness) //! //! Bitcoin hash types (Txid, BlockHash, TokenID, OutPointHash, PoolID) use **reversed byte display**. //! When a hash is displayed as hex (e.g., in block explorers or APIs), it shows the bytes in //! reverse order compared to how they're stored internally. //! //! This means: //! - `hex.parse::()` reverses the input bytes //! - `Txid::to_hex()` reverses the output bytes //! - `Txid::into_inner()` / `Txid::from_byte_array()` work with internal (non-reversed) bytes //! //! **NEVER use `hex::decode()` directly on user-provided hash hex strings!** //! Instead, use `display_hex_to_blob::(hex)` or `hex.parse::()?.to_blob()`. //! //! **PubkeyHash is the exception** - it does NOT reverse bytes. use anyhow::{Context, Result}; use std::str::FromStr; 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.as_byte_array().to_vec() } } impl ToBlob for BlockHash { fn to_blob(&self) -> Vec { self.as_byte_array().to_vec() } } impl ToBlob for TokenID { fn to_blob(&self) -> Vec { self.as_byte_array().to_vec() } } impl ToBlob for OutPointHash { fn to_blob(&self) -> Vec { self.as_byte_array().to_vec() } } impl ToBlob for PoolID { fn to_blob(&self) -> Vec { self.as_byte_array().to_vec() } } // Implement ToBlob for 20-byte PubkeyHash impl ToBlob for PubkeyHash { fn to_blob(&self) -> Vec { self.as_byte_array().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_byte_array(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_byte_array(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_byte_array(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_byte_array(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_byte_array(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_byte_array(arr)) } } /// Convert a display-format hex string to blob bytes for database storage. /// /// This correctly handles the byte reversal that Bitcoin hash types use. /// Use this instead of `hex::decode()` for hash types! /// /// # Example /// ```ignore /// // User provides token ID in display format (from API, block explorer, etc.) /// let user_input = "abcd1234..."; /// let blob = display_hex_to_blob::(user_input)?; /// // Now `blob` can be used in SQL queries against BLOB columns /// ``` pub fn display_hex_to_blob(hex: &str) -> Result> where T: FromStr + ToBlob, { let hash = T::from_str(hex).map_err(|_| anyhow::anyhow!("invalid hash hex"))?; Ok(hash.to_blob()) } /// Convert blob bytes from database to display-format hex string. /// /// This correctly handles the byte reversal that Bitcoin hash types use. /// Use this instead of relying on SQL `hex()` function for hash types! /// /// # Example /// ```ignore /// let blob: Vec = row.get(0)?; /// let display_hex = blob_to_display_hex::(&blob)?; /// // Now `display_hex` is in the format users expect /// ``` pub fn blob_to_display_hex(blob: &[u8]) -> Result where T: FromBlob + std::fmt::Display, { let hash = T::from_blob(blob)?; Ok(hash.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_txid_roundtrip() { let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; let txid = hex.parse::().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_string(), hex); } #[test] fn test_blockhash_roundtrip() { let hex = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; let hash = hex.parse::().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 = hex.parse::().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 = hex.parse::().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 = hex.parse::().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()); } // ==================== ENDIANNESS TESTS ==================== // These tests use asymmetric (non-palindromic) hex values to catch byte-order bugs. // The original tests used values like [0x11; 32] which are the same reversed. /// Proves that hex::decode() differs from from_hex().to_blob() for Bitcoin hash types. /// This is the core test that would have caught the LE/BE bug. #[test] fn test_hex_decode_differs_from_display_hex_to_blob() { // Asymmetric hex - NOT a palindrome, so reversal is detectable let display_hex = "0102030405060708091011121314151617181920212223242526272829303132"; // WRONG: raw hex::decode (no byte reversal) let raw_bytes = hex::decode(display_hex).unwrap(); // CORRECT: parse as TokenID then get blob (handles reversal) let token = display_hex.parse::().unwrap(); let blob_bytes = token.to_blob(); // These MUST be different because Bitcoin hashes reverse bytes for display assert_ne!( raw_bytes, blob_bytes, "hex::decode and to_blob MUST differ for Bitcoin hash types!" ); // Verify the reversal: first byte of display becomes last byte of blob assert_eq!(raw_bytes[0], 0x01, "raw_bytes should start with 0x01"); assert_eq!( blob_bytes[0], 0x32, "blob_bytes should start with 0x32 (reversed)" ); assert_eq!(raw_bytes[31], 0x32, "raw_bytes should end with 0x32"); assert_eq!( blob_bytes[31], 0x01, "blob_bytes should end with 0x01 (reversed)" ); } /// Proves that SQL hex(blob) would produce wrong format compared to to_hex(). #[test] fn test_sql_hex_vs_to_hex() { let display_hex = "0102030405060708091011121314151617181920212223242526272829303132"; let token = display_hex.parse::().unwrap(); let blob = token.to_blob(); // Simulate what SQL hex() does: just uppercase hex of raw bytes (no reversal) let sql_hex_output = hex::encode(&blob).to_lowercase(); // SQL hex() output is NOT what users expect assert_ne!( sql_hex_output, display_hex, "SQL hex() should NOT match display format" ); // Correct way: parse blob back to TokenID and use to_hex() let recovered = TokenID::from_blob(&blob).unwrap(); assert_eq!( recovered.to_string(), display_hex, "to_hex() should match original display format" ); } /// Test the helper function display_hex_to_blob #[test] fn test_display_hex_to_blob_helper() { let display_hex = "0102030405060708091011121314151617181920212223242526272829303132"; // Helper should produce same result as manual from_hex + to_blob let helper_result = display_hex_to_blob::(display_hex).unwrap(); let manual_result = display_hex.parse::().unwrap().to_blob(); assert_eq!(helper_result, manual_result); // And it should NOT equal raw hex::decode let raw_decode = hex::decode(display_hex).unwrap(); assert_ne!(helper_result, raw_decode); } /// Test the helper function blob_to_display_hex #[test] fn test_blob_to_display_hex_helper() { let display_hex = "0102030405060708091011121314151617181920212223242526272829303132"; let token = display_hex.parse::().unwrap(); let blob = token.to_blob(); // Helper should produce correct display format let helper_result = blob_to_display_hex::(&blob).unwrap(); assert_eq!(helper_result, display_hex); // Raw hex::encode should NOT match let raw_encode = hex::encode(&blob); assert_ne!(raw_encode, display_hex); } /// Confirm PubkeyHash does NOT reverse bytes (it's the exception). #[test] fn test_pubkeyhash_does_not_reverse() { let hex_str = "0102030405060708091011121314151617181920"; // For PubkeyHash, raw hex::decode SHOULD equal to_blob let raw_bytes = hex::decode(hex_str).unwrap(); let pkh = hex_str.parse::().unwrap(); let blob_bytes = pkh.to_blob(); assert_eq!(raw_bytes, blob_bytes, "PubkeyHash should NOT reverse bytes"); } /// Test all Bitcoin hash types that DO reverse bytes. #[test] fn test_all_reversed_types() { let display_hex = "0102030405060708091011121314151617181920212223242526272829303132"; let raw_bytes = hex::decode(display_hex).unwrap(); // Txid reverses let txid_blob = display_hex.parse::().unwrap().to_blob(); assert_ne!(raw_bytes, txid_blob, "Txid should reverse"); // BlockHash reverses let blockhash_blob = display_hex.parse::().unwrap().to_blob(); assert_ne!(raw_bytes, blockhash_blob, "BlockHash should reverse"); // TokenID reverses let tokenid_blob = display_hex.parse::().unwrap().to_blob(); assert_ne!(raw_bytes, tokenid_blob, "TokenID should reverse"); // OutPointHash reverses let outpoint_blob = display_hex.parse::().unwrap().to_blob(); assert_ne!(raw_bytes, outpoint_blob, "OutPointHash should reverse"); // PoolID reverses let poolid_blob = display_hex.parse::().unwrap().to_blob(); assert_ne!(raw_bytes, poolid_blob, "PoolID should reverse"); } }