use std::cell::RefMut;
use arch_program::{input_to_sign::InputToSign, rune::RuneAmount, utxo::UtxoMeta};
use bitcoin::{ScriptBuf, Transaction};
use satellite_bitcoin::utxo_info::UtxoInfoTrait;
use satellite_bitcoin::{fee_rate::FeeRate, TransactionBuilder};
#[cfg(feature = "runes")]
use arch_program::rune::RuneId;
#[cfg(feature = "runes")]
use ordinals::Runestone;
use satellite_bitcoin::generic::fixed_set::FixedCapacitySet;
#[cfg(feature = "utxo-consolidation")]
use satellite_bitcoin::utxo_info::FixedOptionF64;
use super::error::StateShardError;
use super::StateShard;
use satellite_lang::prelude::Owner;
use satellite_lang::ZeroCopy;
pub fn ensure_rune_utxo_present_in_all<'info, RS, U, S>(
selected_shards: &[RefMut<'info, S>],
) -> super::error::Result<()>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let all_have_rune = selected_shards
.iter()
.all(|shard| shard.rune_utxo().is_some());
if all_have_rune {
Ok(())
} else {
Err(StateShardError::NotEnoughRuneUtxos)
}
}
fn remove_utxos_from_shards<'info, RS, U, S>(
selected_shards: &mut [RefMut<'info, S>],
utxos_to_remove: &[UtxoMeta],
) -> super::error::Result<()>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
for shard in selected_shards.iter_mut() {
for utxo_to_remove in utxos_to_remove {
shard.btc_utxos_retain(&mut |utxo| utxo.meta() != utxo_to_remove);
if let Some(rune_utxo) = shard.rune_utxo() {
if rune_utxo.meta() == utxo_to_remove {
shard.clear_rune_utxo();
}
}
}
}
Ok(())
}
fn select_best_shard_to_add_btc_to<'info, RS, U, S>(
selected_shards: &[RefMut<'info, S>],
) -> Option<usize>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let mut best_idx: Option<usize> = None;
let mut smallest_total: u64 = u64::MAX;
for (idx, shard) in selected_shards.iter().enumerate() {
let spare = shard.btc_utxos_len() < shard.btc_utxos_max_len();
let sum: u64 = shard.btc_utxos().iter().map(|u| u.value()).sum();
if spare && sum < smallest_total {
smallest_total = sum;
best_idx = Some(idx);
}
}
best_idx
}
#[allow(clippy::too_many_arguments)]
fn update_shards_utxos<'info, RS, U, S>(
selected_shards: &mut [RefMut<'info, S>],
utxos_to_remove: &[UtxoMeta],
new_rune_utxos: Vec<U>,
mut new_btc_utxos: Vec<U>,
fee_rate: &FeeRate,
) -> super::error::Result<()>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
remove_utxos_from_shards(selected_shards, utxos_to_remove)?;
let mut rune_utxo_iter = new_rune_utxos.into_iter();
for shard in selected_shards.iter_mut() {
if shard.rune_utxo().is_none() {
if let Some(utxo) = rune_utxo_iter.next() {
shard.set_rune_utxo(utxo);
}
}
}
if rune_utxo_iter.next().is_some() {
return Err(StateShardError::ExcessRuneUtxos.into());
}
new_btc_utxos.sort_by(|a, b| b.value().cmp(&a.value()));
for mut utxo in new_btc_utxos.into_iter() {
let target_idx = select_best_shard_to_add_btc_to(selected_shards)
.ok_or(StateShardError::ShardsAreFullOfBtcUtxos)?;
let shard = &mut selected_shards[target_idx];
#[cfg(feature = "utxo-consolidation")]
{
let has_no_consolidation = shard
.btc_utxos()
.iter()
.any(|u| u.needs_consolidation().is_none());
if has_no_consolidation {
*utxo.needs_consolidation_mut() = FixedOptionF64::some(fee_rate.0);
}
}
let success = shard.add_btc_utxo(utxo).is_some();
if !success {
return Err(StateShardError::ShardsAreFullOfBtcUtxos.into());
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn update_shards_after_transaction<
'info,
const MAX_USER_UTXOS: usize,
const MAX_SHARDS_PER_PROGRAM: usize,
RS,
U,
S,
>(
transaction_builder: &TransactionBuilder<MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM, RS>,
selected_shards: &mut [RefMut<'info, S>],
program_script_pubkey: &ScriptBuf,
fee_rate: &FeeRate,
) -> super::error::Result<()>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let (utxos_to_remove, mut new_program_utxos) = get_modified_program_utxos_in_transaction(
program_script_pubkey,
&transaction_builder.transaction,
transaction_builder.inputs_to_sign.as_slice(),
);
#[cfg(feature = "runes")]
let (new_rune_utxos, new_btc_utxos) = {
let runestone = &transaction_builder.runestone;
let new_rune_utxos = update_modified_program_utxos_with_rune_amount(
&mut new_program_utxos,
runestone,
&transaction_builder.total_rune_inputs,
)?;
(new_rune_utxos, new_program_utxos)
};
#[cfg(not(feature = "runes"))]
let (new_rune_utxos, new_btc_utxos) = (Vec::<U>::new(), new_program_utxos);
update_shards_utxos(
selected_shards,
&utxos_to_remove,
new_rune_utxos,
new_btc_utxos,
fee_rate,
)
}
#[allow(clippy::too_many_arguments)]
pub fn update_shards_after_transaction_split<
'info,
const MAX_USER_UTXOS: usize,
const MAX_SHARDS_PER_PROGRAM: usize,
RS,
U,
S,
>(
transaction_builder: &TransactionBuilder<MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM, RS>,
rune_selected_shards: &mut [RefMut<'info, S>],
btc_selected_shards: &mut [RefMut<'info, S>],
program_script_pubkey: &ScriptBuf,
fee_rate: &FeeRate,
) -> super::error::Result<()>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
S: StateShard<U, RS> + ZeroCopy + Owner,
{
let (utxos_to_remove, mut new_program_utxos) = get_modified_program_utxos_in_transaction(
program_script_pubkey,
&transaction_builder.transaction,
transaction_builder.inputs_to_sign.as_slice(),
);
#[cfg(feature = "runes")]
let (new_rune_utxos, new_btc_utxos) = {
let runestone = &transaction_builder.runestone;
let new_rune_utxos = update_modified_program_utxos_with_rune_amount(
&mut new_program_utxos,
runestone,
&transaction_builder.total_rune_inputs,
)?;
(new_rune_utxos, new_program_utxos)
};
#[cfg(not(feature = "runes"))]
let (new_rune_utxos, new_btc_utxos) = (Vec::<U>::new(), new_program_utxos);
update_shards_utxos::<RS, U, S>(
rune_selected_shards,
&utxos_to_remove,
new_rune_utxos,
Vec::new(), fee_rate,
)?;
update_shards_utxos::<RS, U, S>(
btc_selected_shards,
&utxos_to_remove,
Vec::new(), new_btc_utxos,
fee_rate,
)
}
fn get_modified_program_utxos_in_transaction<RS, U>(
program_script_pubkey: &ScriptBuf,
transaction: &Transaction,
inputs_to_sign: &[InputToSign],
) -> (Vec<UtxoMeta>, Vec<U>)
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
{
use satellite_bitcoin::bytes::txid_to_bytes_big_endian;
let mut utxos_to_remove = Vec::with_capacity(inputs_to_sign.len());
let mut program_outputs = Vec::with_capacity(transaction.output.len() / 2);
let txid_bytes = txid_to_bytes_big_endian(&transaction.compute_txid());
for input in inputs_to_sign {
let outpoint = transaction.input[input.index as usize].previous_output;
utxos_to_remove.push(UtxoMeta::from(
txid_to_bytes_big_endian(&outpoint.txid),
outpoint.vout,
));
}
for (index, output) in transaction.output.iter().enumerate() {
if output.script_pubkey == *program_script_pubkey {
program_outputs.push(U::new(
UtxoMeta::from(txid_bytes, index as u32),
output.value.to_sat(),
));
}
}
(utxos_to_remove, program_outputs)
}
#[cfg(feature = "runes")]
fn update_modified_program_utxos_with_rune_amount<RS, U>(
new_program_outputs: &mut Vec<U>,
runestone: &Runestone,
prev_rune_amount: &RS,
) -> super::error::Result<Vec<U>>
where
RS: FixedCapacitySet<Item = RuneAmount> + Default,
U: UtxoInfoTrait<RS>,
{
let mut remaining_rune_amount = prev_rune_amount.clone();
let mut rune_utxos = vec![];
for edict in &runestone.edicts {
let rune_amount = edict.amount;
let index = edict.output;
let rune_id = RuneId::new(edict.id.block, edict.id.tx);
let pos = new_program_outputs
.iter()
.position(|u| u.meta().vout() == index);
if let Some(pos) = pos {
let output = new_program_outputs
.get_mut(pos)
.ok_or(StateShardError::OutputEdictIsNotInTransaction)?;
output.runes_mut().insert_or_modify::<StateShardError, _>(
RuneAmount {
id: rune_id,
amount: rune_amount,
},
|rune_input| {
rune_input.amount = rune_input
.amount
.checked_add(rune_amount)
.ok_or(StateShardError::RuneAmountAdditionOverflow)?;
Ok(())
},
)?;
}
if let Some(remaining) = remaining_rune_amount
.iter_mut()
.find(|rune_amount| rune_amount.id == rune_id)
{
remaining.amount = remaining
.amount
.checked_sub(rune_amount)
.ok_or(StateShardError::NotEnoughRuneInShards)?;
}
}
if let Some(pointer_index) = runestone.pointer {
for rune_amount in remaining_rune_amount.iter() {
if let Some(output) = new_program_outputs
.iter_mut()
.find(|u| u.meta().vout() == pointer_index)
{
output.runes_mut().insert_or_modify::<StateShardError, _>(
RuneAmount {
id: rune_amount.id,
amount: rune_amount.amount,
},
|rune_input| {
rune_input.amount = rune_input
.amount
.checked_add(rune_amount.amount)
.ok_or(StateShardError::RuneAmountAdditionOverflow)?;
Ok(())
},
)?;
}
}
} else {
for rune_amount in remaining_rune_amount.iter() {
if rune_amount.amount > 0 {
return Err(StateShardError::RunestonePointerIsNotInTransaction);
}
}
}
let mut i = new_program_outputs.len();
while i > 0 {
i -= 1;
if new_program_outputs[i].runes().len() > 0 {
let rune_utxo = new_program_outputs.swap_remove(i);
rune_utxos.push(rune_utxo);
}
}
rune_utxos.reverse();
Ok(rune_utxos)
}
#[cfg(test)]
mod tests_loader {
use super::*;
use super::super::tests::common::{
add_btc_utxos_bulk, create_shard, leak_loaders_from_vec, MockShardZc, MAX_BTC_UTXOS,
};
use satellite_bitcoin::utxo_info::{SingleRuneSet, UtxoInfo, UtxoInfoTrait};
use satellite_bitcoin::TransactionBuilder as TB;
#[allow(unused_macros)]
macro_rules! new_tb {
($max_utxos:expr, $max_shards:expr) => {
TB::<$max_utxos, $max_shards, SingleRuneSet>::new()
};
}
fn create_utxo(
value: u64,
txid_byte: u8,
vout: u32,
) -> satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet> {
let txid = [txid_byte; 32];
let meta = UtxoMeta::from(txid, vout);
let utxo_info = UtxoInfo::new(meta, value);
utxo_info
}
fn fee_rate() -> FeeRate {
FeeRate(1.0)
}
mod select_best_shard_to_add_btc_to {
use super::*;
#[test]
fn selects_shard_with_smallest_total_btc() {
let shard_low = create_shard(50);
let shard_medium = create_shard(100);
let shard_high = create_shard(200);
let shards_vec = vec![shard_medium, shard_low, shard_high];
let loaders = leak_loaders_from_vec(shards_vec);
let shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
let best = super::super::select_best_shard_to_add_btc_to::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&shard_refs);
assert_eq!(best, Some(1)); }
#[test]
fn returns_none_when_all_shards_are_full() {
let mut shard0 = create_shard(0);
let mut shard1 = create_shard(0);
add_btc_utxos_bulk(&mut shard0, &vec![1u64; MAX_BTC_UTXOS]);
add_btc_utxos_bulk(&mut shard1, &vec![1u64; MAX_BTC_UTXOS]);
let shards_vec = vec![shard0, shard1];
let loaders = leak_loaders_from_vec(shards_vec);
let shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
let res = super::super::select_best_shard_to_add_btc_to::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&shard_refs);
assert_eq!(res, None);
}
#[test]
fn skips_full_shard_and_selects_available_one() {
let mut shard_full = create_shard(0);
add_btc_utxos_bulk(&mut shard_full, &vec![1u64; MAX_BTC_UTXOS]);
let shard_available = create_shard(500);
let shards_vec = vec![shard_full, shard_available];
let loaders = leak_loaders_from_vec(shards_vec);
let shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
let res = super::super::select_best_shard_to_add_btc_to::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&shard_refs);
assert_eq!(res, Some(1)); }
}
mod update_shards_utxos_tests {
use super::*;
fn setup_shard_loaders(
shard0: MockShardZc,
shard1: MockShardZc,
) -> &'static [satellite_lang::prelude::AccountLoader<'static, MockShardZc>] {
let shards_vec = vec![shard0, shard1];
leak_loaders_from_vec(shards_vec)
}
#[test]
fn distributes_new_utxos_and_handles_runes() {
let loaders = setup_shard_loaders(create_shard(0), create_shard(0));
let new_rune_utxo = create_utxo(546, 10, 0);
let new_btc_big = create_utxo(200, 11, 0);
let new_btc_small = create_utxo(100, 12, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
let result = super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![new_rune_utxo.clone()],
vec![new_btc_big.clone(), new_btc_small.clone()],
&fee_rate(),
);
assert!(result.is_ok());
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
let shard0_btc_len = shard0_ref.btc_utxos_len();
let shard0_rune_present = shard0_ref.rune_utxo().is_some();
assert_eq!(shard0_btc_len, 1);
assert!(shard0_rune_present);
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
let shard1_btc_len = shard1_ref.btc_utxos_len();
let shard1_rune_present = shard1_ref.rune_utxo().is_some();
assert_eq!(shard1_btc_len, 1);
assert!(!shard1_rune_present);
}
#[test]
fn errors_when_btc_utxo_vector_overflows() {
let mut shard0 = create_shard(0);
add_btc_utxos_bulk(&mut shard0, &vec![1u64; MAX_BTC_UTXOS]);
let mut shard1 = create_shard(0);
add_btc_utxos_bulk(&mut shard1, &vec![1u64; MAX_BTC_UTXOS]);
let loaders = setup_shard_loaders(shard0, shard1);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
let err = super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![create_utxo(1, 99, 0)],
&fee_rate(),
)
.unwrap_err();
assert_eq!(err, StateShardError::ShardsAreFullOfBtcUtxos);
}
#[test]
fn succeeds_after_removal_creates_capacity() {
let mut shard0 = MockShardZc::default();
let utxo_to_remove = create_utxo(100, 120, 0);
shard0.add_btc_utxo(utxo_to_remove.clone());
let filler: Vec<u64> = vec![1u64; MAX_BTC_UTXOS - 1];
add_btc_utxos_bulk(&mut shard0, &filler);
let shard1 = MockShardZc::default();
let shards_vec = vec![shard0, shard1];
let loaders = leak_loaders_from_vec(shards_vec);
let new_utxo = create_utxo(200, 122, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[*utxo_to_remove.meta()],
vec![],
vec![new_utxo.clone()],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert_eq!(shard0_ref.btc_utxos_len(), MAX_BTC_UTXOS - 1);
assert!(!shard0_ref
.btc_utxos()
.iter()
.any(|u| u.eq_meta(&utxo_to_remove)));
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
assert_eq!(shard1_ref.btc_utxos_len(), 1);
assert!(shard1_ref.btc_utxos().iter().any(|u| u.eq_meta(&new_utxo)));
}
#[test]
fn replaces_rune_utxo_correctly() {
let old_rune = create_utxo(546, 130, 0);
let new_rune = create_utxo(546, 131, 0);
let mut shard0 = MockShardZc::default();
shard0.set_rune_utxo(old_rune.clone());
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[*old_rune.meta()],
vec![new_rune.clone()],
vec![],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
let r = shard0_ref.rune_utxo().expect("rune utxo expected");
assert!(r.eq_meta(&new_rune));
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
assert!(shard1_ref.rune_utxo().is_none());
}
#[cfg(feature = "utxo-consolidation")]
#[test]
fn sets_needs_consolidation_flag_when_applicable() {
let mut shard0 = MockShardZc::default();
add_btc_utxos_bulk(&mut shard0, &[1]);
let mut shard1 = MockShardZc::default();
add_btc_utxos_bulk(&mut shard1, &[100]);
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let new_utxo = create_utxo(5, 83, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![new_utxo.clone()],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
let inserted = shard0_ref.btc_utxos().last().unwrap();
assert!(inserted.needs_consolidation().is_some());
assert_eq!(inserted.needs_consolidation().get().unwrap(), fee_rate().0);
}
#[cfg(feature = "utxo-consolidation")]
#[test]
fn does_not_set_consolidation_flag_when_shard_has_zero_utxos() {
let mut shard0 = MockShardZc::default();
add_btc_utxos_bulk(&mut shard0, &[]);
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let new_utxo = create_utxo(10, 151, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![new_utxo.clone()],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard1_ref = loaders[1].load().unwrap();
assert_eq!(shard1_ref.btc_utxos_len(), 0);
}
#[test]
fn skips_inserting_rune_when_already_present() {
let existing_rune = create_utxo(546, 30, 0);
let mut shard0 = MockShardZc::default();
shard0.set_rune_utxo(existing_rune.clone());
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let new_rune = create_utxo(546, 31, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![new_rune.clone()],
vec![],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(shard0_ref.rune_utxo().unwrap().eq_meta(&existing_rune));
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
assert!(shard1_ref.rune_utxo().is_some());
assert!(shard1_ref.rune_utxo().unwrap().eq_meta(&new_rune));
}
#[test]
fn handles_no_new_runes_when_shards_have_none() {
let shard0 = MockShardZc::default();
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let btc_utxo = create_utxo(1_000, 140, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut shard_refs, &[], vec![], vec![btc_utxo], &fee_rate())
.unwrap();
drop(shard_refs);
for loader in loaders.iter() {
let shard_ref = loader.load().unwrap();
assert!(shard_ref.rune_utxo().is_none());
}
}
#[cfg(feature = "utxo-consolidation")]
#[test]
fn first_new_btc_utxo_is_flagged_when_existing_utxos_have_none() {
let mut shard0 = MockShardZc::default();
add_btc_utxos_bulk(&mut shard0, &[1, 1, 1, 1, 1]);
let mut shard1 = MockShardZc::default();
add_btc_utxos_bulk(&mut shard1, &[100]);
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let new_utxo = create_utxo(50, 240, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![new_utxo.clone()],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
let inserted = shard0_ref.btc_utxos().last().unwrap();
assert!(inserted.needs_consolidation().is_some());
}
#[cfg(feature = "utxo-consolidation")]
#[test]
fn case1_one_existing_without_flag_then_new_flagged_and_single_none() {
let mut shard = MockShardZc::default();
add_btc_utxos_bulk(&mut shard, &[10]);
let loaders = leak_loaders_from_vec(vec![shard]);
let new_utxo = create_utxo(20, 241, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![new_utxo.clone()],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard_ref = loaders[0].load().unwrap();
let none_count = shard_ref
.btc_utxos()
.iter()
.filter(|u| u.needs_consolidation().is_none())
.count();
assert_eq!(none_count, 1);
let inserted = shard_ref.btc_utxos().last().unwrap();
assert!(inserted.needs_consolidation().is_some());
assert_eq!(inserted.needs_consolidation().get().unwrap(), fee_rate().0);
}
#[cfg(feature = "utxo-consolidation")]
#[test]
fn case2_all_existing_flagged_first_new_none_second_flagged_and_single_none() {
let mut shard = MockShardZc::default();
add_btc_utxos_bulk(&mut shard, &[5, 7]);
let loaders = leak_loaders_from_vec(vec![shard]);
{
use satellite_bitcoin::utxo_info::FixedOptionF64;
let mut s = loaders[0].load_mut().unwrap();
for u in s.btc_utxos_mut().iter_mut() {
*u.needs_consolidation_mut() = FixedOptionF64::some(fee_rate().0);
}
drop(s);
}
let new_a = create_utxo(30, 242, 0);
let new_b = create_utxo(25, 243, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![new_a, new_b],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard_ref = loaders[0].load().unwrap();
let len = shard_ref.btc_utxos_len();
let inserted_slice = &shard_ref.btc_utxos()[len.saturating_sub(2)..];
let none_inserted = inserted_slice
.iter()
.filter(|u| u.needs_consolidation().is_none())
.count();
let some_inserted = inserted_slice
.iter()
.filter(|u| u.needs_consolidation().is_some())
.count();
assert_eq!(none_inserted, 1);
assert_eq!(some_inserted, 1);
let total_none = shard_ref
.btc_utxos()
.iter()
.filter(|u| u.needs_consolidation().is_none())
.count();
assert_eq!(total_none, 1);
}
#[cfg(feature = "utxo-consolidation")]
#[test]
fn case3_empty_first_new_none_following_flagged_and_single_none() {
let shard = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard]);
let new_a = create_utxo(40, 244, 0);
let new_b = create_utxo(35, 245, 0);
let new_c = create_utxo(10, 246, 0);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_utxos::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut shard_refs,
&[],
vec![],
vec![new_a, new_b, new_c],
&fee_rate(),
)
.unwrap();
drop(shard_refs);
let shard_ref = loaders[0].load().unwrap();
let total_none = shard_ref
.btc_utxos()
.iter()
.filter(|u| u.needs_consolidation().is_none())
.count();
assert_eq!(total_none, 1);
}
}
mod remove_utxos_from_shards {
use super::*;
#[test]
fn removes_btc_and_rune_utxos_across_shards() {
let utxo_to_remove = create_utxo(1_000, 200, 0);
let meta_to_remove = *utxo_to_remove.meta();
let mut shard0 = MockShardZc::default();
shard0.add_btc_utxo(utxo_to_remove.clone());
shard0.set_rune_utxo(utxo_to_remove.clone());
let mut shard1 = MockShardZc::default();
shard1.add_btc_utxo(utxo_to_remove.clone());
shard1.set_rune_utxo(utxo_to_remove.clone());
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::remove_utxos_from_shards::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut shard_refs, &[meta_to_remove])
.unwrap();
drop(shard_refs);
for loader in loaders.iter() {
let shard_ref = loader.load().unwrap();
assert_eq!(shard_ref.btc_utxos_len(), 0);
assert!(shard_ref.rune_utxo().is_none());
}
}
#[test]
fn ignores_utxo_missing_in_some_shards() {
let utxo_to_remove = create_utxo(500, 201, 0);
let meta_to_remove = *utxo_to_remove.meta();
let mut shard0 = MockShardZc::default();
shard0.add_btc_utxo(utxo_to_remove.clone());
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::remove_utxos_from_shards::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut shard_refs, &[meta_to_remove])
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert_eq!(shard0_ref.btc_utxos_len(), 0);
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
assert_eq!(shard1_ref.btc_utxos_len(), 0);
}
#[test]
fn handles_empty_utxos_to_remove() {
let shard0 = create_shard(1000);
let shard1 = create_shard(2000);
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::remove_utxos_from_shards::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut shard_refs, &[])
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert_eq!(shard0_ref.btc_utxos_len(), 1);
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
assert_eq!(shard1_ref.btc_utxos_len(), 1);
}
#[test]
fn works_when_shard_has_no_rune_utxo() {
let utxo_to_remove = create_utxo(1_000, 60, 0);
let meta = *utxo_to_remove.meta();
let mut shard = MockShardZc::default();
shard.add_btc_utxo(utxo_to_remove.clone());
let loaders = leak_loaders_from_vec(vec![shard]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::remove_utxos_from_shards::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut shard_refs, &[meta])
.unwrap();
drop(shard_refs);
let shard_ref = loaders[0].load().unwrap();
assert_eq!(shard_ref.btc_utxos_len(), 0);
}
#[test]
fn removes_multiple_utxos_from_multiple_shards() {
let utxo_a = create_utxo(500, 250, 0);
let utxo_b = create_utxo(600, 251, 0);
let mut shard0 = MockShardZc::default();
shard0.add_btc_utxo(utxo_a.clone());
shard0.add_btc_utxo(utxo_b.clone());
let mut shard1 = MockShardZc::default();
shard1.add_btc_utxo(utxo_a.clone());
shard1.add_btc_utxo(utxo_b.clone());
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::remove_utxos_from_shards::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut shard_refs, &[*utxo_a.meta(), *utxo_b.meta()])
.unwrap();
drop(shard_refs);
for loader in loaders.iter() {
let shard_ref = loader.load().unwrap();
assert_eq!(shard_ref.btc_utxos_len(), 0);
}
}
}
mod get_modified_program_utxos_in_transaction {
use super::*;
use arch_program::input_to_sign::InputToSign;
use bitcoin::absolute::LockTime;
use bitcoin::transaction::Version;
use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
#[test]
fn identifies_program_outputs_correctly() {
let script = ScriptBuf::new();
let tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint::null(),
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(1000),
script_pubkey: script.clone(),
}],
};
let inputs = vec![InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
}];
let (removed, added): (
Vec<UtxoMeta>,
Vec<satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>>,
) = super::super::get_modified_program_utxos_in_transaction::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
>(&script, &tx, &inputs);
assert_eq!(removed.len(), 1);
assert_eq!(added.len(), 1);
assert_eq!(added[0].value, 1000);
}
#[test]
fn handles_multiple_inputs_to_sign() {
let script = ScriptBuf::new();
let outpoint1 = {
let mut o = OutPoint::null();
o.vout = 0;
o
};
let outpoint2 = {
let mut o = OutPoint::null();
o.vout = 1;
o
};
let tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![
TxIn {
previous_output: outpoint1,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
},
TxIn {
previous_output: outpoint2,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
},
],
output: vec![],
};
let inputs = vec![
InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
},
InputToSign {
index: 1,
signer: arch_program::pubkey::Pubkey::default(),
},
];
let (removed, _added): (
Vec<UtxoMeta>,
Vec<satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>>,
) = super::super::get_modified_program_utxos_in_transaction::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
>(&script, &tx, &inputs);
assert_eq!(removed.len(), 2);
assert!(removed.iter().any(|m| m.vout() == 0));
assert!(removed.iter().any(|m| m.vout() == 1));
}
#[test]
fn handles_multiple_program_outputs() {
let script = ScriptBuf::new();
let tx = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![],
output: vec![
TxOut {
value: Amount::from_sat(1_000),
script_pubkey: script.clone(),
},
TxOut {
value: Amount::from_sat(2_000),
script_pubkey: ScriptBuf::from_bytes(vec![0x51]),
},
TxOut {
value: Amount::from_sat(3_000),
script_pubkey: script.clone(),
},
],
};
let (_removed, added): (
Vec<UtxoMeta>,
Vec<satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>>,
) = super::super::get_modified_program_utxos_in_transaction::<
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
>(&script, &tx, &[]);
assert_eq!(added.len(), 2);
assert_eq!(added[0].value, 1_000);
assert_eq!(added[0].meta.vout(), 0);
assert_eq!(added[1].value, 3_000);
assert_eq!(added[1].meta.vout(), 2);
}
}
mod update_shards_after_transaction {
use super::*;
use arch_program::input_to_sign::InputToSign;
use bitcoin::absolute::LockTime;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use bitcoin::hashes::Hash;
use bitcoin::transaction::Version;
use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
#[test]
fn integrates_all_helpers() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let existing_utxo = create_utxo(5_000, 200, 0);
let txid_200 =
bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[200u8; 32]).unwrap());
let input_outpoint = OutPoint {
txid: txid_200,
vout: 0,
};
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(4_500),
script_pubkey: program_script.clone(),
}],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
let mut shard0 = MockShardZc::default();
shard0.add_btc_utxo(existing_utxo.clone());
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut builder, &mut shard_refs, &program_script, &fee_rate())
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(!shard0_ref
.btc_utxos()
.iter()
.any(|u| u.eq_meta(&existing_utxo)));
let shard0_len = shard0_ref.btc_utxos_len();
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
let shard1_len = shard1_ref.btc_utxos_len();
drop(shard1_ref);
let total = shard0_len + shard1_len;
assert_eq!(total, 1);
}
#[cfg(feature = "runes")]
#[test]
fn handles_rune_utxo_spending_and_creation() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let existing_rune_utxo = create_utxo(546, 210, 0);
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(1, 0),
amount: 100,
})
.unwrap();
let txid_210 =
bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[210u8; 32]).unwrap());
let input_outpoint = OutPoint {
txid: txid_210,
vout: 0,
};
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(1),
edicts: vec![ordinals::Edict {
id: ordinals::RuneId { block: 1, tx: 0 },
amount: 60,
output: 0,
}],
..Default::default()
};
let mut shard0 = MockShardZc::default();
shard0.set_rune_utxo(existing_rune_utxo.clone());
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(&mut builder, &mut shard_refs, &program_script, &fee_rate())
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
let shard0_has_rune = shard0_ref.rune_utxo().is_some();
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
let shard1_has_rune = shard1_ref.rune_utxo().is_some();
drop(shard1_ref);
let has_rune = shard0_has_rune || shard1_has_rune;
assert!(has_rune);
}
#[test]
fn propagates_overflow_error_when_all_shards_full() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::from_sat(1),
script_pubkey: ScriptBuf::new(),
}],
};
let mut shard0 = MockShardZc::default();
let mut shard1 = MockShardZc::default();
for i in 0..MockShardZc::btc_utxos_max_len(&shard0) {
shard0.add_btc_utxo(create_utxo(1, 220, i as u32));
shard1.add_btc_utxo(create_utxo(1, 221, i as u32));
}
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders
.iter()
.map(|loader| loader.load_mut().unwrap())
.collect();
let err = super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&ScriptBuf::new(),
&fee_rate(),
)
.unwrap_err();
assert_eq!(err, StateShardError::ShardsAreFullOfBtcUtxos);
}
#[cfg(feature = "runes")]
#[test]
fn assigns_rune_utxo_when_pointer_exists_but_remaining_is_zero() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid_1 = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[1u8; 32]).unwrap());
let input_outpoint = OutPoint {
txid: txid_1,
vout: 0,
};
let non_program_script = ScriptBuf::from_bytes(vec![0x51]);
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
TxOut {
value: Amount::from_sat(546),
script_pubkey: non_program_script,
},
],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(1, 0),
amount: 6,
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![ordinals::Edict {
id: ordinals::RuneId { block: 1, tx: 0 },
amount: 6,
output: 1,
}],
..Default::default()
};
let shard0 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(shard0_ref.rune_utxo().is_some());
}
#[cfg(feature = "runes")]
#[test]
fn assigns_rune_utxos_when_edicts_have_zero_amounts() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid_2 = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[2u8; 32]).unwrap());
let input_outpoint = OutPoint {
txid: txid_2,
vout: 0,
};
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![
ordinals::Edict {
id: ordinals::RuneId { block: 1, tx: 0 },
amount: 0,
output: 0,
},
ordinals::Edict {
id: ordinals::RuneId { block: 1, tx: 0 },
amount: 0,
output: 1,
},
],
..Default::default()
};
let shard0 = MockShardZc::default();
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap();
drop(shard_refs);
for loader in loaders.iter() {
let shard_ref = loader.load().unwrap();
assert!(shard_ref.rune_utxo().is_some());
}
}
#[cfg(feature = "runes")]
#[test]
fn single_rune_id_all_consumed_pointer_zero() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[4u8; 32]).unwrap());
let input_outpoint = OutPoint { txid, vout: 0 };
let non_program_script = ScriptBuf::from_bytes(vec![0x51]);
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
TxOut {
value: Amount::from_sat(546),
script_pubkey: non_program_script,
},
],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(1, 0),
amount: 5,
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![ordinals::Edict {
id: ordinals::RuneId { block: 1, tx: 0 },
amount: 5,
output: 1,
}],
..Default::default()
};
let shard0 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(shard0_ref.rune_utxo().is_some());
}
#[cfg(feature = "runes")]
#[test]
fn preserves_existing_rune_utxo_and_inserts_missing_only() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[5u8; 32]).unwrap());
let input_outpoint = OutPoint { txid, vout: 0 };
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
}],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(3, 0),
amount: 0,
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![],
..Default::default()
};
let mut shard0 = MockShardZc::default();
let existing_rune = create_utxo(546, 50, 0);
shard0.set_rune_utxo(existing_rune.clone());
let shard1 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0, shard1]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(shard0_ref.rune_utxo().is_some());
drop(shard0_ref);
let shard1_ref = loaders[1].load().unwrap();
assert!(shard1_ref.rune_utxo().is_some());
}
#[cfg(feature = "runes")]
#[test]
fn duplicate_zero_amount_edicts_merge_and_classify() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[6u8; 32]).unwrap());
let input_outpoint = OutPoint { txid, vout: 0 };
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
}],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(4, 0),
amount: 0,
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![
ordinals::Edict {
id: ordinals::RuneId { block: 4, tx: 0 },
amount: 0,
output: 0,
},
ordinals::Edict {
id: ordinals::RuneId { block: 4, tx: 0 },
amount: 0,
output: 0,
},
],
..Default::default()
};
let shard0 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(shard0_ref.rune_utxo().is_some());
}
#[cfg(feature = "runes")]
#[test]
fn error_when_more_rune_utxos_than_available_shards() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[7u8; 32]).unwrap());
let input_outpoint = OutPoint { txid, vout: 0 };
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
},
],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(9, 9),
amount: 0,
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![
ordinals::Edict {
id: ordinals::RuneId { block: 9, tx: 9 },
amount: 0,
output: 0,
},
ordinals::Edict {
id: ordinals::RuneId { block: 9, tx: 9 },
amount: 0,
output: 1,
},
],
..Default::default()
};
let shard0 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
let err = super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap_err();
drop(shard_refs);
assert_eq!(err, StateShardError::ExcessRuneUtxos);
}
#[cfg(feature = "runes")]
#[test]
fn pointer_present_no_edicts_total_inputs_zero() {
const MAX_USER_UTXOS: usize = 4;
const MAX_SHARDS_PER_PROGRAM: usize = 4;
let mut builder: satellite_bitcoin::TransactionBuilder<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
> = new_tb!(MAX_USER_UTXOS, MAX_SHARDS_PER_PROGRAM);
let program_script = ScriptBuf::new();
let txid = bitcoin::Txid::from_raw_hash(Sha256dHash::from_slice(&[3u8; 32]).unwrap());
let input_outpoint = OutPoint { txid, vout: 0 };
builder.transaction = Transaction {
version: Version::TWO,
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: input_outpoint,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::default(),
}],
output: vec![TxOut {
value: Amount::from_sat(546),
script_pubkey: program_script.clone(),
}],
};
builder
.inputs_to_sign
.push(InputToSign {
index: 0,
signer: arch_program::pubkey::Pubkey::default(),
})
.unwrap();
builder
.total_rune_inputs
.insert(arch_program::rune::RuneAmount {
id: arch_program::rune::RuneId::new(10, 1),
amount: 0,
})
.unwrap();
builder.runestone = Runestone {
pointer: Some(0),
edicts: vec![],
..Default::default()
};
let shard0 = MockShardZc::default();
let loaders = leak_loaders_from_vec(vec![shard0]);
let mut shard_refs: Vec<_> = loaders.iter().map(|l| l.load_mut().unwrap()).collect();
super::super::update_shards_after_transaction::<
MAX_USER_UTXOS,
MAX_SHARDS_PER_PROGRAM,
SingleRuneSet,
satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
MockShardZc,
>(
&mut builder,
&mut shard_refs,
&program_script,
&FeeRate(1.0),
)
.unwrap();
drop(shard_refs);
let shard0_ref = loaders[0].load().unwrap();
assert!(shard0_ref.rune_utxo().is_some());
}
}
}