Skip to main content

hashiverse_lib/protocol/rpc/
rpc_response.rs

1//! # RPC response packet encode / decode
2//!
3//! The outbound-server and inbound-client halves of a response, symmetric to
4//! [`crate::protocol::rpc::rpc_request`]:
5//!
6//! - [`RpcResponsePacketTx`] — server side. `encode` signs the request's
7//!   `pow_content_hash` with the server's private key and embeds the signature in the
8//!   header. The signature binds the response to *this* specific request and *this*
9//!   specific server identity, so clients can reject replays across servers or across
10//!   requests. Payload is optionally compressed.
11//! - [`RpcResponsePacketRx`] — client side. Parses the server-identity fields
12//!   (verification key, PQ commitment, sponsor id, PoW timestamp, PoW hash, salt) from
13//!   the header, re-derives the server id PoW to check it is sufficient, and verifies
14//!   the signature against the request's content hash — all statelessly, so no peer
15//!   database is required to validate the response of a freshly-discovered server.
16
17use crate::protocol::payload::payload::PayloadResponseKind;
18use crate::tools::server_id::ServerId;
19use crate::tools::time::{TimeMillis, TimeMillisBytes, TIME_MILLIS_BYTES};
20use crate::tools::types::{Hash, Id, PQCommitmentBytes, Pow, Salt, Signature, SignatureKey, VerificationKeyBytes, HASH_BYTES, ID_BYTES, PQ_COMMITMENT_BYTES, SALT_BYTES, SIGNATURE_BYTES, VERIFICATION_KEY_BYTES};
21use crate::tools::{compression, config, signing, BytesGatherer};
22use bitflags::bitflags;
23use bytes::{Buf, Bytes};
24
25bitflags! {
26    pub struct RpcResponsePacketTxFlags: u8 {
27        const COMPRESSED = 1 << 0;
28    }
29}
30
31/// The encoder for an outbound RPC response.
32///
33/// `RpcResponsePacketTx` is a type-level tag — its sole associated function, `encode`,
34/// assembles the wire response given the server's identity fields, the `pow_content_hash`
35/// from the corresponding request, and a payload. The server signs the request's
36/// `pow_content_hash` with its [`SignatureKey`] and emits the signature on the response so
37/// the caller can prove the response was produced by the intended destination peer for
38/// *this* specific request — a defence against both response substitution and replayed
39/// responses.
40///
41/// Paired with [`RpcResponsePacketRx`] on the decode side.
42pub struct RpcResponsePacketTx;
43
44impl RpcResponsePacketTx {
45    #[allow(clippy::too_many_arguments)] // protocol layer — each arg is a wire field
46    pub fn encode(
47        server_id_signature_key: &SignatureKey,
48        server_id_verification_key_bytes: &VerificationKeyBytes,
49        server_id_pq_commitment_bytes: &PQCommitmentBytes,
50        server_id_verification_sponsor_id: &Id,
51        server_id_timestamp: &TimeMillis,
52        server_id_hash: &Hash,
53        server_id_salt: &Salt,
54        pow_content_hash: &Hash,
55        flags: RpcResponsePacketTxFlags,
56        payload_response_kind: PayloadResponseKind,
57        payload_uncompressed: BytesGatherer,
58    ) -> anyhow::Result<BytesGatherer> {
59        // Do we actually need to compress this?
60        let payload_compressed: BytesGatherer = match flags.contains(RpcResponsePacketTxFlags::COMPRESSED) {
61            true => compression::compress_for_speed(&payload_uncompressed.to_bytes())?,
62            false => payload_uncompressed,
63        };
64
65        let payload_compressed_len = payload_compressed.len();
66
67        // Check that it is not too large...
68        if payload_compressed_len > config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE {
69            anyhow::bail!("response payload size exceeds maximum allowed size: {} > {}", payload_compressed_len, config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE);
70        }
71
72        let pow_content_hash_signature = signing::sign(server_id_signature_key, pow_content_hash.as_ref());
73
74    // All small header fields go into accumulator
75    let mut result = BytesGatherer::default();
76    result.put_u8(1); // Version = 1 (for now)
77    result.put_u8(flags.bits());
78    result.put_u16_le(payload_response_kind as u16);
79    result.put_slice(server_id_verification_key_bytes.as_ref());
80    result.put_slice(server_id_pq_commitment_bytes.as_ref());
81    result.put_slice(server_id_verification_sponsor_id.as_ref());
82    result.put_slice(server_id_timestamp.encode_be().as_ref());
83    result.put_slice(server_id_hash.as_ref());
84    result.put_slice(server_id_salt.as_ref());
85    result.put_slice(pow_content_hash_signature.as_ref());
86    result.put_u32_le(payload_compressed_len as u32);
87    result.put_bytes_gatherer(payload_compressed);
88
89        Ok(result)
90    }
91}
92
93/// The client-side view of an inbound RPC response, after header parsing, PoW checks, and
94/// signature verification.
95///
96/// By the time an `RpcResponsePacketRx` exists, the decoder has already proved that the
97/// remote server really signed over the request's `pow_content_hash` and that the signing
98/// identity matches the destination the caller intended. Callers see only the
99/// [`PayloadResponseKind`] (so they can pick the right payload deserializer) and the still-
100/// compressed body bytes.
101///
102/// Paired with [`RpcResponsePacketTx`] as the decode side of the same wire format.
103pub struct RpcResponsePacketRx {
104    pub response_request_kind: PayloadResponseKind,
105    pub bytes: Bytes,
106}
107
108impl RpcResponsePacketRx {
109    pub fn decode(destination_id: &Id, pow_content_hash: &Hash, pow_min: Pow, mut response_bytes: Bytes) -> anyhow::Result<Self> {
110        // Do we have enough bytes for the header?
111        if response_bytes.len() < size_of::<u8>() + size_of::<u8>() + size_of::<u16>() + VERIFICATION_KEY_BYTES + PQ_COMMITMENT_BYTES + ID_BYTES + TIME_MILLIS_BYTES + HASH_BYTES + SALT_BYTES + SIGNATURE_BYTES + size_of::<u32>() {
112            anyhow::bail!("RpcResponsePacket is too short for header");
113        }
114
115        let version = response_bytes.get_u8();
116        if 1 != version {
117            anyhow::bail!("Unsupported RpcRequestPacket version: {}", version);
118        }
119
120        let flags = RpcResponsePacketTxFlags::from_bits(response_bytes.get_u8()).ok_or_else(|| anyhow::anyhow!("Invalid RpcResponsePacket flags"))?;
121        let response_request_kind = PayloadResponseKind::from_u16(response_bytes.get_u16_le())?;
122        let server_id_verification_key = VerificationKeyBytes(response_bytes.slice(..VERIFICATION_KEY_BYTES).as_ref().try_into()?);
123        response_bytes.advance(VERIFICATION_KEY_BYTES);
124        let server_id_pq_commitment_bytes = PQCommitmentBytes(response_bytes.slice(..PQ_COMMITMENT_BYTES).as_ref().try_into()?);
125        response_bytes.advance(PQ_COMMITMENT_BYTES);
126        let server_id_verification_sponsor_id = Id(response_bytes.slice(..ID_BYTES).as_ref().try_into()?);
127        response_bytes.advance(ID_BYTES);
128        let server_id_verification_timestamp_bytes: TimeMillisBytes = TimeMillisBytes(response_bytes.slice(..TIME_MILLIS_BYTES).as_ref().try_into()?);
129        response_bytes.advance(TIME_MILLIS_BYTES);
130        let server_id_verification_hash = Hash(response_bytes.slice(..HASH_BYTES).as_ref().try_into()?);
131        response_bytes.advance(HASH_BYTES);
132        let server_id_verification_salt = Salt(response_bytes.slice(..SALT_BYTES).as_ref().try_into()?);
133        response_bytes.advance(SALT_BYTES);
134
135        let pow_content_hash_signature: Signature = Signature(response_bytes.slice(..SIGNATURE_BYTES).as_ref().try_into()?);
136        response_bytes.advance(SIGNATURE_BYTES);
137
138        let response_payload_len = response_bytes.get_u32_le() as usize;
139
140        if response_payload_len > config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE {
141            anyhow::bail!("RpcResponsePacket payload too large: {} > {}", response_payload_len, config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE);
142        }
143
144        // Do we have enough bytes for the payload?
145        if response_bytes.len() < response_payload_len {
146            anyhow::bail!("RpcResponsePacket is too short for payload");
147        }
148
149        let response_payload = response_bytes.slice(..response_payload_len);
150        response_bytes.advance(response_payload_len);
151
152        // Sanity check - did we use all the packet?
153        if !response_bytes.is_empty() {
154            anyhow::bail!("RpcResponsePacket is too long");
155        }
156
157        // Ensure that the server id is pow sufficient (server identity PoW uses Id::zero() as sponsor)
158        let (pow, pow_hash) = ServerId::pow_measure(
159            &server_id_verification_sponsor_id,
160            &server_id_verification_key,
161            &server_id_pq_commitment_bytes,
162            &server_id_verification_timestamp_bytes,
163            &server_id_verification_hash,
164            &server_id_verification_salt,
165        )?;
166        if pow < pow_min {
167            anyhow::bail!(format!("Server ID pow is not sufficient: {} < {}", pow, pow_min));
168        }
169
170        // Let's check that the server is who they say they are - unless of course we were querying Id::zero()
171        let id = ServerId::server_pow_hash_to_id(pow_hash)?;
172        if id != *destination_id {
173            if !destination_id.is_zero() {
174                anyhow::bail!("Server ID verification failed");
175            }
176        }
177
178        // Check that the server has signed our initial content hash
179        let verification_key = server_id_verification_key.to_verification_key()?;
180        signing::verify(&verification_key, &pow_content_hash_signature, pow_content_hash.as_ref())?;
181
182        // Do we need to decompress?
183        let response_payload_decompressed = match flags.contains(RpcResponsePacketTxFlags::COMPRESSED) {
184            true => compression::decompress(response_payload.as_ref())?.to_bytes(),
185            false => response_payload,
186        };
187
188        Ok(Self {
189            response_request_kind,
190            bytes: response_payload_decompressed,
191        })
192    }
193}