180 lines
5.2 KiB
Rust
180 lines
5.2 KiB
Rust
|
|
// 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<u8>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Trait for converting BLOB bytes back to hash types.
|
||
|
|
pub trait FromBlob: Sized {
|
||
|
|
fn from_blob(bytes: &[u8]) -> Result<Self>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implement ToBlob for 32-byte hash types
|
||
|
|
impl ToBlob for Txid {
|
||
|
|
fn to_blob(&self) -> Vec<u8> {
|
||
|
|
self.into_inner().to_vec()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ToBlob for BlockHash {
|
||
|
|
fn to_blob(&self) -> Vec<u8> {
|
||
|
|
self.into_inner().to_vec()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ToBlob for TokenID {
|
||
|
|
fn to_blob(&self) -> Vec<u8> {
|
||
|
|
self.into_inner().to_vec()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ToBlob for OutPointHash {
|
||
|
|
fn to_blob(&self) -> Vec<u8> {
|
||
|
|
self.into_inner().to_vec()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ToBlob for PoolID {
|
||
|
|
fn to_blob(&self) -> Vec<u8> {
|
||
|
|
self.into_inner().to_vec()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implement ToBlob for 20-byte PubkeyHash
|
||
|
|
impl ToBlob for PubkeyHash {
|
||
|
|
fn to_blob(&self) -> Vec<u8> {
|
||
|
|
self.into_inner().to_vec()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Implement FromBlob for 32-byte hash types
|
||
|
|
impl FromBlob for Txid {
|
||
|
|
fn from_blob(bytes: &[u8]) -> Result<Self> {
|
||
|
|
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<Self> {
|
||
|
|
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<Self> {
|
||
|
|
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<Self> {
|
||
|
|
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<Self> {
|
||
|
|
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<Self> {
|
||
|
|
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());
|
||
|
|
}
|
||
|
|
}
|