Skip to main content

zebra_state/service/finalized_state/zebra_db/
transparent.rs

1//! Provides high-level access to database:
2//! - unspent [`transparent::Output`]s (UTXOs),
3//! - spent [`transparent::Output`]s, and
4//! - transparent address indexes.
5//!
6//! This module makes sure that:
7//! - all disk writes happen inside a RocksDB transaction, and
8//! - format-specific invariants are maintained.
9//!
10//! # Correctness
11//!
12//! [`crate::constants::state_database_format_version_in_code()`] must be incremented
13//! each time the database format (column, serialization, etc) changes.
14
15use 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
49/// The name of the transaction hash by spent outpoints column family.
50pub const TX_LOC_BY_SPENT_OUT_LOC: &str = "tx_loc_by_spent_out_loc";
51
52/// The name of the [balance](AddressBalanceLocation) by transparent address column family.
53pub const BALANCE_BY_TRANSPARENT_ADDR: &str = "balance_by_transparent_addr";
54
55/// The name of the [`BALANCE_BY_TRANSPARENT_ADDR`] column family's merge operator
56pub const BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP: &str = "fetch_add_balance_and_received";
57
58/// A RocksDB merge operator for the [`BALANCE_BY_TRANSPARENT_ADDR`] column family.
59pub fn fetch_add_balance_and_received(
60    _: &[u8],
61    existing_val: Option<&[u8]>,
62    operands: &rocksdb::MergeOperands,
63) -> Option<Vec<u8>> {
64    // # Correctness
65    //
66    // Merge operands are ordered, but may be combined without an existing value in partial merges, so
67    // we may need to return a negative balance here.
68    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
76/// The type for reading value pools from the database.
77///
78/// This constant should be used so the compiler can detect incorrectly typed accesses to the
79/// column family.
80pub type TransactionLocationBySpentOutputLocationCf<'cf> =
81    TypedColumnFamily<'cf, OutputLocation, TransactionLocation>;
82
83impl ZebraDb {
84    // Column family convenience methods
85
86    /// Returns a typed handle to the transaction location by spent output location column family.
87    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    // Read transparent methods
95
96    /// Returns the [`TransactionLocation`] for a transaction that spent the output
97    /// at the provided [`OutputLocation`], if it is in the finalized state.
98    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    /// Returns a handle to the `balance_by_transparent_addr` RocksDB column family.
106    pub fn address_balance_cf(&self) -> &ColumnFamily {
107        self.db.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap()
108    }
109
110    /// Returns the [`AddressBalanceLocation`] for a [`transparent::Address`],
111    /// if it is in the finalized state.
112    #[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    /// Returns the balance and received balance for a [`transparent::Address`],
123    /// if it is in the finalized state.
124    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    /// Returns the first output that sent funds to a [`transparent::Address`],
133    /// if it is in the finalized state.
134    ///
135    /// This location is used as an efficient index key for addresses.
136    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    /// Returns the [`OutputLocation`] for a [`transparent::OutPoint`].
142    ///
143    /// This method returns the locations of spent and unspent outpoints.
144    /// Returns `None` if the output was never in the finalized state.
145    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    /// Returns the transparent output for a [`transparent::OutPoint`],
153    /// if it is unspent in the finalized state.
154    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    /// Returns the [`TransactionLocation`] of the transaction that spent the given
161    /// [`transparent::OutPoint`], if it is unspent in the finalized state and its
162    /// spending transaction hash has been indexed.
163    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    /// Returns the transparent output for an [`OutputLocation`],
169    /// if it is unspent in the finalized state.
170    #[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    /// Returns the unspent transparent outputs for a [`transparent::Address`] in the finalized
189    /// chain `query_height_range`, at most `limit` of them.
190    ///
191    /// If the address has no finalized UTXOs, or the `query_height_range` is totally outside
192    /// the finalized block range, returns an empty list.
193    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    /// Returns the unspent output locations for a [`transparent::Address`] in the finalized
206    /// chain `query_height_range`, at most `limit` of them.
207    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        // Skip this address if its first UTXO is after the end height.
219        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    /// Returns the outputs at `output_locations`.
230    fn utxos_by_locations(
231        &self,
232        output_locations: BTreeSet<OutputLocation>,
233    ) -> BTreeMap<OutputLocation, transparent::Output> {
234        // Ignore any outputs spent by blocks committed during this query
235        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    /// Returns the unspent transparent output locations for a [`transparent::Address`] in the
247    /// finalized chain `query_height_range`, at most `limit` of them.
248    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        // A potentially invalid key representing the first UTXO sent to the address,
260        // or the query start height.
261        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    /// Returns the transaction hash for an [`TransactionLocation`].
272    #[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    /// Returns the transaction IDs that sent or received funds to `address`,
280    /// in the finalized chain `query_height_range`, at most `limit` of them.
281    ///
282    /// If address has no finalized sends or receives,
283    /// or the `query_height_range` is totally outside the finalized block range,
284    /// returns an empty list.
285    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    /// Returns the locations of transactions that sent or received funds to `address`,
297    /// in the finalized chain `query_height_range`, at most `limit` of them.
298    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        // Skip this address if it was first used after the end height.
310        //
311        // The address location is the output location of the first UTXO sent to the address,
312        // and addresses can not spend funds until they receive their first UTXO.
313        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    /// Returns the transaction IDs at `transaction_locations`.
324    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    /// Returns the locations of any transactions that sent or received from a [`transparent::Address`],
341    /// in the finalized chain `query_height_range`, at most `limit` of them.
342    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        // A potentially invalid key representing the first UTXO send to the address,
352        // or the query start height.
353        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    // Address index queries
364
365    /// Returns the total transparent balance and received balance for `addresses` in the finalized chain.
366    ///
367    /// If none of the addresses have a balance, returns zeroes.
368    ///
369    /// # Correctness
370    ///
371    /// Callers should apply the non-finalized balance change for `addresses` to the returned balances.
372    ///
373    /// The total balances will only be correct if the non-finalized chain matches the finalized state.
374    /// Specifically, the root of the partial non-finalized chain must be a child block of the finalized tip.
375    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    /// Returns the UTXOs for `addresses` in the finalized chain `query_height_range`,
396    /// at most `limit` of them in total, the first in chain order.
397    ///
398    /// If none of the addresses has finalized UTXOs, returns an empty list.
399    ///
400    /// Scanning each address up to `limit` is enough: the first `limit` UTXOs across all
401    /// addresses can only contain UTXOs that are among the first `limit` for their own address.
402    ///
403    /// # Correctness
404    ///
405    /// Callers should apply the non-finalized UTXO changes for `addresses` to the returned UTXOs.
406    ///
407    /// The UTXOs will only be correct if the non-finalized chain matches or overlaps with
408    /// the finalized state.
409    ///
410    /// Specifically, a block in the partial chain must be a child block of the finalized tip.
411    /// (But the child block does not have to be the partial chain root.)
412    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        // Truncate the locations before looking up outputs, so the lookups are bounded by
419        // `limit` across all addresses.
420        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    /// Returns the transaction IDs that sent or received funds to `addresses`,
434    /// in the finalized chain `query_height_range`, at most `limit` of them in total,
435    /// the first in chain order.
436    ///
437    /// If none of the addresses has finalized sends or receives,
438    /// or the `query_height_range` is totally outside the finalized block range,
439    /// returns an empty list.
440    ///
441    /// # Correctness
442    ///
443    /// Callers should combine the non-finalized transactions for `addresses`
444    /// with the returned transactions.
445    ///
446    /// The transaction IDs will only be correct if the non-finalized chain matches or overlaps with
447    /// the finalized state.
448    ///
449    /// Specifically, a block in the partial chain must be a child block of the finalized tip.
450    /// (But the child block does not have to be the partial chain root.)
451    ///
452    /// This condition does not apply if there is only one address.
453    /// Since address transactions are only appended by blocks, and this query reads them in order,
454    /// it is impossible to get inconsistent transactions for a single address.
455    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        // Truncate the locations before looking up hashes, so the lookups are bounded by
462        // `limit` across all addresses.
463        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
477/// Drops the last entries of `set` until it has at most `limit`, so memory and lookups stay
478/// bounded by the limit across all addresses, not just per address.
479fn 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    /// Prepare a database batch containing `finalized.block`'s transparent transaction indexes,
489    /// and return it (without actually writing anything).
490    #[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        // Update the in-memory `address_balances` transaction-by-transaction, debiting inputs
509        // before crediting outputs within each transaction. This ordering keeps every
510        // intermediate per-address balance within the consensus range, even when the block
511        // contains a same-address transparent self-spend chain whose batch credit-first
512        // intermediate balance would otherwise exceed MAX_MONEY.
513        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        // Write the new and spent transparent output index entries. These passes no longer
522        // touch `address_balances`; they only read each entry's `address_location()`.
523        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        // Index the transparent addresses that spent in each transaction
537        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    /// Update `address_balances` in memory for the transparent transfers in `transactions`,
556    /// processed transaction-by-transaction in block order, debiting inputs before crediting
557    /// outputs within each transaction.
558    ///
559    /// This mirrors `zcashd`'s `UpdateCoins` and is what allows a same-address transparent
560    /// self-spend chain in one block to be applied without the intermediate per-address
561    /// balance exceeding `MAX_MONEY`. For any consensus-valid block, every per-step
562    /// intermediate balance stays inside the [`Amount`] constraint of the enclosing
563    /// `AddressBalanceLocationUpdates` variant.
564    ///
565    /// This function does not touch the RocksDB batch; index writes are still handled by
566    /// [`Self::prepare_new_transparent_outputs_batch`] and
567    /// [`Self::prepare_spent_transparent_outputs_batch`], which read but no longer mutate
568    /// `address_balances`.
569    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                // Debit transparent inputs first. Coinbase inputs have no outpoint, so
589                // `filter_map(Input::outpoint)` skips them.
590                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                // Then credit transparent outputs.
606                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    /// Prepare a database batch for the new UTXOs in `new_outputs_by_out_loc`.
642    ///
643    /// Adds the following changes to this batch:
644    /// - insert created UTXOs,
645    /// - insert transparent address UTXO index entries, and
646    /// - insert transparent address transaction entries,
647    ///
648    /// without actually writing anything.
649    ///
650    /// `address_balances` must already be populated for every transparent address that
651    /// receives one of these outputs (see
652    /// [`Self::prepare_transparent_address_balance_updates`]); this function only reads
653    /// `address_location()` from it.
654    ///
655    /// # Errors
656    ///
657    /// - This method doesn't currently return any errors, but it might in future
658    #[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        // Index all new transparent outputs
673        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                // Create a link from the AddressLocation to the new OutputLocation in the database.
690                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                // Create a link from the AddressLocation to the new TransactionLocation in the database.
699                // Unlike the OutputLocation link, this will never be deleted.
700                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            // Use the OutputLocation to store a copy of the new Output in the database.
708            // (For performance reasons, we don't want to deserialize the whole transaction
709            // to get an output.)
710            self.zs_insert(&utxo_by_out_loc, new_output_location, unspent_output);
711        }
712    }
713
714    /// Prepare a database batch for the spent outputs in `spent_utxos_by_out_loc`.
715    ///
716    /// Adds the following changes to this batch:
717    /// - delete spent UTXOs, and
718    /// - delete transparent address UTXO index entries,
719    ///
720    /// without actually writing anything.
721    ///
722    /// `address_balances` must already be populated for every transparent address that
723    /// spends one of these outputs (see
724    /// [`Self::prepare_transparent_address_balance_updates`]); this function only reads
725    /// `address_location()` from it.
726    ///
727    /// # Errors
728    ///
729    /// - This method doesn't currently return any errors, but it might in future
730    #[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        // Mark all transparent inputs as spent.
743        //
744        // Coinbase inputs represent new coins, so there are no UTXOs to mark as spent.
745        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            // Fetch the link from the address to the AddressLocation, from memory.
750            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                // Delete the link from the AddressLocation to the spent OutputLocation in the database.
763                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            // Delete the OutputLocation, and the copy of the spent Output in the database.
770            self.zs_delete(&utxo_by_out_loc, spent_output_location);
771        }
772    }
773
774    /// Prepare a database batch indexing the transparent addresses that spent in this transaction.
775    ///
776    /// Adds the following changes to this batch:
777    /// - index spending transactions for each spent transparent output
778    ///   (this is different from the transaction that created the output),
779    ///
780    /// without actually writing anything.
781    ///
782    /// # Errors
783    ///
784    /// - This method doesn't currently return any errors, but it might in future
785    #[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        // Index the transparent addresses that spent in this transaction.
804        //
805        // Coinbase inputs represent new coins, so there are no UTXOs to mark as spent.
806        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            // Fetch the balance, and the link from the address to the AddressLocation, from memory.
813            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                // Create a link from the AddressLocation to the spent TransactionLocation in the database.
826                // Unlike the OutputLocation link, this will never be deleted.
827                //
828                // The value is the location of this transaction,
829                // not the transaction the spent output is from.
830                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    /// Prepare a database batch containing `finalized.block`'s:
850    /// - transparent address balance changes,
851    ///
852    /// and return it (without actually writing anything).
853    ///
854    /// # Errors
855    ///
856    /// - This method doesn't currently return any errors, but it might in future
857    #[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        // Update all the changed address balances in the database.
866        // Some of these balances are new, and some are updates
867        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}