• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / catalyst-libs / 17376694348

01 Sep 2025 11:48AM UTC coverage: 67.994% (+0.1%) from 67.899%
17376694348

Pull #507

github

web-flow
Merge c49eaa49b into c20638587
Pull Request #507: fix(rust/rbac-registration): Validate witness for stake address and payment address URIs in certificate

59 of 81 new or added lines in 4 files covered. (72.84%)

3 existing lines in 2 files now uncovered.

13197 of 19409 relevant lines covered (67.99%)

3038.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.36
/rust/rbac-registration/src/cardano/cip509/cip509.rs
1
//! Cardano Improvement Proposal 509 (CIP-509) metadata module.
2
//! Doc Reference: <https://github.com/input-output-hk/catalyst-CIPs/tree/x509-envelope-metadata/CIP-XXXX>
3
//! CDDL Reference: <https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/x509-envelope.cddl>
4

5
use std::{
6
    borrow::Cow,
7
    collections::{HashMap, HashSet},
8
};
9

10
use anyhow::{anyhow, Context};
11
use cardano_blockchain_types::{
12
    hashes::{Blake2b256Hash, TransactionId, BLAKE_2B256_SIZE},
13
    pallas_addresses::{Address, ShelleyAddress},
14
    pallas_primitives::{conway, Nullable},
15
    pallas_traverse::MultiEraTx,
16
    MetadatumLabel, MultiEraBlock, TxnIndex,
17
};
18
use catalyst_types::{
19
    catalyst_id::{role_index::RoleId, CatalystId},
20
    cbor_utils::{report_duplicated_key, report_missing_keys},
21
    problem_report::ProblemReport,
22
    uuid::UuidV4,
23
};
24
use cbork_utils::decode_helper::{decode_bytes, decode_helper, decode_map_len};
25
use minicbor::{
26
    decode::{self},
27
    Decode, Decoder,
28
};
29
use strum_macros::FromRepr;
30
use tracing::warn;
31
use uuid::Uuid;
32

33
use crate::cardano::cip509::{
34
    decode_context::DecodeContext,
35
    rbac::Cip509RbacMetadata,
36
    types::{PaymentHistory, TxInputHash, ValidationSignature},
37
    utils::Cip0134UriSet,
38
    validation::{
39
        validate_aux, validate_cert_addrs, validate_role_data, validate_self_sign_cert,
40
        validate_txn_inputs_hash,
41
    },
42
    x509_chunks::X509Chunks,
43
    Payment, PointTxnIdx, RoleData,
44
};
45

46
/// A x509 metadata envelope.
47
///
48
/// The envelope is required to prevent replayability attacks. See [this document] for
49
/// more details.
50
///
51
/// [this document]: https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md
52
#[derive(Debug, Clone)]
53
#[allow(clippy::module_name_repetitions)]
54
pub struct Cip509 {
55
    /// A registration purpose (`UUIDv4`).
56
    ///
57
    /// The purpose is defined by the consuming dApp.
58
    purpose: Option<UuidV4>,
59
    /// Transaction inputs hash.
60
    txn_inputs_hash: Option<TxInputHash>,
61
    /// An optional hash of the previous transaction.
62
    ///
63
    /// The hash must always be present except for the first registration transaction.
64
    prv_tx_id: Option<TransactionId>,
65
    /// Metadata.
66
    ///
67
    /// This field encoded in chunks. See [`X509Chunks`] for more details.
68
    metadata: Option<Cip509RbacMetadata>,
69
    /// A validation signature.
70
    validation_signature: Option<ValidationSignature>,
71
    /// A payment history.
72
    ///
73
    /// The history is only tracked for the addresses that are passed to `Cip509`
74
    /// constructors.
75
    payment_history: PaymentHistory,
76
    /// A hash of the transaction from which this registration is extracted.
77
    txn_hash: TransactionId,
78
    /// A point (slot) and a transaction index identifying the block and the transaction
79
    /// that this `Cip509` was extracted from.
80
    origin: PointTxnIdx,
81
    /// A catalyst ID.
82
    ///
83
    /// This field is only present in role 0 registrations.
84
    catalyst_id: Option<CatalystId>,
85
    /// Raw aux data associated with the transaction that CIP509 is attached to,
86
    raw_aux_data: Vec<u8>,
87
    /// A report potentially containing all the issues occurred during `Cip509` decoding
88
    /// and validation.
89
    ///
90
    /// The data located in `Cip509` is only considered valid if
91
    /// `ProblemReport::is_problematic()` returns false.
92
    report: ProblemReport,
93
}
94

