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