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}