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}