cuprate_consensus/transactions/
contextual_data.rs1use std::{borrow::Cow, collections::HashSet};
15
16use indexmap::IndexMap;
17use monero_oxide::transaction::{Input, Timelock};
18use tower::ServiceExt;
19use tracing::instrument;
20
21use cuprate_consensus_rules::{
22 transactions::{
23 get_absolute_offsets, insert_ring_member_ids, DecoyInfo, Rings, TransactionError,
24 TxRingMembersInfo,
25 },
26 ConsensusError, HardFork, TxVersion,
27};
28
29use cuprate_types::{
30 blockchain::{BlockchainReadRequest, BlockchainResponse},
31 output_cache::OutputCache,
32 OutputOnChain,
33};
34
35use crate::{
36 transactions::TransactionVerificationData, Database, ExtendedConsensusError,
37 VerificationContext,
38};
39
40fn get_ring_members_for_inputs(
44 get_outputs: impl Fn(u64, u64) -> Option<OutputOnChain>,
45 inputs: &[Input],
46) -> Result<Vec<Vec<OutputOnChain>>, TransactionError> {
47 inputs
48 .iter()
49 .map(|inp| match inp {
50 Input::ToKey {
51 amount,
52 key_offsets,
53 ..
54 } => {
55 let offsets = get_absolute_offsets(key_offsets)?;
56 Ok(offsets
57 .iter()
58 .map(|offset| {
59 get_outputs(amount.unwrap_or(0), *offset)
60 .ok_or(TransactionError::RingMemberNotFoundOrInvalid)
61 })
62 .collect::<Result<_, TransactionError>>()?)
63 }
64 Input::Gen(_) => Err(TransactionError::IncorrectInputType),
65 })
66 .collect::<Result<_, TransactionError>>()
67}
68
69pub fn new_ring_member_info(
73 used_outs: Vec<Vec<OutputOnChain>>,
74 decoy_info: Option<DecoyInfo>,
75 tx_version: TxVersion,
76) -> Result<TxRingMembersInfo, TransactionError> {
77 Ok(TxRingMembersInfo {
78 youngest_used_out_height: used_outs
79 .iter()
80 .map(|inp_outs| {
81 inp_outs
82 .last()
84 .map(|out| out.height)
85 .expect("Input must have ring members")
86 })
87 .max()
89 .expect("Tx must have inputs"),
90 time_locked_outs: used_outs
91 .iter()
92 .flat_map(|inp_outs| {
93 inp_outs
94 .iter()
95 .filter_map(|out| match out.time_lock {
96 Timelock::None => None,
97 Timelock::Block(_) | Timelock::Time(_) => Some(out.time_lock),
98 })
99 .collect::<Vec<_>>()
100 })
101 .collect(),
102 rings: new_rings(used_outs, tx_version),
103 decoy_info,
104 })
105}
106
107fn new_rings(outputs: Vec<Vec<OutputOnChain>>, tx_version: TxVersion) -> Rings {
109 match tx_version {
110 TxVersion::RingSignatures => Rings::Legacy(
111 outputs
112 .into_iter()
113 .map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect::<Vec<_>>())
114 .collect::<Vec<_>>(),
115 ),
116 TxVersion::RingCT => Rings::RingCT(
117 outputs
118 .into_iter()
119 .map(|inp_outs| {
120 inp_outs
121 .into_iter()
122 .map(|out| [out.key, out.commitment])
123 .collect::<_>()
124 })
125 .collect::<_>(),
126 ),
127 }
128}
129
130pub async fn get_output_cache<D: Database>(
134 txs_verification_data: impl Iterator<Item = &TransactionVerificationData>,
135 mut database: D,
136) -> Result<OutputCache, ExtendedConsensusError> {
137 let mut outputs = IndexMap::new();
138
139 for tx_v_data in txs_verification_data {
140 insert_ring_member_ids(&tx_v_data.tx.prefix().inputs, &mut outputs)
141 .map_err(ConsensusError::Transaction)?;
142 }
143
144 let BlockchainResponse::Outputs(outputs) = database
145 .ready()
146 .await?
147 .call(BlockchainReadRequest::Outputs {
148 outputs,
149 get_txid: false,
150 })
151 .await?
152 else {
153 unreachable!();
154 };
155
156 Ok(outputs)
157}
158
159pub async fn batch_get_ring_member_info<D: Database>(
164 txs_verification_data: impl Iterator<Item = &TransactionVerificationData> + Clone,
165 hf: HardFork,
166 verification_context: &mut VerificationContext<D>,
167) -> Result<Vec<TxRingMembersInfo>, ExtendedConsensusError> {
168 let outputs = match verification_context {
169 VerificationContext::BatchPrepareCache(cache) => Cow::Borrowed(&cache.output_cache),
170 VerificationContext::Database(database) => {
171 let mut outputs = IndexMap::new();
172
173 for tx_v_data in txs_verification_data.clone() {
174 insert_ring_member_ids(&tx_v_data.tx.prefix().inputs, &mut outputs)
175 .map_err(ConsensusError::Transaction)?;
176 }
177
178 let BlockchainResponse::Outputs(outputs) = database
179 .ready()
180 .await?
181 .call(BlockchainReadRequest::Outputs {
182 outputs,
183 get_txid: false,
184 })
185 .await?
186 else {
187 unreachable!();
188 };
189
190 Cow::Owned(outputs)
191 }
192 };
193
194 Ok(txs_verification_data
195 .map(move |tx_v_data| {
196 let numb_outputs = |amt| outputs.number_outs_with_amount(amt);
197
198 let ring_members_for_tx = get_ring_members_for_inputs(
199 |amt, idx| outputs.get_output(amt, idx).copied(),
200 &tx_v_data.tx.prefix().inputs,
201 )
202 .map_err(ConsensusError::Transaction)?;
203
204 let decoy_info = if hf == HardFork::V1 {
205 None
206 } else {
207 Some(
209 DecoyInfo::new(&tx_v_data.tx.prefix().inputs, numb_outputs, hf)
210 .map_err(ConsensusError::Transaction)?,
211 )
212 };
213
214 new_ring_member_info(ring_members_for_tx, decoy_info, tx_v_data.version)
215 .map_err(ConsensusError::Transaction)
216 })
217 .collect::<Result<_, _>>()?)
218}
219
220#[instrument(level = "debug", skip_all)]
226pub async fn batch_get_decoy_info<'a, 'b, D: Database>(
227 txs_verification_data: impl Iterator<Item = &'a TransactionVerificationData> + Clone,
228 hf: HardFork,
229 verification_context: &mut VerificationContext<D>,
230) -> Result<
231 impl Iterator<Item = Result<DecoyInfo, ConsensusError>> + sealed::Captures<(&'a (), &'b ())>,
232 ExtendedConsensusError,
233> {
234 assert_ne!(hf, HardFork::V1);
236
237 let unique_input_amounts = txs_verification_data
239 .clone()
240 .flat_map(|tx_info| {
241 tx_info.tx.prefix().inputs.iter().map(|input| match input {
242 Input::ToKey { amount, .. } => amount.unwrap_or(0),
243 Input::Gen(_) => 0,
244 })
245 })
246 .collect::<HashSet<_>>();
247
248 tracing::debug!(
249 "Getting the amount of outputs with certain amounts for {} amounts",
250 unique_input_amounts.len()
251 );
252
253 let outputs_with_amount = match verification_context {
254 VerificationContext::BatchPrepareCache(cache) => unique_input_amounts
255 .into_iter()
256 .map(|amount| (amount, cache.output_cache.number_outs_with_amount(amount)))
257 .collect(),
258 VerificationContext::Database(database) => {
259 let BlockchainResponse::NumberOutputsWithAmount(outputs_with_amount) = database
260 .ready()
261 .await?
262 .call(BlockchainReadRequest::NumberOutputsWithAmount(
263 unique_input_amounts.into_iter().collect(),
264 ))
265 .await?
266 else {
267 unreachable!();
268 };
269
270 outputs_with_amount
271 }
272 };
273
274 Ok(txs_verification_data.map(move |tx_v_data| {
275 DecoyInfo::new(
276 &tx_v_data.tx.prefix().inputs,
277 |amt| outputs_with_amount.get(&amt).copied().unwrap_or(0),
278 hf,
279 )
280 .map_err(ConsensusError::Transaction)
281 }))
282}
283
284mod sealed {
285 pub trait Captures<U> {}
289 impl<T: ?Sized, U> Captures<U> for T {}
290}