diff --git a/Cargo.lock b/Cargo.lock index 08d39c1d2b3de..fbdf0297d4531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,7 @@ dependencies = [ name = "beefy-primitives" version = "4.0.0-dev" dependencies = [ + "hex", "hex-literal", "parity-scale-codec", "scale-info", diff --git a/client/beefy/src/gossip.rs b/client/beefy/src/gossip.rs index 8a43b5a039478..dc59f664caa91 100644 --- a/client/beefy/src/gossip.rs +++ b/client/beefy/src/gossip.rs @@ -30,7 +30,7 @@ use wasm_timer::Instant; use beefy_primitives::{ crypto::{Public, Signature}, - MmrRootHash, VoteMessage, + VoteMessage, }; use crate::keystore::BeefyKeystore; @@ -142,9 +142,7 @@ where sender: &PeerId, mut data: &[u8], ) -> ValidationResult { - if let Ok(msg) = - VoteMessage::, Public, Signature>::decode(&mut data) - { + if let Ok(msg) = VoteMessage::, Public, Signature>::decode(&mut data) { let msg_hash = twox_64(data); let round = msg.commitment.block_number; @@ -178,9 +176,7 @@ where fn message_expired<'a>(&'a self) -> Box bool + 'a> { let known_votes = self.known_votes.read(); Box::new(move |_topic, mut data| { - let msg = match VoteMessage::, Public, Signature>::decode( - &mut data, - ) { + let msg = match VoteMessage::, Public, Signature>::decode(&mut data) { Ok(vote) => vote, Err(_) => return true, }; @@ -214,9 +210,7 @@ where return do_rebroadcast } - let msg = match VoteMessage::, Public, Signature>::decode( - &mut data, - ) { + let msg = match VoteMessage::, Public, Signature>::decode(&mut data) { Ok(vote) => vote, Err(_) => return true, }; @@ -237,9 +231,11 @@ mod tests { use sc_network_test::Block; use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; - use beefy_primitives::{crypto::Signature, Commitment, MmrRootHash, VoteMessage, KEY_TYPE}; - use crate::keystore::{tests::Keyring, BeefyKeystore}; + use beefy_primitives::{ + crypto::Signature, known_payload_ids, Commitment, MmrRootHash, Payload, VoteMessage, + KEY_TYPE, + }; use super::*; @@ -345,10 +341,7 @@ mod tests { } } - fn sign_commitment( - who: &Keyring, - commitment: &Commitment, - ) -> Signature { + fn sign_commitment(who: &Keyring, commitment: &Commitment) -> Signature { let store: SyncCryptoStorePtr = std::sync::Arc::new(LocalKeystore::in_memory()); SyncCryptoStore::ecdsa_generate_new(&*store, KEY_TYPE, Some(&who.to_seed())).unwrap(); let beefy_keystore: BeefyKeystore = Some(store).into(); @@ -362,11 +355,8 @@ mod tests { let sender = sc_network::PeerId::random(); let mut context = TestContext; - let commitment = Commitment { - payload: MmrRootHash::default(), - block_number: 3_u64, - validator_set_id: 0, - }; + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, MmrRootHash::default().encode()); + let commitment = Commitment { payload, block_number: 3_u64, validator_set_id: 0 }; let signature = sign_commitment(&Keyring::Alice, &commitment); diff --git a/client/beefy/src/notification.rs b/client/beefy/src/notification.rs index 6099c9681447b..f394ae6c840a2 100644 --- a/client/beefy/src/notification.rs +++ b/client/beefy/src/notification.rs @@ -24,8 +24,7 @@ use sp_runtime::traits::{Block, NumberFor}; use parking_lot::Mutex; /// Stream of signed commitments returned when subscribing. -pub type SignedCommitment = - beefy_primitives::SignedCommitment, beefy_primitives::MmrRootHash>; +pub type SignedCommitment = beefy_primitives::SignedCommitment>; /// Stream of signed commitments returned when subscribing. type SignedCommitmentStream = TracingUnboundedReceiver>; diff --git a/client/beefy/src/round.rs b/client/beefy/src/round.rs index e9f5ad2062433..db41f0f465db6 100644 --- a/client/beefy/src/round.rs +++ b/client/beefy/src/round.rs @@ -53,14 +53,14 @@ fn threshold(authorities: usize) -> usize { authorities - faulty } -pub(crate) struct Rounds { - rounds: BTreeMap<(Hash, Number), RoundTracker>, +pub(crate) struct Rounds { + rounds: BTreeMap<(Payload, Number), RoundTracker>, validator_set: ValidatorSet, } -impl Rounds +impl Rounds where - H: Ord + Hash, + P: Ord + Hash, N: Ord + AtLeast32BitUnsigned + MaybeDisplay, { pub(crate) fn new(validator_set: ValidatorSet) -> Self { @@ -70,8 +70,8 @@ where impl Rounds where - H: Ord + Hash, - N: Ord + AtLeast32BitUnsigned + MaybeDisplay, + H: Ord + Hash + Clone, + N: Ord + AtLeast32BitUnsigned + MaybeDisplay + Clone, { pub(crate) fn validator_set_id(&self) -> ValidatorSetId { self.validator_set.id @@ -81,9 +81,9 @@ where self.validator_set.validators.clone() } - pub(crate) fn add_vote(&mut self, round: (H, N), vote: (Public, Signature)) -> bool { + pub(crate) fn add_vote(&mut self, round: &(H, N), vote: (Public, Signature)) -> bool { if self.validator_set.validators.iter().any(|id| vote.0 == *id) { - self.rounds.entry(round).or_default().add_vote(vote) + self.rounds.entry(round.clone()).or_default().add_vote(vote) } else { false } @@ -179,7 +179,7 @@ mod tests { let mut rounds = Rounds::>::new(validators); assert!(rounds.add_vote( - (H256::from_low_u64_le(1), 1), + &(H256::from_low_u64_le(1), 1), (Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")) )); @@ -187,21 +187,21 @@ mod tests { // invalid vote assert!(!rounds.add_vote( - (H256::from_low_u64_le(1), 1), + &(H256::from_low_u64_le(1), 1), (Keyring::Dave.public(), Keyring::Dave.sign(b"I am committed")) )); assert!(!rounds.is_done(&(H256::from_low_u64_le(1), 1))); assert!(rounds.add_vote( - (H256::from_low_u64_le(1), 1), + &(H256::from_low_u64_le(1), 1), (Keyring::Bob.public(), Keyring::Bob.sign(b"I am committed")) )); assert!(!rounds.is_done(&(H256::from_low_u64_le(1), 1))); assert!(rounds.add_vote( - (H256::from_low_u64_le(1), 1), + &(H256::from_low_u64_le(1), 1), (Keyring::Charlie.public(), Keyring::Charlie.sign(b"I am committed")) )); @@ -225,31 +225,31 @@ mod tests { // round 1 rounds.add_vote( - (H256::from_low_u64_le(1), 1), + &(H256::from_low_u64_le(1), 1), (Keyring::Alice.public(), Keyring::Alice.sign(b"I am committed")), ); rounds.add_vote( - (H256::from_low_u64_le(1), 1), + &(H256::from_low_u64_le(1), 1), (Keyring::Bob.public(), Keyring::Bob.sign(b"I am committed")), ); // round 2 rounds.add_vote( - (H256::from_low_u64_le(2), 2), + &(H256::from_low_u64_le(2), 2), (Keyring::Alice.public(), Keyring::Alice.sign(b"I am again committed")), ); rounds.add_vote( - (H256::from_low_u64_le(2), 2), + &(H256::from_low_u64_le(2), 2), (Keyring::Bob.public(), Keyring::Bob.sign(b"I am again committed")), ); // round 3 rounds.add_vote( - (H256::from_low_u64_le(3), 3), + &(H256::from_low_u64_le(3), 3), (Keyring::Alice.public(), Keyring::Alice.sign(b"I am still committed")), ); rounds.add_vote( - (H256::from_low_u64_le(3), 3), + &(H256::from_low_u64_le(3), 3), (Keyring::Bob.public(), Keyring::Bob.sign(b"I am still committed")), ); diff --git a/client/beefy/src/worker.rs b/client/beefy/src/worker.rs index 3f52686930332..fa48e64c12b4e 100644 --- a/client/beefy/src/worker.rs +++ b/client/beefy/src/worker.rs @@ -36,8 +36,8 @@ use sp_runtime::{ use beefy_primitives::{ crypto::{AuthorityId, Public, Signature}, - BeefyApi, Commitment, ConsensusLog, MmrRootHash, SignedCommitment, ValidatorSet, - VersionedCommitment, VoteMessage, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, + known_payload_ids, BeefyApi, Commitment, ConsensusLog, MmrRootHash, Payload, SignedCommitment, + ValidatorSet, VersionedCommitment, VoteMessage, BEEFY_ENGINE_ID, GENESIS_AUTHORITY_SET_ID, }; use crate::{ @@ -79,7 +79,7 @@ where /// Min delta in block numbers between two blocks, BEEFY should vote on min_block_delta: u32, metrics: Option, - rounds: round::Rounds>, + rounds: round::Rounds>, finality_notifications: FinalityNotifications, /// Best block we received a GRANDPA notification for best_grandpa_block: NumberFor, @@ -262,8 +262,9 @@ where return }; + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, mmr_root.encode()); let commitment = Commitment { - payload: mmr_root, + payload, block_number: notification.header.number(), validator_set_id: self.rounds.validator_set_id(), }; @@ -301,10 +302,10 @@ where } } - fn handle_vote(&mut self, round: (MmrRootHash, NumberFor), vote: (Public, Signature)) { + fn handle_vote(&mut self, round: (Payload, NumberFor), vote: (Public, Signature)) { self.gossip_validator.note_round(round.1); - let vote_added = self.rounds.add_vote(round, vote); + let vote_added = self.rounds.add_vote(&round, vote); if vote_added && self.rounds.is_done(&round) { if let Some(signatures) = self.rounds.drop(&round) { @@ -352,7 +353,7 @@ where |notification| async move { debug!(target: "beefy", "🥩 Got vote message: {:?}", notification); - VoteMessage::, Public, Signature>::decode( + VoteMessage::, Public, Signature>::decode( &mut ¬ification.message[..], ) .ok() diff --git a/primitives/beefy/Cargo.toml b/primitives/beefy/Cargo.toml index 23e98012027c7..e38af745cd714 100644 --- a/primitives/beefy/Cargo.toml +++ b/primitives/beefy/Cargo.toml @@ -18,8 +18,8 @@ sp-runtime = { version = "4.0.0-dev", path = "../runtime", default-features = fa sp-std = { version = "4.0.0-dev", path = "../std", default-features = false } [dev-dependencies] +hex = "0.4.3" hex-literal = "0.3" - sp-keystore = { version = "0.10.0-dev", path = "../keystore" } [features] diff --git a/primitives/beefy/src/commitment.rs b/primitives/beefy/src/commitment.rs index d9e4de6e19bb7..667c03cc2284c 100644 --- a/primitives/beefy/src/commitment.rs +++ b/primitives/beefy/src/commitment.rs @@ -15,10 +15,65 @@ // See the License for the specific language governing permissions and // limitations under the License. +use codec::{Decode, Encode}; use sp_std::{cmp, prelude::*}; use crate::{crypto::Signature, ValidatorSetId}; +/// Id of different payloads in the [`Commitment`] data +pub type BeefyPayloadId = [u8; 2]; + +/// Registry of all known [`BeefyPayloadId`]. +pub mod known_payload_ids { + use crate::BeefyPayloadId; + + /// A [`Payload`] identifier for Merkle Mountain Range root hash. + /// + /// Encoded value should contain a [`beefy_primitives::MmrRootHash`] type (i.e. 32-bytes hash). + pub const MMR_ROOT_ID: BeefyPayloadId = *b"mh"; +} + +/// A BEEFY payload type allowing for future extensibility of adding additional kinds of payloads. +/// +/// The idea is to store a vector of SCALE-encoded values with an extra identifier. +/// Identifiers MUST be sorted by the [`BeefyPayloadId`] to allow efficient lookup of expected +/// value. Duplicated identifiers are disallowed. It's okay for different implementations to only +/// support a subset of possible values. +#[derive(Decode, Encode, Debug, PartialEq, Eq, Clone, Ord, PartialOrd, Hash)] +pub struct Payload(Vec<(BeefyPayloadId, Vec)>); + +impl Payload { + /// Construct a new payload given an initial vallue + pub fn new(id: BeefyPayloadId, value: Vec) -> Self { + Self(vec![(id, value)]) + } + + /// Returns a raw payload under given `id`. + /// + /// If the [`BeefyPayloadId`] is not found in the payload `None` is returned. + pub fn get_raw(&self, id: &BeefyPayloadId) -> Option<&Vec> { + let index = self.0.binary_search_by(|probe| probe.0.cmp(id)).ok()?; + Some(&self.0[index].1) + } + + /// Returns a decoded payload value under given `id`. + /// + /// In case the value is not there or it cannot be decoded does not match `None` is returned. + pub fn get_decoded(&self, id: &BeefyPayloadId) -> Option { + self.get_raw(id).and_then(|raw| T::decode(&mut &raw[..]).ok()) + } + + /// Push a `Vec` with a given id into the payload vec. + /// This method will internally sort the payload vec after every push. + /// + /// Returns self to allow for daisy chaining. + pub fn push_raw(mut self, id: BeefyPayloadId, value: Vec) -> Self { + self.0.push((id, value)); + self.0.sort_by_key(|(id, _)| *id); + self + } +} + /// A commitment signed by GRANDPA validators as part of BEEFY protocol. /// /// The commitment contains a [payload](Commitment::payload) extracted from the finalized block at @@ -26,16 +81,17 @@ use crate::{crypto::Signature, ValidatorSetId}; /// GRANDPA validators collect signatures on commitments and a stream of such signed commitments /// (see [SignedCommitment]) forms the BEEFY protocol. #[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode)] -pub struct Commitment { - /// The payload being signed. +pub struct Commitment { + /// A collection of payloads to be signed, see [`Payload`] for details. /// - /// This should be some form of cumulative representation of the chain (think MMR root hash). - /// The payload should also contain some details that allow the light client to verify next - /// validator set. The protocol does not enforce any particular format of this data, - /// nor how often it should be present in commitments, however the light client has to be - /// provided with full validator set whenever it performs the transition (i.e. importing first - /// block with [validator_set_id](Commitment::validator_set_id) incremented). - pub payload: TPayload, + /// One of the payloads should be some form of cumulative representation of the chain (think + /// MMR root hash). Additionally one of the payloads should also contain some details that + /// allow the light client to verify next validator set. The protocol does not enforce any + /// particular format of this data, nor how often it should be present in commitments, however + /// the light client has to be provided with full validator set whenever it performs the + /// transition (i.e. importing first block with + /// [validator_set_id](Commitment::validator_set_id) incremented). + pub payload: Payload, /// Finalized block number this commitment is for. /// @@ -57,20 +113,18 @@ pub struct Commitment { pub validator_set_id: ValidatorSetId, } -impl cmp::PartialOrd for Commitment +impl cmp::PartialOrd for Commitment where TBlockNumber: cmp::Ord, - TPayload: cmp::Eq, { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl cmp::Ord for Commitment +impl cmp::Ord for Commitment where TBlockNumber: cmp::Ord, - TPayload: cmp::Eq, { fn cmp(&self, other: &Self) -> cmp::Ordering { self.validator_set_id @@ -81,9 +135,9 @@ where /// A commitment with matching GRANDPA validators' signatures. #[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode)] -pub struct SignedCommitment { +pub struct SignedCommitment { /// The commitment signatures are collected for. - pub commitment: Commitment, + pub commitment: Commitment, /// GRANDPA validators' signatures for the commitment. /// /// The length of this `Vec` must match number of validators in the current set (see @@ -91,7 +145,7 @@ pub struct SignedCommitment { pub signatures: Vec>, } -impl SignedCommitment { +impl SignedCommitment { /// Return the number of collected signatures. pub fn no_of_signatures(&self) -> usize { self.signatures.iter().filter(|x| x.is_some()).count() @@ -102,10 +156,10 @@ impl SignedCommitment { /// to the block justifications for the block for which the signed commitment /// has been generated. #[derive(Clone, Debug, PartialEq, codec::Encode, codec::Decode)] -pub enum VersionedCommitment { +pub enum VersionedCommitment { #[codec(index = 1)] /// Current active version - V1(SignedCommitment), + V1(SignedCommitment), } #[cfg(test)] @@ -119,9 +173,9 @@ mod tests { use crate::{crypto, KEY_TYPE}; - type TestCommitment = Commitment; - type TestSignedCommitment = SignedCommitment; - type TestVersionedCommitment = VersionedCommitment; + type TestCommitment = Commitment; + type TestSignedCommitment = SignedCommitment; + type TestVersionedCommitment = VersionedCommitment; // The mock signatures are equivalent to the ones produced by the BEEFY keystore fn mock_signatures() -> (crypto::Signature, crypto::Signature) { @@ -148,8 +202,9 @@ mod tests { #[test] fn commitment_encode_decode() { // given + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, "Hello World!".encode()); let commitment: TestCommitment = - Commitment { payload: "Hello World!".into(), block_number: 5, validator_set_id: 0 }; + Commitment { payload, block_number: 5, validator_set_id: 0 }; // when let encoded = codec::Encode::encode(&commitment); @@ -160,7 +215,7 @@ mod tests { assert_eq!( encoded, hex_literal::hex!( - "3048656c6c6f20576f726c6421050000000000000000000000000000000000000000000000" + "046d68343048656c6c6f20576f726c6421050000000000000000000000000000000000000000000000" ) ); } @@ -168,8 +223,9 @@ mod tests { #[test] fn signed_commitment_encode_decode() { // given + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, "Hello World!".encode()); let commitment: TestCommitment = - Commitment { payload: "Hello World!".into(), block_number: 5, validator_set_id: 0 }; + Commitment { payload, block_number: 5, validator_set_id: 0 }; let sigs = mock_signatures(); @@ -187,10 +243,11 @@ mod tests { assert_eq!( encoded, hex_literal::hex!( - "3048656c6c6f20576f726c64210500000000000000000000000000000000000000000000001000 - 0001558455ad81279df0795cc985580e4fb75d72d948d1107b2ac80a09abed4da8480c746cc321f2319a5e99a830e314d - 10dd3cd68ce3dc0c33c86e99bcb7816f9ba01012d6e1f8105c337a86cdd9aaacdc496577f3db8c55ef9e6fd48f2c5c05a - 2274707491635d8ba3df64f324575b7b2a34487bca2324b6a0046395a71681be3d0c2a00" + "046d68343048656c6c6f20576f726c6421050000000000000000000000000000000000000000000000 + 10000001558455ad81279df0795cc985580e4fb75d72d948d1107b2ac80a09abed4da8480c746cc321 + f2319a5e99a830e314d10dd3cd68ce3dc0c33c86e99bcb7816f9ba01012d6e1f8105c337a86cdd9aaa + cdc496577f3db8c55ef9e6fd48f2c5c05a2274707491635d8ba3df64f324575b7b2a34487bca2324b6a + 0046395a71681be3d0c2a00" ) ); } @@ -198,8 +255,9 @@ mod tests { #[test] fn signed_commitment_count_signatures() { // given + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, "Hello World!".encode()); let commitment: TestCommitment = - Commitment { payload: "Hello World!".into(), block_number: 5, validator_set_id: 0 }; + Commitment { payload, block_number: 5, validator_set_id: 0 }; let sigs = mock_signatures(); @@ -222,7 +280,8 @@ mod tests { block_number: u128, validator_set_id: crate::ValidatorSetId, ) -> TestCommitment { - Commitment { payload: "Hello World!".into(), block_number, validator_set_id } + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, "Hello World!".encode()); + Commitment { payload, block_number, validator_set_id } } // given @@ -241,8 +300,9 @@ mod tests { #[test] fn versioned_commitment_encode_decode() { + let payload = Payload::new(known_payload_ids::MMR_ROOT_ID, "Hello World!".encode()); let commitment: TestCommitment = - Commitment { payload: "Hello World!".into(), block_number: 5, validator_set_id: 0 }; + Commitment { payload, block_number: 5, validator_set_id: 0 }; let sigs = mock_signatures(); diff --git a/primitives/beefy/src/lib.rs b/primitives/beefy/src/lib.rs index 790b915ab98db..cb3cf601a76bc 100644 --- a/primitives/beefy/src/lib.rs +++ b/primitives/beefy/src/lib.rs @@ -35,7 +35,9 @@ mod commitment; pub mod mmr; pub mod witness; -pub use commitment::{Commitment, SignedCommitment, VersionedCommitment}; +pub use commitment::{ + known_payload_ids, BeefyPayloadId, Commitment, Payload, SignedCommitment, VersionedCommitment, +}; use codec::{Codec, Decode, Encode}; use scale_info::TypeInfo; @@ -118,9 +120,9 @@ pub enum ConsensusLog { /// A vote message is a direct vote created by a BEEFY node on every voting round /// and is gossiped to its peers. #[derive(Debug, Decode, Encode, TypeInfo)] -pub struct VoteMessage { +pub struct VoteMessage { /// Commit to information extracted from a finalized block - pub commitment: Commitment, + pub commitment: Commitment, /// Node authority id pub id: Id, /// Node signature diff --git a/primitives/beefy/src/witness.rs b/primitives/beefy/src/witness.rs index c28a464e72df5..3ead08bdd7cb3 100644 --- a/primitives/beefy/src/witness.rs +++ b/primitives/beefy/src/witness.rs @@ -40,9 +40,9 @@ use crate::{ /// Ethereum Mainnet), in a commit-reveal like scheme, where first we submit only the signed /// commitment witness and later on, the client picks only some signatures to verify at random. #[derive(Debug, PartialEq, Eq, codec::Encode, codec::Decode)] -pub struct SignedCommitmentWitness { +pub struct SignedCommitmentWitness { /// The full content of the commitment. - pub commitment: Commitment, + pub commitment: Commitment, /// The bit vector of validators who signed the commitment. pub signed_by: Vec, // TODO [ToDr] Consider replacing with bitvec crate @@ -51,9 +51,7 @@ pub struct SignedCommitmentWitness { pub signatures_merkle_root: TMerkleRoot, } -impl - SignedCommitmentWitness -{ +impl SignedCommitmentWitness { /// Convert [SignedCommitment] into [SignedCommitmentWitness]. /// /// This takes a [SignedCommitment], which contains full signatures @@ -63,7 +61,7 @@ impl /// /// Returns the full list of signatures along with the witness. pub fn from_signed( - signed: SignedCommitment, + signed: SignedCommitment, merkelize: TMerkelize, ) -> (Self, Vec>) where @@ -86,12 +84,11 @@ mod tests { use super::*; use codec::Decode; - use crate::{crypto, KEY_TYPE}; + use crate::{crypto, known_payload_ids, Payload, KEY_TYPE}; - type TestCommitment = Commitment; - type TestSignedCommitment = SignedCommitment; - type TestSignedCommitmentWitness = - SignedCommitmentWitness>>; + type TestCommitment = Commitment; + type TestSignedCommitment = SignedCommitment; + type TestSignedCommitmentWitness = SignedCommitmentWitness>>; // The mock signatures are equivalent to the ones produced by the BEEFY keystore fn mock_signatures() -> (crypto::Signature, crypto::Signature) { @@ -116,8 +113,10 @@ mod tests { } fn signed_commitment() -> TestSignedCommitment { + let payload = + Payload::new(known_payload_ids::MMR_ROOT_ID, "Hello World!".as_bytes().to_vec()); let commitment: TestCommitment = - Commitment { payload: "Hello World!".into(), block_number: 5, validator_set_id: 0 }; + Commitment { payload, block_number: 5, validator_set_id: 0 }; let sigs = mock_signatures(); @@ -152,10 +151,11 @@ mod tests { assert_eq!( encoded, hex_literal::hex!( - "3048656c6c6f20576f726c64210500000000000000000000000000000000000000000000001000 - 00010110000001558455ad81279df0795cc985580e4fb75d72d948d1107b2ac80a09abed4da8480c746cc321f2319a5e9 - 9a830e314d10dd3cd68ce3dc0c33c86e99bcb7816f9ba01012d6e1f8105c337a86cdd9aaacdc496577f3db8c55ef9e6fd - 48f2c5c05a2274707491635d8ba3df64f324575b7b2a34487bca2324b6a0046395a71681be3d0c2a00" + "046d683048656c6c6f20576f726c642105000000000000000000000000000000000000000000000010 + 0000010110000001558455ad81279df0795cc985580e4fb75d72d948d1107b2ac80a09abed4da8480c + 746cc321f2319a5e99a830e314d10dd3cd68ce3dc0c33c86e99bcb7816f9ba01012d6e1f8105c337a86 + cdd9aaacdc496577f3db8c55ef9e6fd48f2c5c05a2274707491635d8ba3df64f324575b7b2a34487bc + a2324b6a0046395a71681be3d0c2a00" ) ); }