1use kaspa_consensus_core::{
2 hashing,
3 tx::{CovenantBinding, VerifiableTransaction},
4};
5use kaspa_hashes::Hash;
6use kaspa_txscript_errors::CovenantsError;
7use std::{collections::HashMap, sync::LazyLock};
8
9#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
14pub struct CovenantInputContext {
15 pub auth_outputs: Vec<usize>,
19}
20
21impl Default for CovenantInputContext {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl CovenantInputContext {
28 pub fn new() -> Self {
29 Self { auth_outputs: Default::default() }
30 }
31}
32
33#[derive(Default)]
38#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
39pub struct CovenantSharedContext {
40 pub input_indices: Vec<usize>,
42
43 pub output_indices: Vec<usize>,
45}
46
47#[derive(Default)]
51#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
52pub struct CovenantsContext {
53 pub input_ctxs: HashMap<usize, CovenantInputContext>,
55
56 pub shared_ctxs: HashMap<Hash, CovenantSharedContext>,
58}
59
60pub static EMPTY_COV_CONTEXT: LazyLock<CovenantsContext> = LazyLock::new(CovenantsContext::default);
61
62impl CovenantsContext {
63 pub(crate) fn auth_output_index(&self, input_idx: usize, k: usize) -> Result<usize, CovenantsError> {
67 let auth_outputs = self.input_ctxs.get(&input_idx).map(|ctx| ctx.auth_outputs.as_slice()).unwrap_or_default();
68 auth_outputs.get(k).copied().ok_or(CovenantsError::InvalidAuthCovOutIndex(k, input_idx, auth_outputs.len()))
69 }
70
71 pub(crate) fn num_auth_outputs(&self, input_idx: usize) -> usize {
75 self.input_ctxs.get(&input_idx).map_or(0, |ctx| ctx.auth_outputs.len())
76 }
77
78 pub(crate) fn num_covenant_inputs(&self, covenant_id: Hash) -> usize {
79 self.shared_ctxs.get(&covenant_id).map_or(0, |ctx| ctx.input_indices.len())
80 }
81
82 pub(crate) fn covenant_input_index(&self, covenant_id: Hash, k: usize) -> Result<usize, CovenantsError> {
83 let input_indices = self.shared_ctxs.get(&covenant_id).map(|ctx| ctx.input_indices.as_slice()).unwrap_or_default();
84 input_indices.get(k).copied().ok_or(CovenantsError::InvalidCovInIndex(covenant_id, k))
85 }
86
87 pub(crate) fn num_covenant_outputs(&self, covenant_id: Hash) -> usize {
88 self.shared_ctxs.get(&covenant_id).map_or(0, |ctx| ctx.output_indices.len())
89 }
90
91 pub(crate) fn covenant_output_index(&self, covenant_id: Hash, k: usize) -> Result<usize, CovenantsError> {
92 let output_indices = self.shared_ctxs.get(&covenant_id).map(|ctx| ctx.output_indices.as_slice()).unwrap_or_default();
93 output_indices.get(k).copied().ok_or(CovenantsError::InvalidCovOutIndex(covenant_id, k))
94 }
95
96 pub fn from_tx(tx: &impl VerifiableTransaction) -> Result<Self, CovenantsError> {
102 let mut ctx = CovenantsContext::default();
103
104 let mut genesis_ctxs: HashMap<(usize, Hash), Vec<usize>> = HashMap::new();
106
107 for (i, (_, entry)) in tx.populated_inputs().enumerate() {
108 if let Some(covenant_id) = entry.covenant_id {
109 ctx.shared_ctxs.entry(covenant_id).or_default().input_indices.push(i);
110 }
111 }
112
113 for (i, output) in tx.outputs().iter().enumerate() {
114 let Some(CovenantBinding { covenant_id, authorizing_input }) = output.covenant else {
115 continue;
116 };
117
118 let auth_input_idx = authorizing_input as usize;
119
120 let Some(utxo_entry) = tx.utxo(auth_input_idx) else {
121 return Err(CovenantsError::AuthInputOutOfBounds(i, authorizing_input));
122 };
123
124 match utxo_entry.covenant_id {
125 Some(input_covenant_id) if input_covenant_id == covenant_id => {
126 ctx.input_ctxs.entry(auth_input_idx).or_insert_with(CovenantInputContext::new).auth_outputs.push(i);
130
131 ctx.shared_ctxs
132 .get_mut(&covenant_id)
133 .expect("Shared context should've been created by the authorizing input")
134 .output_indices
135 .push(i);
136 }
137 Some(_) | None => {
138 genesis_ctxs.entry((auth_input_idx, covenant_id)).or_default().push(i);
143 }
144 }
145 }
146
147 for ((auth_input_idx, covenant_id), output_indices) in genesis_ctxs.into_iter() {
150 let input = tx.inputs().get(auth_input_idx).expect("utxo(auth_input) existed above");
151
152 let expected_id = hashing::covenant_id::covenant_id(
153 input.previous_outpoint,
154 output_indices.into_iter().map(|i| (i as u32, tx.outputs().get(i).expect("enumerated above"))),
155 );
156
157 if expected_id != covenant_id {
158 return Err(CovenantsError::WrongGenesisCovenantId(auth_input_idx, covenant_id));
159 }
160 }
161
162 Ok(ctx)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use kaspa_consensus_core::{
170 hashing,
171 subnets::SubnetworkId,
172 tx::{
173 CovenantBinding, GenesisCovenantGroup, PopulatedTransaction, Transaction, TransactionInput, TransactionOutpoint,
174 TransactionOutput, UtxoEntry,
175 },
176 };
177
178 struct OutputConfig {
179 value: u64,
180 authorizing_input: usize,
181 covenant_group: u64, }
183
184 fn create_genesis_tx(
191 input_covenant_ids: Vec<Option<u64>>,
192 outputs: Vec<OutputConfig>,
193 compute_correct_ids: bool,
194 ) -> (Transaction, Vec<UtxoEntry>) {
195 let inputs: Vec<_> = input_covenant_ids
197 .iter()
198 .enumerate()
199 .map(|(i, _)| {
200 let outpoint = TransactionOutpoint::new((i as u64).into(), 0);
201 TransactionInput::new(outpoint, vec![], 0, 0)
202 })
203 .collect();
204
205 let utxos: Vec<_> = input_covenant_ids
206 .iter()
207 .map(|&cov_id| UtxoEntry {
208 amount: 1000,
209 script_public_key: Default::default(),
210 block_daa_score: 0,
211 is_coinbase: false,
212 covenant_id: cov_id.map(|id| id.into()),
213 })
214 .collect();
215
216 let tx_outputs: Vec<_> = outputs
218 .iter()
219 .map(|cfg| TransactionOutput {
220 value: cfg.value,
221 script_public_key: Default::default(),
222 covenant: Some(CovenantBinding {
223 covenant_id: cfg.covenant_group.into(), authorizing_input: cfg.authorizing_input as u16,
225 }),
226 })
227 .collect();
228
229 let mut tx = Transaction::new(0, inputs, tx_outputs, 0, SubnetworkId::default(), 0, vec![]);
230
231 if compute_correct_ids {
232 let mut genesis_groups: HashMap<(usize, u64), Vec<usize>> = HashMap::new();
234
235 for (i, cfg) in outputs.iter().enumerate() {
236 let input_cov_id = input_covenant_ids.get(cfg.authorizing_input).copied().flatten();
237
238 if input_cov_id != Some(cfg.covenant_group) {
239 genesis_groups.entry((cfg.authorizing_input, cfg.covenant_group)).or_default().push(i);
240 }
241 }
242
243 let genesis_groups: Vec<(usize, Vec<usize>)> =
244 genesis_groups.into_iter().map(|((auth_input_idx, _), output_indices)| (auth_input_idx, output_indices)).collect();
245
246 for (auth_input_idx, output_indices) in &genesis_groups {
247 let outpoint = tx.inputs[*auth_input_idx].previous_outpoint;
248 let expected_id =
249 hashing::covenant_id::covenant_id(outpoint, output_indices.iter().map(|&i| (i as u32, &tx.outputs[i])));
250 for &output_idx in output_indices {
251 tx.outputs[output_idx].covenant.as_mut().unwrap().covenant_id = expected_id;
252 }
253 }
254
255 let mut api_tx = tx.clone();
257 for &output_idx in genesis_groups.iter().flat_map(|(_, output_indices)| output_indices.iter()) {
258 api_tx.outputs[output_idx].covenant = None;
259 }
260 let api_groups: Vec<_> = genesis_groups
261 .iter()
262 .map(|(auth_input_idx, output_indices)| {
263 GenesisCovenantGroup::new(*auth_input_idx as u16, output_indices.iter().map(|&i| i as u32).collect())
264 })
265 .collect();
266 api_tx.populate_genesis_covenants(&api_groups).unwrap();
267
268 for &output_idx in genesis_groups.iter().flat_map(|(_, output_indices)| output_indices.iter()) {
269 assert_eq!(tx.outputs[output_idx].covenant, api_tx.outputs[output_idx].covenant);
270 }
271 }
272
273 tx.finalize();
274 (tx, utxos)
275 }
276
277 #[test]
278 fn test_genesis_single_output() {
279 let (tx, entries) =
280 create_genesis_tx(vec![None], vec![OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 }], true);
281 let populated_tx = PopulatedTransaction::new(&tx, entries);
282 let ctx = CovenantsContext::from_tx(&populated_tx).unwrap();
283
284 assert!(ctx.input_ctxs.is_empty());
286 assert!(ctx.shared_ctxs.is_empty());
287 }
288
289 #[test]
290 fn test_genesis_multiple_outputs() {
291 let (tx, entries) = create_genesis_tx(
292 vec![None],
293 vec![
294 OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 },
295 OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 },
296 ],
297 true,
298 );
299 let populated_tx = PopulatedTransaction::new(&tx, entries);
300 let ctx = CovenantsContext::from_tx(&populated_tx).unwrap();
301
302 assert!(ctx.input_ctxs.is_empty());
304 assert!(ctx.shared_ctxs.is_empty());
305 }
306
307 #[test]
308 fn test_genesis_invalid_covenant_id() {
309 let (tx, entries) = create_genesis_tx(
310 vec![None],
311 vec![OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 }],
312 false, );
314 let populated_tx = PopulatedTransaction::new(&tx, entries);
315 let result = CovenantsContext::from_tx(&populated_tx);
316 assert!(matches!(result, Err(CovenantsError::WrongGenesisCovenantId(0, _))));
317 }
318
319 #[test]
320 fn test_genesis_single_input_multiple_covenant_groups() {
321 let (tx, entries) = create_genesis_tx(
323 vec![None],
324 vec![
325 OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 }, OutputConfig { value: 100, authorizing_input: 0, covenant_group: 2 }, OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 }, ],
329 true,
330 );
331 let populated_tx = PopulatedTransaction::new(&tx, entries);
332 let ctx = CovenantsContext::from_tx(&populated_tx).unwrap();
333
334 assert!(ctx.input_ctxs.is_empty());
336 assert!(ctx.shared_ctxs.is_empty());
337 }
338
339 #[test]
340 fn test_genesis_multiple_inputs() {
341 let (tx, entries) = create_genesis_tx(
342 vec![None, None],
343 vec![
344 OutputConfig { value: 100, authorizing_input: 0, covenant_group: 1 },
345 OutputConfig { value: 100, authorizing_input: 1, covenant_group: 2 },
346 ],
347 true,
348 );
349 let populated_tx = PopulatedTransaction::new(&tx, entries);
350 let ctx = CovenantsContext::from_tx(&populated_tx).unwrap();
351
352 assert!(ctx.input_ctxs.is_empty());
354 assert!(ctx.shared_ctxs.is_empty());
355 }
356
357 #[test]
358 fn test_continuation_with_genesis() {
359 let (tx, entries) = create_genesis_tx(
365 vec![Some(42)],
366 vec![
367 OutputConfig { value: 100, authorizing_input: 0, covenant_group: 42 }, OutputConfig { value: 100, authorizing_input: 0, covenant_group: 100 }, OutputConfig { value: 100, authorizing_input: 0, covenant_group: 200 }, OutputConfig { value: 100, authorizing_input: 0, covenant_group: 100 }, ],
372 true,
373 );
374 let populated_tx = PopulatedTransaction::new(&tx, entries);
375 let actual_ctx = CovenantsContext::from_tx(&populated_tx).unwrap();
376
377 let covenant_42: Hash = 42u64.into();
380 let expected_ctx = CovenantsContext {
381 input_ctxs: HashMap::from_iter([(0, CovenantInputContext { auth_outputs: vec![0] })]),
382 shared_ctxs: HashMap::from_iter([(
383 covenant_42,
384 CovenantSharedContext { input_indices: vec![0], output_indices: vec![0] },
385 )]),
386 };
387
388 assert_eq!(actual_ctx, expected_ctx);
389 }
390
391 #[test]
392 fn test_authorizing_input_out_of_bounds() {
393 let input = TransactionInput::new(TransactionOutpoint::new(1u64.into(), 0), vec![], 0, 0);
395 let utxo = UtxoEntry {
396 amount: 1000,
397 script_public_key: Default::default(),
398 block_daa_score: 0,
399 is_coinbase: false,
400 covenant_id: None,
401 };
402
403 let output = TransactionOutput {
404 value: 100,
405 script_public_key: Default::default(),
406 covenant: Some(CovenantBinding {
407 covenant_id: 1u64.into(),
408 authorizing_input: 1, }),
410 };
411
412 let mut tx = Transaction::new(0, vec![input], vec![output], 0, SubnetworkId::default(), 0, vec![]);
413 tx.finalize();
414
415 let populated_tx = PopulatedTransaction::new(&tx, vec![utxo]);
416 let result = CovenantsContext::from_tx(&populated_tx);
417 assert!(matches!(result, Err(CovenantsError::AuthInputOutOfBounds(0, 1))));
418 }
419
420 #[test]
421 fn test_no_covenant_outputs() {
422 let input = TransactionInput::new(TransactionOutpoint::new(1u64.into(), 0), vec![], 0, 0);
424 let utxo = UtxoEntry {
425 amount: 1000,
426 script_public_key: Default::default(),
427 block_daa_score: 0,
428 is_coinbase: false,
429 covenant_id: Some(42u64.into()),
430 };
431
432 let output1 = TransactionOutput::new(100, Default::default());
434 let output2 = TransactionOutput::new(200, Default::default());
435
436 let mut tx = Transaction::new(0, vec![input], vec![output1, output2], 0, SubnetworkId::default(), 0, vec![]);
437 tx.finalize();
438
439 let populated_tx = PopulatedTransaction::new(&tx, vec![utxo]);
440 let actual_ctx = CovenantsContext::from_tx(&populated_tx).unwrap();
441
442 let covenant_42: Hash = 42u64.into();
445 let expected_ctx = CovenantsContext {
446 input_ctxs: HashMap::new(),
447 shared_ctxs: HashMap::from_iter([(covenant_42, CovenantSharedContext { input_indices: vec![0], output_indices: vec![] })]),
448 };
449
450 assert_eq!(actual_ctx, expected_ctx);
451 }
452}