Skip to content

Commit bf390fd

Browse files
authored
Merge pull request #146 from input-output-hk/byap/check-pool-metadata-hash
Add checking of pool metadata hash
2 parents 30a5585 + 2eb14bc commit bf390fd

File tree

3 files changed

+111
-8
lines changed

3 files changed

+111
-8
lines changed

modules/rest_blockfrost/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ acropolis_common = { path = "../../common" }
1313
anyhow = "1.0"
1414
async-trait = "0.1"
1515
bech32 = "0.11"
16+
blake2 = "0.10.6"
1617
caryatid_sdk = "0.12"
1718
caryatid_module_rest_server = "0.14"
1819
config = "0.15.11"

modules/rest_blockfrost/src/handlers/pools.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ use caryatid_sdk::Context;
1616
use rust_decimal::Decimal;
1717
use std::{sync::Arc, time::Duration};
1818

19-
use crate::types::{PoolEpochStateRest, PoolExtendedRest, PoolMetadataRest, PoolRetirementRest};
20-
use crate::utils::fetch_pool_metadata;
2119
use crate::{handlers_config::HandlersConfig, types::PoolRelayRest};
20+
use crate::{
21+
types::{PoolEpochStateRest, PoolExtendedRest, PoolMetadataRest, PoolRetirementRest},
22+
utils::{fetch_pool_metadata_as_bytes, verify_pool_metadata_hash, PoolMetadataJson},
23+
};
2224