95
/// Enum of CIP509 metadatum with its associated unsigned integer value.
96
#[allow(clippy::module_name_repetitions)]
97
#[derive(FromRepr, Debug, PartialEq, Copy, Clone)]
98
#[repr(u8)]
99
enum Cip509IntIdentifier {
100
    /// Purpose.
101
    Purpose = 0,
102
    /// Transaction inputs hash.
103
    TxInputsHash = 1,
104
    /// Previous transaction ID.
105
    PreviousTxId = 2,
106
    /// Validation signature.
107
    ValidationSignature = 99,
108
}
109

110
impl Cip509 {
111
    /// Returns a `Cip509` instance if it is present in the given transaction, otherwise
112
    /// `None` is returned.
113
    ///
114
    /// # Errors
115
    ///
116
    /// An error is only returned if the data is completely corrupted. In all other cases
117
    /// the `Cip509` structure contains fully or partially decoded data.
118
    pub fn new(
18✔
119
        block: &MultiEraBlock,
18✔
120
        index: TxnIndex,
18✔
121
        track_payment_addresses: &[ShelleyAddress],
18✔
122
    ) -> Result<Option<Self>, anyhow::Error> {
18✔
123
        // Find the transaction and decode the relevant data.
124
        let txns = block.txs();
18✔
125
        let txn = txns.get(usize::from(index)).ok_or_else(|| {
18✔
126
            anyhow!(
×
127
                "Invalid transaction index {index:?}, transactions count = {}",
×
128
                txns.len()
×
129
            )
130
        })?;
×
131
        let MultiEraTx::Conway(txn) = txn else {
18✔
132
            return Ok(None);
×
133
        };
134
        let raw_aux_data = match &txn.auxiliary_data {
18✔
135
            Nullable::Some(v) => v.raw_cbor(),
14✔
136
            _ => return Ok(None),
4✔
137
        };
138

139
        let Some(metadata) = block.txn_metadata(index, MetadatumLabel::CIP509_RBAC) else {
14✔
140
            return Ok(None);
2✔
141
        };
142

143
        let mut decoder = Decoder::new(metadata.as_ref());
12✔
144
        let mut report = ProblemReport::new("Decoding and validating Cip509");
12✔
145
        let origin = PointTxnIdx::from_block(block, index);
12✔
146
        let payment_history = payment_history(txn, track_payment_addresses, &origin, &report);
12✔
147
        let mut decode_context = DecodeContext {
12✔
148
            origin,
12✔
149
            txn,
12✔
150
            payment_history,
12✔
151
            report: &mut report,
12✔
152
        };
12✔
153
        let mut cip509 =
12✔
154
            Cip509::decode(&mut decoder, &mut decode_context).context("Failed to decode Cip509")?;
12✔
155

156
        cip509.raw_aux_data = raw_aux_data.to_vec();
12✔
157

158
        // Perform the validation.
159

160
        // Chain root (no previous transaction ID) must contain Role 0
161
        if cip509.previous_transaction().is_none() && cip509.role_data(RoleId::Role0).is_none() {
12✔
162
            cip509
×
163
                .report
×
164
                .missing_field("Chain root role data", "Missing Role 0");
×
165
        }
12✔
166

167
        if let Some(txn_inputs_hash) = &cip509.txn_inputs_hash {
12✔
168
            validate_txn_inputs_hash(txn_inputs_hash, txn, &cip509.report);
12✔
169
        }
12✔
170
        validate_aux(
12✔
171
            raw_aux_data,
12✔
172
            txn.transaction_body.auxiliary_data_hash.as_ref(),
12✔
173
            &cip509.report,
12✔
174
        );
175
        if let Some(metadata) = &cip509.metadata {
12✔
176
            cip509.catalyst_id = validate_role_data(metadata, block.network(), &cip509.report);
12✔
177
            // General check for all roles, check whether the addresses in the certificate URIs are
12✔
178
            // witnessed in the transaction.
12✔
179
            validate_cert_addrs(txn, cip509.certificate_uris(), &report);
12✔
180
            validate_self_sign_cert(metadata, &report);
12✔
181
        }
12✔
182

183
        Ok(Some(cip509))
12✔
184
    }
18✔
185

186
    /// Returns a list of Cip509 instances from all the transactions of the given block.
187
    pub fn from_block(
6✔
188
        block: &MultiEraBlock,
6✔
189
        track_payment_addresses: &[ShelleyAddress],
6✔
190
    ) -> Vec<Self> {
6✔
191
        let mut result = Vec::new();
6✔
192

193
        for index in 0..block.decode().tx_count() {
12✔
194
            let index = TxnIndex::from(index);
12✔
195
            match Self::new(block, index, track_payment_addresses) {
12✔
196
                Ok(Some(v)) => result.push(v),
6✔
197
                // Normal situation: there is no Cip509 data in this transaction.
198
                Ok(None) => {},
6✔
199
                Err(e) => {
×
200
                    warn!(
×
201
                        "Unable to extract Cip509 from the {} block {index:?} transaction: {e:?}",
×
202
                        block.point()
×
203
                    );
204
                },
205
            }
206
        }
207

208
        result
6✔
209
    }
6✔
210

211
    /// Returns all role numbers present in this `Cip509` instance.
212
    #[must_use]
213
    pub fn all_roles(&self) -> Vec<RoleId> {
×
214
        if let Some(metadata) = &self.metadata {
×
215
            metadata.role_data.keys().copied().collect()
×
216
        } else {
217
            Vec::new()
×
218
        }
219
    }
×
220

221
    /// Returns a role data for the given role if it is present.
222
    #[must_use]
223
    pub fn role_data(
13✔
224
        &self,
13✔
225
        role: RoleId,
13✔
226
    ) -> Option<&RoleData> {
13✔
227
        self.metadata.as_ref().and_then(|m| m.role_data.get(&role))
13✔
228
    }
13✔
229

230
    /// Returns a purpose of this registration.
231
    #[must_use]
232
    pub fn purpose(&self) -> Option<UuidV4> {
×
233
        self.purpose
×
234
    }
×
235

236
    /// Returns a hash of the previous transaction.
237
    #[must_use]
238
    pub fn previous_transaction(&self) -> Option<TransactionId> {
22✔
239
        self.prv_tx_id
22✔
240
    }
22✔
241

242
    /// Returns a problem report.
243
    #[must_use]
244
    pub fn report(&self) -> &ProblemReport {
16✔
245
        &self.report
16✔
246
    }
16✔
247

248
    /// Returns a point and a transaction index where this data is originating from.
249
    #[must_use]
250
    pub fn origin(&self) -> &PointTxnIdx {
12✔
251
        &self.origin
12✔
252
    }
12✔
253

254
    /// Returns a hash of the transaction where this data is originating from.
255
    #[must_use]
256
    pub fn txn_hash(&self) -> TransactionId {
8✔
257
        self.txn_hash
8✔
258
    }
8✔
259

260
    /// Returns URIs contained in both x509 and c509 certificates of `Cip509` metadata.
261
    #[must_use]
262
    pub fn certificate_uris(&self) -> Option<&Cip0134UriSet> {
15✔
263
        self.metadata.as_ref().map(|m| &m.certificate_uris)
15✔
264
    }
15✔
265

266
    /// Returns a transaction inputs hash.
267
    #[must_use]
268
    pub fn txn_inputs_hash(&self) -> Option<&TxInputHash> {
×
269
        self.txn_inputs_hash.as_ref()
×
270
    }
×
271

272
    /// Returns a Catalyst ID of this registration if role 0 is present.
273
    #[must_use]
274
    pub fn catalyst_id(&self) -> Option<&CatalystId> {
1✔
275
        self.catalyst_id.as_ref()
1✔
276
    }
1✔
277

278
    /// Returns a list of addresses extracted from certificate URIs of a specific role.
279
    #[must_use]
NEW
280
    pub fn certificate_addresses(
×
NEW
281
        &self,
×
NEW
282
        role: usize,
×
NEW
283
    ) -> HashSet<Address> {
×
284
        self.metadata
×
285
            .as_ref()
×
NEW
286
            .map(|m| m.certificate_uris.role_addresses(role))
×
287
            .unwrap_or_default()
×
288
    }
×
289

290
    /// Return validation signature.
291
    #[must_use]
292
    pub fn validation_signature(&self) -> Option<&ValidationSignature> {
2✔
293
        self.validation_signature.as_ref()
2✔
294
    }
2✔
295

296
    /// Raw aux data associated with the transaction that CIP509 is attached to,
297
    #[must_use]
298
    pub fn raw_aux_data(&self) -> &[u8] {
2✔
299
        self.raw_aux_data.as_ref()
2✔
300
    }
2✔
301

302
    /// Returns a `Cip509` RBAC metadata.
303
    #[must_use]
304
    pub fn metadata(&self) -> Option<&Cip509RbacMetadata> {
1✔
305
        self.metadata.as_ref()
1✔
306
    }
1✔
307

308
    /// Returns `Cip509` fields consuming the structure if it was successfully decoded and
309
    /// validated otherwise return the problem report that contains all the encountered
310
    /// issues.
311
    ///
312
    /// # Errors
313
    ///
314
    /// - `Err(ProblemReport)`
315
    pub fn consume(self) -> Result<(UuidV4, Cip509RbacMetadata, PaymentHistory), ProblemReport> {
11✔
316
        match (
317
            self.purpose,
11✔
318
            self.txn_inputs_hash,
11✔
319
            self.metadata,
11✔
320
            self.validation_signature,
11✔
321
        ) {
322
            (Some(purpose), Some(_), Some(metadata), Some(_)) if !self.report.is_problematic() => {
11✔
323
                Ok((purpose, metadata, self.payment_history))
8✔
324
            },
325

326
            _ => Err(self.report),
3✔
327
        }
328
    }
11✔
329
}
330

