diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index b6061c306..716ed137e 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -224,6 +224,7 @@ upnp utimensat vitss vkey +vkeywitness VKEYS voteplan voteplans diff --git a/hermes/crates/cardano-chain-follower/src/lib.rs b/hermes/crates/cardano-chain-follower/src/lib.rs index 318555082..c26a940c0 100644 --- a/hermes/crates/cardano-chain-follower/src/lib.rs +++ b/hermes/crates/cardano-chain-follower/src/lib.rs @@ -21,6 +21,7 @@ mod point; mod snapshot_id; mod stats; mod utils; +mod witness; pub use chain_sync_config::ChainSyncConfig; pub use chain_update::{ChainUpdate, Kind}; diff --git a/hermes/crates/cardano-chain-follower/src/multi_era_block_data.rs b/hermes/crates/cardano-chain-follower/src/multi_era_block_data.rs index 6786ff68c..c0ce01830 100644 --- a/hermes/crates/cardano-chain-follower/src/multi_era_block_data.rs +++ b/hermes/crates/cardano-chain-follower/src/multi_era_block_data.rs @@ -17,6 +17,7 @@ use crate::{ metadata, point::{ORIGIN_POINT, UNKNOWN_POINT}, stats::stats_invalid_block, + witness::TxWitness, Network, Point, }; @@ -52,6 +53,9 @@ pub struct MultiEraBlockInner { data: SelfReferencedMultiEraBlock, /// Decoded Metadata in the transactions in the block. metadata: metadata::DecodedTransaction, + /// A map of public key hashes to the public key and transaction numbers they are in. + #[allow(dead_code)] + witness_map: Option, } /// Multi-era block. @@ -97,6 +101,8 @@ impl MultiEraBlock { let self_ref_block = builder.try_build()?; let decoded_block = self_ref_block.borrow_block(); + let witness_map = TxWitness::new(&decoded_block.txs()).ok(); + let slot = decoded_block.slot(); let point = Point::new(slot, decoded_block.hash().to_vec()); @@ -146,6 +152,7 @@ impl MultiEraBlock { previous: previous.clone(), data: self_ref_block, metadata, + witness_map, }), }) } @@ -228,6 +235,12 @@ impl MultiEraBlock { pub fn txn_raw_metadata(&self, txn_idx: usize, label: u64) -> Option>> { self.inner.metadata.get_raw_metadata(txn_idx, label) } + + /// Returns the witness map for the block. + #[allow(dead_code)] + pub(crate) fn witness_map(&self) -> Option<&TxWitness> { + self.inner.witness_map.as_ref() + } } impl Display for MultiEraBlock { @@ -308,7 +321,7 @@ impl PartialOrd for MultiEraBlock { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use std::ops::Add; use anyhow::Ok; @@ -343,12 +356,18 @@ mod tests { .expect("Failed to decode hex block.") } - /// Allegra Test Block data - fn alonzo_block() -> Vec { + /// Alonzo Test Block data + pub(crate) fn alonzo_block() -> Vec { hex::decode(include_str!("./../test_data/allegra.block")) .expect("Failed to decode hex block.") } + /// Babbage Test Block data + pub(crate) fn babbage_block() -> Vec { + hex::decode(include_str!("./../test_data/babbage.block")) + .expect("Failed to decode hex block.") + } + /// An array of test blocks fn test_blocks() -> Vec { vec![ diff --git a/hermes/crates/cardano-chain-follower/src/utils.rs b/hermes/crates/cardano-chain-follower/src/utils.rs index f620eed7d..0b793240e 100644 --- a/hermes/crates/cardano-chain-follower/src/utils.rs +++ b/hermes/crates/cardano-chain-follower/src/utils.rs @@ -1,5 +1,7 @@ //! Simple general purpose utility functions. +use blake2b_simd::{self, Params}; + /// Convert T to an i16. (saturate if out of range.) #[allow(dead_code)] pub(crate) fn i16_from_saturating>(value: T) -> i16 { @@ -28,3 +30,20 @@ pub(crate) fn usize_from_saturating< } } } + +/// Convert the given value to `blake2b_244` array. +pub(crate) fn blake2b_244(value: &[u8]) -> anyhow::Result<[u8; 28]> { + let h = Params::new().hash_length(28).hash(value); + let b = h.as_bytes(); + b.try_into() + .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_244, expected 28 got {}", b.len())) +} + +#[allow(dead_code)] +/// Convert the given value to `blake2b_256` array. +pub(crate) fn blake2b_256(value: &[u8]) -> anyhow::Result<[u8; 32]> { + let h = Params::new().hash_length(32).hash(value); + let b = h.as_bytes(); + b.try_into() + .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_256, expected 32 got {}", b.len())) +} diff --git a/hermes/crates/cardano-chain-follower/src/witness.rs b/hermes/crates/cardano-chain-follower/src/witness.rs new file mode 100644 index 000000000..9b640a571 --- /dev/null +++ b/hermes/crates/cardano-chain-follower/src/witness.rs @@ -0,0 +1,136 @@ +//! Transaction Witness +use std::fmt::{Display, Formatter}; + +use dashmap::DashMap; +use pallas::{codec::utils::Bytes, ledger::traverse::MultiEraTx}; + +use crate::utils::blake2b_244; + +/// `WitnessMap` type of `DashMap` with +/// key as [u8; 28] = (`blake2b_244` hash of the public key) +/// value as (Bytes, Vec) = (public key, tx index within the block) +#[allow(dead_code)] +pub(crate) type WitnessMap = DashMap<[u8; 28], (Bytes, Vec)>; + +#[derive(Debug)] +#[allow(dead_code)] +/// `TxWitness` struct to store the witness data. +pub(crate) struct TxWitness(WitnessMap); + +#[allow(dead_code)] +impl TxWitness { + /// Create a new `TxWitness` from a list of `MultiEraTx`. + pub(crate) fn new(txs: &[MultiEraTx]) -> anyhow::Result { + let map: WitnessMap = DashMap::new(); + for (i, tx) in txs.iter().enumerate() { + match tx { + MultiEraTx::AlonzoCompatible(tx, _) => { + let witness_set = &tx.transaction_witness_set; + if let Some(vkey_witness_set) = witness_set.vkeywitness.clone() { + for vkey_witness in vkey_witness_set { + let vkey_hash = blake2b_244(&vkey_witness.vkey)?; + let tx_num = u8::try_from(i)?; + map.entry(vkey_hash) + .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) + .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); + } + }; + }, + MultiEraTx::Babbage(tx) => { + let witness_set = &tx.transaction_witness_set; + if let Some(vkey_witness_set) = witness_set.vkeywitness.clone() { + for vkey_witness in vkey_witness_set { + let vkey_hash = blake2b_244(&vkey_witness.vkey)?; + let tx_num = u8::try_from(i)?; + map.entry(vkey_hash) + .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) + .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); + } + } + }, + MultiEraTx::Conway(tx) => { + let witness_set = &tx.transaction_witness_set; + if let Some(vkey_witness_set) = &witness_set.vkeywitness.clone() { + for vkey_witness in vkey_witness_set { + let vkey_hash = blake2b_244(&vkey_witness.vkey)?; + let tx_num = u8::try_from(i)?; + map.entry(vkey_hash) + .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) + .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); + } + } + }, + _ => {}, + }; + } + Ok(Self(map)) + } + + /// Check whether the public key hash is in the given transaction number. + pub(crate) fn check_witness_in_tx(&self, vkey_hash: &[u8; 28], tx_num: u8) -> bool { + self.0 + .get(vkey_hash) + .map_or(false, |entry| entry.1.contains(&tx_num)) + } + + /// Get the actual address from the given public key hash. + pub(crate) fn get_witness_pk_addr(&self, vkey_hash: &[u8; 28]) -> Option { + self.0.get(vkey_hash).map(|entry| entry.0.clone()) + } +} + +impl Display for TxWitness { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for data in &self.0 { + let vkey_hash = hex::encode(data.key()); + let vkey: Vec = data.0.clone().into(); + let vkey_encoded = hex::encode(&vkey); + writeln!( + f, + "Key Hash: {}, PublicKey: {}, Tx: {:?}", + vkey_hash, vkey_encoded, data.1 + )?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::multi_era_block_data::tests::{alonzo_block, babbage_block}; + + #[test] + fn tx_witness() { + let alonzo = alonzo_block(); + let alonzo_block = pallas::ledger::traverse::MultiEraBlock::decode(&alonzo) + .expect("Failed to decode MultiEraBlock"); + let txs_alonzo = alonzo_block.txs(); + let tx_witness_alonzo = TxWitness::new(&txs_alonzo).expect("Failed to create TxWitness"); + let vkey1_hash: [u8; 28] = + hex::decode("6082eb618d161a704207a0b3a9609e820111570d94d1e711b005386c") + .expect("Failed to decode vkey1_hash") + .try_into() + .expect("Invalid length of vkey1_hash"); + println!("{tx_witness_alonzo}"); + assert!(tx_witness_alonzo.get_witness_pk_addr(&vkey1_hash).is_some()); + assert!(tx_witness_alonzo.check_witness_in_tx(&vkey1_hash, 0)); + + let babbage = babbage_block(); + let babbage_block = pallas::ledger::traverse::MultiEraBlock::decode(&babbage) + .expect("Failed to decode MultiEraBlock"); + let txs_babbage = babbage_block.txs(); + let tx_witness_babbage = TxWitness::new(&txs_babbage).expect("Failed to create TxWitness"); + let vkey2_hash: [u8; 28] = + hex::decode("ba4ab50bdecca85162f3b8114739bc5ba3aaa6490e2b1d15ad0f9c66") + .expect("Failed to decode vkey2_hash") + .try_into() + .expect("Invalid length of vkey2_hash"); + println!("{tx_witness_babbage}"); + assert!(tx_witness_babbage + .get_witness_pk_addr(&vkey2_hash) + .is_some()); + assert!(tx_witness_babbage.check_witness_in_tx(&vkey2_hash, 0)); + } +} diff --git a/hermes/crates/cardano-chain-follower/test_data/babbage.block b/hermes/crates/cardano-chain-follower/test_data/babbage.block new file mode 100644 index 000000000..aeb4df9c1 --- /dev/null +++ b/hermes/crates/cardano-chain-follower/test_data/babbage.block @@ -0,0 +1 @@ +820685828a1a002751831a0409c74758208f359d2ca9a3e513b574cc88c93346506e2520f9939eadfc2e02e6c251a94769582069b5785508a9898605833a7c346c9fa294eb9e6793e0887a0230a1e026ebd1fa5820e9fff58586a4a588f15f371b4d7bfc04344b6a281bce39a7ab81fe5b6751cbac825840df4ca9e1e98212e3bfa0356274e63890304710237e92973de89d8dfba015f3eb8e925557cadcb70031cd644fe84b043f260e81342bf865d811409ae2aefb528458509aab92e40a5c75252e190d6860fbe0274ee6f7de16d12256df1dbd1f08b0f835fc0f333395d8d94464e5181e72c3fd935573005f03656191e9e95855f8fbcfd3456a6c268df905c805c9e510dff0df0f190c505820e6aebff5e8b2049914014d88821ef94b873b219ea32b44a50161b7bd2442deb5845820356e1f77552adeab6d94c2383cb4f7a5fb1b644858a6639cf9f71da339ad1e17081901ff5840fc25623ba50cf9f2858463dca7a3b1c703b97bd49d54e965d2df9602424986bf26e29ab4dac9b82f185cb825cd4a0b6dbc90374defd9ca20206cd25f47dc01058209015901c07d47377a4d51a9ed20b203308e73c500db49947189f6c92e951ab0d24b131d20d0f157fdf10a90aad4c8c6173dcd8d16b834ff47f1914556ae14b8015d7edc0ea0c33c70c7867e06a2952efe9066ceae6e1b9b23c59c76148f01917099657153af53c0cb5bb0a601d6e7e504033c2522fe069dda3a488d189032fd4ff59273526b263cabf7966326054aa26bb3d3e24010aa1edec3a545c80a1e1e357b0bea164c6663240e865cff3e29d30862b99f1ce52ca042f5d7d065bc29cc7d4ccce45acce31cc0da588c4ddec1fd4e3444ace1bf583df247ba168e0375862090fa4f49fa95fd28df6ce88f72e303d1f9cd4353abdd2d1a4472164d15220142e922b03eb6657152f340cf460514bbe57e892b81fe633d395773c00b91025237ed84ef5c6541d6bccb5a56f736b294fc6500aaa9e958c7b41bd422200545efbf0e5eb0ff95fc2394ab22cb543854aab426f60ab5dc04fa9ca3d9cc71a2abcf00c1ef8287f9bd9f7508fc5cc6a4513faac33b05b06304883ec50acb9c1ee3033604ce4eebe75beba7e97a731f92c6e779ecf700c3220ec992752b49708cab9d9a079803a01832cb14da7c59d069c4538cadd38c90b6502ce3b311424eaba7676f3dbc720b84ab008382582022d427bf2262a44dc3a3a2c9555696a741d779eb89afecf624e91a1ebdf3335400825820533e9251fd0ed7ac8613fe3d05813ce3bde7d96a6e446019a93421a0c6b785a50282582090b06350ad57403e8dcdb49d4f9fad5de1b244668fd5a0c98cd08b78d5f343b3010183a300581d704e3b75eb68829ca9e63b541f3645f78afe8d119932a7a2528171e74901821a001e8480a1581c5cf48c225e057f8da0787eae1e2bc62a1cf1d2ee34c7e2a9498b19b5a1484e6f64654665656401028201d8185840d87a9fd8799f581cba4ab50bdecca85162f3b8114739bc5ba3aaa6490e2b1d15ad0f9c66d8799fd8799f1b0000000da0e77c201b00000191449a86f5ffffffff82581d60ba4ab50bdecca85162f3b8114739bc5ba3aaa6490e2b1d15ad0f9c661a0089544082581d60ba4ab50bdecca85162f3b8114739bc5ba3aaa6490e2b1d15ad0f9c661a00ac398c021a00044c8c031a0409c766081a0409c6ee0b5820dbf505c29f869273f15ea579723e9ca9bcf31fdb8c4cb4e4085268f3714b99b90d818258206f8ebcf2a2b04f5cb5f977aa1e41001301634e64a532a8de1e98147cc84e6502010e81581cba4ab50bdecca85162f3b8114739bc5ba3aaa6490e2b1d15ad0f9c661082581d60ba4ab50bdecca85162f3b8114739bc5ba3aaa6490e2b1d15ad0f9c661a00524801111a00370c3f1281825820efd8a48f69e5270cb73b438067e1b566d39758329a45e5d6d36755f2c732618400ab00838258201df5c710d3251bea8457e05813101bd7f8defaba978114939ffa38035d42672a02825820e04bce69a5659295c235502d5fdb62b29e43f773e698c97e533e394d92d9228001825820ec4b6e2455e84a8aaa365cdc4c41ee446314ea74c209e698f1785c94c9952910000183a300581d704e3b75eb68829ca9e63b541f3645f78afe8d119932a7a2528171e74901821a001e8480a1581c5cf48c225e057f8da0787eae1e2bc62a1cf1d2ee34c7e2a9498b19b5a1484e6f64654665656401028201d8185840d87a9fd8799f581c3f2728ec78ef8b0f356e91a5662ff3124add324a7b7f5aeed69362f4d8799fd8799f1b0000000da0cc2bf01b00000191449a8523ffffffff82581d603f2728ec78ef8b0f356e91a5662ff3124add324a7b7f5aeed69362f41a0089544082581d603f2728ec78ef8b0f356e91a5662ff3124add324a7b7f5aeed69362f41a00a1cf51021a0004529a031a0409c766081a0409c6ee0b5820ed60fdd0e4e9cc43b5fa9a601a3d013c6a8f26916ff80e9b5c5a41813493f04e0d8182582040d412944923070bbdeca4887e30983b497e455e62a150209d522c4f2eec82f5020e81581c3f2728ec78ef8b0f356e91a5662ff3124add324a7b7f5aeed69362f41082581d603f2728ec78ef8b0f356e91a5662ff3124add324a7b7f5aeed69362f41a00455948111a00370c3f1281825820efd8a48f69e5270cb73b438067e1b566d39758329a45e5d6d36755f2c732618400ab00838258203efd5491b62cec43b8421eeb9978d9544a451cf938b454278dabfe508a867175008258204d1deb722bdab493e69840eca2fbb2af5eed36a82d2cd196754ce4af2618ecb902825820f1883a3368b6baa87f5d051d3e7fedb0c571519333f38860b54ec7b4dec3253e010183a300581d704e3b75eb68829ca9e63b541f3645f78afe8d119932a7a2528171e74901821a001e8480a1581c5cf48c225e057f8da0787eae1e2bc62a1cf1d2ee34c7e2a9498b19b5a1484e6f64654665656401028201d8185840d87a9fd8799f581c17942ff3849b623d24e31ec709c1c94c53b9240311820a9601ad4af0d8799fd8799f1b0000000da0cc2bf01b00000191449a8522ffffffff82581d6017942ff3849b623d24e31ec709c1c94c53b9240311820a9601ad4af01a0089544082581d6017942ff3849b623d24e31ec709c1c94c53b9240311820a9601ad4af01a0073c968021a00044c8c031a0409c766081a0409c6ee0b5820dbf505c29f869273f15ea579723e9ca9bcf31fdb8c4cb4e4085268f3714b99b90d818258204c6d1cb7cba6122de41fb91be0f78b61316399096d6f54bd7e3f2f76dd63b92e020e81581c17942ff3849b623d24e31ec709c1c94c53b9240311820a9601ad4af01082581d6017942ff3849b623d24e31ec709c1c94c53b9240311820a9601ad4af01a004df567111a00370c3f1281825820efd8a48f69e5270cb73b438067e1b566d39758329a45e5d6d36755f2c732618400a50081825820aa7bbea2a5101b867d969ad7e6ff7d95d440199ef4ba6621751d25521566101400018182583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739031b00000002525569af021a00032471075820420e72fc38474ce192fc9ba39380a11ddf9e5ac96a814d3fdb68c24cbf81e4600f0084a200818258209a12ec259e59bd8fc60244062f294f11555539f5fd45e77d8a57f16b5987dc4458401944ed87d70eb6646aa840b9914c462b1e4e3513831d1f5ec817a6966d00c0b6f12bf24b0576ea9593ed4f9c9dac2f0d9e15473088e64f688f3dea4416dbe90d0581840000d87980821a0012a1311a15d0fa8ba20081825820499300f082db931c561f42b9ca7d5ea4b288ae74cf1969d1fbd7bf41e7b02e8e5840504c78c1fe246730ca7c2469ba6731cf7e0aff42bd027091283568fe25d2a9f35da1593fb89e5bfd2f7d2db58316ee86ed1c8380e8fb77d61362c78f64374a010581840002d87980821a0012eff61a1622a0d4a20081825820ae01a85ba5eb8e67830cf6b422cb819cac1eaf0c7f62dbc496f12d9735b170d7584004dd05c7ea8b1ab964f67bfe4515e18451b23ee7950c8224f9b355aa1005ba1c7051951e82883c9cb25be7ada388fe2b708eaa6d9f1590516f2782f8b5ff20050581840000d87980821a0012a1311a15d0fa8ba100818258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c01602058401d84bfa159ac070a11834e2a41b38f205ce1628ccdc693fe2000e21c680e6ead1a23a23043bb99ac1de8460fee2f5b40d3a736566b9434d1b551b115956b5004a103a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c0150152a368b96d95f61ae69513dd5f6e92a0258204d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c20b8c58401b3d030866084fcb259de07496d3197e913a39fd628a3db0a4ed6839261a00c51cb0a5b9c16194064132ace375ea23c75c60659400cba304d0d689c00086195d5840ff28714da02c35e7295815ba58b77f227e576fa254c464e2f9c6f9dfa900a0208250033c054a468c38e08819601d073c034a4727a524ff39995477443c1fca235840839c927599b253887f50487c1caf757c0aaf79bc3fcacd42252b8f2ae1f1a8b282929ca22bb5c2885cc23a66005c0cc1ca20142b82310c3a137d44c1943e40995840a7a7ce5c3475b5887a3765ede2ff3b7bfea90f255e2edf37fd44e27f26b8e6cf408aef4b20bebf7257b3dabc7eda65fff4ed278b50219f0a52367ff5b80e46b758403875f55a394d17a5d9a6b1a1deff5b2206e9e9734e9fbefa6a1cdfeb7a104546dfb6e46c46feaeb65a7f4648c276e29e87b27bc053bffef79359300220d0c3875840f2a05cc4880317358e19c758fd9ab9917551ce3987af2e35d73b6958a0f5732784621b0c92f68a93537f16f48445424890f955d7a597c13c2eb54a82b39f0307584097507df5fef916fabb6dafdfb516fb9184783e2cb4e89d048a6c1e5c04818bdb76ffb5cbef1fbe452658d904cd152ee72a3bfc6efe1199fb3b51f1979629cd4e5840fdb7df511723d4cead3d2b2eb9c1f18cbbfcf9f5cc8eac46dc03cd55fcac3303c391437f50400923e65c02e981af5461b6867a47fb25ebe9b0fb4d9e41ec210e58404b9011000206414523c0990f9ee20b5d8a745393d3febaf6413a448b994f1567eb7945df7a0ab44afd55561e0190b376d411026c5d7a4a49a19e0bd3f5addd6c5840492fde46eee8d75b587286291dfeb6a78fdf59c1a6bfa2717b1f41dfa878756140ce7c77504b64b094b870ade78569566eec66369133af5aa8c8eab9f95e29df58409ec10be251547101b24c495c8ff4fa55378dbb4a5c6e89b18a12ac033343d61c3b7f5fba725b51536d92a5cbfaef9be6d24a3e5b3d75a1c0e29e42f523567fac4d0f8200811c822d2210b97f5708186358407b322d37df11460b98e13f6c3c4d5d4985ad984768d09f77516e8e0f61ed24e646c466a995c2bf2b547302b96d4582be9b65f8d52f9fbc4857e7bef79948860180 \ No newline at end of file