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`],
189    /// if they are in the finalized state.
190    pub fn address_utxos(
191        &self,
192        address: &transparent::Address,
193    ) -> BTreeMap<OutputLocation, transparent::Output> {
194        let address_location = match self.address_location(address) {
195            Some(address_location) => address_location,
196            None => return BTreeMap::new(),
197        };
198
199        let output_locations = self.address_utxo_locations(address_location);
200
201        // Ignore any outputs spent by blocks committed during this query
202        output_locations
203            .iter()
204            .filter_map(|&addr_out_loc| {
205                Some((
206                    addr_out_loc.unspent_output_location(),
207                    self.utxo_by_location(addr_out_loc.unspent_output_location())?
208                        .utxo
209                        .output,
210                ))
211            })
212            .collect()
213    }
214
215    /// Returns the unspent transparent output locations for a [`transparent::Address`],
216    /// if they are in the finalized state.
217    pub fn address_utxo_locations(
218        &self,
219        address_location: AddressLocation,
220    ) -> BTreeSet<AddressUnspentOutput> {
221        let utxo_loc_by_transparent_addr_loc = self
222            .db
223            .cf_handle("utxo_loc_by_transparent_addr_loc")
224            .unwrap();
225
226        // Manually fetch the entire addresses' UTXO locations
227        let mut addr_unspent_outputs = BTreeSet::new();
228
229        // An invalid key representing the minimum possible output
230        let mut unspent_output = AddressUnspentOutput::address_iterator_start(address_location);
231
232        loop {
233            // Seek to a valid entry for this address, or the first entry for the next address
234            unspent_output = match self
235                .db
236                .zs_next_key_value_from(&utxo_loc_by_transparent_addr_loc, &unspent_output)
237            {
238                Some((unspent_output, ())) => unspent_output,
239                // We're finished with the final address in the column family
240                None => break,
241            };
242
243            // We found the next address, so we're finished with this address
244            if unspent_output.address_location() != address_location {
245                break;
246            }
247
248            addr_unspent_outputs.insert(unspent_output);
249
250            // A potentially invalid key representing the next possible output
251            unspent_output.address_iterator_next();
252        }
253
254        addr_unspent_outputs
255    }
256
257    /// Returns the transaction hash for an [`TransactionLocation`].
258    #[allow(clippy::unwrap_in_result)]
259    pub fn tx_id_by_location(&self, tx_location: TransactionLocation) -> Option<transaction::Hash> {
260        let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap();
261
262        self.db.zs_get(&hash_by_tx_loc, &tx_location)
263    }
264
265    /// Returns the transaction IDs that sent or received funds to `address`,
266    /// in the finalized chain `query_height_range`.
267    ///
268    /// If address has no finalized sends or receives,
269    /// or the `query_height_range` is totally outside the finalized block range,
270    /// returns an empty list.
271    pub fn address_tx_ids(
272        &self,
273        address: &transparent::Address,
274        query_height_range: RangeInclusive<Height>,
275    ) -> BTreeMap<TransactionLocation, transaction::Hash> {
276        let address_location = match self.address_location(address) {
277            Some(address_location) => address_location,
278            None => return BTreeMap::new(),
279        };
280
281        // Skip this address if it was first used after the end height.
282        //
283        // The address location is the output location of the first UTXO sent to the address,
284        // and addresses can not spend funds until they receive their first UTXO.
285        if address_location.height() > *query_height_range.end() {
286            return BTreeMap::new();
287        }
288
289        let transaction_locations =
290            self.address_transaction_locations(address_location, query_height_range);
291
292        transaction_locations
293            .iter()
294            .map(|&tx_loc| {
295                (
296                    tx_loc.transaction_location(),
297                    self.tx_id_by_location(tx_loc.transaction_location())
298                        .expect("transactions whose locations are stored must exist"),
299                )
300            })
301            .collect()
302    }
303
304    /// Returns the locations of any transactions that sent or received from a [`transparent::Address`],
305    /// if they are in the finalized state.
306    pub fn address_transaction_locations(
307        &self,
308        address_location: AddressLocation,
309        query_height_range: RangeInclusive<Height>,
310    ) -> BTreeSet<AddressTransaction> {
311        let tx_loc_by_transparent_addr_loc =
312            self.db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
313
314        // A potentially invalid key representing the first UTXO send to the address,
315        // or the query start height.
316        let transaction_location_range =
317            AddressTransaction::address_iterator_range(address_location, query_height_range);
318
319        self.db
320            .zs_forward_range_iter(&tx_loc_by_transparent_addr_loc, transaction_location_range)
321            .map(|(tx_loc, ())| tx_loc)
322            .collect()
323    }
324
325    // Address index queries
326
327    /// Returns the total transparent balance and received balance for `addresses` in the finalized chain.
328    ///
329    /// If none of the addresses have a balance, returns zeroes.
330    ///
331    /// # Correctness
332    ///
333    /// Callers should apply the non-finalized balance change for `addresses` to the returned balances.
334    ///
335    /// The total balances will only be correct if the non-finalized chain matches the finalized state.
336    /// Specifically, the root of the partial non-finalized chain must be a child block of the finalized tip.
337    pub fn partial_finalized_transparent_balance(
338        &self,
339        addresses: &HashSet<transparent::Address>,
340    ) -> (Amount<NonNegative>, u64) {
341        let balance: amount::Result<(Amount<NonNegative>, u64)> = addresses
342            .iter()
343            .filter_map(|address| self.address_balance(address))
344            .try_fold(
345                (Amount::zero(), 0),
346                |(a_balance, a_received): (Amount<NonNegative>, u64), (b_balance, b_received)| {
347                    let received = a_received.saturating_add(b_received);
348                    Ok(((a_balance + b_balance)?, received))
349                },
350            );
351
352        balance.expect(
353            "unexpected amount overflow: value balances are valid, so partial sum should be valid",
354        )
355    }
356
357    /// Returns the UTXOs for `addresses` in the finalized chain.
358    ///
359    /// If none of the addresses has finalized UTXOs, returns an empty list.
360    ///
361    /// # Correctness
362    ///
363    /// Callers should apply the non-finalized UTXO changes for `addresses` to the returned UTXOs.
364    ///
365    /// The UTXOs will only be correct if the non-finalized chain matches or overlaps with
366    /// the finalized state.
367    ///
368    /// Specifically, a block in the partial chain must be a child block of the finalized tip.
369    /// (But the child block does not have to be the partial chain root.)
370    pub fn partial_finalized_address_utxos(
371        &self,
372        addresses: &HashSet<transparent::Address>,
373    ) -> BTreeMap<OutputLocation, transparent::Output> {
374        addresses
375            .iter()
376            .flat_map(|address| self.address_utxos(address))
377            .collect()
378    }
379
380    /// Returns the transaction IDs that sent or received funds to `addresses`,
381    /// in the finalized chain `query_height_range`.
382    ///
383    /// If none of the addresses has finalized sends or receives,
384    /// or the `query_height_range` is totally outside the finalized block range,
385    /// returns an empty list.
386    ///
387    /// # Correctness
388    ///
389    /// Callers should combine the non-finalized transactions for `addresses`
390    /// with the returned transactions.
391    ///
392    /// The transaction IDs will only be correct if the non-finalized chain matches or overlaps with
393    /// the finalized state.
394    ///
395    /// Specifically, a block in the partial chain must be a child block of the finalized tip.
396    /// (But the child block does not have to be the partial chain root.)
397    ///
398    /// This condition does not apply if there is only one address.
399    /// Since address transactions are only appended by blocks, and this query reads them in order,
400    /// it is impossible to get inconsistent transactions for a single address.
401    pub fn partial_finalized_transparent_tx_ids(
402        &self,
403        addresses: &HashSet<transparent::Address>,
404        query_height_range: RangeInclusive<Height>,
405    ) -> BTreeMap<TransactionLocation, transaction::Hash> {
406        addresses
407            .iter()
408            .flat_map(|address| self.address_tx_ids(address, query_height_range.clone()))
409            .collect()
410    }
411}
412
413impl DiskWriteBatch {
414    /// Prepare a database batch containing `finalized.block`'s transparent transaction indexes,
415    /// and return it (without actually writing anything).
416    ///
417    /// If this method returns an error, it will be propagated,
418    /// and the batch should not be written to the database.
419    #[allow(clippy::too_many_arguments)]
420    pub fn prepare_transparent_transaction_batch(
421        &mut self,
422        zebra_db: &ZebraDb,
423        network: &Network,
424        finalized: &FinalizedBlock,
425        new_outputs_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
426        spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
427        spent_utxos_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
428        #[cfg(feature = "indexer")] out_loc_by_outpoint: &HashMap<
429            transparent::OutPoint,
430            OutputLocation,
431        >,
432        mut address_balances: AddressBalanceLocationUpdates,
433    ) {
434        let db = &zebra_db.db;
435        let FinalizedBlock { block, height, .. } = finalized;
436
437        // Update the in-memory `address_balances` transaction-by-transaction, debiting inputs
438        // before crediting outputs within each transaction. This ordering keeps every
439        // intermediate per-address balance within the consensus range, even when the block
440        // contains a same-address transparent self-spend chain whose batch credit-first
441        // intermediate balance would otherwise exceed MAX_MONEY.
442        Self::prepare_transparent_address_balance_updates(
443            network,
444            *height,
445            &block.transactions,
446            spent_utxos_by_outpoint,
447            &mut address_balances,
448        );
449
450        // Write the new and spent transparent output index entries. These passes no longer
451        // touch `address_balances`; they only read each entry's `address_location()`.
452        self.prepare_new_transparent_outputs_batch(
453            db,
454            network,
455            new_outputs_by_out_loc,
456            &address_balances,
457        );
458        self.prepare_spent_transparent_outputs_batch(
459            db,
460            network,
461            spent_utxos_by_out_loc,
462            &address_balances,
463        );
464
465        // Index the transparent addresses that spent in each transaction
466        for (tx_index, transaction) in block.transactions.iter().enumerate() {
467            let spending_tx_location = TransactionLocation::from_usize(*height, tx_index);
468
469            self.prepare_spending_transparent_tx_ids_batch(
470                zebra_db,
471                network,
472                spending_tx_location,
473                transaction,
474                spent_utxos_by_outpoint,
475                #[cfg(feature = "indexer")]
476                out_loc_by_outpoint,
477                &address_balances,
478            );
479        }
480
481        self.prepare_transparent_balances_batch(db, address_balances);
482    }
483
484    /// Update `address_balances` in memory for the transparent transfers in `transactions`,
485    /// processed transaction-by-transaction in block order, debiting inputs before crediting
486    /// outputs within each transaction.
487    ///
488    /// This mirrors `zcashd`'s `UpdateCoins` and is what allows a same-address transparent
489    /// self-spend chain in one block to be applied without the intermediate per-address
490    /// balance exceeding `MAX_MONEY`. For any consensus-valid block, every per-step
491    /// intermediate balance stays inside the [`Amount`] constraint of the enclosing
492    /// `AddressBalanceLocationUpdates` variant.
493    ///
494    /// This function does not touch the RocksDB batch; index writes are still handled by
495    /// [`Self::prepare_new_transparent_outputs_batch`] and
496    /// [`Self::prepare_spent_transparent_outputs_batch`], which read but no longer mutate
497    /// `address_balances`.
498    fn prepare_transparent_address_balance_updates(
499        network: &Network,
500        height: Height,
501        transactions: &[Arc<Transaction>],
502        spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
503        address_balances: &mut AddressBalanceLocationUpdates,
504    ) {
505        fn update_per_tx<
506            C: Constraint + Copy + std::fmt::Debug,
507            T: std::ops::DerefMut<Target = AddressBalanceLocationInner<C>>
508                + From<AddressBalanceLocationInner<C>>,
509        >(
510            addr_locs: &mut HashMap<transparent::Address, T>,
511            network: &Network,
512            height: Height,
513            transactions: &[Arc<Transaction>],
514            spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
515        ) {
516            for (tx_index, transaction) in transactions.iter().enumerate() {
517                // Debit transparent inputs first. Coinbase inputs have no outpoint, so
518                // `filter_map(Input::outpoint)` skips them.
519                for spent_outpoint in transaction.inputs().iter().filter_map(Input::outpoint) {
520                    let spent_utxo = spent_utxos_by_outpoint
521                        .get(&spent_outpoint)
522                        .expect("spent outpoint must already be resolved");
523                    if let Some(sending_address) = spent_utxo.output.address(network) {
524                        let addr_loc = addr_locs
525                            .get_mut(&sending_address)
526                            .expect("spent outputs must already have an address balance");
527
528                        addr_loc
529                            .spend_output(&spent_utxo.output)
530                            .expect("balance underflow already checked");
531                    }
532                }
533
534                // Then credit transparent outputs.
535                for (output_index, output) in transaction.outputs().iter().enumerate() {
536                    if let Some(receiving_address) = output.address(network) {
537                        let new_output_location =
538                            OutputLocation::from_usize(height, tx_index, output_index);
539
540                        let addr_loc = addr_locs.entry(receiving_address).or_insert_with(|| {
541                            AddressBalanceLocationInner::new(new_output_location).into()
542                        });
543
544                        addr_loc
545                            .receive_output(output)
546                            .expect("balance overflow already checked");
547                    }
548                }
549            }
550        }
551
552        match address_balances {
553            AddressBalanceLocationUpdates::Merge(balance_changes) => update_per_tx(
554                balance_changes,
555                network,
556                height,
557                transactions,
558                spent_utxos_by_outpoint,
559            ),
560            AddressBalanceLocationUpdates::Insert(balances) => update_per_tx(
561                balances,
562                network,
563                height,
564                transactions,
565                spent_utxos_by_outpoint,
566            ),
567        }
568    }
569
570    /// Prepare a database batch for the new UTXOs in `new_outputs_by_out_loc`.
571    ///
572    /// Adds the following changes to this batch:
573    /// - insert created UTXOs,
574    /// - insert transparent address UTXO index entries, and
575    /// - insert transparent address transaction entries,
576    ///
577    /// without actually writing anything.
578    ///
579    /// `address_balances` must already be populated for every transparent address that
580    /// receives one of these outputs (see
581    /// [`Self::prepare_transparent_address_balance_updates`]); this function only reads
582    /// `address_location()` from it.
583    ///
584    /// # Errors
585    ///
586    /// - This method doesn't currently return any errors, but it might in future
587    #[allow(clippy::unwrap_in_result)]
588    pub fn prepare_new_transparent_outputs_batch(
589        &mut self,
590        db: &DiskDb,
591        network: &Network,
592        new_outputs_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
593        address_balances: &AddressBalanceLocationUpdates,
594    ) {
595        let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap();
596        let utxo_loc_by_transparent_addr_loc =
597            db.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
598        let tx_loc_by_transparent_addr_loc =
599            db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
600
601        // Index all new transparent outputs
602        for (new_output_location, utxo) in new_outputs_by_out_loc {
603            let unspent_output = &utxo.output;
604            let receiving_address = unspent_output.address(network);
605
606            if let Some(receiving_address) = receiving_address {
607                let receiving_address_location = match address_balances {
608                    AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes
609                        .get(&receiving_address)
610                        .expect("address must be in address_balances after the balance update pass")
611                        .address_location(),
612                    AddressBalanceLocationUpdates::Insert(balances) => balances
613                        .get(&receiving_address)
614                        .expect("address must be in address_balances after the balance update pass")
615                        .address_location(),
616                };
617
618                // Create a link from the AddressLocation to the new OutputLocation in the database.
619                let address_unspent_output =
620                    AddressUnspentOutput::new(receiving_address_location, *new_output_location);
621                self.zs_insert(
622                    &utxo_loc_by_transparent_addr_loc,
623                    address_unspent_output,
624                    (),
625                );
626
627                // Create a link from the AddressLocation to the new TransactionLocation in the database.
628                // Unlike the OutputLocation link, this will never be deleted.
629                let address_transaction = AddressTransaction::new(
630                    receiving_address_location,
631                    new_output_location.transaction_location(),
632                );
633                self.zs_insert(&tx_loc_by_transparent_addr_loc, address_transaction, ());
634            }
635
636            // Use the OutputLocation to store a copy of the new Output in the database.
637            // (For performance reasons, we don't want to deserialize the whole transaction
638            // to get an output.)
639            self.zs_insert(&utxo_by_out_loc, new_output_location, unspent_output);
640        }
641    }
642
643    /// Prepare a database batch for the spent outputs in `spent_utxos_by_out_loc`.
644    ///
645    /// Adds the following changes to this batch:
646    /// - delete spent UTXOs, and
647    /// - delete transparent address UTXO index entries,
648    ///
649    /// without actually writing anything.
650    ///
651    /// `address_balances` must already be populated for every transparent address that
652    /// spends one of these outputs (see
653    /// [`Self::prepare_transparent_address_balance_updates`]); this function only reads
654    /// `address_location()` from it.
655    ///
656    /// # Errors
657    ///
658    /// - This method doesn't currently return any errors, but it might in future
659    #[allow(clippy::unwrap_in_result)]
660    pub fn prepare_spent_transparent_outputs_batch(
661        &mut self,
662        db: &DiskDb,
663        network: &Network,
664        spent_utxos_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
665        address_balances: &AddressBalanceLocationUpdates,
666    ) {
667        let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap();
668        let utxo_loc_by_transparent_addr_loc =
669            db.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
670
671        // Mark all transparent inputs as spent.
672        //
673        // Coinbase inputs represent new coins, so there are no UTXOs to mark as spent.
674        for (spent_output_location, utxo) in spent_utxos_by_out_loc {
675            let spent_output = &utxo.output;
676            let sending_address = spent_output.address(network);
677
678            // Fetch the link from the address to the AddressLocation, from memory.
679            if let Some(sending_address) = sending_address {
680                let address_location = match address_balances {
681                    AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes
682                        .get(&sending_address)
683                        .expect("spent outputs must already have an address balance")
684                        .address_location(),
685                    AddressBalanceLocationUpdates::Insert(balances) => balances
686                        .get(&sending_address)
687                        .expect("spent outputs must already have an address balance")
688                        .address_location(),
689                };
690
691                // Delete the link from the AddressLocation to the spent OutputLocation in the database.
692                let address_spent_output =
693                    AddressUnspentOutput::new(address_location, *spent_output_location);
694
695                self.zs_delete(&utxo_loc_by_transparent_addr_loc, address_spent_output);
696            }
697
698            // Delete the OutputLocation, and the copy of the spent Output in the database.
699            self.zs_delete(&utxo_by_out_loc, spent_output_location);
700        }
701    }
702
703    /// Prepare a database batch indexing the transparent addresses that spent in this transaction.
704    ///
705    /// Adds the following changes to this batch:
706    /// - index spending transactions for each spent transparent output
707    ///   (this is different from the transaction that created the output),
708    ///
709    /// without actually writing anything.
710    ///
711    /// # Errors
712    ///
713    /// - This method doesn't currently return any errors, but it might in future
714    #[allow(clippy::unwrap_in_result, clippy::too_many_arguments)]
715    pub fn prepare_spending_transparent_tx_ids_batch(
716        &mut self,
717        zebra_db: &ZebraDb,
718        network: &Network,
719        spending_tx_location: TransactionLocation,
720        transaction: &Transaction,
721        spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
722        #[cfg(feature = "indexer")] out_loc_by_outpoint: &HashMap<
723            transparent::OutPoint,
724            OutputLocation,
725        >,
726        address_balances: &AddressBalanceLocationUpdates,
727    ) {
728        let db = &zebra_db.db;
729        let tx_loc_by_transparent_addr_loc =
730            db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
731
732        // Index the transparent addresses that spent in this transaction.
733        //
734        // Coinbase inputs represent new coins, so there are no UTXOs to mark as spent.
735        for spent_outpoint in transaction.inputs().iter().filter_map(Input::outpoint) {
736            let spent_utxo = spent_utxos_by_outpoint
737                .get(&spent_outpoint)
738                .expect("unexpected missing spent output");
739            let sending_address = spent_utxo.output.address(network);
740
741            // Fetch the balance, and the link from the address to the AddressLocation, from memory.
742            if let Some(sending_address) = sending_address {
743                let sending_address_location = match address_balances {
744                    AddressBalanceLocationUpdates::Merge(balance_changes) => balance_changes
745                        .get(&sending_address)
746                        .expect("spent outputs must already have an address balance")
747                        .address_location(),
748                    AddressBalanceLocationUpdates::Insert(balances) => balances
749                        .get(&sending_address)
750                        .expect("spent outputs must already have an address balance")
751                        .address_location(),
752                };
753
754                // Create a link from the AddressLocation to the spent TransactionLocation in the database.
755                // Unlike the OutputLocation link, this will never be deleted.
756                //
757                // The value is the location of this transaction,
758                // not the transaction the spent output is from.
759                let address_transaction =
760                    AddressTransaction::new(sending_address_location, spending_tx_location);
761                self.zs_insert(&tx_loc_by_transparent_addr_loc, address_transaction, ());
762            }
763
764            #[cfg(feature = "indexer")]
765            {
766                let spent_output_location = out_loc_by_outpoint
767                    .get(&spent_outpoint)
768                    .expect("spent outpoints must already have output locations");
769
770                let _ = zebra_db
771                    .tx_loc_by_spent_output_loc_cf()
772                    .with_batch_for_writing(self)
773                    .zs_insert(spent_output_location, &spending_tx_location);
774            }
775        }
776    }
777
778    /// Prepare a database batch containing `finalized.block`'s:
779    /// - transparent address balance changes,
780    ///
781    /// and return it (without actually writing anything).
782    ///
783    /// # Errors
784    ///
785    /// - This method doesn't currently return any errors, but it might in future
786    #[allow(clippy::unwrap_in_result)]
787    pub fn prepare_transparent_balances_batch(
788        &mut self,
789        db: &DiskDb,
790        address_balances: AddressBalanceLocationUpdates,
791    ) {
792        let balance_by_transparent_addr = db.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap();
793
794        // Update all the changed address balances in the database.
795        // Some of these balances are new, and some are updates
796        match address_balances {
797            AddressBalanceLocationUpdates::Merge(balance_changes) => {
798                for (address, address_balance_location_change) in balance_changes.into_iter() {
799                    self.zs_merge(
800                        &balance_by_transparent_addr,
801                        address,
802                        address_balance_location_change,
803                    );
804                }
805            }
806
807            AddressBalanceLocationUpdates::Insert(balances) => {
808                for (address, address_balance_location_change) in balances.into_iter() {
809                    self.zs_insert(
810                        &balance_by_transparent_addr,
811                        address,
812                        address_balance_location_change,
813                    );
814                }
815            }
816        };
817    }
818}