331
impl Decode<'_, DecodeContext<'_, '_>> for Cip509 {
332
    fn decode(
12✔
333
        d: &mut Decoder,
12✔
334
        decode_context: &mut DecodeContext,
12✔
335
    ) -> Result<Self, decode::Error> {
12✔
336
        let context = "Decoding Cip509";
12✔
337

338
        // It is ok to return error here because we were unable to decode anything, but everywhere
339
        // below we should try to recover as much data as possible and not to return early.
340
        let map_len = decode_map_len(d, context)?;
12✔
341

342
        let mut purpose = None;
12✔
343
        let mut txn_inputs_hash = None;
12✔
344
        let mut prv_tx_id = None;
12✔
345
        let mut validation_signature = None;
12✔
346
        let mut metadata = None;
12✔
347

348
        let mut found_keys = Vec::new();
12✔
349
        let mut is_metadata_found = false;
12✔
350

351
        for index in 0..map_len {
53✔
352
            // We don't want to consume key here because it can be a part of chunked metadata that
353
            // is decoded below.
354
            let Ok(key) = d.probe().u8() else {
53✔
355
                decode_context.report.other(
×
356
                    &format!("Unable to decode map key ({index} index)"),
×
357
                    context,
×
358
                );
359
                break;
×
360
            };
361
            if let Some(key) = Cip509IntIdentifier::from_repr(key) {
53✔
362
                // Consume the key. This should never fail because we used `probe` above.
363
                let _: u8 = decode_helper(d, context, &mut ())?;
41✔
364

365
                if report_duplicated_key(&found_keys, &key, index, "Cip509", decode_context.report)
41✔
366
                {
367
                    continue;
×
368
                }
41✔
369
                found_keys.push(key);
41✔
370

371
                match key {
41✔
372
                    Cip509IntIdentifier::Purpose => {
373
                        match decode_purpose(d, context, decode_context.report) {
12✔
374
                            Ok(v) => purpose = v,
12✔
375
                            Err(()) => break,
×
376
                        }
377
                    },
378
                    Cip509IntIdentifier::TxInputsHash => {
379
                        match decode_input_hash(d, context, decode_context.report) {
12✔
380
                            Ok(v) => txn_inputs_hash = v,
12✔
381
                            Err(()) => break,
×
382
                        }
383
                    },
384
                    Cip509IntIdentifier::PreviousTxId => {
385
                        match decode_previous_transaction_id(d, context, decode_context.report) {
5✔
386
                            Ok(v) => prv_tx_id = v,
5✔
387
                            Err(()) => break,
×
388
                        }
389
                    },
390
                    Cip509IntIdentifier::ValidationSignature => {
391
                        match decode_validation_signature(d, context, decode_context.report) {
12✔
392
                            Ok(v) => validation_signature = v,
12✔
393
                            Err(()) => break,
×
394
                        }
395
                    },
396
                }
397
            } else {
398
                // Handle the x509 chunks 10 11 12
399
                // Technically it is possible to store multiple copies (or different instances) of
400
                // metadata, but it isn't allowed. See this link for more details:
401
                // https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md#keys-10-11-or-12---x509-chunked-data
402
                if is_metadata_found {
12✔
403
                    decode_context.report.duplicate_field(
×
404
                        "metadata",
×
405
                        "Only one instance of the chunked metadata should be present",
×
406
                        context,
×
407
                    );
408
                    continue;
×
409
                }
12✔
410
                is_metadata_found = true;
12✔
411

412
                match X509Chunks::decode(d, decode_context) {
12✔
413
                    Ok(chunks) => metadata = chunks.into(),
12✔
414
                    Err(e) => {
×
415
                        decode_context.report.other(
×
416
                            &format!("Unable to decode metadata from chunks: {e:?}"),
×
417
                            context,
×
418
                        );
419
                        break;
×
420
                    },
421
                }
422
            }
423
        }
424

425
        let required_keys = [
12✔
426
            Cip509IntIdentifier::Purpose,
12✔
427
            Cip509IntIdentifier::TxInputsHash,
12✔
428
            Cip509IntIdentifier::ValidationSignature,
12✔
429
        ];
12✔
430
        report_missing_keys(&found_keys, &required_keys, context, decode_context.report);
12✔
431
        if !is_metadata_found {
12✔
432
            decode_context
×
433
                .report
×
434
                .missing_field("metadata (10, 11 or 12 chunks)", context);
×
435
        }
12✔
436

437
        let txn_hash = Blake2b256Hash::from(
12✔
438
            MultiEraTx::Conway(Box::new(Cow::Borrowed(decode_context.txn))).hash(),
12✔
439
        )
440
        .into();
12✔
441
        Ok(Self {
12✔
442
            purpose,
12✔
443
            txn_inputs_hash,
12✔
444
            prv_tx_id,
12✔
445
            metadata,
12✔
446
            validation_signature,
12✔
447
            payment_history: HashMap::new(),
12✔
448
            txn_hash,
12✔
449
            origin: decode_context.origin.clone(),
12✔
450
            catalyst_id: None,
12✔
451
            raw_aux_data: Vec::new(),
12✔
452
            report: decode_context.report.clone(),
12✔
453
        })
12✔
454
    }
