From 699e0c634313e8063f2b0b1bda79de8178d8c9d9 Mon Sep 17 00:00:00 2001 From: lisicky Date: Fri, 29 Nov 2024 18:49:01 +0800 Subject: [PATCH] change drep bech32 encoder to respect cip129 --- .../governance/cip129_decoder.rs | 145 ++++++++++++++++++ rust/src/protocol_types/governance/drep.rs | 89 +++++++++-- rust/src/protocol_types/governance/mod.rs | 2 + .../tests/protocol_types/governance/common.rs | 41 +++++ .../tests/serialization/governance/common.rs | 8 +- 5 files changed, 266 insertions(+), 19 deletions(-) create mode 100644 rust/src/protocol_types/governance/cip129_decoder.rs diff --git a/rust/src/protocol_types/governance/cip129_decoder.rs b/rust/src/protocol_types/governance/cip129_decoder.rs new file mode 100644 index 00000000..5b3108bd --- /dev/null +++ b/rust/src/protocol_types/governance/cip129_decoder.rs @@ -0,0 +1,145 @@ +use crate::{CredType, Credential, Ed25519KeyHash, GovernanceActionId, JsError, ScriptHash, TransactionHash}; +use bech32::{ToBase32, FromBase32}; +use std::convert::TryFrom; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum GovIdType { + CCHot = 0x0, + CCCold = 0x1, + DRep = 0x2, +} + +impl TryFrom for GovIdType { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(GovIdType::CCHot), + 0x1 => Ok(GovIdType::CCCold), + 0x2 => Ok(GovIdType::DRep), + _ => Err("Invalid KeyType"), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum CredentialType { + KeyHash = 0x2, + ScriptHash = 0x3, +} + +impl TryFrom for CredentialType { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0x2 => Ok(CredentialType::KeyHash), + 0x3 => Ok(CredentialType::ScriptHash), + _ => Err("Invalid CredentialType"), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum GovernanceIdentifier { + GovCredential { + gov_id_type: GovIdType, + credential: Credential + }, + GovAction(GovernanceActionId), +} + +impl GovernanceIdentifier { + pub(crate) fn encode(&self) -> Vec { + match self { + GovernanceIdentifier::GovCredential { + gov_id_type, + credential + } => { + let (cred_type, cred_bytes) = match &credential.0 { + CredType::Key(key_hash) => (CredentialType::KeyHash, key_hash.to_bytes()), + CredType::Script(script_hash) => (CredentialType::ScriptHash, script_hash.to_bytes()), + }; + let header = ((*gov_id_type as u8) << 4) | (cred_type as u8); + let mut bytes = vec![header]; + bytes.extend_from_slice(&cred_bytes); + bytes + } + GovernanceIdentifier::GovAction(gov_action_id) => { + let mut bytes = gov_action_id.transaction_id.to_bytes(); + let index = gov_action_id.index().to_be_bytes(); + bytes.extend_from_slice(&index); + bytes + } + } + } + + pub(crate) fn decode(prefix: &str, bytes: &[u8]) -> Result { + match prefix { + "drep" | "cc_hot" | "cc_cold" => { + if bytes.len() < 1 { + return Err(JsError::from_str("Invalid data length")); + } + let header = bytes[0]; + let gov_id_type = GovIdType::try_from(header >> 4). + map_err(|_| JsError::from_str("Invalid GovIdType"))?; + let credential_type = CredentialType::try_from(header & 0x0F) + .map_err(|_| JsError::from_str("Invalid CredentialType"))?; + let credential_bytes = bytes[1..].to_vec(); + let credential = match credential_type { + CredentialType::KeyHash => { + Credential::from_keyhash(&Ed25519KeyHash::from_bytes(credential_bytes) + .map_err(|_| JsError::from_str("Invalid key hash"))?) + } + CredentialType::ScriptHash => { + Credential::from_scripthash(&ScriptHash::from_bytes(credential_bytes) + .map_err(|_| JsError::from_str("Invalid script hash"))?) + } + }; + Ok(GovernanceIdentifier::GovCredential { + gov_id_type, + credential, + }) + } + "gov_action" => { + if bytes.len() < 33 { + return Err(JsError::from_str("Invalid data length")); + } + let tx_id = bytes[0..32].to_vec(); + let index_bytes = &bytes[32..]; + let index = match index_bytes.len() { + 1 => u16::from(index_bytes[0]), + 2 => u16::from_be_bytes([index_bytes[0], index_bytes[1]]), + _ => return Err(JsError::from_str("Invalid index length")), + }; + let tx_hash = TransactionHash::from_bytes(tx_id) + .map_err(|_| JsError::from_str("Invalid transaction hash"))?; + let governance_action_id = GovernanceActionId::new(&tx_hash, index.into()); + Ok(GovernanceIdentifier::GovAction(governance_action_id)) + } + _ => Err(JsError::from_str("Unknown prefix")), + } + } + + pub(crate) fn to_bech32(&self) -> Result { + let (prefix, data) = match self { + GovernanceIdentifier::GovCredential { gov_id_type, .. } => { + let prefix = match gov_id_type { + GovIdType::CCHot => "cc_hot", + GovIdType::CCCold => "cc_cold", + GovIdType::DRep => "drep", + }; + (prefix, self.encode()) + } + GovernanceIdentifier::GovAction { .. } => ("gov_action", self.encode()), + }; + let bech32_data = data.to_base32(); + bech32::encode(prefix, bech32_data).map_err(|e| JsError::from_str(&e.to_string())) + } + + pub(crate) fn from_bech32(s: &str) -> Result { + let (prefix, data) = bech32::decode(s).map_err(|e| JsError::from_str(&e.to_string()))?; + let bytes = Vec::::from_base32(&data).map_err(|e| JsError::from_str(&e.to_string()))?; + GovernanceIdentifier::decode(&prefix, &bytes) + } +} \ No newline at end of file diff --git a/rust/src/protocol_types/governance/drep.rs b/rust/src/protocol_types/governance/drep.rs index 7d96e9eb..773f6ce9 100644 --- a/rust/src/protocol_types/governance/drep.rs +++ b/rust/src/protocol_types/governance/drep.rs @@ -1,5 +1,7 @@ -use crate::*; +use std::convert::TryFrom; use bech32::ToBase32; +use crate::*; +use crate::protocol_types::governance::cip129_decoder::{GovIdType, GovernanceIdentifier}; #[derive( Clone, @@ -95,27 +97,45 @@ impl DRep { } } - pub fn to_bech32(&self) -> Result { - let (hrp, data) = match &self.0 { - DRepEnum::KeyHash(keyhash) => Ok(("drep", keyhash.to_bytes())), - DRepEnum::ScriptHash(scripthash) => Ok(("drep_script", scripthash.to_bytes())), - DRepEnum::AlwaysAbstain => { - Err(JsError::from_str("Cannot convert AlwaysAbstain to bech32")) - } - DRepEnum::AlwaysNoConfidence => Err(JsError::from_str( - "Cannot convert AlwaysNoConfidence to bech32", - )), - }?; - bech32::encode(&hrp, data.to_base32()).map_err(|e| JsError::from_str(&format! {"{:?}", e})) + pub fn to_bech32(&self, cip_129_format: bool) -> Result { + if cip_129_format { + let gov_identifier: GovernanceIdentifier = self.try_into()?; + gov_identifier.to_bech32().map_err(|e| JsError::from_str(&e.to_string())) + } else { + let (hrp, data) = match &self.0 { + DRepEnum::KeyHash(keyhash) => Ok(("drep_vkh", keyhash.to_bytes())), + DRepEnum::ScriptHash(scripthash) => Ok(("drep_script", scripthash.to_bytes())), + DRepEnum::AlwaysAbstain => { + Err(JsError::from_str("Cannot convert AlwaysAbstain to bech32")) + } + DRepEnum::AlwaysNoConfidence => Err(JsError::from_str( + "Cannot convert AlwaysNoConfidence to bech32", + )), + }?; + bech32::encode(&hrp, data.to_base32()).map_err(|e| JsError::from_str(&format! {"{:?}", e})) + } } pub fn from_bech32(bech32_str: &str) -> Result { let (hrp, u5data) = bech32::decode(bech32_str).map_err(|e| JsError::from_str(&e.to_string()))?; let data: Vec = bech32::FromBase32::from_base32(&u5data) - .map_err(|_| JsError::from_str("Malformed DRep"))?; - let kind = match hrp.as_str() { + .map_err(|e: bech32::Error| JsError::from_str(&format!("Malformed DRep base32: {}", &e.to_string())))?; + let prefix = hrp.as_str(); + match prefix { + "drep" => match data.len() { + 28 => Self::from_bech32_internal(prefix, data), + 29 => GovernanceIdentifier::from_bech32(bech32_str)?.try_into(), + _ => Err(JsError::from_str("Malformed DRep (drep1 byte len)")) + }, + _ => Self::from_bech32_internal(prefix, data), + } + } + + fn from_bech32_internal(prefix: &str, data: Vec) -> Result { + let kind = match prefix { "drep" => DRepKind::KeyHash, + "drep_vkh" => DRepKind::KeyHash, "drep_script" => DRepKind::ScriptHash, _ => return Err(JsError::from_str("Malformed DRep")), }; @@ -133,3 +153,42 @@ impl DRep { Ok(DRep(drep)) } } + +impl TryFrom<&DRep> for Credential { + type Error = JsError; + + fn try_from(drep: &DRep) -> Result { + match &drep.0 { + DRepEnum::KeyHash(keyhash) => Ok(Credential(CredType::Key(keyhash.clone()))), + DRepEnum::ScriptHash(scripthash) => Ok(Credential(CredType::Script(scripthash.clone()))), + DRepEnum::AlwaysAbstain => Err(JsError::from_str("Cannot convert AlwaysAbstain to Credential")), + DRepEnum::AlwaysNoConfidence => Err(JsError::from_str("Cannot convert AlwaysNoConfidence to Credential")), + } + } +} + +impl TryFrom<&DRep> for GovernanceIdentifier { + type Error = JsError; + + fn try_from(drep: &DRep) -> Result { + let credential = drep.try_into()?; + Ok(GovernanceIdentifier::GovCredential { + gov_id_type: GovIdType::DRep, + credential, + }) + } +} + +impl TryFrom for DRep { + type Error = JsError; + + fn try_from(gov_id: GovernanceIdentifier) -> Result { + match gov_id { + GovernanceIdentifier::GovCredential { + gov_id_type: GovIdType::DRep, + credential, + } => Ok(DRep::new_from_credential(&credential)), + _ => Err(JsError::from_str("Cannot convert GovernanceActionId to DRep")), + } + } +} diff --git a/rust/src/protocol_types/governance/mod.rs b/rust/src/protocol_types/governance/mod.rs index b0ffbabf..2e8e8084 100644 --- a/rust/src/protocol_types/governance/mod.rs +++ b/rust/src/protocol_types/governance/mod.rs @@ -23,4 +23,6 @@ mod governance_action_ids; pub use governance_action_ids::*; mod proposals; +mod cip129_decoder; + pub use proposals::*; diff --git a/rust/src/tests/protocol_types/governance/common.rs b/rust/src/tests/protocol_types/governance/common.rs index c88964b1..a087c399 100644 --- a/rust/src/tests/protocol_types/governance/common.rs +++ b/rust/src/tests/protocol_types/governance/common.rs @@ -265,3 +265,44 @@ fn voting_procedures_setters_getters_test() { assert!(governance_action_ids_2.0.contains(&governance_action_id_2)); assert!(governance_action_ids_2.0.contains(&governance_action_id_3)); } + + +#[test] +fn drep_bech32_129_parsing_key_test() { + let drep1 = DRep::from_bech32("drep1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapzh3avs7").unwrap(); + let drep2 = DRep::from_bech32("drep_vkh1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapz3kx3ey").unwrap(); + let drep3 = DRep::from_bech32("drep1ytsg4j3k9sgsxye6uu3trxpcqcq6h7t8p9e42e6wgjzt5yggls86y").unwrap(); + assert_eq!(drep1.kind(), DRepKind::KeyHash); + assert_eq!(drep2.kind(), DRepKind::KeyHash); + assert_eq!(drep3.kind(), DRepKind::KeyHash); + assert_eq!(drep1.to_key_hash().unwrap(), drep2.to_key_hash().unwrap()); + assert_eq!(drep1.to_key_hash().unwrap(), drep3.to_key_hash().unwrap()); +} + +#[test] +fn drep_bech32_129_parsing_script_test() { + let drep1 = DRep::from_bech32("drep_script1dja6lg0xdt4tfrd7r2svc3ywh5xqrl6w85axjp0gtdu6xw6h2wn").unwrap(); + let drep2 = DRep::from_bech32("drep1ydkthtapue4w4dydhcd2pnzy367scq0lfc7n56g9apdhngcaf8d6w").unwrap(); + assert_eq!(drep1.kind(), DRepKind::ScriptHash); + assert_eq!(drep2.kind(), DRepKind::ScriptHash); + assert_eq!(drep1.to_script_hash().unwrap(), drep2.to_script_hash().unwrap()); + +} + +#[test] +fn drep_bech32_to_bech() { + let drep1 = DRep::from_bech32("drep1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapzh3avs7").unwrap(); + let drep2 = DRep::from_bech32("drep_vkh1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapz3kx3ey").unwrap(); + let drep3 = DRep::from_bech32("drep_script1dja6lg0xdt4tfrd7r2svc3ywh5xqrl6w85axjp0gtdu6xw6h2wn").unwrap(); + assert_eq!(drep1.to_bech32(false).unwrap(), "drep_vkh1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapz3kx3ey"); + assert_eq!(drep2.to_bech32(false).unwrap(), "drep_vkh1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapz3kx3ey"); + assert_eq!(drep3.to_bech32(false).unwrap(), "drep_script1dja6lg0xdt4tfrd7r2svc3ywh5xqrl6w85axjp0gtdu6xw6h2wn"); +} + +#[test] +fn drep_bech32_to_bech_cip_129() { + let drep1 = DRep::from_bech32("drep1uz9v5d3vzyp3xwh8y2ceswqxqx4ljecfwd2kwnjysjapzh3avs7").unwrap(); + let drep2 = DRep::from_bech32("drep_script1dja6lg0xdt4tfrd7r2svc3ywh5xqrl6w85axjp0gtdu6xw6h2wn").unwrap(); + assert_eq!(drep1.to_bech32(true).unwrap(), "drep1ytsg4j3k9sgsxye6uu3trxpcqcq6h7t8p9e42e6wgjzt5yggls86y"); + assert_eq!(drep2.to_bech32(true).unwrap(), "drep1ydkthtapue4w4dydhcd2pnzy367scq0lfc7n56g9apdhngcaf8d6w"); +} \ No newline at end of file diff --git a/rust/src/tests/serialization/governance/common.rs b/rust/src/tests/serialization/governance/common.rs index 930f1476..f5bbbed0 100644 --- a/rust/src/tests/serialization/governance/common.rs +++ b/rust/src/tests/serialization/governance/common.rs @@ -76,7 +76,7 @@ fn drep_always_no_confidence_ser_round_trip() { #[test] fn drep_to_from_bech32_keshhash() { let drep = DRep::new_key_hash(&fake_key_hash(1)); - let bech32 = drep.to_bech32().unwrap(); + let bech32 = drep.to_bech32(false).unwrap(); let drep_deser = DRep::from_bech32(&bech32).unwrap(); assert_eq!(drep, drep_deser); } @@ -84,7 +84,7 @@ fn drep_to_from_bech32_keshhash() { #[test] fn drep_to_from_bech32_script_hash() { let drep = DRep::new_script_hash(&fake_script_hash(1)); - let bech32 = drep.to_bech32().unwrap(); + let bech32 = drep.to_bech32(false).unwrap(); let drep_deser = DRep::from_bech32(&bech32).unwrap(); assert_eq!(drep, drep_deser); } @@ -92,14 +92,14 @@ fn drep_to_from_bech32_script_hash() { #[test] fn drep_to_from_bech32_always_abstain() { let drep = DRep::new_always_abstain(); - let bech32 = drep.to_bech32(); + let bech32 = drep.to_bech32(false); assert!(bech32.is_err()); } #[test] fn drep_to_from_bech32_always_no_confidence() { let drep = DRep::new_always_no_confidence(); - let bech32 = drep.to_bech32(); + let bech32 = drep.to_bech32(false); assert!(bech32.is_err()); }