1use std::{
16 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
17 ops::RangeInclusive,
18 sync::Arc,
19};
20
21use rocksdb::ColumnFamily;
22use zebra_chain::{
23 amount::{self, Amount, Constraint, NonNegative},
24 block::Height,
25 parameters::Network,
26 transaction::{self, Transaction},
27 transparent::{self, Input},
28};
29
30use crate::{
31 request::FinalizedBlock,
32 service::finalized_state::{
33 disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk},
34 disk_format::{
35 transparent::{
36 AddressBalanceLocation, AddressBalanceLocationChange, AddressBalanceLocationInner,
37 AddressBalanceLocationUpdates, AddressLocation, AddressTransaction,
38 AddressUnspentOutput, OutputLocation,
39 },
40 TransactionLocation,
41 },
42 zebra_db::ZebraDb,
43 },
44 FromDisk, IntoDisk,
45};
46
47use super::super::TypedColumnFamily;
48
49pub const TX_LOC_BY_SPENT_OUT_LOC: &str = "tx_loc_by_spent_out_loc";
51
52pub const BALANCE_BY_TRANSPARENT_ADDR: &str = "balance_by_transparent_addr";
54
55pub const BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP: &str = "fetch_add_balance_and_received";
57
58pub fn fetch_add_balance_and_received(
60 _: &[u8],
61 existing_val: Option<&[u8]>,
62 operands: &rocksdb::MergeOperands,
63) -> Option<Vec<u8>> {
64 existing_val
69 .into_iter()
70 .chain(operands)
71 .map(AddressBalanceLocationChange::from_bytes)
72 .reduce(|a, b| (a + b).expect("address balance/received should not overflow"))
73 .map(|address_balance_location| address_balance_location.as_bytes().to_vec())
74}
75
76pub type TransactionLocationBySpentOutputLocationCf<'cf> =
81 TypedColumnFamily<'cf, OutputLocation, TransactionLocation>;
82
83impl ZebraDb {
84 pub(crate) fn tx_loc_by_spent_output_loc_cf(
88 &self,
89 ) -> TransactionLocationBySpentOutputLocationCf<'_> {
90 TransactionLocationBySpentOutputLocationCf::new(&self.db, TX_LOC_BY_SPENT_OUT_LOC)
91 .expect("column family was created when database was created")
92 }
93
94 pub fn tx_location_by_spent_output_location(
99 &self,
100 output_location: &OutputLocation,
101 ) -> Option<TransactionLocation> {
102 self.tx_loc_by_spent_output_loc_cf().zs_get(output_location)
103 }
104
105 pub fn address_balance_cf(&self) -> &ColumnFamily {
107 self.db.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap()
108 }
109
110 #[allow(clippy::unwrap_in_result)]
113 pub fn address_balance_location(
114 &self,
115 address: &transparent::Address,
116 ) -> Option<AddressBalanceLocation> {
117 let balance_by_transparent_addr = self.address_balance_cf();
118
119 self.db.zs_get(&balance_by_transparent_addr, address)
120 }
121
122 pub fn address_balance(
125 &self,
126 address: &transparent::Address,
127 ) -> Option<(Amount<NonNegative>, u64)> {
128 self.address_balance_location(address)
129 .map(|abl| (abl.balance(), abl.received()))
130 }
131
132 pub fn address_location(&self, address: &transparent::Address) -> Option<AddressLocation> {
137 self.address_balance_location(address)
138 .map(|abl| abl.address_location())
139 }
140
141 pub fn output_location(&self, outpoint: &transparent::OutPoint) -> Option<OutputLocation> {
146 self.transaction_location(outpoint.hash)
147 .map(|transaction_location| {
148 OutputLocation::from_outpoint(transaction_location, outpoint)
149 })
150 }
151
152 pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::OrderedUtxo> {
155 let output_location = self.output_location(outpoint)?;
156
157 self.utxo_by_location(output_location)
158 }
159
160 pub fn spending_tx_loc(&self, outpoint: &transparent::OutPoint) -> Option<TransactionLocation> {
164 let output_location = self.output_location(outpoint)?;
165 self.tx_location_by_spent_output_location(&output_location)
166 }
167
168 #[allow(clippy::unwrap_in_result)]
171 pub fn utxo_by_location(
172 &self,
173 output_location: OutputLocation,
174 ) -> Option<transparent::OrderedUtxo> {
175 let utxo_by_out_loc = self.db.cf_handle("utxo_by_out_loc").unwrap();
176
177 let output = self.db.zs_get(&utxo_by_out_loc, &output_location)?;
178
179 let utxo = transparent::OrderedUtxo::new(
180 output,
181 output_location.height(),
182 output_location.transaction_index().as_usize(),
183 );
184
185 Some(utxo)
186 }
187
188 pub fn address_utxos(
194 &self,
195 address: &transparent::Address,
196 query_height_range: RangeInclusive<Height>,
197 limit: Option<usize>,
198 ) -> BTreeMap<OutputLocation, transparent::Output> {
199 let output_locations =
200 self.address_unspent_output_locations(address, query_height_range, limit);
201
202 self.utxos_by_locations(output_locations)
203 }
204
205 fn address_unspent_output_locations(
208 &self,
209 address: &transparent::Address,
210 query_height_range: RangeInclusive<Height>,
211 limit: Option<usize>,
212 ) -> BTreeSet<OutputLocation> {
213 let address_location = match self.address_location(address) {
214 Some(address_location) => address_location,
215 None => return BTreeSet::new(),
216 };
217
218 if address_location.height() > *query_height_range.end() {
220 return BTreeSet::new();
221 }
222
223 self.address_utxo_locations(address_location, query_height_range, limit)
224 .iter()
225 .map(|addr_out_loc| addr_out_loc.unspent_output_location())
226 .collect()
227 }
228
229 fn utxos_by_locations(
231 &self,
232 output_locations: BTreeSet<OutputLocation>,
233 ) -> BTreeMap<OutputLocation, transparent::Output> {
234 output_locations
236 .into_iter()
237 .filter_map(|output_location| {
238 Some((
239 output_location,
240 self.utxo_by_location(output_location)?.utxo.output,
241 ))
242 })
243 .collect()
244 }
245
246 pub fn address_utxo_locations(
249 &self,
250 address_location: AddressLocation,
251 query_height_range: RangeInclusive<Height>,
252 limit: Option<usize>,
253 ) -> BTreeSet<AddressUnspentOutput> {
254 let utxo_loc_by_transparent_addr_loc = self
255 .db
256 .cf_handle("utxo_loc_by_transparent_addr_loc")
257 .unwrap();
258
259 let unspent_output_range =
262 AddressUnspentOutput::address_iterator_range(address_location, query_height_range);
263
264 self.db
265 .zs_forward_range_iter(&utxo_loc_by_transparent_addr_loc, unspent_output_range)
266 .map(|(unspent_output, ())| unspent_output)
267 .take(limit.unwrap_or(usize::MAX))
268 .collect()
269 }
270
271 #[allow(clippy::unwrap_in_result)]
273 pub fn tx_id_by_location(&self, tx_location: TransactionLocation) -> Option<transaction::Hash> {
274 let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap();
275
276 self.db.zs_get(&hash_by_tx_loc, &tx_location)
277 }
278
279 pub fn address_tx_ids(
286 &self,
287 address: &transparent::Address,
288 query_height_range: RangeInclusive<Height>,
289 limit: Option<usize>,
290 ) -> BTreeMap<TransactionLocation, transaction::Hash> {
291 let transaction_locations = self.address_tx_locations(address, query_height_range, limit);
292
293 self.tx_ids_by_locations(transaction_locations)
294 }
295
296 fn address_tx_locations(
299 &self,
300 address: &transparent::Address,
301 query_height_range: RangeInclusive<Height>,
302 limit: Option<usize>,
303 ) -> BTreeSet<TransactionLocation> {
304 let address_location = match self.address_location(address) {
305 Some(address_location) => address_location,
306 None => return BTreeSet::new(),
307 };
308
309 if address_location.height() > *query_height_range.end() {
314 return BTreeSet::new();
315 }
316
317 self.address_transaction_locations(address_location, query_height_range, limit)
318 .iter()
319 .map(|tx_loc| tx_loc.transaction_location())
320 .collect()
321 }
322
323 fn tx_ids_by_locations(
325 &self,
326 transaction_locations: BTreeSet<TransactionLocation>,
327 ) -> BTreeMap<TransactionLocation, transaction::Hash> {
328 transaction_locations
329 .into_iter()
330 .map(|tx_loc| {
331 (
332 tx_loc,
333 self.tx_id_by_location(tx_loc)
334 .expect("transactions whose locations are stored must exist"),
335 )
336 })
337 .collect()
338 }
339
340 pub fn address_transaction_locations(
343 &self,
344 address_location: AddressLocation,
345 query_height_range: RangeInclusive<Height>,
346 limit: Option<usize>,
347 ) -> BTreeSet<AddressTransaction> {
348 let tx_loc_by_transparent_addr_loc =
349 self.db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
350
351 let transaction_location_range =
354 AddressTransaction::address_iterator_range(address_location, query_height_range);
355
356 self.db
357 .zs_forward_range_iter(&tx_loc_by_transparent_addr_loc, transaction_location_range)
358 .map(|(tx_loc, ())| tx_loc)
359 .take(limit.unwrap_or(usize::MAX))
360 .collect()
361 }
362
363 pub fn partial_finalized_transparent_balance(
376 &self,
377 addresses: &HashSet<transparent::Address>,
378 ) -> (Amount<NonNegative>, u64) {
379 let balance: amount::Result<(Amount<NonNegative>, u64)> = addresses
380 .iter()
381 .filter_map(|address| self.address_balance(address))
382 .try_fold(
383 (Amount::zero(), 0),
384 |(a_balance, a_received): (Amount<NonNegative>, u64), (b_balance, b_received)| {
385 let received = a_received.saturating_add(b_received);
386 Ok(((a_balance + b_balance)?, received))
387 },
388 );
389
390 balance.expect(
391 "unexpected amount overflow: value balances are valid, so partial sum should be valid",
392 )
393 }
394
395 pub fn partial_finalized_address_utxos(
413 &self,
414 addresses: &HashSet<transparent::Address>,
415 query_height_range: RangeInclusive<Height>,
416 limit: Option<usize>,
417 ) -> BTreeMap<OutputLocation, transparent::Output> {
418 let mut output_locations = BTreeSet::new();
421 for address in addresses {
422 output_locations.extend(self.address_unspent_output_locations(
423 address,
424 query_height_range.clone(),
425 limit,
426 ));
427 truncate_to_limit(&mut output_locations, limit);
428 }
429
430 self.utxos_by_locations(output_locations)
431 }
432
433 pub fn partial_finalized_transparent_tx_ids(
456 &self,
457 addresses: &HashSet<transparent::Address>,
458 query_height_range: RangeInclusive<Height>,
459 limit: Option<usize>,
460 ) -> BTreeMap<TransactionLocation, transaction::Hash> {
461 let mut transaction_locations = BTreeSet::new();
464 for address in addresses {
465 transaction_locations.extend(self.address_tx_locations(
466 address,
467 query_height_range.clone(),
468 limit,
469 ));
470 truncate_to_limit(&mut transaction_locations, limit);
471 }
472
473 self.tx_ids_by_locations(transaction_locations)
474 }
475}
476
477fn truncate_to_limit<T: Ord>(set: &mut BTreeSet<T>, limit: Option<usize>) {
480 if let Some(limit) = limit {
481 while set.len() > limit {
482 set.pop_last();
483 }
484 }
485}
486
487impl DiskWriteBatch {
488 #[allow(clippy::too_many_arguments)]
491 pub fn prepare_transparent_transaction_batch(
492 &mut self,
493 zebra_db: &ZebraDb,
494 network: &Network,
495 finalized: &FinalizedBlock,
496 new_outputs_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
497 spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
498 spent_utxos_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
499 #[cfg(feature = "indexer")] out_loc_by_outpoint: &HashMap<
500 transparent::OutPoint,
501 OutputLocation,
502 >,
503 mut address_balances: AddressBalanceLocationUpdates,
504 ) {
505 let db = &zebra_db.db;
506 let FinalizedBlock { block, height, .. } = finalized;
507
508 Self::prepare_transparent_address_balance_updates(
514 network,
515 *height,
516 &block.transactions,
517 spent_utxos_by_outpoint,
518 &mut address_balances,
519 );
520
521 self.prepare_new_transparent_outputs_batch(
524 db,
525 network,
526 new_outputs_by_out_loc,
527 &address_balances,
528 );
529 self.prepare_spent_transparent_outputs_batch(
530 db,
531 network,
532 spent_utxos_by_out_loc,
533 &address_balances,
534 );
535
536 for (tx_index, transaction) in block.transactions.iter().enumerate() {
538 let spending_tx_location = TransactionLocation::from_usize(*height, tx_index);
539
540 self.prepare_spending_transparent_tx_ids_batch(
541 zebra_db,
542 network,
543 spending_tx_location,
544 transaction,
545 spent_utxos_by_outpoint,
546 #[cfg(feature = "indexer")]
547 out_loc_by_outpoint,
548 &address_balances,
549 );
550 }
551
552 self.prepare_transparent_balances_batch(db, address_balances);
553 }
554
555 fn prepare_transparent_address_balance_updates(
570 network: &Network,
571 height: Height,
572 transactions: &[Arc<Transaction>],
573 spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
574 address_balances: &mut AddressBalanceLocationUpdates,
575 ) {
576 fn update_per_tx<
577 C: Constraint + Copy + std::fmt::Debug,
578 T: std::ops::DerefMut<Target = AddressBalanceLocationInner<C>>
579 + From<AddressBalanceLocationInner<C>>,
580 >(
581 addr_locs: &mut HashMap<transparent::Address, T>,
582 network: &Network,
583 height: Height,
584 transactions: &[Arc<Transaction>],
585 spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
586 ) {
587 for (tx_index, transaction) in transactions.iter().enumerate() {
588 for spent_outpoint in transaction.inputs().iter().filter_map(Input::outpoint) {
591 let spent_utxo = spent_utxos_by_outpoint
592 .get(&spent_outpoint)
593 .expect("spent outpoint must already be resolved");
594 if let Some(sending_address) = spent_utxo.output.address(network) {
595 let addr_loc = addr_locs
596 .get_mut(&sending_address)
597 .expect("spent outputs must already have an address balance");
598
599 addr_loc
600 .spend_output(&spent_utxo.output)
601 .expect("balance underflow already checked");
602 }
603 }
604
605 for (output_index, output) in transaction.outputs().iter().enumerate() {
607 if let Some(receiving_address) = output.address(network) {
608 let new_output_location =
609 OutputLocation::from_usize(height, tx_index, output_index);
610
611 let addr_loc = addr_locs.entry(receiving_address).or_insert_with(|| {
612 AddressBalanceLocationInner::new(new_output_location).into()
613 });
614
615 addr_loc
616 .receive_output(output)
617 .expect("balance overflow already checked");
618 }
619 }
620 }
621 }
622
623 match address_balances {
624 AddressBalanceLocationUpdates::Merge(balance_changes) => update_per_tx(
625 balance_changes,
626 network,
627 height,
628 transactions,
629 spent_utxos_by_outpoint,
630 ),
631 AddressBalanceLocationUpdates::Insert(balances) => update_per_tx(
632 balances,
633 network,
634 height,
635 transactions,
636 spent_utxos_by_outpoint,
637 ),
638 }
639 }
640
641 #[allow(clippy::unwrap_in_result)]
659 pub fn prepare_new_transparent_outputs_batch(
660 &mut self,
661 db: &DiskDb,
662 network: &Network,
663 new_outputs_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
664 address_balances: &AddressBalanceLocationUpdates,
665 ) {
666 let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap();
667 let utxo_loc_by_transparent_addr_loc =
668 db.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
669 let tx_loc_by_transparent_addr_loc =
670 db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
671
672 for (new_output_location, utxo) in new_outputs_by_out_loc {
674 let unspent_output = &utxo.output;
675 let receiving_address = unspent_output.address(network);
676
677 if let Some(receiving_address) = receiving_address {
678 let receiving_address_location = match address_balances {
679 AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes
680 .get(&receiving_address)
681 .expect("address must be in address_balances after the balance update pass")
682 .address_location(),
683 AddressBalanceLocationUpdates::Insert(balances) => balances
684 .get(&receiving_address)
685 .expect("address must be in address_balances after the balance update pass")
686 .address_location(),
687 };
688
689 let address_unspent_output =
691 AddressUnspentOutput::new(receiving_address_location, *new_output_location);
692 self.zs_insert(
693 &utxo_loc_by_transparent_addr_loc,
694 address_unspent_output,
695 (),
696 );
697
698 let address_transaction = AddressTransaction::new(
701 receiving_address_location,
702 new_output_location.transaction_location(),
703 );
704 self.zs_insert(&tx_loc_by_transparent_addr_loc, address_transaction, ());
705 }
706
707 self.zs_insert(&utxo_by_out_loc, new_output_location, unspent_output);
711 }
712 }
713
714 #[allow(clippy::unwrap_in_result)]
731 pub fn prepare_spent_transparent_outputs_batch(
732 &mut self,
733 db: &DiskDb,
734 network: &Network,
735 spent_utxos_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
736 address_balances: &AddressBalanceLocationUpdates,
737 ) {
738 let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap();
739 let utxo_loc_by_transparent_addr_loc =
740 db.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
741
742 for (spent_output_location, utxo) in spent_utxos_by_out_loc {
746 let spent_output = &utxo.output;
747 let sending_address = spent_output.address(network);
748
749 if let Some(sending_address) = sending_address {
751 let address_location = match address_balances {
752 AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes
753 .get(&sending_address)
754 .expect("spent outputs must already have an address balance")
755 .address_location(),
756 AddressBalanceLocationUpdates::Insert(balances) => balances
757 .get(&sending_address)
758 .expect("spent outputs must already have an address balance")
759 .address_location(),
760 };
761
762 let address_spent_output =
764 AddressUnspentOutput::new(address_location, *spent_output_location);
765
766 self.zs_delete(&utxo_loc_by_transparent_addr_loc, address_spent_output);
767 }
768
769 self.zs_delete(&utxo_by_out_loc, spent_output_location);
771 }
772 }
773
774 #[allow(clippy::unwrap_in_result, clippy::too_many_arguments)]
786 pub fn prepare_spending_transparent_tx_ids_batch(
787 &mut self,
788 zebra_db: &ZebraDb,
789 network: &Network,
790 spending_tx_location: TransactionLocation,
791 transaction: &Transaction,
792 spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
793 #[cfg(feature = "indexer")] out_loc_by_outpoint: &HashMap<
794 transparent::OutPoint,
795 OutputLocation,
796 >,
797 address_balances: &AddressBalanceLocationUpdates,
798 ) {
799 let db = &zebra_db.db;
800 let tx_loc_by_transparent_addr_loc =
801 db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
802
803 for spent_outpoint in transaction.inputs().iter().filter_map(Input::outpoint) {
807 let spent_utxo = spent_utxos_by_outpoint
808 .get(&spent_outpoint)
809 .expect("unexpected missing spent output");
810 let sending_address = spent_utxo.output.address(network);
811
812 if let Some(sending_address) = sending_address {
814 let sending_address_location = match address_balances {
815 AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes
816 .get(&sending_address)
817 .expect("spent outputs must already have an address balance")
818 .address_location(),
819 AddressBalanceLocationUpdates::Insert(balances) => balances
820 .get(&sending_address)
821 .expect("spent outputs must already have an address balance")
822 .address_location(),
823 };
824
825 let address_transaction =
831 AddressTransaction::new(sending_address_location, spending_tx_location);
832 self.zs_insert(&tx_loc_by_transparent_addr_loc, address_transaction, ());
833 }
834
835 #[cfg(feature = "indexer")]
836 {
837 let spent_output_location = out_loc_by_outpoint
838 .get(&spent_outpoint)
839 .expect("spent outpoints must already have output locations");
840
841 let _ = zebra_db
842 .tx_loc_by_spent_output_loc_cf()
843 .with_batch_for_writing(self)
844 .zs_insert(spent_output_location, &spending_tx_location);
845 }
846 }
847 }
848
849 #[allow(clippy::unwrap_in_result)]
858 pub fn prepare_transparent_balances_batch(
859 &mut self,
860 db: &DiskDb,
861 address_balances: AddressBalanceLocationUpdates,
862 ) {
863 let balance_by_transparent_addr = db.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap();
864
865 match address_balances {
868 AddressBalanceLocationUpdates::Merge(balance_changes) => {
869 for (address, address_balance_location_change) in balance_changes.into_iter() {
870 self.zs_merge(
871 &balance_by_transparent_addr,
872 address,
873 address_balance_location_change,
874 );
875 }
876 }
877
878 AddressBalanceLocationUpdates::Insert(balances) => {
879 for (address, address_balance_location_change) in balances.into_iter() {
880 self.zs_insert(
881 &balance_by_transparent_addr,
882 address,
883 address_balance_location_change,
884 );
885 }
886 }
887 };
888 }
889}