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}