77pub mod rbac;
88pub mod types;
99pub mod utils;
10- pub ( crate ) mod validation;
1110pub mod x509_chunks;
1211
13- use cardano_blockchain_types:: hashes:: { Blake2b256Hash , BLAKE_2B256_SIZE } ;
12+ pub ( crate ) mod validation;
13+
14+ use anyhow:: anyhow;
15+ use cardano_blockchain_types:: {
16+ hashes:: { Blake2b256Hash , BLAKE_2B256_SIZE } ,
17+ MultiEraBlock , Slot , TxnIndex ,
18+ } ;
1419use catalyst_types:: problem_report:: ProblemReport ;
1520use minicbor:: {
1621 decode:: { self } ,
1722 Decode , Decoder ,
1823} ;
19- use pallas:: ledger:: traverse:: MultiEraTx ;
24+ use pallas:: { codec :: utils :: Nullable , ledger:: traverse:: MultiEraTx } ;
2025use strum_macros:: FromRepr ;
26+ use tracing:: warn;
2127use uuid:: Uuid ;
2228use validation:: {
2329 validate_aux, validate_payment_key, validate_role_singing_key, validate_stake_public_key,
@@ -26,10 +32,13 @@ use validation::{
2632
2733use super :: transaction:: witness:: TxWitness ;
2834use crate :: {
29- cardano:: cip509:: {
30- rbac:: { Cip509RbacMetadata , RoleData , RoleNumber } ,
31- types:: { TxInputHash , ValidationSignature } ,
32- x509_chunks:: X509Chunks ,
35+ cardano:: {
36+ cip509:: {
37+ rbac:: { Cip509RbacMetadata , RoleData , RoleNumber } ,
38+ types:: { TxInputHash , ValidationSignature } ,
39+ x509_chunks:: X509Chunks ,
40+ } ,
41+ transaction:: raw_aux_data:: RawAuxData ,
3342 } ,
3443 utils:: {
3544 decode_helper:: {
@@ -49,7 +58,7 @@ pub const LABEL: u64 = 509;
4958/// more details.
5059///
5160/// [this document]: https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md
52- #[ derive( Debug , PartialEq , Clone , Default ) ]
61+ #[ derive( Debug , Clone ) ]
5362pub struct Cip509 {
5463 /// A registration purpose (`UUIDv4`).
5564 ///
@@ -65,8 +74,18 @@ pub struct Cip509 {
6574 ///
6675 /// This field encoded in chunks. See [`X509Chunks`] for more details.
6776 metadata : Option < Cip509RbacMetadata > ,
68- /// Validation signature.
77+ /// A validation signature.
6978 validation_signature : Option < ValidationSignature > ,
79+ /// A report potentially containing all the issues occurred during `Cip509` decoding
80+ /// and validation.
81+ ///
82+ /// The data located in `Cip509` is only considered valid if
83+ /// `ProblemReport::is_problematic()` returns false.
84+ report : ProblemReport ,
85+ /// A slot identifying the block that this `Cip509` was extracted from.
86+ slot : Slot ,
87+ /// A transaction index.
88+ transaction_index : TxnIndex ,
7089}
7190
7291/// Validation value for CIP509 metadatum.
@@ -110,8 +129,105 @@ enum Cip509IntIdentifier {
110129}
111130
112131impl Cip509 {
113- // TODO: FIXME: Replace validation with construction from `MultiEraBlock` and `TxnIndex`.
114- // (https://github.com/input-output-hk/catalyst-libs/pull/127#discussion_r1901549418)
132+ /// Returns a `Cip509` instance if it is present in the given transaction, otherwise
133+ /// `None` is returned.
134+ ///
135+ /// # Errors
136+ ///
137+ /// An error is only returned if the data is completely corrupted. In all other cases
138+ /// the `Cip509` structure contains fully or partially decoded data.
139+ pub fn new ( block : & MultiEraBlock , index : TxnIndex ) -> Result < Option < Self > , anyhow:: Error > {
140+ let block = block. decode ( ) ;
141+ let transactions = block. txs ( ) ;
142+ let transaction = transactions. get ( usize:: from ( index) ) . ok_or_else ( || {
143+ anyhow ! (
144+ "Invalid transaction index {index:?}, transactions count = {}" ,
145+ block. tx_count( )
146+ )
147+ } ) ?;
148+
149+ let MultiEraTx :: Conway ( transaction) = transaction else {
150+ return Err ( anyhow ! ( "Unsupported era: {}" , transaction. era( ) ) ) ;
151+ } ;
152+
153+ let auxiliary_data = match & transaction. auxiliary_data {
154+ Nullable :: Some ( v) => v. raw_cbor ( ) ,
155+ _ => return Ok ( None ) ,
156+ } ;
157+ let auxiliary_data = RawAuxData :: new ( auxiliary_data) ;
158+ let Some ( metadata) = auxiliary_data. get_metadata ( 509 ) else {
159+ return Ok ( None ) ;
160+ } ;
161+
162+ let mut decoder = Decoder :: new ( metadata. as_slice ( ) ) ;
163+ let mut report = ProblemReport :: new ( "Decoding and validating Cip509" ) ;
164+ let mut cip509 = match Cip509 :: decode ( & mut decoder, & mut report) {
165+ Ok ( v) => v,
166+ Err ( e) => {
167+ report. other ( & format ! ( "{e:?}" ) , "Failed to decode Cip509" ) ;
168+ return Ok ( Some ( Self :: with_slot_and_index (
169+ report,
170+ block. slot ( ) . into ( ) ,
171+ index,
172+ ) ) ) ;
173+ } ,
174+ } ;
175+ cip509. slot = block. slot ( ) . into ( ) ;
176+ cip509. transaction_index = index;
177+
178+ // After this point the decoding is finished and the structure shouldn't be modified
179+ // except of populating the problem report during validation.
180+ let cip509 = cip509;
181+
182+ // TODO: FIXME: Validation!
183+ todo ! ( ) ;
184+
185+ Ok ( Some ( cip509) )
186+ }
187+
188+ /// Returns a list of Cip509 instances from all the transactions of the given block.
189+ pub fn from_block ( block : & MultiEraBlock ) -> Vec < Self > {
190+ let mut result = Vec :: new ( ) ;
191+
192+ let decoded_block = block. decode ( ) ;
193+ for index in 0 ..decoded_block. tx_count ( ) {
194+ let index = TxnIndex :: from_saturating ( index) ;
195+ match Self :: new ( block, index) {
196+ Ok ( Some ( v) ) => result. push ( v) ,
197+ // Normal situation: there is no Cip509 data in this transaction.
198+ Ok ( None ) => { } ,
199+ Err ( e) => {
200+ warn ! (
201+ "Unable to extract Cip509 from the {} block {index:?} transaction: {e:?}" ,
202+ decoded_block. slot( )
203+ ) ;
204+ } ,
205+ }
206+ }
207+
208+ result
209+ }
210+
211+ /// Creates an "empty" `Cip509` instance with all fields set to `None`. Non-optional
212+ /// fields set to dummy values that must be overwritten.
213+ fn with_report ( report : ProblemReport ) -> Self {
214+ Self :: with_slot_and_index ( report, 0 . into ( ) , TxnIndex :: from_saturating ( 0 ) )
215+ }
216+
217+ /// Creates an "empty" `Cip509` instance with all fields set to `None`. Should only be
218+ /// used internally.
219+ fn with_slot_and_index ( report : ProblemReport , slot : Slot , transaction_index : TxnIndex ) -> Self {
220+ Self {
221+ purpose : None ,
222+ txn_inputs_hash : None ,
223+ prv_tx_id : None ,
224+ metadata : None ,
225+ validation_signature : None ,
226+ report,
227+ slot,
228+ transaction_index,
229+ }
230+ }
115231
116232 /// Basic validation for CIP509
117233 /// The validation include the following:
@@ -173,50 +289,63 @@ impl Cip509 {
173289
174290 /// Returns all role numbers present in this `Cip509` instance.
175291 pub fn all_roles ( & self ) -> Vec < RoleNumber > {
176- self . metadata
177- . map ( |m| m . role_data . keys ( ) )
178- . iter ( )
179- . flatten ( )
180- . collect ( )
292+ if let Some ( metadata ) = & self . metadata {
293+ metadata . role_data . keys ( ) . cloned ( ) . collect ( )
294+ } else {
295+ Vec :: new ( )
296+ }
181297 }
182298
183299 /// Returns a role data for the given role if it is present.
184300 pub fn role_data ( & self , role : RoleNumber ) -> Option < & RoleData > {
185- self . metadata . and_then ( |m| m. role_data . get ( & role) )
301+ self . metadata . as_ref ( ) . and_then ( |m| m. role_data . get ( & role) )
186302 }
187303
188- // TODO: FIXME: Consume fields in the registration chain, use methods everywhere else.
189- // /// Returns a registration purpose.
190- // #[must_use]
191- // pub fn purpose(&self) -> Option<Uuid> {
192- // self.purpose
193- // }
194- //
195- // // TODO: FIXME:
196- // // txn_inputs_hash?
197- //
198- // /// Returns an identifier of the previous transaction.
199- // #[must_use]
200- // pub fn previous_transaction(&self) -> Option<&Blake2b256Hash> {
201- // self.prv_tx_id.as_ref()
202- // }
203- //
204- // /// Returns CIP509 metadata.
205- // #[must_use]
206- // pub fn metadata(&self) -> Option<&Cip509RbacMetadata> {
207- // self.metadata.as_ref()
208- // }
209- //
210- // // TODO: FIXME:
211- // // validation_signature?
304+ /// Returns a hash of the previous transaction.
305+ pub fn previous_transaction ( & self ) -> Option < Blake2b256Hash > {
306+ self . prv_tx_id
307+ }
308+
309+ /// Returns a problem report
310+ pub fn report ( & self ) -> & ProblemReport {
311+ & self . report
312+ }
313+
314+ /// Returns a slot and a transaction index where this data is originating from.
315+ pub fn origin ( & self ) -> ( Slot , TxnIndex ) {
316+ ( self . slot , self . transaction_index )
317+ }
318+
319+ /// Returns `Cip509` fields consuming the structure if it was successfully decoded and
320+ /// validated otherwise return the problem report that contains all the encountered
321+ /// issues.
322+ pub fn try_consume (
323+ self ,
324+ ) -> Result < ( Uuid , TxInputHash , Cip509RbacMetadata , ValidationSignature ) , ProblemReport > {
325+ match (
326+ self . purpose ,
327+ self . txn_inputs_hash ,
328+ self . metadata ,
329+ self . validation_signature ,
330+ ) {
331+ ( Some ( purpose) , Some ( txn_inputs_hash) , Some ( metadata) , Some ( validation_signature) )
332+ if !self . report . is_problematic ( ) =>
333+ {
334+ Ok ( ( purpose, txn_inputs_hash, metadata, validation_signature) )
335+ } ,
336+
337+ _ => Err ( self . report ) ,
338+ }
339+ }
212340}
213341
214342impl Decode < ' _ , ProblemReport > for Cip509 {
215343 fn decode ( d : & mut Decoder , report : & mut ProblemReport ) -> Result < Self , decode:: Error > {
216344 let context = "Cip509" ;
217345 let map_len = decode_map_len ( d, context) ?;
218346
219- let mut result = Self :: default ( ) ;
347+ let mut result = Self :: with_report ( report. clone ( ) ) ;
348+
220349 let mut found_keys = Vec :: new ( ) ;
221350 let mut is_metadata_found = false ;
222351
0 commit comments