use anyhow::{Context, Result};
use bitcoin_hashes::Hash;
use crate::chaindef::{OutPointHash, ScriptHash};
use crate::query::queryfilter::QueryFilter;
use crate::store::{db_decode, db_encode, Row};
use super::DBRow;
const SCRIPTHASHINDEX_CF: &str = "scripthash";
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub enum OutputFlags {
FundingNone = 1 << 1,
FundingHasTokens = 1 << 2,
SpentNone = 1 << 3,
SpentHasTokens = 1 << 4,
}
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub enum ScriptHashIndexValue {
Funding { txid: [u8; 32], vout: u32 },
Spending { txid: [u8; 32], vin: u32 },
}
const FLAGS_BYTE_OFFSET: usize = 36;
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub struct ScriptHashIndexKey {
pub scripthash: [u8; 32],
height_be: [u8; 4],
flags: u8,
outpointhash: [u8; 32],
}
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub struct ScriptHashIndexRow {
key: ScriptHashIndexKey,
value: ScriptHashIndexValue,
}
impl ScriptHashIndexRow {
pub fn new_funding(
scripthash: &ScriptHash,
outpointhash: &OutPointHash,
flags: OutputFlags,
txid: [u8; 32],
vout: u32,
height: u32,
) -> ScriptHashIndexRow {
debug_assert!(matches!(
flags,
OutputFlags::FundingNone | OutputFlags::FundingHasTokens
));
ScriptHashIndexRow {
key: ScriptHashIndexKey {
scripthash: scripthash.into_inner(),
height_be: height.to_be_bytes(),
flags: flags as u8,
outpointhash: outpointhash.into_inner(),
},
value: ScriptHashIndexValue::Funding { txid, vout },
}
}
pub fn new_spending(
scripthash: &ScriptHash,
outpointhash: &OutPointHash,
flags: OutputFlags,
txid: [u8; 32],
vin: u32,
height: u32,
) -> ScriptHashIndexRow {
debug_assert!(matches!(
flags,
OutputFlags::SpentNone | OutputFlags::SpentHasTokens
));
ScriptHashIndexRow {
key: ScriptHashIndexKey {
scripthash: scripthash.into_inner(),
height_be: height.to_be_bytes(),
flags: flags as u8,
outpointhash: outpointhash.into_inner(),
},
value: ScriptHashIndexValue::Spending { txid, vin },
}
}
pub fn filter_include_all(scripthash: [u8; 32]) -> Vec<u8> {
scripthash.to_vec()
}
pub fn filter_by_outputs_and_inputs(
scripthash: [u8; 32],
filter: &QueryFilter,
) -> (Vec<u8>, Option<(u8, usize)>) {
match (filter.token_only, filter.exclude_tokens) {
(true, false) => (
Self::filter_include_all(scripthash),
Some((
OutputFlags::FundingHasTokens as u8 | OutputFlags::SpentHasTokens as u8,
FLAGS_BYTE_OFFSET,
)),
),
(false, true) => (
Self::filter_include_all(scripthash),
Some((
OutputFlags::FundingNone as u8 | OutputFlags::SpentNone as u8,
FLAGS_BYTE_OFFSET,
)),
),
(true, true) => (
Self::filter_include_all(scripthash),
Some((0, FLAGS_BYTE_OFFSET)),
),
(false, false) => (Self::filter_include_all(scripthash), None),
}
}
pub fn filter_by_outputs(
scripthash: [u8; 32],
filter: &QueryFilter,
) -> (Vec<u8>, Option<(u8, usize)>) {
match (filter.token_only, filter.exclude_tokens) {
(true, false) => (
Self::filter_include_all(scripthash),
Some((OutputFlags::FundingHasTokens as u8, FLAGS_BYTE_OFFSET)),
),
(false, true) => (
Self::filter_include_all(scripthash),
Some((OutputFlags::FundingNone as u8, FLAGS_BYTE_OFFSET)),
),
(true, true) => (
Self::filter_include_all(scripthash),
Some((0, FLAGS_BYTE_OFFSET)),
),
(false, false) => (
Self::filter_include_all(scripthash),
Some((
OutputFlags::FundingNone as u8 | OutputFlags::FundingHasTokens as u8,
FLAGS_BYTE_OFFSET,
)),
),
}
}
pub fn filter_by_spends(
scripthash: [u8; 32],
filter: &QueryFilter,
) -> (Vec<u8>, Option<(u8, usize)>) {
match (filter.token_only, filter.exclude_tokens) {
(true, false) => (
Self::filter_include_all(scripthash),
Some((OutputFlags::SpentHasTokens as u8, FLAGS_BYTE_OFFSET)),
),
(false, true) => (
Self::filter_include_all(scripthash),
Some((OutputFlags::SpentNone as u8, FLAGS_BYTE_OFFSET)),
),
(true, true) => (
Self::filter_include_all(scripthash),
Some((0, FLAGS_BYTE_OFFSET)),
),
(false, false) => (
Self::filter_include_all(scripthash),
Some((
OutputFlags::SpentNone as u8 | OutputFlags::SpentHasTokens as u8,
FLAGS_BYTE_OFFSET,
)),
),
}
}
pub fn outpointhash(&self) -> OutPointHash {
OutPointHash::from_inner(self.key.outpointhash)
}
pub fn outpointhash_inner(&self) -> &[u8; 32] {
&self.key.outpointhash
}
pub fn has_token(&self) -> bool {
matches!(self.key.flags as u32, x if x == OutputFlags::FundingHasTokens as u32 || x == OutputFlags::SpentHasTokens as u32)
}
pub fn get_height(&self) -> u32 {
u32::from_be_bytes(self.key.height_be)
}
pub fn get_scripthash_inner(&self) -> &[u8; 32] {
&self.key.scripthash
}
pub fn get_txid(&self) -> [u8; 32] {
match &self.value {
ScriptHashIndexValue::Funding { txid, .. } => *txid,
ScriptHashIndexValue::Spending { txid, .. } => *txid,
}
}
pub fn is_funding(&self) -> bool {
matches!(self.key.flags as u32, x if x == OutputFlags::FundingNone as u32 || x == OutputFlags::FundingHasTokens as u32)
}
pub fn is_spending(&self) -> bool {
matches!(self.key.flags as u32, x if x == OutputFlags::SpentNone as u32 || x == OutputFlags::SpentHasTokens as u32)
}
pub fn spending_txid(&self) -> Option<[u8; 32]> {
match &self.value {
ScriptHashIndexValue::Spending { txid, .. } => Some(*txid),
_ => None,
}
}
pub fn spending_vin(&self) -> Option<u32> {
match &self.value {
ScriptHashIndexValue::Spending { vin, .. } => Some(*vin),
_ => None,
}
}
}
fn deserialize_value(value: &[u8]) -> Result<ScriptHashIndexValue> {
db_decode(value).context("failed to deserialize ScriptHashIndexRow value")
}
impl DBRow for ScriptHashIndexRow {
const CF: &'static str = SCRIPTHASHINDEX_CF;
fn to_row(&self) -> Row {
Row {
key: db_encode(&self.key).unwrap().into_boxed_slice(),
value: db_encode(&self.value).unwrap().into_boxed_slice(),
}
}
fn from_row(row: &Row) -> Self {
let value = deserialize_value(&row.value).expect("failed to parse ScriptHashIndexRow");
Self {
key: db_decode(&row.key).expect("failed to read key for ScriptHashIndexRow"),
value,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_from_row() {
let txid = [1u8; 32];
let row = ScriptHashIndexRow::new_funding(
&ScriptHash::hash(&[42]),
&OutPointHash::hash(&[11]),
OutputFlags::FundingHasTokens,
txid,
5,
42,
);
let decoded_row = ScriptHashIndexRow::from_row(&row.to_row());
assert_eq!(row, decoded_row);
assert_eq!(row.get_txid(), txid);
assert_eq!(row.get_height(), 42);
}
#[test]
fn test_spending_row() {
let txid = [1u8; 32];
let row = ScriptHashIndexRow::new_spending(
&ScriptHash::hash(&[42]),
&OutPointHash::hash(&[11]),
OutputFlags::SpentHasTokens,
txid,
5,
100,
);
assert!(row.is_spending());
assert!(row.has_token());
assert_eq!(row.get_height(), 100);
assert_eq!(row.spending_txid(), Some(txid));
assert_eq!(row.spending_vin(), Some(5));
let decoded_row = ScriptHashIndexRow::from_row(&row.to_row());
assert_eq!(row, decoded_row);
}
#[test]
fn test_filter_by_scripthash_with_token() {
let scripthash = ScriptHash::hash(&[42]);
let txid = [1u8; 32];
let row_with_token = ScriptHashIndexRow::new_funding(
&scripthash,
&OutPointHash::hash(&[11]),
OutputFlags::FundingHasTokens,
txid,
0,
42,
)
.to_row();
let row_without_token = ScriptHashIndexRow::new_funding(
&scripthash,
&OutPointHash::hash(&[11]),
OutputFlags::FundingNone,
txid,
0,
42,
)
.to_row();
let (filter_token_prefix, filter_token_bitmask) = ScriptHashIndexRow::filter_by_outputs(
scripthash.into_inner(),
&QueryFilter::filter_token_only(),
);
let (all_prefix, all_bitmask) =
ScriptHashIndexRow::filter_by_outputs(scripthash.into_inner(), &QueryFilter::default());
let (exclude_token_prefix, exclude_token_bitmask) = ScriptHashIndexRow::filter_by_outputs(
scripthash.into_inner(),
&QueryFilter::filter_exclude_tokens(),
);
let matches_filter = |row: &Row, prefix: &[u8], bitmask: Option<(u8, usize)>| -> bool {
let key = row.key.to_vec();
let prefix_matches = key.iter().zip(prefix.iter()).all(|(x, y)| x == y);
if !prefix_matches {
return false;
}
match bitmask {
Some((mask, pos)) => key[pos] & mask != 0,
None => true,
}
};
assert!(matches_filter(
&row_with_token,
&filter_token_prefix,
filter_token_bitmask
));
assert!(!matches_filter(
&row_without_token,
&filter_token_prefix,
filter_token_bitmask
));
assert!(matches_filter(&row_with_token, &all_prefix, all_bitmask));
assert!(matches_filter(&row_without_token, &all_prefix, all_bitmask));
assert!(!matches_filter(
&row_with_token,
&exclude_token_prefix,
exclude_token_bitmask
));
assert!(matches_filter(
&row_without_token,
&exclude_token_prefix,
exclude_token_bitmask
));
}
#[test]
fn test_filter_prefix_encoding() {
use crate::store::db_encode;
let scripthash = [0x42u8; 32];
let encoded_array = db_encode(&scripthash).unwrap();
let dummy_key = ScriptHashIndexKey {
scripthash,
height_be: [0u8; 4],
flags: 0,
outpointhash: [0u8; 32],
};
let encoded_key = db_encode(&dummy_key).unwrap();
let prefix_from_key = &encoded_key[0..32];
let row = ScriptHashIndexRow::new_funding(
&ScriptHash::from_inner(scripthash),
&OutPointHash::hash(&[11]),
OutputFlags::FundingNone,
[1u8; 32],
0,
42,
);
let row_key = row.to_row().key;
let prefix_from_row = &row_key[0..32];
assert_eq!(
encoded_array, prefix_from_key,
"Encoding just the scripthash array should match the key prefix"
);
assert_eq!(
prefix_from_key, prefix_from_row,
"Prefix from encoded key should match actual row key prefix"
);
let (filter_with_flag_prefix, filter_with_flag_bitmask) =
ScriptHashIndexRow::filter_by_outputs(scripthash, &QueryFilter::filter_token_only());
assert_eq!(filter_with_flag_prefix.len(), 32);
assert_eq!(&filter_with_flag_prefix[0..32], prefix_from_key);
assert!(filter_with_flag_bitmask.is_some());
let (bitmask, pos) = filter_with_flag_bitmask.unwrap();
assert_eq!(pos, FLAGS_BYTE_OFFSET);
assert_eq!(bitmask, OutputFlags::FundingHasTokens as u8);
let row_with_flag = ScriptHashIndexRow::new_funding(
&ScriptHash::from_inner(scripthash),
&OutPointHash::hash(&[22]),
OutputFlags::FundingHasTokens,
[2u8; 32],
0,
43,
);
let row_with_flag_key = row_with_flag.to_row().key;
assert_eq!(&row_with_flag_key[0..32], &filter_with_flag_prefix[0..32]);
assert_eq!(row_with_flag_key[FLAGS_BYTE_OFFSET] & bitmask, bitmask);
assert_eq!(row_key[FLAGS_BYTE_OFFSET] & bitmask, 0);
}
}