use super::database::UtxoDatabase;
use super::memory_age::Pin;
use super::types::{
outpoint_to_output_key, output_key_to_outpoint, IdCodec, OutputDetail, OutputId, OutputKey,
OUTPUT_ID_DELETED,
};
use blvm_protocol::transaction::is_coinbase;
use blvm_protocol::types::SharedByteString;
use blvm_protocol::{Block, OutPoint, UTXO};
use rustc_hash::FxHashMap;
use std::sync::Arc;
pub struct PartialSpendSession {
pub pin: Pin,
pub external_keys: Vec<OutputKey>,
pub intra_block_keys: Vec<OutputKey>,
pub height: i32,
pub db: Arc<UtxoDatabase>,
}
pub struct SpendSession {
pub details: Vec<OutputDetail>,
pub key_to_idx: FxHashMap<OutputKey, usize>,
pub local_spends: FxHashMap<OutputKey, OutputDetail>,
pub pin: Pin,
}
impl PartialSpendSession {
pub fn complete(self) -> anyhow::Result<SpendSession> {
let PartialSpendSession {
pin,
external_keys,
intra_block_keys,
height,
db,
} = self;
let mut ids: Vec<OutputId> = vec![OutputId::MAX; external_keys.len()];
db.query(&external_keys, &mut ids, height);
let mut fetch_order: Vec<(usize, OutputId)> = ids
.iter()
.enumerate()
.filter(|(_, &id)| id != OutputId::MAX && id != OUTPUT_ID_DELETED)
.map(|(i, &id)| (i, id))
.collect();
fetch_order.sort_unstable_by_key(|&(_, id)| IdCodec::decode(id).0);
let fetch_ids: Vec<OutputId> = fetch_order.iter().map(|&(_, id)| id).collect();
let mut raw_details: Vec<OutputDetail> = Vec::with_capacity(fetch_ids.len());
db.fetch(&fetch_ids, &mut raw_details)?;
let mut key_to_idx: FxHashMap<OutputKey, usize> =
FxHashMap::with_capacity_and_hasher(fetch_order.len(), Default::default());
for (fetch_rank, (key_idx, _id)) in fetch_order.iter().enumerate() {
let key = external_keys[*key_idx];
key_to_idx.insert(key, fetch_rank);
}
let details = raw_details;
let local_spends = resolve_intra_block(&db, intra_block_keys, height)?;
Ok(SpendSession {
details,
key_to_idx,
local_spends,
pin,
})
}
}
impl SpendSession {
pub fn append(
db: Arc<UtxoDatabase>,
block: &Block,
tx_ids: &[[u8; 32]],
height: i32,
) -> anyhow::Result<PartialSpendSession> {
let (pin, external_keys, intra_block_keys) =
db.append_and_classify(block, tx_ids, height)?;
Ok(PartialSpendSession {
pin,
external_keys,
intra_block_keys,
height,
db,
})
}
pub fn resolve(
db: &UtxoDatabase,
block: &Block,
tx_ids: &[[u8; 32]],
height: i32,
) -> anyhow::Result<Self> {
let pin = db.append(block, tx_ids, height)?;
let tx_id_set: std::collections::HashSet<[u8; 32]> = tx_ids[1..].iter().copied().collect();
let mut external_keys: Vec<OutputKey> = Vec::new();
let mut intra_block_keys: Vec<OutputKey> = Vec::new();
for tx in block.transactions.iter() {
if is_coinbase(tx) {
continue;
}
for input in tx.inputs.iter() {
let key = outpoint_to_output_key(&input.prevout);
if tx_id_set.contains(&input.prevout.hash) {
intra_block_keys.push(key);
} else {
external_keys.push(key);
}
}
}
#[cfg(feature = "rayon")]
{
use rayon::prelude::*;
external_keys.par_sort_unstable();
}
#[cfg(not(feature = "rayon"))]
external_keys.sort_unstable();
external_keys.dedup();
let mut ids: Vec<OutputId> = vec![OutputId::MAX; external_keys.len()];
db.query(&external_keys, &mut ids, height);
let mut fetch_order: Vec<(usize, OutputId)> = ids
.iter()
.enumerate()
.filter(|(_, &id)| id != OutputId::MAX && id != OUTPUT_ID_DELETED)
.map(|(i, &id)| (i, id))
.collect();
fetch_order.sort_unstable_by_key(|&(_, id)| IdCodec::decode(id).0);
let fetch_ids: Vec<OutputId> = fetch_order.iter().map(|&(_, id)| id).collect();
let mut raw_details: Vec<OutputDetail> = Vec::with_capacity(fetch_ids.len());
db.fetch(&fetch_ids, &mut raw_details)?;
let mut key_to_idx: FxHashMap<OutputKey, usize> =
FxHashMap::with_capacity_and_hasher(fetch_order.len(), Default::default());
for (fetch_rank, (key_idx, _id)) in fetch_order.iter().enumerate() {
let key = external_keys[*key_idx];
key_to_idx.insert(key, fetch_rank);
}
let details = raw_details;
let local_spends = resolve_intra_block(db, intra_block_keys, height)?;
Ok(SpendSession {
details,
key_to_idx,
local_spends,
pin,
})
}
}
fn resolve_intra_block(
db: &UtxoDatabase,
mut keys: Vec<OutputKey>,
height: i32,
) -> anyhow::Result<FxHashMap<OutputKey, OutputDetail>> {
if keys.is_empty() {
return Ok(FxHashMap::default());
}
keys.sort_unstable();
keys.dedup();
let mut ids = vec![OutputId::MAX; keys.len()];
db.query(&keys, &mut ids, height + 1);
let mut fetch_pairs: Vec<(usize, OutputId)> = ids
.iter()
.enumerate()
.filter(|(_, &id)| id != OutputId::MAX && id != OUTPUT_ID_DELETED)
.map(|(i, &id)| (i, id))
.collect();
fetch_pairs.sort_unstable_by_key(|&(_, id)| IdCodec::decode(id).0);
let fetch_ids: Vec<OutputId> = fetch_pairs.iter().map(|&(_, id)| id).collect();
let mut raw: Vec<OutputDetail> = Vec::with_capacity(fetch_ids.len());
db.fetch(&fetch_ids, &mut raw)?;
let mut map = FxHashMap::with_capacity_and_hasher(fetch_pairs.len(), Default::default());
let mut raw_iter = raw.into_iter();
for &(key_idx, _) in fetch_pairs.iter() {
if let Some(d) = raw_iter.next() {
map.insert(keys[key_idx], d); }
}
Ok(map)
}
#[cfg(feature = "production")]
pub fn session_to_utxo_set(session: &SpendSession) -> blvm_protocol::UtxoSet {
let total = session.details.len() + session.local_spends.len();
let mut map: blvm_protocol::UtxoSet = Default::default();
map.reserve(total);
session_fill_utxo_set(session, &mut map);
map
}
#[cfg(feature = "production")]
pub fn session_fill_utxo_set(session: &SpendSession, out: &mut blvm_protocol::UtxoSet) {
let total = session.details.len() + session.local_spends.len();
out.clear();
out.reserve(total);
for (key, idx) in &session.key_to_idx {
let d = &session.details[*idx];
let utxo = UTXO {
value: d.header.amount,
script_pubkey: SharedByteString::from(d.script.as_slice()),
height: d.header.height as u64,
is_coinbase: d.header.is_coinbase(),
};
let op = output_key_to_outpoint(key);
out.insert(op, Arc::new(utxo));
}
for (key, d) in &session.local_spends {
let utxo = UTXO {
value: d.header.amount,
script_pubkey: SharedByteString::from(d.script.as_slice()),
height: d.header.height as u64,
is_coinbase: d.header.is_coinbase(),
};
let op = output_key_to_outpoint(key);
out.insert(op, Arc::new(utxo));
}
}
#[cfg(not(feature = "production"))]
pub fn session_to_utxo_set(session: &SpendSession) -> blvm_protocol::UtxoSet {
let total = session.details.len() + session.local_spends.len();
let mut map = std::collections::HashMap::with_capacity(total);
session_fill_utxo_set_non_prod(session, &mut map);
map
}
#[cfg(not(feature = "production"))]
pub fn session_fill_utxo_set(session: &SpendSession, out: &mut blvm_protocol::UtxoSet) {
session_fill_utxo_set_non_prod(session, out);
}
#[cfg(not(feature = "production"))]
fn session_fill_utxo_set_non_prod(session: &SpendSession, map: &mut blvm_protocol::UtxoSet) {
let total = session.details.len() + session.local_spends.len();
map.clear();
map.reserve(total);
for (key, idx) in &session.key_to_idx {
let d = &session.details[*idx];
let utxo = UTXO {
value: d.header.amount,
script_pubkey: SharedByteString::from(d.script.as_slice()),
height: d.header.height as u64,
is_coinbase: d.header.is_coinbase(),
};
let op = output_key_to_outpoint(key);
map.insert(op, Arc::new(utxo));
}
for (key, d) in &session.local_spends {
let utxo = UTXO {
value: d.header.amount,
script_pubkey: SharedByteString::from(d.script.as_slice()),
height: d.header.height as u64,
is_coinbase: d.header.is_coinbase(),
};
let op = output_key_to_outpoint(key);
map.insert(op, Arc::new(utxo));
}
}
#[cfg(test)]
mod tests {
use super::super::database::UtxoDatabase;
use super::*;
use blvm_protocol::{
Block, BlockHeader, OutPoint, Transaction, TransactionInput, TransactionOutput,
};
use tempfile::NamedTempFile;
fn make_txid(n: u8) -> [u8; 32] {
let mut id = [0u8; 32];
id[0] = n;
id
}
fn coinbase_tx(value: i64) -> Transaction {
Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0u8; 32],
index: 0xFFFFFFFF,
},
sequence: 0xFFFFFFFF,
script_sig: vec![],
}]
.into(),
outputs: vec![TransactionOutput {
value,
script_pubkey: vec![0x76, 0xa9, 0x14, 0xab],
}]
.into(),
lock_time: 0,
}
}
fn spend_tx(prev_hash: [u8; 32], prev_vout: u32, value: i64) -> Transaction {
Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: prev_hash,
index: prev_vout,
},
sequence: 0xFFFFFFFF,
script_sig: vec![],
}]
.into(),
outputs: vec![TransactionOutput {
value,
script_pubkey: vec![0x51],
}]
.into(),
lock_time: 0,
}
}
fn make_block(txs: Vec<Transaction>) -> Block {
Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [0u8; 32],
timestamp: 0,
bits: 0,
nonce: 0,
},
transactions: txs.into_boxed_slice(),
}
}
#[test]
fn test_resolve_simple_external() {
let tmp = NamedTempFile::new().unwrap();
let db = UtxoDatabase::open(tmp.path(), 0).unwrap();
let txid100 = make_txid(100);
let block100 = make_block(vec![coinbase_tx(5_000_000_000)]);
let _pin = db.append(&block100, &[txid100], 100).unwrap();
let txid101_cb = make_txid(101);
let txid101_spend = make_txid(102);
let block101 = make_block(vec![
coinbase_tx(5_000_000_000),
spend_tx(txid100, 0, 4_999_000_000),
]);
let tx_ids = vec![txid101_cb, txid101_spend];
let session = SpendSession::resolve(&db, &block101, &tx_ids, 101).unwrap();
let mut key: OutputKey = [0u8; 36];
key[..32].copy_from_slice(&txid100);
assert!(
session.key_to_idx.contains_key(&key) || !session.local_spends.is_empty(),
"should have resolved the external spend"
);
}
#[test]
fn test_session_to_utxo_set_has_entry() {
let tmp = NamedTempFile::new().unwrap();
let db = UtxoDatabase::open(tmp.path(), 0).unwrap();
let txid = make_txid(200);
let block = make_block(vec![Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0u8; 32],
index: 0xFFFFFFFF,
},
sequence: 0xFFFFFFFF,
script_sig: vec![],
}]
.into(),
outputs: vec![
TransactionOutput {
value: 1_000,
script_pubkey: vec![0x51],
},
TransactionOutput {
value: 2_000,
script_pubkey: vec![0x52],
},
]
.into(),
lock_time: 0,
}]);
let _pin = db.append(&block, &[txid], 200).unwrap();
let txid201_cb = make_txid(201);
let txid201_sp = make_txid(202);
let block201 = make_block(vec![coinbase_tx(5_000_000_000), spend_tx(txid, 0, 900)]);
let tx_ids = vec![txid201_cb, txid201_sp];
let session = SpendSession::resolve(&db, &block201, &tx_ids, 201).unwrap();
let utxo_set = session_to_utxo_set(&session);
assert!(
!utxo_set.is_empty(),
"utxo_set should have at least the spent output"
);
}
}