zebra_state/service/read/address/
tx_id.rs1use std::{
15 collections::{BTreeMap, HashSet},
16 ops::RangeInclusive,
17};
18
19use zebra_chain::{block::Height, transaction, transparent};
20
21use crate::{
22 service::{
23 finalized_state::ZebraDb, non_finalized_state::Chain, read::FINALIZED_STATE_QUERY_RETRIES,
24 },
25 BoxError, TransactionLocation,
26};
27
28pub fn transparent_tx_ids<C>(
35 chain: Option<C>,
36 db: &ZebraDb,
37 addresses: HashSet<transparent::Address>,
38 query_height_range: RangeInclusive<Height>,
39 max_entries: Option<usize>,
40) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
41where
42 C: AsRef<Chain>,
43{
44 let mut tx_id_error = None;
45
46 for _ in 0..=FINALIZED_STATE_QUERY_RETRIES {
51 let (finalized_tx_ids, finalized_tip_range) =
52 finalized_transparent_tx_ids(db, &addresses, query_height_range.clone(), max_entries);
53
54 let chain_tx_id_changes = chain_transparent_tx_id_changes(
56 chain.as_ref(),
57 &addresses,
58 finalized_tip_range,
59 query_height_range.clone(),
60 );
61
62 match chain_tx_id_changes {
64 Ok(chain_tx_id_changes) => {
65 let tx_ids =
66 apply_tx_id_changes(finalized_tx_ids, chain_tx_id_changes, max_entries);
67
68 return Ok(tx_ids);
69 }
70
71 Err(error) => tx_id_error = Some(Err(error)),
72 }
73 }
74
75 tx_id_error.expect("unexpected missing error: attempts should set error or return")
76}
77
78fn finalized_transparent_tx_ids(
85 db: &ZebraDb,
86 addresses: &HashSet<transparent::Address>,
87 query_height_range: RangeInclusive<Height>,
88 limit: Option<usize>,
89) -> (
90 BTreeMap<TransactionLocation, transaction::Hash>,
91 Option<RangeInclusive<Height>>,
92) {
93 let start_finalized_tip = db.finalized_tip_height();
99
100 let finalized_tx_ids =
101 db.partial_finalized_transparent_tx_ids(addresses, query_height_range, limit);
102
103 let end_finalized_tip = db.finalized_tip_height();
104
105 let finalized_tip_range = if let (Some(start_finalized_tip), Some(end_finalized_tip)) =
106 (start_finalized_tip, end_finalized_tip)
107 {
108 Some(start_finalized_tip..=end_finalized_tip)
109 } else {
110 None
112 };
113
114 (finalized_tx_ids, finalized_tip_range)
115}
116
117fn chain_transparent_tx_id_changes<C>(
124 chain: Option<C>,
125 addresses: &HashSet<transparent::Address>,
126 finalized_tip_range: Option<RangeInclusive<Height>>,
127 query_height_range: RangeInclusive<Height>,
128) -> Result<BTreeMap<TransactionLocation, transaction::Hash>, BoxError>
129where
130 C: AsRef<Chain>,
131{
132 let address_count = addresses.len();
133
134 let finalized_tip_range = match finalized_tip_range {
135 Some(finalized_tip_range) => finalized_tip_range,
136 None => {
137 assert!(
138 chain.is_none(),
139 "unexpected non-finalized chain when finalized state is empty"
140 );
141
142 debug!(
143 ?finalized_tip_range,
144 ?address_count,
145 "chain address tx ID query: state is empty, no tx IDs available",
146 );
147
148 return Ok(Default::default());
149 }
150 };
151
152 let required_min_non_finalized_root = finalized_tip_range.start().0 + 1;
163
164 let finalized_tip_status = required_min_non_finalized_root..=finalized_tip_range.end().0;
168 let finalized_tip_status = if finalized_tip_status.is_empty() {
169 let finalized_tip_height = *finalized_tip_range.end();
170 Ok(finalized_tip_height)
171 } else {
172 let required_non_finalized_overlap = finalized_tip_status;
173 Err(required_non_finalized_overlap)
174 };
175
176 if chain.is_none() {
177 if address_count <= 1 || finalized_tip_status.is_ok() {
178 debug!(
179 ?finalized_tip_status,
180 ?required_min_non_finalized_root,
181 ?finalized_tip_range,
182 ?address_count,
183 "chain address tx ID query: \
184 finalized chain is consistent, and non-finalized chain is empty",
185 );
186
187 return Ok(Default::default());
188 } else {
189 debug!(
192 ?finalized_tip_status,
193 ?required_min_non_finalized_root,
194 ?finalized_tip_range,
195 ?address_count,
196 "chain address tx ID query: \
197 finalized tip query was inconsistent, but non-finalized chain is empty",
198 );
199
200 return Err("unable to get tx IDs: \
201 state was committing a block, and non-finalized chain is empty"
202 .into());
203 }
204 }
205
206 let chain = chain.unwrap();
207 let chain = chain.as_ref();
208
209 let non_finalized_root = chain.non_finalized_root_height();
210 let non_finalized_tip = chain.non_finalized_tip_height();
211
212 assert!(
213 non_finalized_root.0 <= required_min_non_finalized_root,
214 "unexpected chain gap: the best chain is updated after its previous root is finalized",
215 );
216
217 match finalized_tip_status {
218 Ok(finalized_tip_height) => {
219 if finalized_tip_height >= non_finalized_tip {
222 debug!(
223 ?non_finalized_root,
224 ?non_finalized_tip,
225 ?finalized_tip_status,
226 ?finalized_tip_range,
227 ?address_count,
228 "chain address tx ID query: \
229 non-finalized blocks have all been finalized, no new UTXO changes",
230 );
231
232 return Ok(Default::default());
233 }
234 }
235
236 Err(ref required_non_finalized_overlap) => {
237 if address_count > 1 && *required_non_finalized_overlap.end() > non_finalized_tip.0 {
240 debug!(
241 ?non_finalized_root,
242 ?non_finalized_tip,
243 ?finalized_tip_status,
244 ?finalized_tip_range,
245 ?address_count,
246 "chain address tx ID query: \
247 finalized tip query was inconsistent, \
248 some inconsistent blocks are missing from the non-finalized chain, \
249 and the query has multiple addresses",
250 );
251
252 return Err("unable to get tx IDs: \
253 state was committing a block, \
254 that is missing from the non-finalized chain, \
255 and the query has multiple addresses"
256 .into());
257 }
258
259 assert!(
262 address_count <= 1
263 || required_non_finalized_overlap
264 .clone()
265 .all(|height| chain.blocks.contains_key(&Height(height))),
266 "tx ID query inconsistency: \
267 chain must contain required overlap blocks \
268 or query must only have one address",
269 );
270 }
271 }
272
273 Ok(chain.partial_transparent_tx_ids(addresses, query_height_range))
274}
275
276fn apply_tx_id_changes(
278 finalized_tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
279 chain_tx_ids: BTreeMap<TransactionLocation, transaction::Hash>,
280 max_entries: Option<usize>,
281) -> BTreeMap<TransactionLocation, transaction::Hash> {
282 let tx_ids: BTreeMap<_, _> = finalized_tx_ids.into_iter().chain(chain_tx_ids).collect();
285
286 match max_entries {
288 Some(max_entries) => tx_ids.into_iter().take(max_entries).collect(),
289 None => tx_ids,
290 }
291}