zebra_state/service/non_finalized_state/chain/
index.rs1use 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    balance: Amount<NegativeAllowed>,
24
25    tx_ids: MultiSet<transaction::Hash>,
34
35    created_utxos: BTreeMap<OutputLocation, transparent::Output>,
45
46    spent_utxos: BTreeSet<OutputLocation>,
55}
56
57impl
61    UpdateWith<(
62        &transparent::OutPoint,
64        &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
131impl
135    UpdateWith<(
136        &transparent::Input,
138        &transaction::Hash,
141        &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        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    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    #[allow(dead_code)]
229    pub fn balance(&self) -> Amount<NegativeAllowed> {
230        self.balance
231    }
232
233    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    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    #[allow(dead_code)]
281    pub fn created_utxos(&self) -> &BTreeMap<OutputLocation, transparent::Output> {
282        &self.created_utxos
283    }
284
285    #[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
304pub fn transaction_location(ordered_utxo: &transparent::OrderedUtxo) -> TransactionLocation {
306    TransactionLocation::from_usize(ordered_utxo.utxo.height, ordered_utxo.tx_index_in_block)
307}