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