1//! Transparent address indexes for non-finalized chains.
23use std::{
4 collections::{BTreeMap, BTreeSet, HashMap},
5 ops::RangeInclusive,
6};
78use mset::MultiSet;
910use zebra_chain::{
11 amount::{Amount, NegativeAllowed},
12 block::Height,
13 transaction, transparent,
14};
1516use crate::{OutputLocation, TransactionLocation, ValidateContextError};
1718use super::{RevertPosition, UpdateWith};
1920#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct TransparentTransfers {
22/// The partial chain balance for a transparent address.
23balance: Amount<NegativeAllowed>,
2425/// The partial list of transactions that spent or received UTXOs to a transparent address.
26 ///
27 /// Since transactions can only be added to this set, it does not need
28 /// special handling for
29 /// [`ReadStateService`](crate::service::ReadStateService) response
30 /// inconsistencies.
31 ///
32 /// The `getaddresstxids` RPC needs these transaction IDs to be sorted in chain order.
33tx_ids: MultiSet<transaction::Hash>,
3435/// The partial list of UTXOs received by a transparent address.
36 ///
37 /// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
38 /// but it might in future. So Zebra does it anyway.
39 ///
40 /// Optional TODOs:
41 /// - store `Utxo`s in the chain, and just store the created locations for this address
42 /// - if we add an OutputLocation to UTXO, remove this OutputLocation,
43 /// and use the inner OutputLocation to sort Utxos in chain order
44created_utxos: BTreeMap<OutputLocation, transparent::Output>,
4546/// The partial list of UTXOs spent by a transparent address.
47 ///
48 /// The `getaddressutxos` RPC doesn't need these transaction IDs to be sorted in chain order,
49 /// but it might in future. So Zebra does it anyway.
50 ///
51 /// Optional TODO:
52 /// - store spent `Utxo`s by location in the chain, use the chain spent UTXOs to filter,
53 /// and stop storing spent UTXOs by address
54spent_utxos: BTreeSet<OutputLocation>,
55}
5657// A created UTXO
58//
59// TODO: replace arguments with a struct
60impl
61UpdateWith<(
62// The location of the UTXO
63&transparent::OutPoint,
64// The UTXO data
65 // Includes the location of the transaction that created the output
66&transparent::OrderedUtxo,
67 )> for TransparentTransfers
68{
69#[allow(clippy::unwrap_in_result)]
70fn update_chain_tip_with(
71&mut self,
72&(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
73 ) -> Result<(), ValidateContextError> {
74self.balance = (self.balance
75 + created_utxo
76 .utxo
77 .output
78 .value()
79 .constrain()
80 .expect("NonNegative values are always valid NegativeAllowed values"))
81 .expect("total UTXO value has already been checked");
8283let transaction_location = transaction_location(created_utxo);
84let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
8586let previous_entry = self
87.created_utxos
88 .insert(output_location, created_utxo.utxo.output.clone());
89assert_eq!(
90 previous_entry, None,
91"unexpected created output: duplicate update or duplicate UTXO",
92 );
9394self.tx_ids.insert(outpoint.hash);
9596Ok(())
97 }
9899fn revert_chain_with(
100&mut self,
101&(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
102 _position: RevertPosition,
103 ) {
104self.balance = (self.balance
105 - created_utxo
106 .utxo
107 .output
108 .value()
109 .constrain()
110 .expect("NonNegative values are always valid NegativeAllowed values"))
111 .expect("reversing previous balance changes is always valid");
112113let transaction_location = transaction_location(created_utxo);
114let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
115116let removed_entry = self.created_utxos.remove(&output_location);
117assert!(
118 removed_entry.is_some(),
119"unexpected revert of created output: duplicate update or duplicate UTXO",
120 );
121122let tx_id_was_removed = self.tx_ids.remove(&outpoint.hash);
123assert!(
124 tx_id_was_removed,
125"unexpected revert of created output transaction: \
126 duplicate revert, or revert of an output that was never updated",
127 );
128 }
129}
130131// A transparent input
132//
133// TODO: replace arguments with a struct
134impl
135UpdateWith<(
136// The transparent input data
137&transparent::Input,
138// The hash of the transaction the input is from
139 // (not the transaction the spent output was created by)
140&transaction::Hash,
141// The output spent by the input
142 // Includes the location of the transaction that created the output
143&transparent::OrderedUtxo,
144 )> for TransparentTransfers
145{
146#[allow(clippy::unwrap_in_result)]
147fn update_chain_tip_with(
148&mut self,
149&(spending_input, spending_tx_hash, spent_output): &(
150&transparent::Input,
151&transaction::Hash,
152&transparent::OrderedUtxo,
153 ),
154 ) -> Result<(), ValidateContextError> {
155// Spending a UTXO subtracts value from the balance
156self.balance = (self.balance
157 - spent_output
158 .utxo
159 .output
160 .value()
161 .constrain()
162 .expect("NonNegative values are always valid NegativeAllowed values"))
163 .expect("total UTXO value has already been checked");
164165let spent_outpoint = spending_input.outpoint().expect("checked by caller");
166167let spent_output_tx_loc = transaction_location(spent_output);
168let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
169let spend_was_inserted = self.spent_utxos.insert(output_location);
170assert!(
171 spend_was_inserted,
172"unexpected spent output: duplicate update or duplicate spend",
173 );
174175self.tx_ids.insert(*spending_tx_hash);
176177Ok(())
178 }
179180fn revert_chain_with(
181&mut self,
182&(spending_input, spending_tx_hash, spent_output): &(
183&transparent::Input,
184&transaction::Hash,
185&transparent::OrderedUtxo,
186 ),
187 _position: RevertPosition,
188 ) {
189self.balance = (self.balance
190 + spent_output
191 .utxo
192 .output
193 .value()
194 .constrain()
195 .expect("NonNegative values are always valid NegativeAllowed values"))
196 .expect("reversing previous balance changes is always valid");
197198let spent_outpoint = spending_input.outpoint().expect("checked by caller");
199200let spent_output_tx_loc = transaction_location(spent_output);
201let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
202let spend_was_removed = self.spent_utxos.remove(&output_location);
203assert!(
204 spend_was_removed,
205"unexpected revert of spent output: \
206 duplicate revert, or revert of a spent output that was never updated",
207 );
208209let tx_id_was_removed = self.tx_ids.remove(spending_tx_hash);
210assert!(
211 tx_id_was_removed,
212"unexpected revert of spending input transaction: \
213 duplicate revert, or revert of an input that was never updated",
214 );
215 }
216}
217218impl TransparentTransfers {
219/// Returns true if there are no transfers for this address.
220pub fn is_empty(&self) -> bool {
221self.balance == Amount::<NegativeAllowed>::zero()
222 && self.tx_ids.is_empty()
223 && self.created_utxos.is_empty()
224 && self.spent_utxos.is_empty()
225 }
226227/// Returns the partial balance for this address.
228#[allow(dead_code)]
229pub fn balance(&self) -> Amount<NegativeAllowed> {
230self.balance
231 }
232233/// Returns the partial received balance for this address.
234pub fn received(&self) -> u64 {
235let received_utxos = self.created_utxos.values();
236 received_utxos.map(|out| out.value()).map(u64::from).sum()
237 }
238239/// Returns the [`transaction::Hash`]es of the transactions that sent or
240 /// received transparent transfers to this address, in this partial chain,
241 /// filtered by `query_height_range`.
242 ///
243 /// The transactions are returned in chain order.
244 ///
245 /// `chain_tx_loc_by_hash` should be the `tx_loc_by_hash` field from the
246 /// [`Chain`][1] containing this index.
247 ///
248 /// # Panics
249 ///
250 /// If `chain_tx_loc_by_hash` is missing some transaction hashes from this
251 /// index.
252 ///
253 /// [1]: super::super::Chain
254pub fn tx_ids(
255&self,
256 chain_tx_loc_by_hash: &HashMap<transaction::Hash, TransactionLocation>,
257 query_height_range: RangeInclusive<Height>,
258 ) -> BTreeMap<TransactionLocation, transaction::Hash> {
259self.tx_ids
260 .distinct_elements()
261 .filter_map(|tx_hash| {
262let tx_loc = *chain_tx_loc_by_hash
263 .get(tx_hash)
264 .expect("all hashes are indexed");
265266if query_height_range.contains(&tx_loc.height) {
267Some((tx_loc, *tx_hash))
268 } else {
269None
270}
271 })
272 .collect()
273 }
274275/// Returns the new transparent outputs sent to this address,
276 /// in this partial chain, in chain order.
277 ///
278 /// Some of these outputs might already be spent.
279 /// [`TransparentTransfers::spent_utxos`] returns spent UTXOs.
280#[allow(dead_code)]
281pub fn created_utxos(&self) -> &BTreeMap<OutputLocation, transparent::Output> {
282&self.created_utxos
283 }
284285/// Returns the [`OutputLocation`]s of the spent transparent outputs sent to this address,
286 /// in this partial chain, in chain order.
287#[allow(dead_code)]
288pub fn spent_utxos(&self) -> &BTreeSet<OutputLocation> {
289&self.spent_utxos
290 }
291}
292293impl Default for TransparentTransfers {
294fn default() -> Self {
295Self {
296 balance: Amount::zero(),
297 tx_ids: Default::default(),
298 created_utxos: Default::default(),
299 spent_utxos: Default::default(),
300 }
301 }
302}
303304/// Returns the transaction location for an [`transparent::OrderedUtxo`].
305pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation {
306 TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block)
307}