2325
/// Handle `/pools` Blockfrost-compatible endpoint
2426
pub async fn handle_pools_list_blockfrost(
@@ -582,11 +584,25 @@ pub async fn handle_pool_metadata_blockfrost(
582584
)
583585
.await?;
584586

585-
let pool_metadata_json = fetch_pool_metadata(
587+
let pool_metadata_bytes = fetch_pool_metadata_as_bytes(
586588
pool_metadata.url.clone(),
587589
Duration::from_secs(handlers_config.external_api_timeout),
588590
)
589591
.await?;
592+
593+
// Verify hash of the fetched pool metadata, matches with the metadata hash provided by PoolRegistration
594+
if let Err(e) = verify_pool_metadata_hash(&pool_metadata_bytes, &pool_metadata.hash) {
595+
return Ok(RESTResponse::with_text(404, &e));
596+
}
597+
598+
// Convert bytes into an understandable PoolMetadata structure
599+
let Ok(pool_metadata_json) = PoolMetadataJson::try_from(pool_metadata_bytes) else {
600+
return Ok(RESTResponse::with_text(
601+
400,
602+
&format!("Failed PoolMetadata Json conversion"),
603+
));
604+
};
605+
590606
let pool_metadata_rest = PoolMetadataRest {
591607
pool_id: pool_id.to_string(),
592608
hex: hex::encode(spo),
Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,91 @@
11
use std::time::Duration;
22

33
use anyhow::Result;
4+
use blake2::{
5+
digest::{Update, VariableOutput},
6+
Digest,
7+
};
48
use reqwest::Client;
59
use serde::{Deserialize, Serialize};
610

711
#[derive(Serialize, Deserialize)]
812
pub struct PoolMetadataJson {
9-
pub ticker: String,
1013
pub name: String,
1114
pub description: String,
15+
pub ticker: String,
1216
pub homepage: String,
1317
}
1418

15-
pub async fn fetch_pool_metadata(url: String, timeout: Duration) -> Result<PoolMetadataJson> {
19+
impl TryFrom<&[u8]> for PoolMetadataJson {
20+
type Error = serde_json::Error;
21+
22+
/// Returns `PoolMetadataJson`
23+
///
24+
/// # Arguments
25+
///
26+
/// * `value` - Pool metadata (in json) as slice
27+
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
28+
serde_json::from_slice::<Self>(value)
29+
}
30+
}
31+
32+
impl TryFrom<Vec<u8>> for PoolMetadataJson {
33+
type Error = serde_json::Error;
34+
35+
/// Returns `PoolMetadataJson`
36+
///
37+
/// # Arguments
38+
///
39+
/// * `value` - Pool metadata (in json) as bytes
40+
fn try_from(value: Vec<u8>) -> std::result::Result<Self, Self::Error> {
41+
PoolMetadataJson::try_from(value.as_ref())
42+
}
43+
}
44+
45+
/// Fetches pool metadata
46+
///
47+
/// # Returns
48+
///
49+
/// * `Ok<Vec<u8>>` - pool metadata in bytes format
50+
pub async fn fetch_pool_metadata_as_bytes(url: String, timeout: Duration) -> Result<Vec<u8>> {
1651
let client = Client::new();
1752
let response = client.get(url).timeout(timeout).send().await?;
18-
let body = response.json::<PoolMetadataJson>().await?;
19-
Ok(body)
53+
let body = response.bytes().await?;
54+
Ok(body.to_vec())
55+
}
56+
57+
/// Verifies the calculated pool metadata hash, is similar to the expected hash
58+
///
59+
/// # Arguments
60+
///
61+
/// * `pool_metadata` - The pool metadata as bytes
62+
/// * `expected_hash` - The expected hash of the `pool_metadata`
63+
///
64+
/// # Returns
65+
///
66+
/// * `Ok(())` - for successful verification
67+
/// * `Err(<error description>)` - for failed verifaction
68+
pub fn verify_pool_metadata_hash(
69+
pool_metadata: &[u8],
70+
expected_hash: &acropolis_common::types::DataHash,
71+
) -> Result<(), String> {
72+
// hash the serialized metadata
73+
let mut hasher = blake2::Blake2bVar::new(32).map_err(invalid_size_desc)?;
74+
hasher.update(pool_metadata);
75+
76+
let mut hash = vec![0; 32];
77+
hasher.finalize_variable(&mut hash).map_err(invalid_size_desc)?;
78+
79+
if &hash == expected_hash {
80+
return Ok(());
81+
}
82+
83+
Err("pool metadata hash does not match to expected".into())
2084
}
2185

86+
fn invalid_size_desc<T: std::fmt::Display>(e: T) -> String {
87+
format!("Invalid size for hashing pool metadata json {e}")
88+
}
2289
#[cfg(test)]
2390
mod tests {
2491
use super::*;
@@ -27,10 +94,29 @@ mod tests {
2794
async fn test_fetch_pool_metadata() {
2895
let url = "https://raw.githubusercontent.com/Octalus/cardano/master/p.json";
2996
let pool_metadata =
30-
fetch_pool_metadata(url.to_string(), Duration::from_secs(3)).await.unwrap();
97+
fetch_pool_metadata_as_bytes(url.to_string(), Duration::from_secs(3)).await.unwrap();
98+
99+
let pool_metadata = PoolMetadataJson::try_from(pool_metadata).expect("failed to convert");
100+
31101
assert_eq!(pool_metadata.ticker, "OCTAS");
32102
assert_eq!(pool_metadata.name, "OctasPool");
33103
assert_eq!(pool_metadata.description, "Octa's Performance Pool");
34104
assert_eq!(pool_metadata.homepage, "https://octaluso.dyndns.org");
35105
}
106+
107+
#[tokio::test]
108+
async fn test_pool_metadata_hash_verify() {
109+
let url = " https://880w.short.gy/clrsp.json ";
110+
111+
let expected_hash = "3c914463aa1cddb425fba48b21c4db31958ea7a30e077f756a82903f30e04905";
112+
let expected_hash_as_arr = hex::decode(expected_hash).expect("should be able to decode {}");
113+
114+
let pool_metadata =
115+
fetch_pool_metadata_as_bytes(url.to_string(), Duration::from_secs(3)).await.unwrap();
116+
117+
assert_eq!(
118+
verify_pool_metadata_hash(&pool_metadata, &expected_hash_as_arr),
119+
Ok(())
120+
);
121+
}
36122
}

0 commit comments

Comments
 (0)