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