diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index 160d4dfa23..3895d8971b 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -9,7 +9,11 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::bail; use c509_certificate::c509::C509; use pallas::{ - codec::utils::Bytes, crypto::hash::Hash, ledger::traverse::MultiEraTx, + crypto::hash::Hash, + ledger::{ + addresses::{Address, ShelleyAddress, ShelleyPaymentPart}, + traverse::MultiEraTx, + }, network::miniprotocols::Point, }; use payment_history::PaymentHistory; @@ -51,8 +55,8 @@ impl RegistrationChain { /// /// Returns an error if data is invalid pub fn new( - &self, point: Point, tracking_payment_keys: Vec, tx_idx: usize, - txn: &MultiEraTx, cip509: Cip509, + point: Point, tracking_payment_keys: &[ShelleyAddress], tx_idx: usize, txn: &MultiEraTx, + cip509: Cip509, ) -> anyhow::Result { let inner = RegistrationChainInner::new(cip509, tracking_payment_keys, point, tx_idx, txn)?; @@ -124,16 +128,10 @@ impl RegistrationChain { &self.inner.role_data } - /// Get the list of payment keys to track. + /// Get the map of tracked payment keys to its history. #[must_use] - pub fn tracking_payment_keys(&self) -> &Vec { - &self.inner.tracking_payment_keys - } - - /// Get the map of payment key to its history. - #[must_use] - pub fn payment_history(&self) -> &HashMap> { - &self.inner.payment_history + pub fn tracking_payment_history(&self) -> &HashMap> { + &self.inner.tracking_payment_history } } @@ -158,10 +156,8 @@ struct RegistrationChainInner { // Role /// Map of role number to point, transaction index, and role data. role_data: HashMap, - /// List of payment keys to track. - tracking_payment_keys: Arc>, - /// Map of payment key to its history. - payment_history: HashMap>, + /// Map of tracked payment key to its history. + tracking_payment_history: HashMap>, } impl RegistrationChainInner { @@ -179,7 +175,7 @@ impl RegistrationChainInner { /// /// Returns an error if data is invalid fn new( - cip509: Cip509, tracking_payment_keys: Vec, point: Point, tx_idx: usize, + cip509: Cip509, tracking_payment_keys: &[ShelleyAddress], point: Point, tx_idx: usize, txn: &MultiEraTx, ) -> anyhow::Result { // Should be chain root, return immediately if not @@ -207,12 +203,13 @@ impl RegistrationChainInner { let revocations = revocations_list(registration.revocation_list, &point_tx_idx); let role_data_map = chain_root_role_data(registration.role_set, txn, &point_tx_idx)?; - let mut payment_history = HashMap::new(); - for tracking_key in &tracking_payment_keys { - // Keep record of payment history, the payment key that we want to track - let histories = update_payment_history(tracking_key, txn, &point_tx_idx)?; - payment_history.insert(tracking_key.clone(), histories); + let mut tracking_payment_history = HashMap::new(); + // Create a payment history for each tracking payment key + for tracking_key in tracking_payment_keys { + tracking_payment_history.insert(tracking_key.clone(), Vec::new()); } + // Keep record of payment history, the payment key that we want to track + update_tracking_payment_history(&mut tracking_payment_history, txn, &point_tx_idx)?; Ok(Self { purpose, @@ -222,8 +219,7 @@ impl RegistrationChainInner { simple_keys: public_key_map, revocations, role_data: role_data_map, - tracking_payment_keys: Arc::new(tracking_payment_keys), - payment_history, + tracking_payment_history, }) } @@ -280,16 +276,11 @@ impl RegistrationChainInner { update_role_data(&mut new_inner, registration.role_set, txn, &point_tx_idx)?; - for tracking_key in self.tracking_payment_keys.iter() { - let histories = update_payment_history(tracking_key, txn, &point_tx_idx)?; - // If tracking payment key doesn't exist, insert an empty vector, - // then add the histories to the history vector - new_inner - .payment_history - .entry(tracking_key.clone()) - .or_default() - .extend(histories); - } + update_tracking_payment_history( + &mut new_inner.tracking_payment_history, + txn, + &point_tx_idx, + )?; Ok(new_inner) } @@ -448,7 +439,7 @@ fn chain_root_role_data( let encryption_key = role_data.role_encryption_key.clone(); // Get the payment key - let payment_key = get_payment_key_from_tx(txn, role_data.payment_key)?; + let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; // Map of role number to point and role data role_data_map.insert( @@ -496,7 +487,7 @@ fn update_role_data( } }, }; - let payment_key = get_payment_key_from_tx(txn, role_data.payment_key)?; + let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; // Map of role number to point and role data // Note that new role data will overwrite the old one @@ -517,10 +508,10 @@ fn update_role_data( Ok(()) } -/// Helper function for retrieving the payment key from the transaction. -fn get_payment_key_from_tx( +/// Helper function for retrieving the Shelley address from the transaction. +fn get_payment_addr_from_tx( txn: &MultiEraTx, payment_key_ref: Option, -) -> anyhow::Result { +) -> anyhow::Result> { // The index should exist since it pass the basic validation if let Some(key_ref) = payment_key_ref { if let MultiEraTx::Conway(tx) = txn { @@ -533,11 +524,13 @@ fn get_payment_key_from_tx( pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo( o, ) => { - let payment_key: Ed25519PublicKey = - o.address.clone().try_into().map_err(|_| { - anyhow::anyhow!("Failed to convert Vec to Ed25519PublicKey in payment key reference") - })?; - return Ok(payment_key); + let address = + Address::from_bytes(&o.address).map_err(|e| anyhow::anyhow!(e))?; + + if let Address::Shelley(addr) = address { + return Ok(Some(addr.payment().clone())); + } + bail!("Unsupported address type in payment key reference"); }, // Not support legacy form of transaction output pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(_) => { @@ -552,26 +545,34 @@ fn get_payment_key_from_tx( bail!("Unsupported payment key reference to transaction input"); } } - Ok(Ed25519PublicKey::default()) + Ok(None) } /// Update the payment history given the tracking payment keys. -fn update_payment_history( - tracking_key: &Ed25519PublicKey, txn: &MultiEraTx, point_tx_idx: &PointTxIdx, -) -> anyhow::Result> { - let mut payment_history = Vec::new(); +fn update_tracking_payment_history( + tracking_payment_history: &mut HashMap>, txn: &MultiEraTx, + point_tx_idx: &PointTxIdx, +) -> anyhow::Result<()> { if let MultiEraTx::Conway(tx) = txn { // Conway era -> Post alonzo tx output for (index, output) in tx.transaction_body.outputs.iter().enumerate() { match output { pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo(o) => { - let address_bytes: Bytes = tracking_key.clone().into(); - if address_bytes == o.address { + let address = + Address::from_bytes(&o.address).map_err(|e| anyhow::anyhow!(e))?; + let shelley_payment = if let Address::Shelley(addr) = address { + addr.clone() + } else { + bail!("Unsupported address type in update payment history"); + }; + // If the payment key from the output exist in the payment history, add the + // history + if let Some(vec) = tracking_payment_history.get_mut(&shelley_payment) { let output_index: u16 = index.try_into().map_err(|_| { anyhow::anyhow!("Cannot convert usize to u16 in update payment history") })?; - payment_history.push(PaymentHistory::new( + vec.push(PaymentHistory::new( point_tx_idx.clone(), txn.hash(), output_index, @@ -585,5 +586,100 @@ fn update_payment_history( } } } - Ok(payment_history) + Ok(()) +} + +#[cfg(test)] +mod test { + use minicbor::{Decode, Decoder}; + use pallas::{ledger::traverse::MultiEraTx, network::miniprotocols::Point}; + + use super::RegistrationChain; + use crate::cardano::{cip509::Cip509, transaction::raw_aux_data::RawAuxData}; + + fn cip_509_aux_data(tx: &MultiEraTx<'_>) -> Vec { + let raw_auxiliary_data = tx + .as_conway() + .unwrap() + .clone() + .auxiliary_data + .map(|aux| aux.raw_cbor()); + + let raw_cbor_data = match raw_auxiliary_data { + pallas::codec::utils::Nullable::Some(data) => Ok(data), + _ => Err("Auxiliary data not found"), + }; + + let auxiliary_data = RawAuxData::new(raw_cbor_data.expect("Failed to get raw cbor data")); + auxiliary_data + .get_metadata(509) + .expect("Failed to get metadata") + .to_vec() + } + + fn conway_1() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_1.block")) + .expect("Failed to decode hex block.") + } + + fn conway_4() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_4.block")) + .expect("Failed to decode hex block.") + } + + #[test] + fn test_new_and_update_registration() { + let conway_block_data_1 = conway_1(); + let point_1 = Point::new( + 77_429_134, + hex::decode("62483f96613b4c48acd28de482eb735522ac180df61766bdb476a7bf83e7bb98") + .unwrap(), + ); + let multi_era_block_1 = + pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data_1) + .expect("Failed to decode MultiEraBlock"); + + let transactions_1 = multi_era_block_1.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx_1 = transactions_1 + .get(3) + .expect("Failed to get transaction index"); + + let aux_data_1 = cip_509_aux_data(tx_1); + let mut decoder = Decoder::new(aux_data_1.as_slice()); + let cip509_1 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + let tracking_payment_keys = vec![]; + + let registration_chain = + RegistrationChain::new(point_1.clone(), &tracking_payment_keys, 3, tx_1, cip509_1); + // Able to add chain root to the registration chain + assert!(registration_chain.is_ok()); + + let conway_block_data_4 = conway_4(); + let point_4 = Point::new( + 77_436_369, + hex::decode("b174fc697126f05046b847d47e60d66cbedaf25240027f9c07f27150889aac24") + .unwrap(), + ); + + let multi_era_block_4 = + pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data_4) + .expect("Failed to decode MultiEraBlock"); + + let transactions_4 = multi_era_block_4.txs(); + // Second transaction of this test data contains the CIP509 auxiliary data + let tx = transactions_4 + .get(1) + .expect("Failed to get transaction index"); + + let aux_data_4 = cip_509_aux_data(tx); + let mut decoder = Decoder::new(aux_data_4.as_slice()); + let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + + // Update the registration chain + assert!(registration_chain + .unwrap() + .update(point_4.clone(), 1, tx, cip509) + .is_ok()); + } } diff --git a/rust/rbac-registration/src/registration/cardano/role_data.rs b/rust/rbac-registration/src/registration/cardano/role_data.rs index 684671415e..b6b4d947d7 100644 --- a/rust/rbac-registration/src/registration/cardano/role_data.rs +++ b/rust/rbac-registration/src/registration/cardano/role_data.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; -use crate::cardano::cip509::rbac::{pub_key::Ed25519PublicKey, role_data::KeyLocalRef}; +use pallas::ledger::addresses::ShelleyPaymentPart; + +use crate::cardano::cip509::rbac::role_data::KeyLocalRef; /// Role data #[derive(Clone)] @@ -12,7 +14,7 @@ pub struct RoleData { /// An encryption keys to the data within registration. encryption_ref: Option, /// A payment key where reward will be distributed to. - payment_key: Ed25519PublicKey, + payment_key: Option, /// Map of role extended data (10-99) to its data role_extended_data: HashMap>, } @@ -21,7 +23,7 @@ impl RoleData { /// Create an instance of role data. pub(crate) fn new( signing_key_ref: Option, encryption_ref: Option, - payment_key: Ed25519PublicKey, role_extended_data: HashMap>, + payment_key: Option, role_extended_data: HashMap>, ) -> Self { RoleData { signing_key_ref, @@ -45,7 +47,7 @@ impl RoleData { /// Get the payment key. #[must_use] - pub fn payment_key(&self) -> &Ed25519PublicKey { + pub fn payment_key(&self) -> &Option { &self.payment_key } diff --git a/rust/rbac-registration/src/test_data/cardano/conway_4.block b/rust/rbac-registration/src/test_data/cardano/conway_4.block new file mode 100644 index 0000000000..b16edd16df --- /dev/null +++ b/rust/rbac-registration/src/test_data/cardano/conway_4.block @@ -0,0 +1 @@ +820785828a1a002cf24d1a049d95d15820a82d38572884fc69c5b1105dd190651e4fc5c821878a1a166b6bca897983b83d58206f281ba212f118ac018aaeb805dda9f6fd54fe28cecafe7bec1347509510797f5820870dca606397b85d2e4ee1e46bdb9b784a092a1ec25cb8146cf4046456ff861c825840c085ebef69c078f6fcfd53a2eb2e209b6bd1a517390c8971f079fe55a4b30c82641385a618acf269bceb2d3c83a4ad6addc8901cc3bb359da7875c672ed5a6a55850758500c16a229759d672f0c5aea206fe924238915de34dfdf54413d438a27c4bdb453a080d1fe6ce0f3427452d20aeb1d1907d034899d19021e951a1badeef442de3f4f1bd4e9a4ce5924870f068a4021911c65820c8fc1a8e30b35038644284be081128c4a7ad8b3c39edbfeea871084b9b3566cb845820b7e792177f506de2a34d1c4099f7a1687ff7af54caa093d85c5755e709f757fd0d190241584095a171db3d0e3808bca5e84970ead6b0a736e1d5a2b5fc8c169c8fab7bc1a5d3d60b0b582fbbc8c23d43b3301c4b3eede3ac5438bddfebe1cc8e5a8d71a94802820a005901c05ae6633913fc08585a1ddf905f10b2cc1134d914f9f656b58e72cad542e53a90ce0cc9c6f0e819f7243431a5416cbc1a3fa67d8ab8c547a09f78c86c20073208dc3b328c9d70a24a912aa4e170b9d6fbe8e55c03b52dc296435c86a358bc3f20e89f4c92748dff2ed24a71b8377b4683851a52cfd023dccaa92239d9e989fe8d1d360529c15417937f60530a085ee82dd4ed5f1bc46d0bcf2ab68d4729063a7a5c5074cdb289655f23954f7eebfd418e3a133bb53cfa0bd5ff544fc9d0599640b46619d3bfc5d9782c98ee86fcaa1efe8503980fd9219db77c3003ed2cee13d85f510f9a3381d442f5314a91c52ccf01cc796b9fb7ea14d046a00c3f96e6ee2ec28896f72808952cda9aec2feb690ecb59c1a729816f602a58b06fbab79977a9e96ff7eed2dd262eb86cee8f35d4bad55be2919413ff5cd8fbab3acc25f608630fa7796debcdae0239aafb45e949053af2523bb112b5f6be5fc4ece0ac55f59783466565dad12d42b76fb3ad45112c25d0f4c0880b88ad0ff87f207538c5c7cb96690633a72de47fd36980846cfba4865e324b7ccf1ca80382a4eedc72caf1a0d828049fbd7b3906877b89980bd2f0ca5ffc229e667d56fd11c1dd6e3c0ba7ed82ab0082825820276df9bb9d245ea87e6def1a995ad919edacc907c6cbf028e0e552b1c53c10df0182582062e4dfcb06b8401ea1c405d6290c6aede934c7a6cd31ae000c2a5dd34961af5b000182a300581d70bdc22da682cd9aceed5fd48914789fc98c94abc79fed8b40cb8c431401821a001e8480a1581cc13ddf298a5d25aff2933695987912b4f1748bdf0df8e4b5d85f2360a14e50524550524f445f4f5241434c4501028201d81856d8799fd8799f1a0bf85ea8ff1b000001938742fe98ff82581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011587887e021a0006a164031a049d9933081a049d922c0b5820e33c25b2f65204adbf1a8f29ba4c7157c35f9479346e2d6717e23d6786e8b6340d81825820276df9bb9d245ea87e6def1a995ad919edacc907c6cbf028e0e552b1c53c10df010e81581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d20f001082581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011541dea2111a004c4b40a600818258206695b9cac9230af5c8ee50747b1ca3c78a854d181c7e5c6c371de01b80274d31000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f93e7f8021a000304fd07582091e3a1050ba7f37e9d94938ec94a9d187f24b363374c109e3fcc44fedb5de8b70e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0082a30081825820ef603099b2579d5ca273dce1f1257c11f054664ba972fd61c008b58a1b2325c1584049e2d6c6a948a7d1398f200213a65cd15ac2dd491e903892aaca6ad487717f3cad6c66e201dd8fd74f69898f77aa3de953e83df61f2822c01a30eb992f58f0060681590bd7590bd401000033232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323233027222232325335333006533357346074607e0022646605a60586ae84004dd69aba135744002607c00206e64a666ae68c0ecc1000044c8c8c848cc00400c008c0b8d5d09aba2002375a6ae84004c0fc0040e0dd50022999ab9a303a303f002132321233001003002357426ae88c0fc00cd5d0981f00101b8890008a99ab9c490103505435001637540086ea8004cc09c8888c8d4014888d40188c88c94cd54cd4ccd54c0d40e0c0a80a4c0ac01ccc0b08004d40048888888888880100e44cd5ce24812d546865207472616e73616374696f6e206973206e6f74207369676e6564206279206f7261636c65206f776e657200030132533553355335333553036039302b02a253335302135001202103e215333573466ebcc118cd5d0182319aba0375000a6ec4080cd5d01ba835500b03937620400022a66a666036446a0044466603e446a00444a666ae68c12c004400c4cc11848ccc00401c00c00800c00c0040c0cd54c0e80ec8d400488cc0e0008cd54c0f40f88d400488cc0ec008ccd4004dc0a4000e048cdc0800a400000266aa607407646a0024466070004666a002466aa607c07e46a0024466078004604600200244666038066004002466aa607c07e46a0024466078004604000200266602e05c666050400204e6a004404e0060784426a004444a66a0082a666ae68cdd78018238a999ab9a3375e00408e2a666ae68cdc4000a400006e2666ae68cdc4800a410125e80206a07006e06e44072064407e6044014074266ae712401024c310003103a1335738921156f757470757420646f6573206e6f74206d6174636800031153355333573466e2400d20001335738921165072696365206d75737420626520706f7369746976650003103a15335533535353301275a6a00407e06e44a666a00442a666ae68cdc419b8000100935500c03a0331333573466e24004d540300e80c40d00c80c84d4cc049d69a80101f911a801112999a99980980a980f19b8100900b0021533500113330120150030081333012015003008034133301201500300803003a13357389212345787069726174696f6e2074696d65206973206e6f742070726f7065726c792073657400031030030333025200102435302635533530270092100116036202402f302d350080393302f3016337000020060542a666a602e6aa66a60320022602602a442a66a0022004442602e032402e02842a66a6666666ae900048c94ccd5cd181d98200008991999aab9f001202d23233335573e002405e46666aae7cd5d1001119299a9999999aba400123253335734608660900022646666aae7c00480d48cccd55cf9aba20022533530203574200642605e00206a406c07a078608e0020646ea800880c880c880c880c80e4854cd4c070d5d08029098159981a8010008188181aba1005203003703635744004068607e0020546ea800880a880a880a880a80c48400405480548c94ccd5cd181b181d800899191919091998008028018011bad357426ae88008dd69aba10013574460760046ae84c0e80040ccdd50009111a801111a801912999a9998040038020010a99a8018800819814819911192999a80190a999a80190a999a80290980200b0980180a8a999a80210980200b0980180a80c0060a999a80210980180a8980100a0a999a80190980180a8980100a00b8a999a801100700b0068a999a80110a999a80210980180a8980100a0a999a80190980180a8980100a00b8058a999a80190980100a098008098a999a80110980100a0980080980b12999a80110a999a80210a999a802109998040038010008b0b0b0060a999a80190a999a801909998038030010008b0b0b00580691a80091111111003891999999980091199ab9a3370e00400204004644a666ae68cdc38010008098a999ab9a3371200400201401044666ae68cdc400100081001191199ab9a3371200400204004644666ae68cdc480100081181011199ab9a3371000400204604044a666ae68cdc480100088008801112999ab9a33712004002200420024464a666ae68c0c8c0dc0044c8c8cc0994ccd5cd181a181c801099813198038029aba130380023006357426ae88c0e00080c54ccd5cd181a181c800899813198038029aba130380013006357426ae88c0e00040c4dd51aba135744606e0046ea8d5d0981b0008179baa0012325333573460620020522a666ae68c0c000407c0b4c0d0dd50009119192999ab9a303300100815333573460640022601460086ae84c0d400854ccd5cd1818800803017181a8009baa00122233355302302702a335530260272350012233024002300b001333553023027223500222533533355302802b301d01c235001223300a002005006100313302e00400301c001335530260272350012233024002330382253350011300a003221350022253353300c002008112223300200a0041300600300400211009212223001004112220012230302253350011003221330060023004001212223003004233333335748002403040304030460206eb4008806007c94cd5ce2481286578706563746564206f6e6c7920612073696e676c6520636f6e74696e75696e67206f7574707574001615335738920117496e76616c696420646174756d20696e206f757470757400164988880088c8c94ccd5cd1813000899091118010021aba13028002153335734604a002264244460020086ae84c0a000854ccd5cd181200080201098140009baa00111003253353007001213335530150192235300535300935003019222200422353007350042222004225333573466ebc01000854ccd5cd19baf0030011330220060051005100500e3300d00735300f3500201b22222222222200a15335738921024c660016232533357346040604a0022646424660020060046ae84d5d118128011aba1302400101d3754002444006660024002eb4888cc09088cccd55cf80090071191980e9980a180398138009803181300098021aba2003357420040306eac0048c94ccd5cd180e181080089919191919190919998008038028018011aba1357440046ae84004d5d10011aba10013574460420046ae84c080004064dd5000919191999aa999ab9a3370e90030008990911118020029aba13020002153335734603c00226424444600400a6ae84c08000854ccd5cd180e8008990911118008029aba13020002153335734603800226424444600600a6ae84c0800080648034803480348ccd54c048054cc03c894cd40088400c400403494ccd5cd19baf002350010181300600100d3300923253335734603e60480022646424660020060046ae84d5d118120011aba1302300101c37540026a60166a00802e44444444444401860400026ea8d40040408488c00800c48cc004894cd400804c40040208cc02488ccd400c04c008004d400403488ccd5cd19baf002001004007223301c2233335573e002400c466028600a6ae84008c00cd5d10010081bac001100d23253335734602860320022646464646464646464646464646464646464646464642466666666666600202e02a02602201e01a01601200e00a0060046ae84d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d1180c8011aba1301800101137540022002200c464a666ae68c044c0580044dd69aba1301500100e37540024424660020060044446006600400260244422444a66a00220044426600a004666aa600e01600a00800260224422444a66a00226a00600c442666a00a0186008004666aa600e01400a00800244002601e442244a66a00200c44266014600800466aa600c00e00800224002220024400444244660020080062a66ae712411f496e76616c696420646174756d20746f20636865636b4f776e4f757470757400161533573892010350543100162222222222220054c1014000370e90001b8748008dc3a40086e9520005573caae748c8c00400488cc00cc00800800530012cd8799f581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21a000dbba01a01499700ff00010581840001d8799f1b0000019385f96798d8799f1a0bf85ea8ffff821a001862581a18087eb7a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c01602058404ac843657462bd1b0f2db7e1abfa2fa3ccd3105ad2278649e155ceb3cf4e44b3cc2532815570cee26a55910a04db4de50cd3c07784ecfacf4c74706f54cd470382582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b5840c9fe85cdcef2b32d76958d564be1b7bc0124a99594cc9e69e1a4d0cbe8f96eed4ca94fe6f2471382d19d74f610a6a5e01e56d8f6dc2a5519a090ec2e00dc3e05a101a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01505be7b97fa11335b727bbf095a3f0fb7c0258206695b9cac9230af5c8ee50747b1ca3c78a854d181c7e5c6c371de01b80274d310b8758401b3e020864c84e53c9f7a9c54b8d09c6047bc2049aed7438c5575ed4ee265182dd808a52ae8b12083cb540fa9ccc7e0ec3a7b52ad5f134682286f658ff0083f558400dc73df538d858d8d8b89e5247f2000f1f47c43b0c68204480001fc71a179f0685400804400c2484e3013b03195a0a79c94206484bc93b10cbcb474999a5e5a55840b17f9619347e158e3a2a44e3e33eb72d648ae645a0e63d83fde739ee0d7ff6baeaf5767357770a697c2a92095bfe292e427bbc2fee56211ec78389dfa56d13c758400363abd573e840dead26d80c600380b5bc64a3c285550096634883e3a7265b4f18ca836d1d23b991a02a45ac365c9665acb787484da6ee21a10a1212ee5e5e11584012912b55d77a3e82444744ca4581c84877c958f9583f7910e4251d15161126e91b1d11e72525271b11e5870e0f0a0f94f18e080908f7f4f6918af68de8fc1bdd5840ec35fc967e64936c930f279eb8d043d31112e6a71aaaa76fc3455e9d30b90ea671971e5c9ecd4be5b4d87ab3e11f7de3b8984ea353e9a2d8d65c6ad6f9102c46581876789c8667c739c00da3176c46a1b030c4281c4e62afcf3a186358401824797b7e12054dabb6fef573ceba9c3bbed87b522a4141c2357d05d63ffabee59add33aac71f4573e43520a1c000d0c4e7fa2adf7f8e92718320527d23e60580 \ No newline at end of file