1use std::cell::Ref;
44
45use arch_program::{
46 msg,
47 rune::{RuneAmount, RuneId},
48 utxo::UtxoMeta,
49};
50use bitcoin::{Amount, ScriptBuf, TxOut};
51use satellite_bitcoin::generic::fixed_set::FixedCapacitySet;
52use satellite_bitcoin::{
53 constants::DUST_LIMIT, fee_rate::FeeRate, utxo_info::UtxoInfoTrait, TransactionBuilder,
54};
55use satellite_bitcoin::{safe_add, safe_div, safe_mul, safe_sub, MathError};
56
57use super::error::StateShardError;
58use super::StateShard;
59
60#[cfg(feature = "runes")]
61use ordinals::Edict;
62
63use satellite_lang::prelude::Owner;
64use satellite_lang::ZeroCopy;
65
66#[derive(Debug, PartialEq)]
68pub enum DistributionError {
69 TotalBelowDustLimit,
71 Math(MathError),
73}
74
75impl From<MathError> for DistributionError {
76 fn from(value: MathError) -> Self {
77 DistributionError::Math(value)
78 }
79}
80
81#[allow(clippy::too_many_arguments)]
146pub fn redistribute_remaining_btc_to_shards<
147 'info,
148 const MAX_MODIFIED_ACCOUNTS: usize,
149 const MAX_INPUTS_TO_SIGN: usize,
150 RS,
151 U,
152 S,
153>(
154 tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
155 selected_shards: &[Ref<'info, S>],
156 removed_from_shards: u64,
157 program_script_pubkey: &ScriptBuf,
158 fee_rate: &FeeRate,
159) -> Result<Vec<u128>, DistributionError>
160where
161 RS: FixedCapacitySet<Item = RuneAmount> + Default,
162 U: UtxoInfoTrait<RS>,
163 S: StateShard<U, RS> + ZeroCopy + Owner,
164{
165 let remaining_amount = compute_unsettled_btc_in_shards(
166 tx_builder,
167 selected_shards,
168 removed_from_shards,
169 fee_rate,
170 )?;
171
172 let mut distribution =
173 plan_btc_distribution_among_shards(tx_builder, selected_shards, remaining_amount as u128)?;
174
175 distribution.sort_by(|a, b| b.cmp(a));
177
178 for amount in distribution.iter() {
179 let txout = TxOut {
180 value: Amount::from_sat(*amount as u64),
181 script_pubkey: program_script_pubkey.clone(),
182 };
183
184 tx_builder.transaction.output.push(txout);
185 }
186
187 Ok(distribution)
188}
189
190pub fn compute_unsettled_btc_in_shards<
202 'info,
203 const MAX_MODIFIED_ACCOUNTS: usize,
204 const MAX_INPUTS_TO_SIGN: usize,
205 RS,
206 U,
207 S,
208>(
209 tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
210 selected_shards: &[Ref<'info, S>],
211 removed_from_shards: u64,
212 fee_rate: &FeeRate,
213) -> Result<u64, MathError>
214where
215 RS: FixedCapacitySet<Item = RuneAmount> + Default,
216 U: UtxoInfoTrait<RS>,
217 S: StateShard<U, RS> + ZeroCopy + Owner,
218{
219 let mut total_btc_amount: u64 = 0;
223
224 let non_state_inputs = tx_builder.get_non_state_transition_inputs();
228
229 for shard in selected_shards.iter() {
230 let mut sum: u64 = 0;
231 for utxo in shard.btc_utxos().iter() {
232 for input in non_state_inputs.iter() {
233 let spent_meta =
234 UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout);
235 if spent_meta == *utxo.meta() {
236 sum = sum.saturating_add(utxo.value());
237 }
238 }
239 }
240
241 total_btc_amount = safe_add(total_btc_amount, sum)?;
242 }
243
244 let fee_paid_by_program = {
245 #[cfg(feature = "utxo-consolidation")]
246 {
247 tx_builder.get_fee_paid_by_program(fee_rate)
248 }
249 #[cfg(not(feature = "utxo-consolidation"))]
250 {
251 0
252 }
253 };
254
255 let remaining_amount = safe_sub(
256 safe_sub(total_btc_amount, removed_from_shards)?,
257 fee_paid_by_program,
258 )?;
259
260 Ok(remaining_amount)
261}
262
263fn plan_btc_distribution_among_shards<
303 'info,
304 const MAX_MODIFIED_ACCOUNTS: usize,
305 const MAX_INPUTS_TO_SIGN: usize,
306 RS,
307 U,
308 S,
309>(
310 tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
311 selected_shards: &[Ref<'info, S>],
312 amount: u128,
313) -> Result<Vec<u128>, DistributionError>
314where
315 RS: FixedCapacitySet<Item = RuneAmount> + Default,
316 U: UtxoInfoTrait<RS>,
317 S: StateShard<U, RS> + ZeroCopy + Owner,
318{
319 if amount == 0 {
320 return Ok(Vec::new());
321 }
322
323 let mut result = balance_amount_across_shards(
324 tx_builder,
325 selected_shards,
326 &RuneAmount {
327 id: RuneId::BTC,
328 amount,
329 },
330 )?;
331
332 redistribute_sub_dust_values(&mut result, DUST_LIMIT as u128)?;
333 Ok(result)
334}
335
336fn balance_amount_across_shards<
392 'info,
393 const MAX_MODIFIED_ACCOUNTS: usize,
394 const MAX_INPUTS_TO_SIGN: usize,
395 RS,
396 U,
397 S,
398>(
399 tx_builder: &TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
400 selected_shards: &[Ref<'info, S>],
401 rune_amount: &RuneAmount,
402) -> Result<Vec<u128>, MathError>
403where
404 RS: FixedCapacitySet<Item = RuneAmount> + Default,
405 U: UtxoInfoTrait<RS>,
406 S: StateShard<U, RS> + ZeroCopy + Owner,
407{
408 let num_shards = selected_shards.len();
409
410 let mut assigned_amounts: Vec<u128> = Vec::with_capacity(num_shards);
412 let mut total_current_amount: u128 = 0;
413
414 let non_state_inputs = tx_builder.get_non_state_transition_inputs();
418 let is_meta_spent = |meta: &UtxoMeta| {
419 non_state_inputs.iter().any(|input| {
420 let spent_meta =
421 UtxoMeta::from_outpoint(input.previous_output.txid, input.previous_output.vout);
422 spent_meta == *meta
423 })
424 };
425
426 for shard in selected_shards.iter() {
428 let current_res = match rune_amount.id {
429 RuneId::BTC => shard
430 .btc_utxos()
431 .iter()
432 .filter_map(|u| {
433 if is_meta_spent(u.meta()) {
434 None
435 } else {
436 Some(u.value() as u128)
437 }
438 })
439 .sum(),
440 _ => {
441 #[cfg(feature = "runes")]
442 {
443 shard
444 .rune_utxo()
445 .filter(|u| !is_meta_spent(u.meta()))
446 .and_then(|u| u.runes().find(&rune_amount.id).map(|r| r.amount))
447 .unwrap_or(0)
448 }
449 #[cfg(not(feature = "runes"))]
450 {
451 0
452 }
453 }
454 };
455
456 assigned_amounts.push(current_res);
457 total_current_amount = safe_add(total_current_amount, current_res)?;
458 }
459
460 msg!("total_current_amount: {}", total_current_amount);
461
462 let total_after = safe_add(total_current_amount, rune_amount.amount)?;
464 let desired_per_shard = safe_div(total_after, num_shards as u128)?;
465
466 msg!("desired_per_shard: {}", desired_per_shard);
467
468 let mut total_needed = 0u128;
470 for current in assigned_amounts.iter_mut() {
471 let needed = if desired_per_shard > *current {
472 safe_sub(desired_per_shard, *current)?
473 } else {
474 0
475 };
476 total_needed = safe_add(total_needed, needed)?;
477 *current = needed;
478 }
479
480 if total_needed <= rune_amount.amount {
481 let leftover = safe_sub(rune_amount.amount, total_needed)?;
483 let per_shard_extra = safe_div(leftover, num_shards as u128)?;
484 let mut extra_left = leftover % num_shards as u128;
485
486 for amt in assigned_amounts.iter_mut() {
487 *amt = safe_add(*amt, per_shard_extra)?;
488 if extra_left > 0 {
489 *amt = safe_add(*amt, 1)?;
490 extra_left -= 1;
491 }
492 }
493 } else {
494 let mut cumulative = 0u128;
496 let mut cumulative_needed = 0u128;
497
498 for i in 0..num_shards {
499 let needed = assigned_amounts[i];
500 cumulative_needed = safe_add(cumulative_needed, needed)?;
501 let proportional = safe_mul(rune_amount.amount, cumulative_needed)? / total_needed;
502 assigned_amounts[i] = safe_sub(proportional, cumulative)?;
503 cumulative = proportional;
504 }
505 }
506
507 msg!("assigned_amounts: {:?}", assigned_amounts);
508 Ok(assigned_amounts)
509}
510
511fn redistribute_sub_dust_values(
554 amounts: &mut Vec<u128>,
555 dust_limit: u128,
556) -> Result<(), DistributionError> {
557 let mut total_sum: u128 = 0;
560 for &amt in amounts.iter() {
561 total_sum = safe_add(total_sum, amt)?;
562 }
563 if total_sum > 0 && total_sum < dust_limit {
564 return Err(DistributionError::TotalBelowDustLimit);
565 }
566
567 let sum_of_small_amounts: u128 = amounts.iter().filter(|&&amount| amount < dust_limit).sum();
569
570 amounts.retain(|&amount| amount >= dust_limit);
572
573 if amounts.is_empty() {
575 if sum_of_small_amounts >= dust_limit {
576 amounts.push(sum_of_small_amounts);
577 } else {
578 amounts.clear();
579 }
580 return Ok(());
581 }
582
583 let num_amounts = amounts.len() as u128;
585 let to_add = safe_div(sum_of_small_amounts, num_amounts)?;
586 let mut remainder = sum_of_small_amounts % num_amounts;
587
588 for amount in amounts.iter_mut() {
589 *amount = safe_add(*amount, to_add)?;
590 if remainder > 0 {
591 *amount = safe_add(*amount, 1)?;
592 remainder -= 1;
593 }
594 }
595
596 Ok(())
597}
598
599#[cfg(feature = "runes")]
635pub fn compute_unsettled_rune_in_shards<'info, RS, U, S>(
636 selected_shards: &[Ref<'info, S>],
637 removed_from_shards: RS,
638) -> Result<RS, StateShardError>
639where
640 RS: FixedCapacitySet<Item = RuneAmount> + Default,
641 U: UtxoInfoTrait<RS>,
642 S: StateShard<U, RS> + ZeroCopy + Owner,
643{
644 let mut total_rune_amount = RS::default();
645
646 for shard in selected_shards.iter() {
647 if let Some(utxo) = shard.rune_utxo() {
649 for rune in utxo.runes().iter() {
650 let _ = total_rune_amount.insert_or_modify::<StateShardError, _>(
651 RuneAmount {
652 id: rune.id,
653 amount: rune.amount,
654 },
655 |r| {
656 r.amount = safe_add(r.amount, rune.amount)
657 .map_err(|_| StateShardError::RuneAmountAdditionOverflow)?;
658 Ok(())
659 },
660 );
661 }
662 };
663 }
664
665 for rune in removed_from_shards.iter() {
667 if let Some(output_rune) = total_rune_amount.find_mut(&rune.id) {
668 output_rune.amount = safe_sub(output_rune.amount, rune.amount)
669 .map_err(|_| StateShardError::RemovingMoreRunesThanPresentInShards)?;
670 }
671 }
672
673 Ok(total_rune_amount)
674}
675
676#[cfg(feature = "runes")]
724#[allow(clippy::too_many_arguments)]
725pub fn plan_rune_distribution_among_shards<
726 'info,
727 const MAX_MODIFIED_ACCOUNTS: usize,
728 const MAX_INPUTS_TO_SIGN: usize,
729 RS,
730 U,
731 S,
732>(
733 tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
734 selected_shards: &[Ref<'info, S>],
735 amounts: &RS,
736) -> Result<Vec<RS>, StateShardError>
737where
738 RS: FixedCapacitySet<Item = RuneAmount> + Default,
739 U: UtxoInfoTrait<RS>,
740 S: StateShard<U, RS> + ZeroCopy + Owner,
741{
742 let num_shards = selected_shards.len();
743 let mut result: Vec<RS> = (0..num_shards).map(|_| RS::default()).collect();
744
745 for rune_amount in amounts.iter() {
746 let allocs = balance_amount_across_shards(tx_builder, selected_shards, rune_amount)
747 .map_err(|_| StateShardError::MathErrorInBalanceAmountAcrossShards)?;
748
749 for (i, amount) in allocs.iter().enumerate() {
750 result[i].insert_or_modify::<StateShardError, _>(
751 RuneAmount {
752 id: rune_amount.id,
753 amount: *amount,
754 },
755 |r| {
756 r.amount = safe_add(r.amount, *amount)
757 .map_err(|_| StateShardError::RuneAmountAdditionOverflow)?;
758 Ok(())
759 },
760 )?;
761 }
762 }
763
764 Ok(result)
765}
766
767#[cfg(feature = "runes")]
823#[allow(clippy::too_many_arguments)]
824pub fn redistribute_remaining_rune_to_shards<
825 'info,
826 const MAX_MODIFIED_ACCOUNTS: usize,
827 const MAX_INPUTS_TO_SIGN: usize,
828 RS,
829 U,
830 S,
831>(
832 tx_builder: &mut TransactionBuilder<MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN, RS>,
833 selected_shards: &[Ref<'info, S>],
834 removed_from_shards: RS,
835 program_script_pubkey: ScriptBuf,
836) -> Result<Vec<RS>, StateShardError>
837where
838 RS: FixedCapacitySet<Item = RuneAmount> + Default,
839 U: UtxoInfoTrait<RS>,
840 S: StateShard<U, RS> + ZeroCopy + Owner,
841{
842 let remaining_amount = compute_unsettled_rune_in_shards(selected_shards, removed_from_shards)?;
843
844 let mut distribution =
845 plan_rune_distribution_among_shards(tx_builder, selected_shards, &remaining_amount)?;
846
847 distribution.sort_by(|a, b| {
849 let total_a: u128 = a.iter().map(|r| r.amount).sum();
850 let total_b: u128 = b.iter().map(|r| r.amount).sum();
851 total_b.cmp(&total_a)
852 });
853
854 let current_output_index = tx_builder.transaction.output.len();
855 tx_builder.runestone.pointer = Some(current_output_index as u32);
856
857 let mut index = current_output_index;
858 for amount_set in distribution.iter() {
859 tx_builder.transaction.output.push(TxOut {
860 value: Amount::from_sat(DUST_LIMIT),
861 script_pubkey: program_script_pubkey.clone(),
862 });
863
864 if index > current_output_index {
865 for rune_amount in amount_set.iter() {
866 tx_builder.runestone.edicts.push(Edict {
867 id: ordinals::RuneId {
868 block: rune_amount.id.block,
869 tx: rune_amount.id.tx,
870 },
871 amount: rune_amount.amount,
872 output: index as u32,
873 });
874 }
875 }
876
877 index += 1;
878 }
879
880 Ok(distribution)
881}
882
883#[cfg(test)]
884mod tests_loader {
885 use super::super::tests::common::{
886 create_btc_utxo, create_shard, leak_loaders_from_vec, MockShardZc,
887 };
888 use super::*;
889 use satellite_bitcoin::utxo_info::SingleRuneSet;
891 use satellite_lang::prelude::AccountLoader;
892 use std::cell::Ref;
893
894 use satellite_bitcoin::TransactionBuilder as TB;
896
897 #[allow(unused_macros)]
898 macro_rules! new_tb {
899 ($max_modified_accounts:expr, $max_inputs_to_sign:expr) => {
900 TB::<$max_modified_accounts, $max_inputs_to_sign, SingleRuneSet>::new()
901 };
902 }
903
904 pub fn create_shard_refs_from_loaders<'info>(
906 loaders: &'info [AccountLoader<'info, MockShardZc>],
907 indices: &[usize],
908 ) -> Result<Vec<Ref<'info, MockShardZc>>, arch_program::program_error::ProgramError> {
909 let mut refs = Vec::new();
910 for &idx in indices {
911 refs.push(loaders[idx].load()?);
912 }
913 Ok(refs)
914 }
915
916 mod plan_btc_distribution_among_shards {
917 use super::super::super::split;
918
919 use super::*;
920 use satellite_bitcoin::{constants::DUST_LIMIT, utxo_info::SingleRuneSet};
921 use split::plan_btc_distribution_among_shards;
922
923 #[test]
924 fn proportional_distribution_insufficient_remaining() {
925 const MAX_MODIFIED_ACCOUNTS: usize = 0;
926 const MAX_INPUTS_TO_SIGN: usize = 3;
927 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
928
929 let shards: Vec<MockShardZc> =
931 vec![create_shard(100), create_shard(200), create_shard(300)];
932 let loaders = leak_loaders_from_vec(shards);
933 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
934
935 let dist = plan_btc_distribution_among_shards::<
937 MAX_MODIFIED_ACCOUNTS,
938 MAX_INPUTS_TO_SIGN,
939 SingleRuneSet,
940 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
941 MockShardZc,
942 >(&tx_builder, &shard_refs, 150u128);
943 assert!(matches!(dist, Err(DistributionError::TotalBelowDustLimit)));
944 }
945
946 #[test]
947 fn zero_remaining_amount() {
948 const MAX_MODIFIED_ACCOUNTS: usize = 0;
949 const MAX_INPUTS_TO_SIGN: usize = 2;
950 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
951
952 let shards = vec![create_shard(1_000), create_shard(2_000)];
953 let loaders = leak_loaders_from_vec(shards);
954 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
955
956 let dist = plan_btc_distribution_among_shards::<
957 MAX_MODIFIED_ACCOUNTS,
958 MAX_INPUTS_TO_SIGN,
959 SingleRuneSet,
960 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
961 MockShardZc,
962 >(&tx_builder, &shard_refs, 0u128)
963 .unwrap();
964 assert!(dist.is_empty());
965 }
966
967 #[test]
968 fn single_shard() {
969 const MAX_MODIFIED_ACCOUNTS: usize = 0;
970 const MAX_INPUTS_TO_SIGN: usize = 1;
971 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
972
973 let shards = vec![create_shard(500)];
974 let loaders = leak_loaders_from_vec(shards);
975 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
976
977 let dist = plan_btc_distribution_among_shards::<
978 MAX_MODIFIED_ACCOUNTS,
979 MAX_INPUTS_TO_SIGN,
980 SingleRuneSet,
981 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
982 MockShardZc,
983 >(&tx_builder, &shard_refs, 1_000u128)
984 .unwrap();
985
986 assert_eq!(dist, vec![1_000]);
987 }
988
989 #[test]
990 fn empty_shards_all_zero_balances() {
991 const MAX_MODIFIED_ACCOUNTS: usize = 0;
992 const MAX_INPUTS_TO_SIGN: usize = 3;
993 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
994
995 let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
996 let loaders = leak_loaders_from_vec(shards);
997 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
998
999 let dist = plan_btc_distribution_among_shards::<
1000 MAX_MODIFIED_ACCOUNTS,
1001 MAX_INPUTS_TO_SIGN,
1002 SingleRuneSet,
1003 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1004 MockShardZc,
1005 >(&tx_builder, &shard_refs, 1_500u128)
1006 .unwrap();
1007
1008 assert_eq!(dist, vec![1_500]);
1009 }
1010
1011 #[test]
1012 fn remainder_distribution_sub_dust_merge() {
1013 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1014 const MAX_INPUTS_TO_SIGN: usize = 3;
1015 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1016
1017 let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1018 let loaders = leak_loaders_from_vec(shards);
1019 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1020
1021 let amount = 1_001u128;
1022 let dist = plan_btc_distribution_among_shards::<
1023 MAX_MODIFIED_ACCOUNTS,
1024 MAX_INPUTS_TO_SIGN,
1025 SingleRuneSet,
1026 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1027 MockShardZc,
1028 >(&tx_builder, &shard_refs, amount)
1029 .unwrap();
1030 assert_eq!(dist.iter().sum::<u128>(), amount);
1031 assert_eq!(dist, vec![amount]);
1032 }
1033
1034 #[test]
1035 fn used_utxos_excluded() {
1036 use bitcoin::{transaction::Version, OutPoint, ScriptBuf, Sequence, TxIn, Witness};
1037
1038 const MAX_MODIFIED_ACCOUNTS: usize = 1;
1039 const MAX_INPUTS_TO_SIGN: usize = 2;
1040
1041 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1042
1043 let shard1 = create_shard(1_000);
1045 let shard2 = create_shard(1_000);
1046
1047 let used_meta = shard1.btc_utxos()[0].meta;
1049
1050 let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1051 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1052
1053 tx_builder.transaction.version = Version::TWO;
1055 tx_builder.transaction.input.push(TxIn {
1056 previous_output: OutPoint::new(used_meta.to_txid(), used_meta.vout()),
1057 script_sig: ScriptBuf::new(),
1058 sequence: Sequence::MAX,
1059 witness: Witness::new(),
1060 });
1061
1062 let dist = plan_btc_distribution_among_shards::<
1063 MAX_MODIFIED_ACCOUNTS,
1064 MAX_INPUTS_TO_SIGN,
1065 SingleRuneSet,
1066 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1067 MockShardZc,
1068 >(&tx_builder, &shard_refs, 1_000u128)
1069 .unwrap();
1070
1071 assert_eq!(dist, vec![1_000]);
1072 }
1073
1074 #[test]
1075 fn partial_shard_selection() {
1076 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1077 const MAX_INPUTS_TO_SIGN: usize = 4;
1078 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1079
1080 let shards = vec![
1081 create_shard(1_000),
1082 create_shard(2_000),
1083 create_shard(3_000),
1084 create_shard(4_000),
1085 ];
1086 let loaders = leak_loaders_from_vec(shards);
1087 let shard_refs = create_shard_refs_from_loaders(&loaders, &[1, 2]).unwrap();
1088
1089 let dist = plan_btc_distribution_among_shards::<
1090 MAX_MODIFIED_ACCOUNTS,
1091 MAX_INPUTS_TO_SIGN,
1092 SingleRuneSet,
1093 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1094 MockShardZc,
1095 >(&tx_builder, &shard_refs, 2_000u128)
1096 .unwrap();
1097
1098 assert_eq!(dist.iter().sum::<u128>(), 2_000);
1099 assert_eq!(dist, vec![2_000]);
1100 }
1101
1102 #[test]
1103 fn large_numbers() {
1104 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1105 const MAX_INPUTS_TO_SIGN: usize = 2;
1106 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1107
1108 let shards = vec![create_shard(u64::MAX), create_shard(u64::MAX)];
1109 let loaders = leak_loaders_from_vec(shards);
1110 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1111
1112 let dist = plan_btc_distribution_among_shards::<
1113 MAX_MODIFIED_ACCOUNTS,
1114 MAX_INPUTS_TO_SIGN,
1115 SingleRuneSet,
1116 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1117 MockShardZc,
1118 >(&tx_builder, &shard_refs, 1_000u128)
1119 .unwrap();
1120
1121 assert_eq!(dist, vec![1_000]);
1122 }
1123
1124 #[test]
1125 fn split_remaining_amount_even_and_odd() {
1126 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1127 const MAX_INPUTS_TO_SIGN: usize = 2;
1128 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1129
1130 let shards = vec![create_shard(0), create_shard(0)];
1131 let loaders = leak_loaders_from_vec(shards);
1132 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1133
1134 let dist_odd = plan_btc_distribution_among_shards::<
1136 MAX_MODIFIED_ACCOUNTS,
1137 MAX_INPUTS_TO_SIGN,
1138 SingleRuneSet,
1139 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1140 MockShardZc,
1141 >(&tx_builder, &shard_refs, 2_041u128)
1142 .unwrap();
1143 assert_eq!(dist_odd, vec![1_021, 1_020]);
1144 assert_eq!(dist_odd.iter().sum::<u128>(), 2_041);
1145
1146 let dist_even = plan_btc_distribution_among_shards::<
1148 MAX_MODIFIED_ACCOUNTS,
1149 MAX_INPUTS_TO_SIGN,
1150 SingleRuneSet,
1151 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1152 MockShardZc,
1153 >(&tx_builder, &shard_refs, 2_000u128)
1154 .unwrap();
1155 assert_eq!(dist_even, vec![1_000, 1_000]);
1156 }
1157
1158 #[test]
1159 fn split_remaining_amount_with_existing_balances() {
1160 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1161 const MAX_INPUTS_TO_SIGN: usize = 2;
1162 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1163
1164 let shards = vec![create_shard(1_000), create_shard(0)];
1165 let loaders = leak_loaders_from_vec(shards);
1166 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1167
1168 let dist = plan_btc_distribution_among_shards::<
1169 MAX_MODIFIED_ACCOUNTS,
1170 MAX_INPUTS_TO_SIGN,
1171 SingleRuneSet,
1172 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1173 MockShardZc,
1174 >(&tx_builder, &shard_refs, 2_041u128)
1175 .unwrap();
1176
1177 assert_eq!(dist.iter().sum::<u128>(), 2_041);
1178 assert_eq!(dist, vec![2_041]);
1179 }
1180
1181 #[test]
1182 fn single_shard_sub_dust_amount() {
1183 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1184 const MAX_INPUTS_TO_SIGN: usize = 1;
1185 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1186
1187 let shards = vec![create_shard(0)];
1188 let loaders = leak_loaders_from_vec(shards);
1189 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
1190
1191 let dist = plan_btc_distribution_among_shards::<
1192 MAX_MODIFIED_ACCOUNTS,
1193 MAX_INPUTS_TO_SIGN,
1194 SingleRuneSet,
1195 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1196 MockShardZc,
1197 >(&tx_builder, &shard_refs, (DUST_LIMIT as u128) - 1u128);
1198 assert!(matches!(dist, Err(DistributionError::TotalBelowDustLimit)));
1199 }
1200
1201 #[test]
1202 fn single_shard_exact_dust_limit() {
1203 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1204 const MAX_INPUTS_TO_SIGN: usize = 1;
1205 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1206
1207 let shards = vec![create_shard(0)];
1208 let loaders = leak_loaders_from_vec(shards);
1209 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
1210
1211 let dist = plan_btc_distribution_among_shards::<
1212 MAX_MODIFIED_ACCOUNTS,
1213 MAX_INPUTS_TO_SIGN,
1214 SingleRuneSet,
1215 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1216 MockShardZc,
1217 >(&tx_builder, &shard_refs, DUST_LIMIT as u128)
1218 .unwrap();
1219
1220 assert_eq!(dist, vec![DUST_LIMIT as u128]);
1221 }
1222
1223 #[test]
1224 fn two_shards_each_exact_dust_limit() {
1225 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1226 const MAX_INPUTS_TO_SIGN: usize = 2;
1227 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1228
1229 let shards = vec![create_shard(0), create_shard(0)];
1230 let loaders = leak_loaders_from_vec(shards);
1231 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1232
1233 let amount = (DUST_LIMIT as u128) * 2u128;
1234 let dist = plan_btc_distribution_among_shards::<
1235 MAX_MODIFIED_ACCOUNTS,
1236 MAX_INPUTS_TO_SIGN,
1237 SingleRuneSet,
1238 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1239 MockShardZc,
1240 >(&tx_builder, &shard_refs, amount)
1241 .unwrap();
1242
1243 assert_eq!(dist, vec![DUST_LIMIT as u128, DUST_LIMIT as u128]);
1244 }
1245
1246 #[test]
1247 fn mixed_dust_and_non_dust_allocations() {
1248 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1249 const MAX_INPUTS_TO_SIGN: usize = 3;
1250 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1251
1252 let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1253 let loaders = leak_loaders_from_vec(shards);
1254 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1255
1256 let amount = 1_600u128; let dist = plan_btc_distribution_among_shards::<
1258 MAX_MODIFIED_ACCOUNTS,
1259 MAX_INPUTS_TO_SIGN,
1260 SingleRuneSet,
1261 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1262 MockShardZc,
1263 >(&tx_builder, &shard_refs, amount)
1264 .unwrap();
1265
1266 assert_eq!(dist, vec![amount]);
1267 }
1268 }
1269
1270 mod compute_unsettled_btc_in_shards {
1274 use super::super::compute_unsettled_btc_in_shards;
1275 use super::*;
1276 use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, Witness};
1277 use satellite_bitcoin::fee_rate::FeeRate;
1278
1279 #[test]
1280 fn basic_unsettled_calculation() {
1281 const MAX_MODIFIED_ACCOUNTS: usize = 2;
1282 const MAX_INPUTS_TO_SIGN: usize = 2;
1283
1284 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1285
1286 let shard1 = create_shard(1_000);
1288 let shard2 = create_shard(500);
1289
1290 let spent_meta = shard1.btc_utxos()[0].meta;
1292
1293 let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1294 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1295
1296 tx_builder.transaction.input.push(TxIn {
1298 previous_output: OutPoint::new(spent_meta.to_txid(), spent_meta.vout()),
1299 script_sig: ScriptBuf::new(),
1300 sequence: Sequence::MAX,
1301 witness: Witness::new(),
1302 });
1303
1304 let unsettled = compute_unsettled_btc_in_shards::<
1305 MAX_MODIFIED_ACCOUNTS,
1306 MAX_INPUTS_TO_SIGN,
1307 SingleRuneSet,
1308 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1309 MockShardZc,
1310 >(&tx_builder, &shard_refs, 1_000, &FeeRate(1.0))
1311 .unwrap();
1312
1313 assert_eq!(unsettled, 500);
1315 }
1316 }
1317
1318 mod edge_cases {
1322 use super::super::super::tests::common::add_btc_utxos_bulk;
1323 use super::super::super::tests::common::random_utxo_meta;
1324 use super::super::{
1325 balance_amount_across_shards as balance_loader, compute_unsettled_btc_in_shards,
1326 plan_btc_distribution_among_shards, redistribute_sub_dust_values,
1327 };
1328 use super::*;
1329 use bitcoin::{OutPoint, ScriptBuf, Sequence, TxIn, Witness};
1330 use satellite_bitcoin::MathError;
1331 use satellite_bitcoin::{constants::DUST_LIMIT, fee_rate::FeeRate};
1332 use satellite_lang::prelude::AccountLoader;
1333
1334 #[test]
1336 fn redistribute_sub_dust_all_above_dust() {
1337 let mut amounts = vec![1000u128, 2000u128, 3000u128];
1338 let original = amounts.clone();
1339 redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
1340 assert_eq!(amounts, original);
1341 }
1342
1343 #[test]
1344 fn redistribute_sub_dust_all_below_but_sum_above() {
1345 let mut amounts = vec![200u128, 200u128, 200u128];
1346 redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
1347 assert_eq!(amounts, vec![600u128]);
1348 }
1349
1350 #[test]
1351 fn redistribute_sub_dust_mixed_with_remainder() {
1352 let mut amounts = vec![1000u128, 200u128, 300u128, 2000u128]; redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128).unwrap();
1354 assert_eq!(amounts.len(), 2);
1355 assert_eq!(amounts.iter().sum::<u128>(), 3500u128);
1356 assert!(amounts.contains(&1250u128));
1357 assert!(amounts.contains(&2250u128));
1358 }
1359
1360 #[test]
1361 fn redistribute_sub_dust_total_below_dust_returns_error() {
1362 let mut amounts = vec![200u128, 300u128]; let res = redistribute_sub_dust_values(&mut amounts, DUST_LIMIT as u128);
1364 assert!(matches!(
1365 res,
1366 Err(super::super::DistributionError::TotalBelowDustLimit)
1367 ));
1368 }
1369
1370 #[test]
1372 fn plan_btc_distribution_zero_shards() {
1373 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1374 const MAX_INPUTS_TO_SIGN: usize = 0;
1375 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1376
1377 let loaders: &[AccountLoader<'static, MockShardZc>] = &[];
1379 let shard_refs = create_shard_refs_from_loaders(&loaders, &[]).unwrap();
1380
1381 let result = plan_btc_distribution_among_shards::<
1382 MAX_MODIFIED_ACCOUNTS,
1383 MAX_INPUTS_TO_SIGN,
1384 SingleRuneSet,
1385 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1386 MockShardZc,
1387 >(&tx_builder, &shard_refs, 1_000u128);
1388
1389 assert!(matches!(
1390 result,
1391 Err(DistributionError::Math(MathError::DivisionOverflow))
1392 ));
1393 }
1394
1395 #[test]
1397 fn max_capacity_stress() {
1398 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1399 const MAX_INPUTS_TO_SIGN: usize = 10;
1400 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1401
1402 let shards: Vec<MockShardZc> = (0..MAX_INPUTS_TO_SIGN)
1404 .map(|i| {
1405 let mut s = create_shard(0);
1406 let values = vec![1_000u64; 5];
1407 add_btc_utxos_bulk(&mut s, &values);
1408 if i > 0 {
1410 }
1412 s
1413 })
1414 .collect();
1415
1416 let loaders = leak_loaders_from_vec(shards);
1417 let shard_refs =
1418 create_shard_refs_from_loaders(&loaders, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).unwrap();
1419
1420 let dist = plan_btc_distribution_among_shards::<
1421 MAX_MODIFIED_ACCOUNTS,
1422 MAX_INPUTS_TO_SIGN,
1423 SingleRuneSet,
1424 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1425 MockShardZc,
1426 >(&tx_builder, &shard_refs, 10_000u128)
1427 .unwrap();
1428
1429 assert_eq!(dist.iter().sum::<u128>(), 10_000u128);
1430 }
1431
1432 #[test]
1434 fn near_boundary_dust_splits_below() {
1435 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1436 const MAX_INPUTS_TO_SIGN: usize = 3;
1437 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1438
1439 let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1440 let loaders = leak_loaders_from_vec(shards);
1441 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1442
1443 let amount = (DUST_LIMIT as u128) * 3 - 1u128;
1444 let dist = plan_btc_distribution_among_shards::<
1445 MAX_MODIFIED_ACCOUNTS,
1446 MAX_INPUTS_TO_SIGN,
1447 SingleRuneSet,
1448 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1449 MockShardZc,
1450 >(&tx_builder, &shard_refs, amount)
1451 .unwrap();
1452
1453 assert!(dist.len() < 3);
1454 assert_eq!(dist.iter().sum::<u128>(), amount);
1455 }
1456
1457 #[test]
1458 fn near_boundary_dust_splits_above() {
1459 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1460 const MAX_INPUTS_TO_SIGN: usize = 3;
1461 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1462
1463 let shards = vec![create_shard(0), create_shard(0), create_shard(0)];
1464 let loaders = leak_loaders_from_vec(shards);
1465 let shard_refs = create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1466
1467 let amount = (DUST_LIMIT as u128) * 3 + 1u128;
1468 let dist = plan_btc_distribution_among_shards::<
1469 MAX_MODIFIED_ACCOUNTS,
1470 MAX_INPUTS_TO_SIGN,
1471 SingleRuneSet,
1472 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1473 MockShardZc,
1474 >(&tx_builder, &shard_refs, amount)
1475 .unwrap();
1476
1477 assert_eq!(dist.len(), 3);
1478 assert!(dist.iter().all(|&x| x >= DUST_LIMIT as u128));
1479 assert_eq!(dist.iter().sum::<u128>(), amount);
1480 }
1481
1482 #[test]
1484 fn duplicate_meta_utxos_across_shards() {
1485 const MAX_MODIFIED_ACCOUNTS: usize = 1;
1486 const MAX_INPUTS_TO_SIGN: usize = 2;
1487
1488 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1489
1490 let shared_meta = random_utxo_meta(42);
1492 let utxo1 = create_btc_utxo(1_000, 42);
1493 let mut utxo2 = create_btc_utxo(2_000, 42); utxo2.meta = shared_meta; let mut shard1 = create_shard(0);
1497 let mut shard2 = create_shard(0);
1498 shard1.add_btc_utxo(utxo1);
1499 shard2.add_btc_utxo(utxo2);
1500
1501 let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1502 let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1504
1505 tx_builder.transaction.input.push(TxIn {
1507 previous_output: OutPoint::new(shared_meta.to_txid(), shared_meta.vout()),
1508 script_sig: ScriptBuf::new(),
1509 sequence: Sequence::MAX,
1510 witness: Witness::new(),
1511 });
1512
1513 let unsettled = compute_unsettled_btc_in_shards::<
1514 MAX_MODIFIED_ACCOUNTS,
1515 MAX_INPUTS_TO_SIGN,
1516 SingleRuneSet,
1517 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1518 MockShardZc,
1519 >(&tx_builder, &shard_refs, 0, &FeeRate(1.0))
1520 .unwrap();
1521
1522 assert_eq!(unsettled, 3_000);
1523 }
1524
1525 #[test]
1527 fn high_fee_scenario_overflow() {
1528 use arch_program::rune::{RuneAmount, RuneId};
1529 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1530 const MAX_INPUTS_TO_SIGN: usize = 1;
1531
1532 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1533
1534 let shard = create_shard(0);
1535 let loaders = leak_loaders_from_vec(vec![shard]);
1536 let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0]).unwrap();
1537
1538 let rune_amount = RuneAmount {
1540 id: RuneId::BTC,
1541 amount: u128::MAX,
1542 };
1543 let result = balance_loader::<
1544 MAX_MODIFIED_ACCOUNTS,
1545 MAX_INPUTS_TO_SIGN,
1546 SingleRuneSet,
1547 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1548 MockShardZc,
1549 >(&tx_builder, &shard_refs, &rune_amount);
1550
1551 assert_eq!(result.unwrap(), vec![u128::MAX]);
1553 }
1554
1555 #[test]
1557 fn empty_amount_optimization() {
1558 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1559 const MAX_INPUTS_TO_SIGN: usize = 2;
1560
1561 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1562
1563 let original_outputs = tx_builder.transaction.output.len();
1565
1566 let shards = vec![create_shard(1_000), create_shard(2_000)];
1567 let loaders = leak_loaders_from_vec(shards);
1568 let mut shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1569
1570 let dist = super::super::redistribute_remaining_btc_to_shards::<
1571 MAX_MODIFIED_ACCOUNTS,
1572 MAX_INPUTS_TO_SIGN,
1573 SingleRuneSet,
1574 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1575 MockShardZc,
1576 >(
1577 &mut tx_builder,
1578 &mut shard_refs,
1579 0,
1580 &ScriptBuf::new(),
1581 &FeeRate(1.0),
1582 )
1583 .unwrap();
1584
1585 assert!(dist.is_empty());
1586 assert_eq!(tx_builder.transaction.output.len(), original_outputs);
1587 }
1588
1589 #[test]
1591 fn balance_amount_overflow_protection() {
1592 use arch_program::rune::{RuneAmount, RuneId};
1593 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1594 const MAX_INPUTS_TO_SIGN: usize = 2;
1595 let tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1596
1597 let mut shard1 = create_shard(0);
1599 let mut shard2 = create_shard(0);
1600 shard1.add_btc_utxo(create_btc_utxo(u64::MAX, 1));
1601 shard2.add_btc_utxo(create_btc_utxo(u64::MAX, 2));
1602
1603 let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1604 let shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1605
1606 let rune_amount = RuneAmount {
1607 id: RuneId::BTC,
1608 amount: u128::MAX,
1609 };
1610 let res = balance_loader::<
1611 MAX_MODIFIED_ACCOUNTS,
1612 MAX_INPUTS_TO_SIGN,
1613 SingleRuneSet,
1614 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1615 MockShardZc,
1616 >(&tx_builder, &shard_refs, &rune_amount);
1617
1618 assert!(res.is_err());
1619 }
1620
1621 #[cfg(feature = "runes")]
1623 #[test]
1624 fn runestone_pointer_update() {
1625 use bitcoin::{Amount, TxOut};
1626
1627 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1628 const MAX_INPUTS_TO_SIGN: usize = 2;
1629
1630 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1631
1632 tx_builder.transaction.output.push(TxOut {
1634 value: Amount::from_sat(1_000),
1635 script_pubkey: ScriptBuf::new(),
1636 });
1637 tx_builder.transaction.output.push(TxOut {
1638 value: Amount::from_sat(2_000),
1639 script_pubkey: ScriptBuf::new(),
1640 });
1641
1642 let old_output_count = tx_builder.transaction.output.len();
1643
1644 let shards = vec![create_shard(0), create_shard(0)];
1646 let loaders = leak_loaders_from_vec(shards);
1647 let mut shard_refs = super::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1648
1649 crate::split::redistribute_remaining_rune_to_shards::<
1651 MAX_MODIFIED_ACCOUNTS,
1652 MAX_INPUTS_TO_SIGN,
1653 SingleRuneSet,
1654 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1655 MockShardZc,
1656 >(
1657 &mut tx_builder,
1658 &mut shard_refs,
1659 SingleRuneSet::default(),
1660 ScriptBuf::new(),
1661 )
1662 .unwrap();
1663
1664 assert_eq!(tx_builder.runestone.pointer, Some(old_output_count as u32));
1666
1667 for (i, edict) in tx_builder.runestone.edicts.iter().enumerate() {
1669 if i > 0 {
1670 assert_eq!(edict.output, (old_output_count + i) as u32);
1671 }
1672 }
1673 }
1674 }
1675}
1676
1677#[cfg(all(test, feature = "runes"))]
1681mod rune_tests_loader {
1682 use super::*;
1683 use crate::tests::common::{
1685 create_rune_utxo, create_shard, leak_loaders_from_vec, MockShardZc,
1686 };
1687 use arch_program::rune::{RuneAmount, RuneId};
1688 use bitcoin::ScriptBuf;
1689 use satellite_bitcoin::utxo_info::SingleRuneSet;
1690 use satellite_bitcoin::TransactionBuilder as TB;
1691
1692 #[allow(unused_macros)]
1693 macro_rules! new_tb {
1694 ($max_utxos:expr, $max_shards:expr) => {
1695 TB::<$max_utxos, $max_shards, SingleRuneSet>::new()
1696 };
1697 }
1698
1699 #[test]
1703 fn compute_unsettled_rune_basic() {
1704 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1705 const MAX_INPUTS_TO_SIGN: usize = 2;
1706
1707 let mut shard1 = create_shard(0);
1709 let mut shard2 = create_shard(0);
1710 shard1.set_rune_utxo(create_rune_utxo(100, 0));
1711 shard2.set_rune_utxo(create_rune_utxo(50, 1));
1712
1713 let loaders = leak_loaders_from_vec(vec![shard1, shard2]);
1714 let shard_refs =
1715 super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1]).unwrap();
1716
1717 let unsettled = crate::split::compute_unsettled_rune_in_shards::<
1718 SingleRuneSet,
1719 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1720 MockShardZc,
1721 >(&shard_refs, SingleRuneSet::default())
1722 .unwrap();
1723
1724 assert_eq!(unsettled.find(&RuneId::new(1, 1)).unwrap().amount, 150);
1725 }
1726
1727 #[test]
1731 fn plan_rune_distribution_proportional() {
1732 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1733 const MAX_INPUTS_TO_SIGN: usize = 3;
1734
1735 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1736
1737 let mut shard0 = create_shard(0);
1739 let mut shard1 = create_shard(0);
1740 let mut shard2 = create_shard(0);
1741 shard0.set_rune_utxo(create_rune_utxo(100, 0));
1742 shard1.set_rune_utxo(create_rune_utxo(200, 1));
1743 shard2.set_rune_utxo(create_rune_utxo(300, 2));
1744
1745 let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1746 let shard_refs =
1747 super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1748
1749 let mut target = SingleRuneSet::default();
1751 target
1752 .insert(RuneAmount {
1753 id: RuneId::new(1, 1),
1754 amount: 600,
1755 })
1756 .unwrap();
1757
1758 let dist = crate::split::plan_rune_distribution_among_shards::<
1759 MAX_MODIFIED_ACCOUNTS,
1760 MAX_INPUTS_TO_SIGN,
1761 SingleRuneSet,
1762 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1763 MockShardZc,
1764 >(&mut tx_builder, &shard_refs, &target)
1765 .unwrap();
1766
1767 assert_eq!(dist.len(), 3);
1768 let allocs: Vec<u128> = dist
1769 .iter()
1770 .map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
1771 .collect();
1772 assert_eq!(allocs, vec![300, 200, 100]);
1773 }
1774
1775 #[test]
1776 fn plan_rune_distribution_zero_amount_inserts_zero_entries() {
1777 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1778 const MAX_INPUTS_TO_SIGN: usize = 3;
1779
1780 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1781
1782 let mut shard0 = create_shard(0);
1784 let mut shard1 = create_shard(0);
1785 let mut shard2 = create_shard(0);
1786 shard0.set_rune_utxo(create_rune_utxo(100, 0));
1787 shard1.set_rune_utxo(create_rune_utxo(200, 1));
1788 shard2.set_rune_utxo(create_rune_utxo(300, 2));
1789
1790 let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1791 let shard_refs =
1792 super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1793
1794 let mut target = SingleRuneSet::default();
1796 target
1797 .insert(RuneAmount {
1798 id: RuneId::new(1, 1),
1799 amount: 0,
1800 })
1801 .unwrap();
1802
1803 let dist = crate::split::plan_rune_distribution_among_shards::<
1804 MAX_MODIFIED_ACCOUNTS,
1805 MAX_INPUTS_TO_SIGN,
1806 SingleRuneSet,
1807 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1808 MockShardZc,
1809 >(&mut tx_builder, &shard_refs, &target)
1810 .unwrap();
1811
1812 assert_eq!(dist.len(), 3);
1813 for s in dist.iter() {
1814 let r = s.find(&RuneId::new(1, 1)).expect("rune entry present");
1815 assert_eq!(r.amount, 0);
1816 assert_eq!(s.len(), 1);
1817 }
1818 }
1819
1820 #[test]
1821 fn plan_rune_distribution_partial_creates_zero_entry_for_some_shards() {
1822 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1823 const MAX_INPUTS_TO_SIGN: usize = 3;
1824
1825 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1826
1827 let mut shard0 = create_shard(0);
1829 let mut shard1 = create_shard(0);
1830 let mut shard2 = create_shard(0);
1831 shard0.set_rune_utxo(create_rune_utxo(100, 0));
1832 shard1.set_rune_utxo(create_rune_utxo(200, 1));
1833 shard2.set_rune_utxo(create_rune_utxo(300, 2));
1834
1835 let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1836 let shard_refs =
1837 super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1838
1839 let mut target = SingleRuneSet::default();
1841 target
1842 .insert(RuneAmount {
1843 id: RuneId::new(1, 1),
1844 amount: 10,
1845 })
1846 .unwrap();
1847
1848 let dist = crate::split::plan_rune_distribution_among_shards::<
1849 MAX_MODIFIED_ACCOUNTS,
1850 MAX_INPUTS_TO_SIGN,
1851 SingleRuneSet,
1852 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1853 MockShardZc,
1854 >(&mut tx_builder, &shard_refs, &target)
1855 .unwrap();
1856
1857 assert_eq!(dist.len(), 3);
1858 let allocs: Vec<u128> = dist
1859 .iter()
1860 .map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
1861 .collect();
1862 assert!(allocs.contains(&0));
1863 for (i, amt) in allocs.iter().enumerate() {
1864 let r = dist[i].find(&RuneId::new(1, 1)).unwrap();
1865 assert_eq!(r.amount, *amt);
1866 }
1867 }
1868
1869 #[test]
1873 fn redistribute_remaining_rune_distribution() {
1874 const MAX_MODIFIED_ACCOUNTS: usize = 0;
1875 const MAX_INPUTS_TO_SIGN: usize = 3;
1876
1877 let mut tx_builder = new_tb!(MAX_MODIFIED_ACCOUNTS, MAX_INPUTS_TO_SIGN);
1878
1879 let mut shard0 = create_shard(0);
1881 let mut shard1 = create_shard(0);
1882 let mut shard2 = create_shard(0);
1883 shard0.set_rune_utxo(create_rune_utxo(100, 0));
1884 shard1.set_rune_utxo(create_rune_utxo(200, 1));
1885 shard2.set_rune_utxo(create_rune_utxo(300, 2));
1886
1887 let loaders = leak_loaders_from_vec(vec![shard0, shard1, shard2]);
1888 let mut shard_refs =
1889 super::tests_loader::create_shard_refs_from_loaders(&loaders, &[0, 1, 2]).unwrap();
1890
1891 let mut removed = SingleRuneSet::default();
1893 removed
1894 .insert(RuneAmount {
1895 id: RuneId::new(1, 1),
1896 amount: 150,
1897 })
1898 .unwrap();
1899
1900 let dist = crate::split::redistribute_remaining_rune_to_shards::<
1901 MAX_MODIFIED_ACCOUNTS,
1902 MAX_INPUTS_TO_SIGN,
1903 SingleRuneSet,
1904 satellite_bitcoin::utxo_info::UtxoInfo<SingleRuneSet>,
1905 MockShardZc,
1906 >(&mut tx_builder, &mut shard_refs, removed, ScriptBuf::new())
1907 .unwrap();
1908
1909 let mut allocs: Vec<u128> = dist
1911 .iter()
1912 .map(|s| s.find(&RuneId::new(1, 1)).unwrap().amount)
1913 .collect();
1914 allocs.sort_unstable();
1915 assert_eq!(allocs, vec![50, 150, 250]);
1916 }
1917}