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};
19
20use rocksdb::ColumnFamily;
21use zebra_chain::{
22    amount::{self, Amount, NonNegative},
23    block::Height,
24    parameters::Network,
25    transaction::{self, Transaction},
26    transparent::{self, Input},
27};
28
29use crate::{
30    request::FinalizedBlock,
31    service::finalized_state::{
32        disk_db::{DiskDb, DiskWriteBatch, ReadDisk, WriteDisk},
33        disk_format::{
34            transparent::{
35                AddressBalanceLocation, AddressBalanceLocationChange, AddressLocation,
36                AddressTransaction, AddressUnspentOutput, OutputLocation,
37            },
38            TransactionLocation,
39        },
40        zebra_db::ZebraDb,
41    },
42    BoxError, FromDisk, IntoDisk,
43};
44
45use super::super::TypedColumnFamily;
46
47/// The name of the transaction hash by spent outpoints column family.
48pub const TX_LOC_BY_SPENT_OUT_LOC: &str = "tx_loc_by_spent_out_loc";
49
50/// The name of the [balance](AddressBalanceLocation) by transparent address column family.
51pub const BALANCE_BY_TRANSPARENT_ADDR: &str = "balance_by_transparent_addr";
52
53/// The name of the [`BALANCE_BY_TRANSPARENT_ADDR`] column family's merge operator
54pub const BALANCE_BY_TRANSPARENT_ADDR_MERGE_OP: &str = "fetch_add_balance_and_received";
55
56/// A RocksDB merge operator for the [`BALANCE_BY_TRANSPARENT_ADDR`] column family.
57pub fn fetch_add_balance_and_received(
58    _: &[u8],
59    existing_val: Option<&[u8]>,
60    operands: &rocksdb::MergeOperands,
61) -> Option<Vec<u8>> {
62    // # Correctness
63    //
64    // Merge operands are ordered, but may be combined without an existing value in partial merges, so
65    // we may need to return a negative balance here.
66    existing_val
67        .into_iter()
68        .chain(operands)
69        .map(AddressBalanceLocationChange::from_bytes)
70        .reduce(|a, b| (a + b).expect("address balance/received should not overflow"))
71        .map(|address_balance_location| address_balance_location.as_bytes().to_vec())
72}
73
74/// The type for reading value pools from the database.
75///
76/// This constant should be used so the compiler can detect incorrectly typed accesses to the
77/// column family.
78pub type TransactionLocationBySpentOutputLocationCf<'cf> =
79    TypedColumnFamily<'cf, OutputLocation, TransactionLocation>;
80
81impl ZebraDb {
82    // Column family convenience methods
83
84    /// Returns a typed handle to the transaction location by spent output location column family.
85    pub(crate) fn tx_loc_by_spent_output_loc_cf(
86        &self,
87    ) -> TransactionLocationBySpentOutputLocationCf {
88        TransactionLocationBySpentOutputLocationCf::new(&self.db, TX_LOC_BY_SPENT_OUT_LOC)
89            .expect("column family was created when database was created")
90    }
91
92    // Read transparent methods
93
94    /// Returns the [`TransactionLocation`] for a transaction that spent the output
95    /// at the provided [`OutputLocation`], if it is in the finalized state.
96    pub fn tx_location_by_spent_output_location(
97        &self,
98        output_location: &OutputLocation,
99    ) -> Option<TransactionLocation> {
100        self.tx_loc_by_spent_output_loc_cf().zs_get(output_location)
101    }
102
103    /// Returns a handle to the `balance_by_transparent_addr` RocksDB column family.
104    pub fn address_balance_cf(&self) -> &ColumnFamily {
105        self.db.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap()
106    }
107
108    /// Returns the [`AddressBalanceLocation`] for a [`transparent::Address`],
109    /// if it is in the finalized state.
110    #[allow(clippy::unwrap_in_result)]
111    pub fn address_balance_location(
112        &self,
113        address: &transparent::Address,
114    ) -> Option<AddressBalanceLocation> {
115        let balance_by_transparent_addr = self.address_balance_cf();
116
117        self.db.zs_get(&balance_by_transparent_addr, address)
118    }
119
120    /// Returns the balance and received balance for a [`transparent::Address`],
121    /// if it is in the finalized state.
122    pub fn address_balance(
123        &self,
124        address: &transparent::Address,
125    ) -> Option<(Amount<NonNegative>, u64)> {
126        self.address_balance_location(address)
127            .map(|abl| (abl.balance(), abl.received()))
128    }
129
130    /// Returns the first output that sent funds to a [`transparent::Address`],
131    /// if it is in the finalized state.
132    ///
133    /// This location is used as an efficient index key for addresses.
134    pub fn address_location(&self, address: &transparent::Address) -> Option<AddressLocation> {
135        self.address_balance_location(address)
136            .map(|abl| abl.address_location())
137    }
138
139    /// Returns the [`OutputLocation`] for a [`transparent::OutPoint`].
140    ///
141    /// This method returns the locations of spent and unspent outpoints.
142    /// Returns `None` if the output was never in the finalized state.
143    pub fn output_location(&self, outpoint: &transparent::OutPoint) -> Option<OutputLocation> {
144        self.transaction_location(outpoint.hash)
145            .map(|transaction_location| {
146                OutputLocation::from_outpoint(transaction_location, outpoint)
147            })
148    }
149
150    /// Returns the transparent output for a [`transparent::OutPoint`],
151    /// if it is unspent in the finalized state.
152    pub fn utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::OrderedUtxo> {
153        let output_location = self.output_location(outpoint)?;
154
155        self.utxo_by_location(output_location)
156    }
157
158    /// Returns the [`TransactionLocation`] of the transaction that spent the given
159    /// [`transparent::OutPoint`], if it is unspent in the finalized state and its
160    /// spending transaction hash has been indexed.
161    pub fn spending_tx_loc(&self, outpoint: &transparent::OutPoint) -> Option<TransactionLocation> {
162        let output_location = self.output_location(outpoint)?;
163        self.tx_location_by_spent_output_location(&output_location)
164    }
165
166    /// Returns the transparent output for an [`OutputLocation`],
167    /// if it is unspent in the finalized state.
168    #[allow(clippy::unwrap_in_result)]
169    pub fn utxo_by_location(
170        &self,
171        output_location: OutputLocation,
172    ) -> Option<transparent::OrderedUtxo> {
173        let utxo_by_out_loc = self.db.cf_handle("utxo_by_out_loc").unwrap();
174
175        let output = self.db.zs_get(&utxo_by_out_loc, &output_location)?;
176
177        let utxo = transparent::OrderedUtxo::new(
178            output,
179            output_location.height(),
180            output_location.transaction_index().as_usize(),
181        );
182
183        Some(utxo)
184    }
185
186    /// Returns the unspent transparent outputs for a [`transparent::Address`],
187    /// if they are in the finalized state.
188    pub fn address_utxos(
189        &self,
190        address: &transparent::Address,
191    ) -> BTreeMap<OutputLocation, transparent::Output> {
192        let address_location = match self.address_location(address) {
193            Some(address_location) => address_location,
194            None => return BTreeMap::new(),
195        };
196
197        let output_locations = self.address_utxo_locations(address_location);
198
199        // Ignore any outputs spent by blocks committed during this query
200        output_locations
201            .iter()
202            .flat_map(|&addr_out_loc| {
203                Some((
204                    addr_out_loc.unspent_output_location(),
205                    self.utxo_by_location(addr_out_loc.unspent_output_location())?
206                        .utxo
207                        .output,
208                ))
209            })
210            .collect()
211    }
212
213    /// Returns the unspent transparent output locations for a [`transparent::Address`],
214    /// if they are in the finalized state.
215    pub fn address_utxo_locations(
216        &self,
217        address_location: AddressLocation,
218    ) -> BTreeSet<AddressUnspentOutput> {
219        let utxo_loc_by_transparent_addr_loc = self
220            .db
221            .cf_handle("utxo_loc_by_transparent_addr_loc")
222            .unwrap();
223
224        // Manually fetch the entire addresses' UTXO locations
225        let mut addr_unspent_outputs = BTreeSet::new();
226
227        // An invalid key representing the minimum possible output
228        let mut unspent_output = AddressUnspentOutput::address_iterator_start(address_location);
229
230        loop {
231            // Seek to a valid entry for this address, or the first entry for the next address
232            unspent_output = match self
233                .db
234                .zs_next_key_value_from(&utxo_loc_by_transparent_addr_loc, &unspent_output)
235            {
236                Some((unspent_output, ())) => unspent_output,
237                // We're finished with the final address in the column family
238                None => break,
239            };
240
241            // We found the next address, so we're finished with this address
242            if unspent_output.address_location() != address_location {
243                break;
244            }
245
246            addr_unspent_outputs.insert(unspent_output);
247
248            // A potentially invalid key representing the next possible output
249            unspent_output.address_iterator_next();
250        }
251
252        addr_unspent_outputs
253    }
254
255    /// Returns the transaction hash for an [`TransactionLocation`].
256    #[allow(clippy::unwrap_in_result)]
257    pub fn tx_id_by_location(&self, tx_location: TransactionLocation) -> Option<transaction::Hash> {
258        let hash_by_tx_loc = self.db.cf_handle("hash_by_tx_loc").unwrap();
259
260        self.db.zs_get(&hash_by_tx_loc, &tx_location)
261    }
262
263    /// Returns the transaction IDs that sent or received funds to `address`,
264    /// in the finalized chain `query_height_range`.
265    ///
266    /// If address has no finalized sends or receives,
267    /// or the `query_height_range` is totally outside the finalized block range,
268    /// returns an empty list.
269    pub fn address_tx_ids(
270        &self,
271        address: &transparent::Address,
272        query_height_range: RangeInclusive<Height>,
273    ) -> BTreeMap<TransactionLocation, transaction::Hash> {
274        let address_location = match self.address_location(address) {
275            Some(address_location) => address_location,
276            None => return BTreeMap::new(),
277        };
278
279        // Skip this address if it was first used after the end height.
280        //
281        // The address location is the output location of the first UTXO sent to the address,
282        // and addresses can not spend funds until they receive their first UTXO.
283        if address_location.height() > *query_height_range.end() {
284            return BTreeMap::new();
285        }
286
287        let transaction_locations =
288            self.address_transaction_locations(address_location, query_height_range);
289
290        transaction_locations
291            .iter()
292            .map(|&tx_loc| {
293                (
294                    tx_loc.transaction_location(),
295                    self.tx_id_by_location(tx_loc.transaction_location())
296                        .expect("transactions whose locations are stored must exist"),
297                )
298            })
299            .collect()
300    }
301
302    /// Returns the locations of any transactions that sent or received from a [`transparent::Address`],
303    /// if they are in the finalized state.
304    pub fn address_transaction_locations(
305        &self,
306        address_location: AddressLocation,
307        query_height_range: RangeInclusive<Height>,
308    ) -> BTreeSet<AddressTransaction> {
309        let tx_loc_by_transparent_addr_loc =
310            self.db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
311
312        // A potentially invalid key representing the first UTXO send to the address,
313        // or the query start height.
314        let transaction_location_range =
315            AddressTransaction::address_iterator_range(address_location, query_height_range);
316
317        self.db
318            .zs_forward_range_iter(&tx_loc_by_transparent_addr_loc, transaction_location_range)
319            .map(|(tx_loc, ())| tx_loc)
320            .collect()
321    }
322
323    // Address index queries
324
325    /// Returns the total transparent balance and received balance for `addresses` in the finalized chain.
326    ///
327    /// If none of the addresses have a balance, returns zeroes.
328    ///
329    /// # Correctness
330    ///
331    /// Callers should apply the non-finalized balance change for `addresses` to the returned balances.
332    ///
333    /// The total balances will only be correct if the non-finalized chain matches the finalized state.
334    /// Specifically, the root of the partial non-finalized chain must be a child block of the finalized tip.
335    pub fn partial_finalized_transparent_balance(
336        &self,
337        addresses: &HashSet<transparent::Address>,
338    ) -> (Amount<NonNegative>, u64) {
339        let balance: amount::Result<(Amount<NonNegative>, u64)> = addresses
340            .iter()
341            .filter_map(|address| self.address_balance(address))
342            .try_fold(
343                (Amount::zero(), 0),
344                |(a_balance, a_received): (Amount<NonNegative>, u64), (b_balance, b_received)| {
345                    let received = a_received.saturating_add(b_received);
346                    Ok(((a_balance + b_balance)?, received))
347                },
348            );
349
350        balance.expect(
351            "unexpected amount overflow: value balances are valid, so partial sum should be valid",
352        )
353    }
354
355    /// Returns the UTXOs for `addresses` in the finalized chain.
356    ///
357    /// If none of the addresses has finalized UTXOs, returns an empty list.
358    ///
359    /// # Correctness
360    ///
361    /// Callers should apply the non-finalized UTXO changes for `addresses` to the returned UTXOs.
362    ///
363    /// The UTXOs will only be correct if the non-finalized chain matches or overlaps with
364    /// the finalized state.
365    ///
366    /// Specifically, a block in the partial chain must be a child block of the finalized tip.
367    /// (But the child block does not have to be the partial chain root.)
368    pub fn partial_finalized_address_utxos(
369        &self,
370        addresses: &HashSet<transparent::Address>,
371    ) -> BTreeMap<OutputLocation, transparent::Output> {
372        addresses
373            .iter()
374            .flat_map(|address| self.address_utxos(address))
375            .collect()
376    }
377
378    /// Returns the transaction IDs that sent or received funds to `addresses`,
379    /// in the finalized chain `query_height_range`.
380    ///
381    /// If none of the addresses has finalized sends or receives,
382    /// or the `query_height_range` is totally outside the finalized block range,
383    /// returns an empty list.
384    ///
385    /// # Correctness
386    ///
387    /// Callers should combine the non-finalized transactions for `addresses`
388    /// with the returned transactions.
389    ///
390    /// The transaction IDs will only be correct if the non-finalized chain matches or overlaps with
391    /// the finalized state.
392    ///
393    /// Specifically, a block in the partial chain must be a child block of the finalized tip.
394    /// (But the child block does not have to be the partial chain root.)
395    ///
396    /// This condition does not apply if there is only one address.
397    /// Since address transactions are only appended by blocks, and this query reads them in order,
398    /// it is impossible to get inconsistent transactions for a single address.
399    pub fn partial_finalized_transparent_tx_ids(
400        &self,
401        addresses: &HashSet<transparent::Address>,
402        query_height_range: RangeInclusive<Height>,
403    ) -> BTreeMap<TransactionLocation, transaction::Hash> {
404        addresses
405            .iter()
406            .flat_map(|address| self.address_tx_ids(address, query_height_range.clone()))
407            .collect()
408    }
409}
410
411impl DiskWriteBatch {
412    /// Prepare a database batch containing `finalized.block`'s transparent transaction indexes,
413    /// and return it (without actually writing anything).
414    ///
415    /// If this method returns an error, it will be propagated,
416    /// and the batch should not be written to the database.
417    ///
418    /// # Errors
419    ///
420    /// - Propagates any errors from updating note commitment trees
421    #[allow(clippy::too_many_arguments)]
422    pub fn prepare_transparent_transaction_batch(
423        &mut self,
424        zebra_db: &ZebraDb,
425        network: &Network,
426        finalized: &FinalizedBlock,
427        new_outputs_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
428        spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
429        spent_utxos_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
430        #[cfg(feature = "indexer")] out_loc_by_outpoint: &HashMap<
431            transparent::OutPoint,
432            OutputLocation,
433        >,
434        mut address_balances: HashMap<transparent::Address, AddressBalanceLocationChange>,
435    ) -> Result<(), BoxError> {
436        let db = &zebra_db.db;
437        let FinalizedBlock { block, height, .. } = finalized;
438
439        // Update created and spent transparent outputs
440        self.prepare_new_transparent_outputs_batch(
441            db,
442            network,
443            new_outputs_by_out_loc,
444            &mut address_balances,
445        )?;
446        self.prepare_spent_transparent_outputs_batch(
447            db,
448            network,
449            spent_utxos_by_out_loc,
450            &mut address_balances,
451        )?;
452
453        // Index the transparent addresses that spent in each transaction
454        for (tx_index, transaction) in block.transactions.iter().enumerate() {
455            let spending_tx_location = TransactionLocation::from_usize(*height, tx_index);
456
457            self.prepare_spending_transparent_tx_ids_batch(
458                zebra_db,
459                network,
460                spending_tx_location,
461                transaction,
462                spent_utxos_by_outpoint,
463                #[cfg(feature = "indexer")]
464                out_loc_by_outpoint,
465                &address_balances,
466            )?;
467        }
468
469        self.prepare_transparent_balances_batch(db, address_balances)
470    }
471
472    /// Prepare a database batch for the new UTXOs in `new_outputs_by_out_loc`.
473    ///
474    /// Adds the following changes to this batch:
475    /// - insert created UTXOs,
476    /// - insert transparent address UTXO index entries, and
477    /// - insert transparent address transaction entries,
478    ///
479    /// without actually writing anything.
480    ///
481    /// Also modifies the `address_balances` for these new UTXOs.
482    ///
483    /// # Errors
484    ///
485    /// - This method doesn't currently return any errors, but it might in future
486    #[allow(clippy::unwrap_in_result)]
487    pub fn prepare_new_transparent_outputs_batch(
488        &mut self,
489        db: &DiskDb,
490        network: &Network,
491        new_outputs_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
492        address_balances: &mut HashMap<transparent::Address, AddressBalanceLocationChange>,
493    ) -> Result<(), BoxError> {
494        let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap();
495        let utxo_loc_by_transparent_addr_loc =
496            db.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
497        let tx_loc_by_transparent_addr_loc =
498            db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
499
500        // Index all new transparent outputs
501        for (new_output_location, utxo) in new_outputs_by_out_loc {
502            let unspent_output = &utxo.output;
503            let receiving_address = unspent_output.address(network);
504
505            // Update the address balance by adding this UTXO's value
506            if let Some(receiving_address) = receiving_address {
507                // TODO: fix up tests that use missing outputs,
508                //       then replace entry() with get_mut().expect()
509
510                // In memory:
511                // - create the balance for the address, if needed.
512                // - create or fetch the link from the address to the AddressLocation
513                //   (the first location of the address in the chain).
514                let address_balance_location = address_balances
515                    .entry(receiving_address)
516                    .or_insert_with(|| AddressBalanceLocationChange::new(*new_output_location));
517                let receiving_address_location = address_balance_location.address_location();
518
519                // Update the balance for the address in memory.
520                address_balance_location
521                    .receive_output(unspent_output)
522                    .expect("balance overflow already checked");
523
524                // Create a link from the AddressLocation to the new OutputLocation in the database.
525                let address_unspent_output =
526                    AddressUnspentOutput::new(receiving_address_location, *new_output_location);
527                self.zs_insert(
528                    &utxo_loc_by_transparent_addr_loc,
529                    address_unspent_output,
530                    (),
531                );
532
533                // Create a link from the AddressLocation to the new TransactionLocation in the database.
534                // Unlike the OutputLocation link, this will never be deleted.
535                let address_transaction = AddressTransaction::new(
536                    receiving_address_location,
537                    new_output_location.transaction_location(),
538                );
539                self.zs_insert(&tx_loc_by_transparent_addr_loc, address_transaction, ());
540            }
541
542            // Use the OutputLocation to store a copy of the new Output in the database.
543            // (For performance reasons, we don't want to deserialize the whole transaction
544            // to get an output.)
545            self.zs_insert(&utxo_by_out_loc, new_output_location, unspent_output);
546        }
547
548        Ok(())
549    }
550
551    /// Prepare a database batch for the spent outputs in `spent_utxos_by_out_loc`.
552    ///
553    /// Adds the following changes to this batch:
554    /// - delete spent UTXOs, and
555    /// - delete transparent address UTXO index entries,
556    ///
557    /// without actually writing anything.
558    ///
559    /// Also modifies the `address_balances` for these new UTXOs.
560    ///
561    /// # Errors
562    ///
563    /// - This method doesn't currently return any errors, but it might in future
564    #[allow(clippy::unwrap_in_result)]
565    pub fn prepare_spent_transparent_outputs_batch(
566        &mut self,
567        db: &DiskDb,
568        network: &Network,
569        spent_utxos_by_out_loc: &BTreeMap<OutputLocation, transparent::Utxo>,
570        address_balances: &mut HashMap<transparent::Address, AddressBalanceLocationChange>,
571    ) -> Result<(), BoxError> {
572        let utxo_by_out_loc = db.cf_handle("utxo_by_out_loc").unwrap();
573        let utxo_loc_by_transparent_addr_loc =
574            db.cf_handle("utxo_loc_by_transparent_addr_loc").unwrap();
575
576        // Mark all transparent inputs as spent.
577        //
578        // Coinbase inputs represent new coins, so there are no UTXOs to mark as spent.
579        for (spent_output_location, utxo) in spent_utxos_by_out_loc {
580            let spent_output = &utxo.output;
581            let sending_address = spent_output.address(network);
582
583            // Fetch the balance, and the link from the address to the AddressLocation, from memory.
584            if let Some(sending_address) = sending_address {
585                let address_balance_location = address_balances
586                    .get_mut(&sending_address)
587                    .expect("spent outputs must already have an address balance");
588
589                // Update the address balance by subtracting this UTXO's value, in memory.
590                address_balance_location
591                    .spend_output(spent_output)
592                    .expect("balance underflow already checked");
593
594                // Delete the link from the AddressLocation to the spent OutputLocation in the database.
595                let address_spent_output = AddressUnspentOutput::new(
596                    address_balance_location.address_location(),
597                    *spent_output_location,
598                );
599                self.zs_delete(&utxo_loc_by_transparent_addr_loc, address_spent_output);
600            }
601
602            // Delete the OutputLocation, and the copy of the spent Output in the database.
603            self.zs_delete(&utxo_by_out_loc, spent_output_location);
604        }
605
606        Ok(())
607    }
608
609    /// Prepare a database batch indexing the transparent addresses that spent in this transaction.
610    ///
611    /// Adds the following changes to this batch:
612    /// - index spending transactions for each spent transparent output
613    ///   (this is different from the transaction that created the output),
614    ///
615    /// without actually writing anything.
616    ///
617    /// # Errors
618    ///
619    /// - This method doesn't currently return any errors, but it might in future
620    #[allow(clippy::unwrap_in_result, clippy::too_many_arguments)]
621    pub fn prepare_spending_transparent_tx_ids_batch(
622        &mut self,
623        zebra_db: &ZebraDb,
624        network: &Network,
625        spending_tx_location: TransactionLocation,
626        transaction: &Transaction,
627        spent_utxos_by_outpoint: &HashMap<transparent::OutPoint, transparent::Utxo>,
628        #[cfg(feature = "indexer")] out_loc_by_outpoint: &HashMap<
629            transparent::OutPoint,
630            OutputLocation,
631        >,
632        address_balances: &HashMap<transparent::Address, AddressBalanceLocationChange>,
633    ) -> Result<(), BoxError> {
634        let db = &zebra_db.db;
635        let tx_loc_by_transparent_addr_loc =
636            db.cf_handle("tx_loc_by_transparent_addr_loc").unwrap();
637
638        // Index the transparent addresses that spent in this transaction.
639        //
640        // Coinbase inputs represent new coins, so there are no UTXOs to mark as spent.
641        for spent_outpoint in transaction.inputs().iter().filter_map(Input::outpoint) {
642            let spent_utxo = spent_utxos_by_outpoint
643                .get(&spent_outpoint)
644                .expect("unexpected missing spent output");
645            let sending_address = spent_utxo.output.address(network);
646
647            // Fetch the balance, and the link from the address to the AddressLocation, from memory.
648            if let Some(sending_address) = sending_address {
649                let sending_address_location = address_balances
650                    .get(&sending_address)
651                    .expect("spent outputs must already have an address balance")
652                    .address_location();
653
654                // Create a link from the AddressLocation to the spent TransactionLocation in the database.
655                // Unlike the OutputLocation link, this will never be deleted.
656                //
657                // The value is the location of this transaction,
658                // not the transaction the spent output is from.
659                let address_transaction =
660                    AddressTransaction::new(sending_address_location, spending_tx_location);
661                self.zs_insert(&tx_loc_by_transparent_addr_loc, address_transaction, ());
662            }
663
664            #[cfg(feature = "indexer")]
665            {
666                let spent_output_location = out_loc_by_outpoint
667                    .get(&spent_outpoint)
668                    .expect("spent outpoints must already have output locations");
669
670                let _ = zebra_db
671                    .tx_loc_by_spent_output_loc_cf()
672                    .with_batch_for_writing(self)
673                    .zs_insert(spent_output_location, &spending_tx_location);
674            }
675        }
676
677        Ok(())
678    }
679
680    /// Prepare a database batch containing `finalized.block`'s:
681    /// - transparent address balance changes,
682    ///
683    /// and return it (without actually writing anything).
684    ///
685    /// # Errors
686    ///
687    /// - This method doesn't currently return any errors, but it might in future
688    #[allow(clippy::unwrap_in_result)]
689    pub fn prepare_transparent_balances_batch(
690        &mut self,
691        db: &DiskDb,
692        address_balances: HashMap<transparent::Address, AddressBalanceLocationChange>,
693    ) -> Result<(), BoxError> {
694        let balance_by_transparent_addr = db.cf_handle(BALANCE_BY_TRANSPARENT_ADDR).unwrap();
695
696        // Update all the changed address balances in the database.
697        for (address, address_balance_location_change) in address_balances.into_iter() {
698            // Some of these balances are new, and some are updates
699            self.zs_merge(
700                &balance_by_transparent_addr,
701                address,
702                address_balance_location_change,
703            );
704        }
705
706        Ok(())
707    }
708}