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