zebra_state/service/non_finalized_state/chain/
index.rs

1//! Transparent address indexes for non-finalized chains.
2
3use std::{
4    collections::{BTreeMap, BTreeSet, HashMap},
5    ops::RangeInclusive,
6};
7
8use mset::MultiSet;
9
10use zebra_chain::{
11    amount::{Amount, NegativeAllowed},
12    block::Height,
13    transaction, transparent,
14};
15
16use crate::{OutputLocation, TransactionLocation, ValidateContextError};
17
18use super::{RevertPosition, UpdateWith};
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct TransparentTransfers {
22    /// The partial chain balance for a transparent address.
23    balance: Amount<NegativeAllowed>,
24
25    /// 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.
33    tx_ids: MultiSet<transaction::Hash>,
34
35    /// 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
44    created_utxos: BTreeMap<OutputLocation, transparent::Output>,
45
46    /// 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
54    spent_utxos: BTreeSet<OutputLocation>,
55}
56
57// A created UTXO
58//
59// TODO: replace arguments with a struct
60impl
61    UpdateWith<(
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)]
70    fn update_chain_tip_with(
71        &mut self,
72        &(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
73    ) -> Result<(), ValidateContextError> {
74        self.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");
82
83        let transaction_location = transaction_location(created_utxo);
84        let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
85
86        let previous_entry = self
87            .created_utxos
88            .insert(output_location, created_utxo.utxo.output.clone());
89        assert_eq!(
90            previous_entry, None,
91            "unexpected created output: duplicate update or duplicate UTXO",
92        );
93
94        self.tx_ids.insert(outpoint.hash);
95
96        Ok(())
97    }
98
99    fn revert_chain_with(
100        &mut self,
101        &(outpoint, created_utxo): &(&transparent::OutPoint, &transparent::OrderedUtxo),
102        _position: RevertPosition,
103    ) {
104        self.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");
112
113        let transaction_location = transaction_location(created_utxo);
114        let output_location = OutputLocation::from_outpoint(transaction_location, outpoint);
115
116        let removed_entry = self.created_utxos.remove(&output_location);
117        assert!(
118            removed_entry.is_some(),
119            "unexpected revert of created output: duplicate update or duplicate UTXO",
120        );
121
122        let tx_id_was_removed = self.tx_ids.remove(&outpoint.hash);
123        assert!(
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}
130
131// A transparent input
132//
133// TODO: replace arguments with a struct
134impl
135    UpdateWith<(
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)]
147    fn 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
156        self.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");
164
165        let spent_outpoint = spending_input.outpoint().expect("checked by caller");
166
167        let spent_output_tx_loc = transaction_location(spent_output);
168        let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
169        let spend_was_inserted = self.spent_utxos.insert(output_location);
170        assert!(
171            spend_was_inserted,
172            "unexpected spent output: duplicate update or duplicate spend",
173        );
174
175        self.tx_ids.insert(*spending_tx_hash);
176
177        Ok(())
178    }
179
180    fn 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    ) {
189        self.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");
197
198        let spent_outpoint = spending_input.outpoint().expect("checked by caller");
199
200        let spent_output_tx_loc = transaction_location(spent_output);
201        let output_location = OutputLocation::from_outpoint(spent_output_tx_loc, &spent_outpoint);
202        let spend_was_removed = self.spent_utxos.remove(&output_location);
203        assert!(
204            spend_was_removed,
205            "unexpected revert of spent output: \
206             duplicate revert, or revert of a spent output that was never updated",
207        );
208
209        let tx_id_was_removed = self.tx_ids.remove(spending_tx_hash);
210        assert!(
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}
217
218impl TransparentTransfers {
219    /// Returns true if there are no transfers for this address.
220    pub fn is_empty(&self) -> bool {
221        self.balance == Amount::<NegativeAllowed>::zero()
222            && self.tx_ids.is_empty()
223            && self.created_utxos.is_empty()
224            && self.spent_utxos.is_empty()
225    }
226
227    /// Returns the partial balance for this address.
228    #[allow(dead_code)]
229    pub fn balance(&self) -> Amount<NegativeAllowed> {
230        self.balance
231    }
232
233    /// Returns the partial received balance for this address.
234    pub fn received(&self) -> u64 {
235        let received_utxos = self.created_utxos.values();
236        received_utxos.map(|out| out.value()).map(u64::from).sum()
237    }
238
239    /// 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
254    pub 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> {
259        self.tx_ids
260            .distinct_elements()
261            .filter_map(|tx_hash| {
262                let tx_loc = *chain_tx_loc_by_hash
263                    .get(tx_hash)
264                    .expect("all hashes are indexed");
265
266                if query_height_range.contains(&tx_loc.height) {
267                    Some((tx_loc, *tx_hash))
268                } else {
269                    None
270                }
271            })
272            .collect()
273    }
274
275    /// 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)]
281    pub fn created_utxos(&self) -> &BTreeMap<OutputLocation, transparent::Output> {
282        &self.created_utxos
283    }
284
285    /// 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)]
288    pub fn spent_utxos(&self) -> &BTreeSet<OutputLocation> {
289        &self.spent_utxos
290    }
291}
292
293impl Default for TransparentTransfers {
294    fn default() -> Self {
295        Self {
296            balance: Amount::zero(),
297            tx_ids: Default::default(),
298            created_utxos: Default::default(),
299            spent_utxos: Default::default(),
300        }
301    }
302}
303
304/// 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}