12✔
455
}
456

457
/// Records the payment history for the given set of addresses.
458
fn payment_history(
12✔
459
    txn: &conway::Tx,
12✔
460
    track_payment_addresses: &[ShelleyAddress],
12✔
461
    origin: &PointTxnIdx,
12✔
462
    report: &ProblemReport,
12✔
463
) -> HashMap<ShelleyAddress, Vec<Payment>> {
12✔
464
    let hash = MultiEraTx::Conway(Box::new(Cow::Borrowed(txn))).hash();
12✔
465
    let context = format!("Populating payment history for Cip509, transaction = {hash}");
12✔
466

467
    let mut result: HashMap<_, _> = track_payment_addresses
12✔
468
        .iter()
12✔
469
        .cloned()
12✔
470
        .map(|a| (a, Vec::new()))
12✔
471
        .collect();
12✔
472

473
    for (index, output) in txn.transaction_body.outputs.iter().enumerate() {
12✔
474
        let conway::TransactionOutput::PostAlonzo(output) = output else {
12✔
475
            continue;
1✔
476
        };
477

478
        let address = match Address::from_bytes(&output.address) {
11✔
479
            Ok(Address::Shelley(a)) => a,
11✔
480
            Ok(_) => {
481
                continue;
×
482
            },
483
            Err(e) => {
×
484
                report.other(&format!("Invalid output address: {e:?}"), &context);
×
485
                continue;
×
486
            },
487
        };
488

489
        let index = match u16::try_from(index) {
11✔
490
            Ok(v) => v,
11✔
491
            Err(e) => {
×
492
                report.other(&format!("Invalid output index ({index}): {e:?}"), &context);
×
493
                continue;
×
494
            },
495
        };
496

497
        if let Some(history) = result.get_mut(&address) {
11✔
498
            history.push(Payment::new(
×
499
                origin.clone(),
×
500
                hash,
×
501
                index,
×
502
                output.value.clone(),
×
503
            ));
×
504
        }
11✔
505
    }
506

507
    result
12✔
508
}
12✔
509

