use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
sync::Arc,
};
use zebra_chain::{
amount::Amount,
block::{Block, Height},
parameters::Network::Mainnet,
serialization::{ZcashDeserializeInto, ZcashSerialize},
transaction::Transaction,
transparent::{Input, OutPoint, Output, Script},
};
use crate::{
service::{
finalized_state::FinalizedState, non_finalized_state::Chain, read::spent_outputs_for_block,
},
tests::FakeChainHelper,
CheckpointVerifiedBlock, Config,
};
struct CountingAlloc;
thread_local! {
static TRACKING: Cell<bool> = const { Cell::new(false) };
static CURRENT: Cell<isize> = const { Cell::new(0) };
static PEAK: Cell<isize> = const { Cell::new(0) };
static TOTAL: Cell<usize> = const { Cell::new(0) };
}
fn record(delta: isize) {
let _ = TRACKING.try_with(|tracking| {
if !tracking.get() {
return;
}
CURRENT.with(|current| {
let now = current.get() + delta;
current.set(now);
PEAK.with(|peak| peak.set(peak.get().max(now)));
});
if delta > 0 {
TOTAL.with(|total| total.set(total.get() + delta as usize));
}
});
}
#[allow(unsafe_code)]
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = System.alloc(layout);
if !ptr.is_null() {
record(layout.size() as isize);
}
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = System.alloc_zeroed(layout);
if !ptr.is_null() {
record(layout.size() as isize);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout);
record(-(layout.size() as isize));
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_ptr = System.realloc(ptr, layout, new_size);
if !new_ptr.is_null() {
record(new_size as isize - layout.size() as isize);
}
new_ptr
}
}
#[global_allocator]
static GLOBAL: CountingAlloc = CountingAlloc;
struct HeapUse {
peak: usize,
total: usize,
}
fn measure<T>(f: impl FnOnce() -> T) -> (T, HeapUse) {
CURRENT.with(|c| c.set(0));
PEAK.with(|p| p.set(0));
TOTAL.with(|t| t.set(0));
TRACKING.with(|t| t.set(true));
let result = f();
TRACKING.with(|t| t.set(false));
let heap = HeapUse {
peak: PEAK.with(|p| p.get()).max(0) as usize,
total: TOTAL.with(|t| t.get()),
};
(result, heap)
}
fn with_coinbase_outputs(block: Arc<Block>, outputs: Vec<Output>) -> Arc<Block> {
let mut block = Block::clone(&block);
let coinbase = Transaction::clone(&block.transactions[0]).with_transparent_outputs(outputs);
block.transactions[0] = Arc::new(coinbase);
Arc::new(block)
}
fn spending_tx(
template: &Transaction,
outpoints: impl IntoIterator<Item = OutPoint>,
) -> Transaction {
let inputs = outpoints
.into_iter()
.map(|outpoint| Input::PrevOut {
outpoint,
unlock_script: Script::new(&[]),
sequence: u32::MAX,
})
.collect();
template
.clone()
.with_transparent_inputs(inputs)
.with_transparent_outputs(Vec::new())
}
fn small_output() -> Output {
Output::new(Amount::try_from(1).unwrap(), Script::new(&[0x76; 25]))
}
fn commit_chain(
parent_outputs: Vec<Vec<Output>>,
spends: impl Fn(&[Arc<Block>]) -> Vec<OutPoint>,
) -> (FinalizedState, Vec<Arc<Block>>, Arc<Block>) {
let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block should deserialize");
let mut state = FinalizedState::new_with_debug(
&Config::ephemeral(),
&Mainnet,
true,
#[cfg(feature = "elasticsearch")]
false,
false,
)
.expect("opening an ephemeral database should succeed");
let mut tip = genesis.clone();
let mut commit = |block: Arc<Block>| {
state
.commit_finalized_direct(CheckpointVerifiedBlock::from(block).into(), None, "test")
.expect("fake block should commit");
};
commit(genesis.clone());
let mut parents = Vec::new();
for outputs in parent_outputs {
let parent = with_coinbase_outputs(tip.make_fake_child(), outputs);
commit(parent.clone());
parents.push(parent.clone());
tip = parent;
}
let spend = spending_tx(&genesis.transactions[0], spends(&parents));
let mut spender = Block::clone(&with_coinbase_outputs(
tip.make_fake_child(),
vec![small_output()],
));
spender.transactions.push(Arc::new(spend));
let spender = Arc::new(spender);
commit(spender.clone());
(state, parents, spender)
}
#[test]
fn spent_outputs_resolves_value_height_and_generated() {
let _init_guard = zebra_test::init();
let spent_value = Amount::try_from(7).expect("valid amount");
let parent_outputs = vec![vec![
Output::new(
Amount::try_from(1).expect("valid amount"),
Script::new(&[0x76; 25]),
),
Output::new(spent_value, Script::new(&[0x76; 25])),
Output::new(
Amount::try_from(3).expect("valid amount"),
Script::new(&[0x76; 25]),
),
]];
let spent_outpoint = |parents: &[Arc<Block>]| OutPoint {
hash: parents[0].transactions[0].hash(),
index: 1,
};
let (state, parents, _spender) =
commit_chain(parent_outputs, |parents| vec![spent_outpoint(parents)]);
let spent = spent_outputs_for_block(None::<Arc<Chain>>, &state.db, Height(2).into())
.expect("block is in the best chain");
let utxo = spent
.get(&spent_outpoint(&parents))
.expect("the spent output is resolved");
assert_eq!(
utxo.output.value(),
spent_value,
"the resolved output is the one the input spends (index 1), not another index",
);
assert_eq!(
utxo.height,
Height(1),
"height is the parent's creation height, not the spending block's height",
);
assert!(
utxo.from_coinbase,
"the spent output was created by a coinbase transaction",
);
}
#[test]
fn spent_outputs_memory_does_not_hold_every_parent() {
let _init_guard = zebra_test::init();
const PARENTS: usize = 200;
const PADDING: usize = 100_000;
let parent_outputs = (0..PARENTS)
.map(|_| {
vec![
small_output(),
Output::new(
Amount::try_from(1).unwrap(),
Script::new(&vec![0x6a; PADDING]),
),
]
})
.collect();
let (state, _parents, _spender) = commit_chain(parent_outputs, |parents| {
parents
.iter()
.map(|parent| OutPoint {
hash: parent.transactions[0].hash(),
index: 0,
})
.collect()
});
let (spent, heap) = measure(|| {
spent_outputs_for_block(
None::<Arc<Chain>>,
&state.db,
Height(PARENTS as u32 + 1).into(),
)
});
let spent = spent.expect("block is in the best chain");
assert_eq!(spent.len(), PARENTS, "every spent output is resolved");
assert!(
heap.peak < 10 * PADDING,
"peak heap {} bytes should stay within a few parents ({PADDING} bytes each)",
heap.peak,
);
}
#[test]
fn spent_outputs_work_is_linear_for_a_shared_parent() {
let _init_guard = zebra_test::init();
const OUTPUTS: usize = 2_000;
let parent_outputs = vec![(0..OUTPUTS).map(|_| small_output()).collect()];
let (state, parents, _spender) = commit_chain(parent_outputs, |parents| {
let hash = parents[0].transactions[0].hash();
(0..OUTPUTS as u32)
.map(|index| OutPoint { hash, index })
.collect()
});
let (spent, heap) =
measure(|| spent_outputs_for_block(None::<Arc<Chain>>, &state.db, Height(2).into()));
let spent = spent.expect("block is in the best chain");
assert_eq!(spent.len(), OUTPUTS, "every spent output is resolved");
let parent_size = parents[0].transactions[0].zcash_serialized_size();
assert!(
heap.total < 50 * parent_size,
"total allocated {} bytes should be a small multiple of the parent ({parent_size} bytes)",
heap.total,
);
}