diff --git a/hs/spec_compliance/src/IC/HTTP/RequestId.hs b/hs/spec_compliance/src/IC/HTTP/RequestId.hs index df0e2a342f7..5c1ba22f3b9 100644 --- a/hs/spec_compliance/src/IC/HTTP/RequestId.hs +++ b/hs/spec_compliance/src/IC/HTTP/RequestId.hs @@ -17,14 +17,14 @@ requestId (GRec hm) = sha256 $ BS.concat $ sort $ map encodeKV $ HM.toList hm requestId _ = error "requestID: expected a record" encodeKV :: (T.Text, GenR) -> BS.ByteString -encodeKV (k, v) = sha256 (toUtf8 k) <> sha256 (encodeVal v) +encodeKV (k, v) = sha256 (toUtf8 k) <> hashVal v -encodeVal :: GenR -> BS.ByteString -encodeVal (GBlob b) = b -encodeVal (GText t) = toUtf8 t -encodeVal (GNat n) = encodeNat n -encodeVal (GRec _) = error "requestID: Nested record" -encodeVal (GList vs) = BS.concat $ map (sha256 . encodeVal) vs +hashVal :: GenR -> BS.ByteString +hashVal (GBlob b) = sha256 b +hashVal (GText t) = sha256 (toUtf8 t) +hashVal (GNat n) = sha256 (encodeNat n) +hashVal val@(GRec _) = requestId val +hashVal (GList vs) = sha256 $ BS.concat $ map hashVal vs encodeNat :: Natural -> BS.ByteString encodeNat = BS.fromStrict . toLEB128 diff --git a/hs/spec_compliance/src/IC/Test/Agent.hs b/hs/spec_compliance/src/IC/Test/Agent.hs index 0e3f8b8e5d0..6ba6dcf87f6 100644 --- a/hs/spec_compliance/src/IC/Test/Agent.hs +++ b/hs/spec_compliance/src/IC/Test/Agent.hs @@ -864,7 +864,7 @@ checkQueryResponse cid rid r = do requestId $ rec [ "status" =: GText "replied", - "reply" =: GBlob (requestId $ rec ["arg" =: GBlob payload]), + "reply" =: rec ["arg" =: GBlob payload], "timestamp" =: GNat t, "request_id" =: GBlob rid ] diff --git a/rs/types/types/src/messages/http.rs b/rs/types/types/src/messages/http.rs index 5ca66da879d..c2b2c750688 100644 --- a/rs/types/types/src/messages/http.rs +++ b/rs/types/types/src/messages/http.rs @@ -18,7 +18,12 @@ use maplit::btreemap; #[cfg(test)] use proptest_derive::Arbitrary; use serde::{ser::SerializeTuple, Deserialize, Serialize}; -use std::{collections::BTreeSet, convert::TryFrom, error::Error, fmt}; +use std::{ + collections::{BTreeMap, BTreeSet}, + convert::TryFrom, + error::Error, + fmt, +}; #[cfg(test)] mod tests; @@ -558,6 +563,7 @@ pub enum RawHttpRequestVal { String(String), U64(u64), Array(Vec), + Map(BTreeMap), } /// The reply to an update call. @@ -594,11 +600,15 @@ impl QueryResponseHash { let self_map_representation = match response { HttpQueryResponse::Replied { reply } => { + let map_of_reply = btreemap! { + "arg".to_string() => RawHttpRequestVal::Bytes(reply.arg.0.clone()), + }; + btreemap! { "request_id".to_string() => Bytes(request.id().as_bytes().to_vec()), "status".to_string() => String("replied".to_string()), "timestamp".to_string() => U64(timestamp.as_nanos_since_unix_epoch()), - "reply".to_string() => Bytes(reply.representation_independent_hash().to_vec()), + "reply".to_string() => Map(map_of_reply) } } HttpQueryResponse::Rejected { @@ -622,6 +632,10 @@ impl QueryResponseHash { Self(hash) } + + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } } impl SignedBytesWithoutDomainSeparator for QueryResponseHash { @@ -678,15 +692,6 @@ pub struct HttpQueryResponseReply { pub arg: Blob, } -impl HttpQueryResponseReply { - /// Returns the representation-independent hash. - pub fn representation_independent_hash(&self) -> [u8; 32] { - hash_of_map(&btreemap! { - "arg".to_string() => RawHttpRequestVal::Bytes(self.arg.0.clone()), - }) - } -} - /// The response to a `read_state` request. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct HttpReadStateResponse { diff --git a/rs/types/types/src/messages/http/tests.rs b/rs/types/types/src/messages/http/tests.rs index 354267d1426..8cbd1d8c951 100644 --- a/rs/types/types/src/messages/http/tests.rs +++ b/rs/types/types/src/messages/http/tests.rs @@ -333,7 +333,7 @@ mod try_from { } } - mod query { + pub(super) mod query { use super::super::to_blob; use super::*; use crate::messages::http::{ @@ -354,7 +354,7 @@ mod try_from { } } - fn default_user_query_content() -> UserQuery { + pub fn default_user_query_content() -> UserQuery { UserQuery { source: UserId::from(fixed::principal_id()), receiver: fixed::canister_id(), @@ -551,6 +551,55 @@ mod try_from { pub use fixed_test_values as fixed; } +mod hashing { + use super::try_from::query; + use crate::{ + messages::{Blob, HttpQueryResponse, HttpQueryResponseReply, QueryResponseHash}, + Time, + }; + use hex_literal::hex; + + #[test] + fn hashing_query_response_reply() { + let time = 2614; + let query_response = HttpQueryResponse::Replied { + reply: HttpQueryResponseReply { + arg: Blob(b"some_bytes".to_vec()), + }, + }; + let user_query = query::default_user_query_content(); + let query_response_hash = QueryResponseHash::new( + &query_response, + &user_query, + Time::from_nanos_since_unix_epoch(time), + ); + assert_eq!( + query_response_hash.as_bytes(), + &hex!("7e94e73d1647506682a6300385bc99a63d1ef655222e5a3235f784ca3e80dca4") + ); + } + + #[test] + fn hashing_query_response_reject() { + let time = 2614; + let query_response = HttpQueryResponse::Rejected { + reject_code: 1, + reject_message: "system error".to_string(), + error_code: "IC500".to_string(), + }; + let user_query = query::default_user_query_content(); + let query_response_hash = QueryResponseHash::new( + &query_response, + &user_query, + Time::from_nanos_since_unix_epoch(time), + ); + assert_eq!( + query_response_hash.as_bytes(), + &hex!("bd80f930dfd3eafdf2d5c03031da9b5ec62963701abcb217d31c84005bd8db87") + ); + } +} + mod cbor_serialization { use crate::{ messages::{ diff --git a/rs/types/types/src/messages/message_id.rs b/rs/types/types/src/messages/message_id.rs index b6ec7c9cb24..ec1c7d4188c 100644 --- a/rs/types/types/src/messages/message_id.rs +++ b/rs/types/types/src/messages/message_id.rs @@ -110,12 +110,12 @@ impl From<[u8; EXPECTED_MESSAGE_ID_LENGTH]> for MessageId { } } -fn hash_string(value: String) -> Vec { - Sha256::hash(&value.into_bytes()).to_vec() +fn hash_string(value: &str) -> Vec { + Sha256::hash(value.as_bytes()).to_vec() } -fn hash_bytes(value: Vec) -> Vec { - Sha256::hash(&value).to_vec() +fn hash_bytes>(value: T) -> Vec { + Sha256::hash(value.as_ref()).to_vec() } fn hash_u64(value: u64) -> Vec { @@ -138,31 +138,32 @@ fn hash_u64(value: u64) -> Vec { } } - hash_bytes(buf[..=i].to_vec()) + hash_bytes(&buf[..=i]) } // arrays, encoded as the concatenation of the hashes of the encodings of the // array elements. -fn hash_array(elements: Vec) -> Vec { +fn hash_array(elements: &[RawHttpRequestVal]) -> Vec { let mut hasher = Sha256::new(); elements - .into_iter() + .iter() // Hash the encoding of all the array elements. .for_each(|e| hasher.write(hash_val(e).as_slice())); hasher.finish().to_vec() // hash the concatenation of the hashes. } -fn hash_val(val: RawHttpRequestVal) -> Vec { +fn hash_val(val: &RawHttpRequestVal) -> Vec { match val { RawHttpRequestVal::String(string) => hash_string(string), RawHttpRequestVal::Bytes(bytes) => hash_bytes(bytes), - RawHttpRequestVal::U64(integer) => hash_u64(integer), + RawHttpRequestVal::U64(integer) => hash_u64(*integer), RawHttpRequestVal::Array(elements) => hash_array(elements), + RawHttpRequestVal::Map(map) => hash_of_map(map).to_vec(), } } -fn hash_key_val(key: String, val: RawHttpRequestVal) -> Vec { - let mut key_hash = hash_string(key); +fn hash_key_val(key: String, val: &RawHttpRequestVal) -> Vec { + let mut key_hash = hash_string(&key); let mut val_hash = hash_val(val); key_hash.append(&mut val_hash); key_hash @@ -172,7 +173,7 @@ fn hash_key_val(key: String, val: RawHttpRequestVal) -> Vec { pub(crate) fn hash_of_map(map: &BTreeMap) -> [u8; 32] { let mut hashes: Vec> = Vec::new(); for (key, val) in map.iter() { - hashes.push(hash_key_val(key.to_string(), val.clone())); + hashes.push(hash_key_val(key.to_string(), val)); } // Computes hash by first sorting by "field name" hash, which is the @@ -250,12 +251,43 @@ mod tests { use crate::{time::expiry_time_from_now, CanisterId, PrincipalId, Time}; use hex_literal::hex; + #[test] + /// The test covers all the supported values of `RawHttpRequestVal` and calculates the `hash_of_map` for a nested map. + /// The expected hash serves as a guard against any future changes to the `hash_of_map` function, ensuring its stability. + fn test_hash_of_map() { + use maplit::btreemap; + use RawHttpRequestVal::*; + + let inner_map_0 = btreemap! { + "key_string_0".to_string() => String("test_string_0".to_string()), + }; + let inner_map_1 = btreemap! { + "key_string_1".to_string() => String("test_string_1".to_string()), + }; + let inner_map_2 = btreemap! { + "key_string_2".to_string() => String("test_string_2".to_string()), + }; + + let outer_map = btreemap! { + "key_bytes".to_string() => Bytes(vec![1; 10]), + "key_string".to_string() => String("test_string".to_string()), + "key_u64".to_string() => U64(42), + "key_array".to_string() => Array(vec![Map(inner_map_0), Map(inner_map_1)]), + "key_inner".to_string() => Map(inner_map_2) + }; + + assert_eq!( + hash_of_map(&outer_map), + hex!("ace3c6e84b170c6235faff2ee1152d831c332a7e3c932fb7d129f973d6913ff2") + ); + } + #[test] fn message_id_icf_key_val_reference_1() { assert_eq!( hash_key_val( "request_type".to_string(), - RawHttpRequestVal::String("call".to_string()) + &RawHttpRequestVal::String("call".to_string()) ), hex!( " @@ -291,7 +323,7 @@ mod tests { #[test] fn message_id_string_reference_1() { assert_eq!( - hash_string("request_type".to_string()), + hash_string("request_type"), hex!("769e6f87bdda39c859642b74ce9763cdd37cb1cd672733e8c54efaa33ab78af9"), ); } @@ -299,7 +331,7 @@ mod tests { #[test] fn message_id_string_reference_2() { assert_eq!( - hash_string("call".to_string()), + hash_string("call"), hex!("7edb360f06acaef2cc80dba16cf563f199d347db4443da04da0c8173e3f9e4ed"), ); } @@ -307,7 +339,7 @@ mod tests { #[test] fn message_id_string_reference_3() { assert_eq!( - hash_string("callee".to_string()), + hash_string("callee"), hex!("92ca4c0ced628df1e7b9f336416ead190bd0348615b6f71a64b21d1b68d4e7e2"), ); } @@ -315,7 +347,7 @@ mod tests { #[test] fn message_id_string_reference_4() { assert_eq!( - hash_string("method_name".to_string()), + hash_string("method_name"), hex!("293536232cf9231c86002f4ee293176a0179c002daa9fc24be9bb51acdd642b6"), ); } @@ -323,7 +355,7 @@ mod tests { #[test] fn message_id_string_reference_5() { assert_eq!( - hash_string("hello".to_string()), + hash_string("hello"), hex!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"), ); } @@ -331,7 +363,7 @@ mod tests { #[test] fn message_id_string_reference_6() { assert_eq!( - hash_string("arg".to_string()), + hash_string("arg"), hex!("b25f03dedd69be07f356a06fe35c1b0ddc0de77dcd9066c4be0c6bbde14b23ff"), ); } @@ -339,7 +371,7 @@ mod tests { #[test] fn message_id_array_reference_1() { assert_eq!( - hash_array(vec![RawHttpRequestVal::String("a".to_string())]), + hash_array(&[RawHttpRequestVal::String("a".to_string())]), // hash(hash("a")) hex!("bf5d3affb73efd2ec6c36ad3112dd933efed63c4e1cbffcfa88e2759c144f2d8"), ); @@ -348,7 +380,7 @@ mod tests { #[test] fn message_id_array_reference_2() { assert_eq!( - hash_array(vec![ + hash_array(&[ RawHttpRequestVal::String("a".to_string()), RawHttpRequestVal::String("b".to_string()), ]), @@ -360,7 +392,7 @@ mod tests { #[test] fn message_id_array_reference_3() { assert_eq!( - hash_array(vec![ + hash_array(&[ RawHttpRequestVal::Bytes(vec![97]), // "a" as a byte string. RawHttpRequestVal::String("b".to_string()), ]), @@ -372,9 +404,9 @@ mod tests { #[test] fn message_id_array_reference_4() { assert_eq!( - hash_array(vec![RawHttpRequestVal::Array(vec![ - RawHttpRequestVal::String("a".to_string()) - ])]), + hash_array(&[RawHttpRequestVal::Array(vec![RawHttpRequestVal::String( + "a".to_string() + )])]), // hash(hash(hash("a")) hex!("eb48bdfa15fc43dbea3aabb1ee847b6e69232c0f0d9705935e50d60cce77877f"), ); @@ -383,7 +415,7 @@ mod tests { #[test] fn message_id_array_reference_5() { assert_eq!( - hash_array(vec![RawHttpRequestVal::Array(vec![ + hash_array(&[RawHttpRequestVal::Array(vec![ RawHttpRequestVal::String("a".to_string()), RawHttpRequestVal::String("b".to_string()) ])]), @@ -395,7 +427,7 @@ mod tests { #[test] fn message_id_array_reference_6() { assert_eq!( - hash_array(vec![ + hash_array(&[ RawHttpRequestVal::Array(vec![ RawHttpRequestVal::String("a".to_string()), RawHttpRequestVal::String("b".to_string()) @@ -412,7 +444,7 @@ mod tests { assert_eq!( // D I D L \0 \253 *" // 68 73 68 76 0 253 42 - hash_bytes(vec![68, 73, 68, 76, 0, 253, 42]), + hash_bytes([68, 73, 68, 76, 0, 253, 42]), hex!("6c0b2ae49718f6995c02ac5700c9c789d7b7862a0d53e6d40a73f1fcd2f70189") ); }