510
/// Decodes purpose.
511
fn decode_purpose(
12✔
512
    d: &mut Decoder,
12✔
513
    context: &str,
12✔
514
    report: &ProblemReport,
12✔
515
) -> Result<Option<UuidV4>, ()> {
12✔
516
    let bytes = match decode_bytes(d, "Cip509 purpose") {
12✔
517
        Ok(v) => v,
12✔
518
        Err(e) => {
×
519
            report.other(&format!("Unable to decode purpose: {e:?}"), context);
×
520
            return Err(());
×
521
        },
522
    };
523

524
    let len = bytes.len();
12✔
525
    let Ok(uuid) = Uuid::try_from(bytes) else {
12✔
526
        report.invalid_value(
×
527
            "purpose",
×
528
            &format!("{len} bytes"),
×
529
            "must be 16 bytes long",
×
530
            context,
×
531
        );
532
        return Ok(None);
×
533
    };
534
    match UuidV4::try_from(uuid) {
12✔
535
        Ok(v) => Ok(Some(v)),
12✔
536
        Err(e) => {
×
537
            report.other(&format!("Invalid purpose UUID: {e:?}"), context);
×
538
            Ok(None)
×
539
        },
540
    }
541
}
12✔
542

543
/// Decodes input hash.
544
fn decode_input_hash(
12✔
545
    d: &mut Decoder,
12✔
546
    context: &str,
12✔
547
    report: &ProblemReport,
12✔
548
) -> Result<Option<TxInputHash>, ()> {
12✔
549
    let bytes = match decode_bytes(d, "Cip509 txn inputs hash") {
12✔
550
        Ok(v) => v,
12✔
551
        Err(e) => {
×
552
            report.other(
×
553
                &format!("Unable to decode transaction inputs hash: {e:?}"),
×
554
                context,
×
555
            );
556
            return Err(());
×
557
        },
558
    };
559

560
    let len = bytes.len();
12✔
561
    if let Ok(v) = TxInputHash::try_from(bytes.as_slice()) {
12✔
562
        Ok(Some(v))
12✔
563
    } else {
564
        report.invalid_value(
×
565
            "transaction inputs hash",
×
566
            &format!("{len} bytes"),
×
567
            "must be 16 bytes long",
×
568
            context,
×
569
        );
570
        Ok(None)
×
571
    }
572
}
12✔
573

