From 0aab8976a656014201efbaa7b9556e5c2d28b04a Mon Sep 17 00:00:00 2001 From: Jannis Pohlmann Date: Wed, 4 Oct 2023 18:22:31 +0200 Subject: [PATCH] feature: improve DeploymentId encoding/decoding This adds support for decoding/encoding to and from hex strings in addition to IPFS hashes / CIDv0. Introducing a dedicated `DeploymentIdError` type makes this a breaking change, but perhaps we're ok with that given the benefits. The reason this is useful is that in various places we store deployment IDs as hex strings (e.g. in the indexer software). Instead of explicitly handling these cases on one-off basis, the changes introduced here allow to treat them all the same. --- toolshed/src/thegraph/mod.rs | 105 +++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/toolshed/src/thegraph/mod.rs b/toolshed/src/thegraph/mod.rs index ebd6390..6f2719e 100644 --- a/toolshed/src/thegraph/mod.rs +++ b/toolshed/src/thegraph/mod.rs @@ -1,6 +1,6 @@ pub mod attestation; -use std::{fmt, str::FromStr}; +use std::{fmt, fmt::LowerHex, str::FromStr}; use alloy_primitives::{Address, BlockHash, BlockNumber, B256}; use serde::{Deserialize, Serialize}; @@ -9,6 +9,7 @@ use sha3::{ digest::{Digest as _, Update as _}, Keccak256, }; +use thiserror::Error; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct BlockPointer { @@ -71,14 +72,49 @@ impl fmt::Debug for SubgraphId { )] pub struct DeploymentId(pub B256); +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum DeploymentIdError { + #[error("invalid IPFS / CIDv0 hash length {length}: {value} (length must be 46)")] + InvalidIpfsHashLength { value: String, length: usize }, + #[error("invalid IPFS / CIDv0 hash \"{value}\": {error}")] + InvalidIpfsHash { + value: String, + error: bs58::decode::Error, + }, + #[error("invalid hex string \"{value}\": {error}")] + InvalidHexString { value: String, error: String }, +} + impl FromStr for DeploymentId { - type Err = bs58::decode::Error; - fn from_str(cid_v0: &str) -> Result { - let mut decoded = [0_u8; 34]; - bs58::decode(cid_v0).onto(&mut decoded)?; - let mut bytes = [0_u8; 32]; - bytes.copy_from_slice(&decoded[2..]); - Ok(Self(bytes.into())) + type Err = DeploymentIdError; + fn from_str(s: &str) -> Result { + if s.starts_with("Qm") { + // Attempt to decode IPFS hash + if s.len() != 46 { + return Err(DeploymentIdError::InvalidIpfsHashLength { + value: s.to_string(), + length: s.len(), + }); + } + let mut decoded = [0_u8; 34]; + bs58::decode(s) + .onto(&mut decoded) + .map_err(|e| DeploymentIdError::InvalidIpfsHash { + value: s.to_string(), + error: e, + })?; + let mut bytes = [0_u8; 32]; + bytes.copy_from_slice(&dbg!(decoded)[2..]); + Ok(Self(bytes.into())) + } else { + // Attempt to decode 32-byte hex string + Ok(s.parse::() + .map(Self) + .map_err(|e| DeploymentIdError::InvalidHexString { + value: s.to_string(), + error: format!("{}", e), + })?) + } } } @@ -97,6 +133,12 @@ impl fmt::Debug for DeploymentId { } } +impl LowerHex for DeploymentId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + #[test] fn subgraph_id_encode() { let bytes: B256 = "0x67486e65165b1474898247760a4b852d70d95782c6325960e5b6b4fd82fed1bd" @@ -116,20 +158,39 @@ fn subgraph_id_encode() { } #[test] -fn deployment_id_encode() { - let ipfs_hash = "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz"; - let hash: B256 = "0x7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89" - .parse() - .unwrap(); +fn deployment_id_decode_and_encode() { + let cid = "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz"; + let id_from_cid = DeploymentId::from_str(cid).expect("parsing from IPFS hash"); - let id1 = DeploymentId(hash); - let id2: DeploymentId = ipfs_hash - .parse() - .expect("failed to create DeploymentId from CIDv0"); + let hex = "0x7d5a99f603f231d53a4f39d1521f98d2e8bb279cf29bebfd0687dc98458e7f89"; + let id_from_hex = DeploymentId::from_str(hex).expect("parsing from hex string"); - assert_eq!(id1.to_string(), ipfs_hash); - assert_eq!(id1.0, hash); - assert_eq!(id2.to_string(), ipfs_hash); - assert_eq!(id2.0, hash); - assert_eq!(id1, id2); + let bytes: B256 = hex.parse().expect("parsing hex string into bytes"); + + assert_eq!(id_from_cid, id_from_hex); + + assert_eq!(id_from_cid.to_string(), cid); + assert_eq!(id_from_hex.to_string(), cid); + + assert_eq!(format!("{id_from_cid:#x}"), hex); + assert_eq!(format!("{id_from_hex:#x}"), hex); + + assert_eq!(id_from_cid.0, bytes); + assert_eq!(id_from_hex.0, bytes); + + assert_eq!( + DeploymentId::from_str("QmA"), + Err(DeploymentIdError::InvalidIpfsHashLength { + value: "QmA".to_string(), + length: 3 + }) + ); + + assert_eq!( + DeploymentId::from_str("0x"), + Err(DeploymentIdError::InvalidHexString { + value: "0x".to_string(), + error: "Invalid string length".to_string() + }) + ); }