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    pub fn encode(
46        server_id_signature_key: &SignatureKey,
47        server_id_verification_key_bytes: &VerificationKeyBytes,
48        server_id_pq_commitment_bytes: &PQCommitmentBytes,
49        server_id_verification_sponsor_id: &Id,
50        server_id_timestamp: &TimeMillis,
51        server_id_hash: &Hash,
52        server_id_salt: &Salt,
53        pow_content_hash: &Hash,
54        flags: RpcResponsePacketTxFlags,
55        payload_response_kind: PayloadResponseKind,
56        payload_uncompressed: BytesGatherer,
57    ) -> anyhow::Result<BytesGatherer> {
58        // Do we actually need to compress this?
59        let payload_compressed: BytesGatherer = match flags.contains(RpcResponsePacketTxFlags::COMPRESSED) {
60            true => compression::compress_for_speed(&payload_uncompressed.to_bytes())?,
61            false => payload_uncompressed,
62        };
63
64        let payload_compressed_len = payload_compressed.len();
65
66        // Check that it is not too large...
67        if payload_compressed_len > config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE {
68            anyhow::bail!("response payload size exceeds maximum allowed size: {} > {}", payload_compressed_len, config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE);
69        }
70
71        let pow_content_hash_signature = signing::sign(server_id_signature_key, pow_content_hash.as_ref());
72
73    // All small header fields go into accumulator
74    let mut result = BytesGatherer::default();
75    result.put_u8(1); // Version = 1 (for now)
76    result.put_u8(flags.bits());
77    result.put_u16_le(payload_response_kind as u16);
78    result.put_slice(server_id_verification_key_bytes.as_ref());
79    result.put_slice(server_id_pq_commitment_bytes.as_ref());
80    result.put_slice(server_id_verification_sponsor_id.as_ref());
81    result.put_slice(server_id_timestamp.encode_be().as_ref());
82    result.put_slice(server_id_hash.as_ref());
83    result.put_slice(server_id_salt.as_ref());
84    result.put_slice(pow_content_hash_signature.as_ref());
85    result.put_u32_le(payload_compressed_len as u32);
86    result.put_bytes_gatherer(payload_compressed);
87
88        Ok(result)
89    }
90}
91
92/// The client-side view of an inbound RPC response, after header parsing, PoW checks, and
93/// signature verification.
94///
95/// By the time an `RpcResponsePacketRx` exists, the decoder has already proved that the
96/// remote server really signed over the request's `pow_content_hash` and that the signing
97/// identity matches the destination the caller intended. Callers see only the
98/// [`PayloadResponseKind`] (so they can pick the right payload deserializer) and the still-
99/// compressed body bytes.
100///
101/// Paired with [`RpcResponsePacketTx`] as the decode side of the same wire format.
102pub struct RpcResponsePacketRx {
103    pub response_request_kind: PayloadResponseKind,
104    pub bytes: Bytes,
105}
106
107impl RpcResponsePacketRx {
108    pub fn decode(destination_id: &Id, pow_content_hash: &Hash, pow_min: Pow, mut response_bytes: Bytes) -> anyhow::Result<Self> {
109        // Do we have enough bytes for the header?
110        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>() {
111            anyhow::bail!("RpcResponsePacket is too short for header");
112        }
113
114        let version = response_bytes.get_u8();
115        if 1 != version {
116            anyhow::bail!("Unsupported RpcRequestPacket version: {}", version);
117        }
118
119        let flags = RpcResponsePacketTxFlags::from_bits(response_bytes.get_u8()).ok_or_else(|| anyhow::anyhow!("Invalid RpcResponsePacket flags"))?;
120        let response_request_kind = PayloadResponseKind::from_u16(response_bytes.get_u16_le())?;
121        let server_id_verification_key = VerificationKeyBytes(response_bytes.slice(..VERIFICATION_KEY_BYTES).as_ref().try_into()?);
122        response_bytes.advance(VERIFICATION_KEY_BYTES);
123        let server_id_pq_commitment_bytes = PQCommitmentBytes(response_bytes.slice(..PQ_COMMITMENT_BYTES).as_ref().try_into()?);
124        response_bytes.advance(PQ_COMMITMENT_BYTES);
125        let server_id_verification_sponsor_id = Id(response_bytes.slice(..ID_BYTES).as_ref().try_into()?);
126        response_bytes.advance(ID_BYTES);
127        let server_id_verification_timestamp_bytes: TimeMillisBytes = TimeMillisBytes(response_bytes.slice(..TIME_MILLIS_BYTES).as_ref().try_into()?);
128        response_bytes.advance(TIME_MILLIS_BYTES);
129        let server_id_verification_hash = Hash(response_bytes.slice(..HASH_BYTES).as_ref().try_into()?);
130        response_bytes.advance(HASH_BYTES);
131        let server_id_verification_salt = Salt(response_bytes.slice(..SALT_BYTES).as_ref().try_into()?);
132        response_bytes.advance(SALT_BYTES);
133
134        let pow_content_hash_signature: Signature = Signature(response_bytes.slice(..SIGNATURE_BYTES).as_ref().try_into()?);
135        response_bytes.advance(SIGNATURE_BYTES);
136
137        let response_payload_len = response_bytes.get_u32_le() as usize;
138
139        if response_payload_len > config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE {
140            anyhow::bail!("RpcResponsePacket payload too large: {} > {}", response_payload_len, config::PROTOCOL_MAX_BLOB_SIZE_RESPONSE);
141        }
142
143        // Do we have enough bytes for the payload?
144        if response_bytes.len() < response_payload_len {
145            anyhow::bail!("RpcResponsePacket is too short for payload");
146        }
147
148        let response_payload = response_bytes.slice(..response_payload_len);
149        response_bytes.advance(response_payload_len);
150
151        // Sanity check - did we use all the packet?
152        if !response_bytes.is_empty() {
153            anyhow::bail!("RpcResponsePacket is too long");
154        }
155
156        // Ensure that the server id is pow sufficient (server identity PoW uses Id::zero() as sponsor)
157        let (pow, pow_hash) = ServerId::pow_measure(
158            &server_id_verification_sponsor_id,
159            &server_id_verification_key,
160            &server_id_pq_commitment_bytes,
161            &server_id_verification_timestamp_bytes,
162            &server_id_verification_hash,
163            &server_id_verification_salt,
164        )?;
165        if pow < pow_min {
166            anyhow::bail!(format!("Server ID pow is not sufficient: {} < {}", pow, pow_min));
167        }
168
169        // Let's check that the server is who they say they are - unless of course we were querying Id::zero()
170        let id = ServerId::server_pow_hash_to_id(pow_hash)?;
171        if id != *destination_id {
172            if !destination_id.is_zero() {
173                anyhow::bail!("Server ID verification failed");
174            }
175        }
176
177        // Check that the server has signed our initial content hash
178        let verification_key = server_id_verification_key.to_verification_key()?;
179        signing::verify(&verification_key, &pow_content_hash_signature, pow_content_hash.as_ref())?;
180
181        // Do we need to decompress?
182        let response_payload_decompressed = match flags.contains(RpcResponsePacketTxFlags::COMPRESSED) {
183            true => compression::decompress(response_payload.as_ref())?.to_bytes(),
184            false => response_payload,
185        };
186
187        Ok(Self {
188            response_request_kind,
189            bytes: response_payload_decompressed,
190        })
191    }
192}