574
/// Decodes previous transaction id.
575
fn decode_previous_transaction_id(
5✔
576
    d: &mut Decoder,
5✔
577
    context: &str,
5✔
578
    report: &ProblemReport,
5✔
579
) -> Result<Option<TransactionId>, ()> {
5✔
580
    let bytes = match decode_bytes(d, "Cip509 previous transaction id") {
5✔
581
        Ok(v) => v,
5✔
582
        Err(e) => {
×
583
            report.other(
×
584
                &format!("Unable to decode previous transaction id: {e:?}"),
×
585
                context,
×
586
            );
587
            return Err(());
×
588
        },
589
    };
590

591
    let len = bytes.len();
5✔
592
    if let Ok(v) = Blake2b256Hash::try_from(bytes) {
5✔
593
        Ok(Some(v.into()))
5✔
594
    } else {
595
        report.invalid_value(
×
596
            "previous transaction hash",
×
597
            &format!("{len} bytes"),
×
598
            &format!("must be {BLAKE_2B256_SIZE} bytes long"),
×
599
            context,
×
600
        );
601
        Ok(None)
×
602
    }
603
}
5✔
604

605
/// Decodes validation signature.
606
fn decode_validation_signature(
12✔
607
    d: &mut Decoder,
12✔
608
    context: &str,
12✔
609
    report: &ProblemReport,
12✔
610
) -> Result<Option<ValidationSignature>, ()> {
12✔
611
    let bytes = match decode_bytes(d, "Cip509 validation signature") {
12✔
612
        Ok(v) => v,
12✔
613
        Err(e) => {
×
614
            report.other(
×
615
                &format!("Unable to decode validation signature: {e:?}"),
×
616
                context,
×
617
            );
618
            return Err(());
×
619
        },
620
    };
621

622
    let len = bytes.len();
12✔
623
    if let Ok(v) = ValidationSignature::try_from(bytes) {
12✔
624
        Ok(Some(v))
12✔
625
    } else {
626
        report.invalid_value(
×
627
            "validation signature",
×
628
            &format!("{len} bytes"),
×
629
            "must be at least 1 byte and at most 64 bytes long",
×
630
            context,
×
631
        );
632
        Ok(None)
×
633
    }
634
}
12✔
635

636
#[cfg(test)]
637
mod tests {
638
    use super::*;
639
    use crate::utils::test;
640

641
    #[test]
642
    fn new() {
1✔
643
        let data = test::block_1();
1✔
644
        let res = Cip509::new(&data.block, data.txn_index, &[])
1✔
645
            .expect("Failed to get Cip509")
1✔
646
            .expect("There must be Cip509 in block");
1✔
647
        assert!(!res.report.is_problematic(), "{:?}", res.report);
1✔
648
        data.assert_valid(&res);
1✔
649
    }
1✔
650

651
    #[test]
652
    fn from_block() {
1✔
653
        let data = test::block_1();
1✔
654
        let res = Cip509::from_block(&data.block, &[]);
1✔
655
        assert_eq!(1, res.len());
1✔
656
        let cip509 = res.first().unwrap();
1✔
657
        assert!(!cip509.report.is_problematic(), "{:?}", cip509.report);
1✔
658
        data.assert_valid(cip509);
1✔
659
    }
1✔
660
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc