1use crate::{
25 metrics::rpc_methods,
26 pool::HopDataPool,
27 runtime_api,
28 types::{
29 submit_signing_payload, HopError, HopHash, PoolStatus, Recipient, RecipientVec,
30 SubmitResult, MAX_RECIPIENTS,
31 },
32};
33use codec::Decode;
34use jsonrpsee::{
35 core::{async_trait, RpcResult},
36 proc_macros::rpc,
37};
38use sp_api::CallApiAt;
39use sp_blockchain::HeaderBackend;
40use sp_core::{Bytes, H256};
41use sp_crypto_hashing::blake2_256;
42use sp_runtime::{
43 traits::{Block as BlockT, IdentifyAccount, Verify},
44 AccountId32, MultiSignature, MultiSigner,
45};
46use std::{marker::PhantomData, sync::Arc};
47
48#[rpc(client, server)]
50pub trait HopApi<BlockHash> {
51 #[method(name = "hop_submit", blocking)]
69 fn submit(
70 &self,
71 data: Bytes,
72 recipients: Vec<Bytes>,
73 signature: Bytes,
74 signer: Bytes,
75 submit_timestamp: u64,
76 ) -> RpcResult<SubmitResult>;
77
78 #[method(name = "hop_claim", blocking)]
97 fn claim(&self, raw_hash: Bytes, signature: Bytes) -> RpcResult<Bytes>;
98
99 #[method(name = "hop_ack", blocking)]
111 fn ack(&self, raw_hash: Bytes, signature: Bytes) -> RpcResult<()>;
112
113 #[method(name = "hop_poolStatus")]
118 fn pool_status(&self) -> RpcResult<PoolStatus>;
119}
120
121pub struct HopRpcServer<C, Block> {
123 pool: Arc<HopDataPool>,
124 client: Arc<C>,
125 _phantom: PhantomData<Block>,
126}
127
128impl<C, Block> HopRpcServer<C, Block> {
129 pub fn new(pool: Arc<HopDataPool>, client: Arc<C>) -> Self {
131 Self { pool, client, _phantom: Default::default() }
132 }
133
134 fn decode_hash(bytes: Bytes) -> Result<HopHash, HopError> {
136 let hash_bytes: [u8; 32] = bytes
137 .0
138 .as_slice()
139 .try_into()
140 .map_err(|_| HopError::InvalidHashLength(bytes.0.len()))?;
141 Ok(HopHash::from(hash_bytes))
142 }
143}
144
145#[async_trait]
146impl<C, Block> HopApiServer<<Block as BlockT>::Hash> for HopRpcServer<C, Block>
147where
148 Block: BlockT,
149 C: HeaderBackend<Block> + CallApiAt<Block> + Send + Sync + 'static,
150{
151 fn submit(
152 &self,
153 data: Bytes,
154 recipients: Vec<Bytes>,
155 signature: Bytes,
156 signer: Bytes,
157 submit_timestamp: u64,
158 ) -> RpcResult<SubmitResult> {
159 let result = self.do_submit(data, recipients, signature, signer, submit_timestamp);
160 if let Err(e) = &result {
161 self.pool.metrics().record_rpc_error(rpc_methods::SUBMIT, e);
162 }
163 Ok(result?)
164 }
165
166 fn claim(&self, raw_hash: Bytes, signature: Bytes) -> RpcResult<Bytes> {
167 let result = Self::decode_hash(raw_hash)
168 .and_then(|hash| self.pool.claim(&hash, &signature.0).map(Bytes));
169 if let Err(e) = &result {
170 self.pool.metrics().record_rpc_error(rpc_methods::CLAIM, e);
171 }
172 Ok(result?)
173 }
174
175 fn ack(&self, raw_hash: Bytes, signature: Bytes) -> RpcResult<()> {
176 let result =
177 Self::decode_hash(raw_hash).and_then(|hash| self.pool.ack(&hash, &signature.0));
178 if let Err(e) = &result {
179 self.pool.metrics().record_rpc_error(rpc_methods::ACK, e);
180 }
181 Ok(result?)
182 }
183
184 fn pool_status(&self) -> RpcResult<PoolStatus> {
185 Ok(self.pool.status())
186 }
187}
188
189impl<C, Block> HopRpcServer<C, Block>
190where
191 Block: BlockT,
192 C: HeaderBackend<Block> + CallApiAt<Block> + Send + Sync + 'static,
193{
194 fn do_submit(
195 &self,
196 data: Bytes,
197 recipients: Vec<Bytes>,
198 signature: Bytes,
199 signer: Bytes,
200 submit_timestamp: u64,
201 ) -> Result<SubmitResult, HopError> {
202 let recipient_keys: RecipientVec = recipients
203 .into_iter()
204 .map(|r| {
205 MultiSigner::decode(&mut &r.0[..])
206 .map(|signer| Recipient { signer, claimed: false })
207 .map_err(|_| HopError::InvalidRecipientKey)
208 })
209 .collect::<Result<Vec<_>, _>>()?
210 .try_into()
211 .map_err(|v: Vec<Recipient>| HopError::TooManyRecipients {
212 provided: v.len(),
213 limit: MAX_RECIPIENTS as usize,
214 })?;
215
216 let signer =
217 MultiSigner::decode(&mut &signer.0[..]).map_err(|_| HopError::InvalidSigner)?;
218 let multi_sig = MultiSignature::decode(&mut &signature.0[..])
219 .map_err(|_| HopError::InvalidSignature)?;
220
221 let chain_info = self.client.info();
222 let best_hash = chain_info.best_hash;
223
224 let data_len = data.0.len();
225
226 let runtime_max = runtime_api::max_promotion_size::<Block, _>(&*self.client, best_hash)
230 .map_err(HopError::from)?;
231 if data_len > runtime_max as usize {
232 return Err(HopError::DataTooLarge(data_len, runtime_max));
233 }
234
235 let account_id: AccountId32 = signer.clone().into_account();
240 let authorized = runtime_api::can_account_promote::<Block, _>(
241 &*self.client,
242 best_hash,
243 account_id.clone(),
244 data_len as u32,
245 )
246 .map_err(HopError::from)?;
247 if !authorized {
248 return Err(HopError::NotAuthorized);
249 }
250
251 let hash = H256(blake2_256(&data.0));
255 let submit_payload = submit_signing_payload(&hash, submit_timestamp);
256 if !multi_sig.verify(&submit_payload[..], &account_id) {
257 return Err(HopError::InvalidSignature);
258 }
259
260 let sender_id: [u8; 32] = account_id.into();
261 self.pool
262 .insert(data.0, recipient_keys, sender_id, signer, multi_sig, submit_timestamp)?;
263 Ok(SubmitResult { pool_status: self.pool.status() })
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::pool::HopDataPool;
271 use codec::Encode;
272 use sp_api::{ApiError, CallApiAtParams};
273 use sp_blockchain::{self, Info};
274 use sp_core::{crypto::Pair, ed25519};
275 use sp_runtime::{
276 traits::{HashingFor, NumberFor},
277 MultiSigner,
278 };
279 use sp_state_machine::InMemoryBackend;
280 use sp_test_primitives::Block;
281 use std::sync::atomic::{AtomicBool, Ordering};
282 use tempfile::TempDir;
283
284 struct MockClient {
285 authorized: AtomicBool,
286 }
287
288 impl MockClient {
289 fn new(authorized: bool) -> Self {
290 Self { authorized: AtomicBool::new(authorized) }
291 }
292 }
293
294 impl HeaderBackend<Block> for MockClient {
295 fn header(
296 &self,
297 _hash: <Block as BlockT>::Hash,
298 ) -> sp_blockchain::Result<Option<<Block as BlockT>::Header>> {
299 Ok(None)
300 }
301
302 fn info(&self) -> Info<Block> {
303 Info {
304 best_hash: Default::default(),
305 best_number: 0u64,
306 genesis_hash: Default::default(),
307 finalized_hash: Default::default(),
308 finalized_number: 0u64,
309 finalized_state: None,
310 number_leaves: 0,
311 block_gap: None,
312 }
313 }
314
315 fn status(
316 &self,
317 _hash: <Block as BlockT>::Hash,
318 ) -> sp_blockchain::Result<sp_blockchain::BlockStatus> {
319 Ok(sp_blockchain::BlockStatus::Unknown)
320 }
321
322 fn number(
323 &self,
324 _hash: <Block as BlockT>::Hash,
325 ) -> sp_blockchain::Result<Option<NumberFor<Block>>> {
326 Ok(None)
327 }
328
329 fn hash(
330 &self,
331 _number: NumberFor<Block>,
332 ) -> sp_blockchain::Result<Option<<Block as BlockT>::Hash>> {
333 Ok(None)
334 }
335 }
336
337 impl CallApiAt<Block> for MockClient {
338 type StateBackend = InMemoryBackend<HashingFor<Block>>;
339
340 fn call_api_at(&self, params: CallApiAtParams<Block>) -> Result<Vec<u8>, ApiError> {
341 match params.function {
342 "HopRuntimeApi_max_promotion_size" => Ok((2u32 * 1024 * 1024).encode()),
343 "HopRuntimeApi_can_account_promote" => {
344 Ok(self.authorized.load(Ordering::Relaxed).encode())
345 },
346 "HopRuntimeApi_is_promoted_on_chain" => Ok(false.encode()),
347 other => Err(ApiError::Application(
348 format!("MockClient: unimplemented runtime API call {}", other).into(),
349 )),
350 }
351 }
352
353 fn runtime_version_at(
354 &self,
355 _at_hash: <Block as BlockT>::Hash,
356 _call_context: sp_api::CallContext,
357 ) -> Result<sp_version::RuntimeVersion, ApiError> {
358 unimplemented!("MockClient::runtime_version_at not used by tests")
359 }
360
361 fn state_at(&self, _at: <Block as BlockT>::Hash) -> Result<Self::StateBackend, ApiError> {
362 unimplemented!("MockClient::state_at not used by tests")
363 }
364
365 fn initialize_extensions(
366 &self,
367 _at: <Block as BlockT>::Hash,
368 _extensions: &mut sp_externalities::Extensions,
369 ) -> Result<(), ApiError> {
370 Ok(())
371 }
372 }
373
374 fn setup(authorized: bool) -> (HopRpcServer<MockClient, Block>, Arc<HopDataPool>, TempDir) {
375 let dir = TempDir::new().unwrap();
376 let pool = Arc::new(
377 HopDataPool::new(
378 1024 * 1024,
379 1024 * 1024,
380 100,
381 dir.path().to_path_buf(),
382 crate::rate_limit::RateLimitConfig::disabled(),
383 crate::metrics::HopMetrics::disabled(),
384 )
385 .unwrap(),
386 );
387 let client = Arc::new(MockClient::new(authorized));
388 let rpc = HopRpcServer::new(pool.clone(), client);
389 (rpc, pool, dir)
390 }
391
392 fn setup_metered() -> (HopRpcServer<MockClient, Block>, Arc<HopDataPool>, TempDir) {
394 let registry = prometheus_endpoint::Registry::new();
395 let dir = TempDir::new().unwrap();
396 let pool = Arc::new(
397 HopDataPool::new(
398 1024 * 1024,
399 1024 * 1024,
400 100,
401 dir.path().to_path_buf(),
402 crate::rate_limit::RateLimitConfig::disabled(),
403 crate::metrics::HopMetrics::new(Some(®istry)).unwrap(),
404 )
405 .unwrap(),
406 );
407 let client = Arc::new(MockClient::new(true));
408 let rpc = HopRpcServer::new(pool.clone(), client);
409 (rpc, pool, dir)
410 }
411
412 fn make_keypair() -> (ed25519::Pair, MultiSigner) {
413 let pair = ed25519::Pair::from_seed(&[1u8; 32]);
414 let signer = MultiSigner::Ed25519(pair.public());
415 (pair, signer)
416 }
417
418 const TEST_SUBMIT_TS: u64 = 1_700_000_000_000;
420
421 fn submit_sig(pair: &ed25519::Pair, data: &[u8], submit_timestamp: u64) -> Bytes {
423 let hash = H256(blake2_256(data));
424 let payload = submit_signing_payload(&hash, submit_timestamp);
425 let multi_sig = MultiSignature::Ed25519(pair.sign(&payload));
426 Bytes(multi_sig.encode())
427 }
428
429 fn claim_sig(pair: &ed25519::Pair, hash: &H256) -> Bytes {
430 use crate::types::{signing_payload, HOP_CLAIM_CONTEXT};
431 let payload = signing_payload(HOP_CLAIM_CONTEXT, hash);
432 Bytes(MultiSignature::Ed25519(pair.sign(&payload)).encode())
433 }
434
435 fn ack_sig(pair: &ed25519::Pair, hash: &H256) -> Bytes {
436 use crate::types::{signing_payload, HOP_ACK_CONTEXT};
437 let payload = signing_payload(HOP_ACK_CONTEXT, hash);
438 Bytes(MultiSignature::Ed25519(pair.sign(&payload)).encode())
439 }
440
441 #[test]
442 fn rpc_metrics_record_errors() {
443 let (rpc, pool, _dir) = setup_metered();
444 let (pair, _) = make_keypair();
445
446 let unknown = H256([9u8; 32]);
448 assert!(rpc.claim(Bytes(unknown.0.to_vec()), claim_sig(&pair, &unknown)).is_err());
449 assert_eq!(pool.metrics().rpc_error_count(rpc_methods::CLAIM, "not_found"), 1);
450
451 assert!(rpc.ack(Bytes(vec![0u8; 4]), Bytes(vec![0u8; 64])).is_err());
453 assert_eq!(pool.metrics().rpc_error_count(rpc_methods::ACK, "invalid_hash_length"), 1);
454 }
455
456 #[test]
457 fn submit_invalid_scale_signer_returns_error() {
458 let (rpc, _, _dir) = setup(true);
459 let (_, valid_signer) = make_keypair();
462 let result = rpc.submit(
463 Bytes(vec![1, 2, 3]),
464 vec![Bytes(valid_signer.encode())],
465 Bytes(vec![0u8; 3]),
466 Bytes(vec![0u8; 3]),
467 TEST_SUBMIT_TS,
468 );
469 assert!(result.is_err());
470 let err = result.unwrap_err();
471 assert!(err.message().contains("SCALE-decode MultiSigner"), "got: {}", err.message());
472 }
473
474 #[test]
475 fn submit_invalid_scale_signature_returns_error() {
476 let (rpc, _, _dir) = setup(true);
477 let (_, signer) = make_keypair();
478 let result = rpc.submit(
479 Bytes(vec![1, 2, 3]),
480 vec![Bytes(signer.encode())],
481 Bytes(vec![0u8; 3]),
482 Bytes(signer.encode()),
483 TEST_SUBMIT_TS,
484 );
485 assert!(result.is_err());
486 let err = result.unwrap_err();
487 assert!(err.message().contains("Invalid signature"), "got: {}", err.message());
488 }
489
490 #[test]
491 fn submit_bad_signature_returns_error() {
492 let (rpc, _, _dir) = setup(true);
493 let (_, signer) = make_keypair();
494 let wrong_pair = ed25519::Pair::from_seed(&[99u8; 32]);
496 let data = vec![1, 2, 3];
497 let sig = submit_sig(&wrong_pair, &data, TEST_SUBMIT_TS);
498
499 let result = rpc.submit(
500 Bytes(data),
501 vec![Bytes(signer.encode())],
502 sig,
503 Bytes(signer.encode()),
504 TEST_SUBMIT_TS,
505 );
506 assert!(result.is_err());
507 let err = result.unwrap_err();
508 assert!(err.message().contains("Invalid signature"), "got: {}", err.message());
509 }
510
511 #[test]
512 fn submit_unauthorized_account_returns_error() {
513 let (rpc, _, _dir) = setup(false);
514 let (pair, signer) = make_keypair();
515 let data = vec![1, 2, 3];
516 let sig = submit_sig(&pair, &data, TEST_SUBMIT_TS);
517
518 let result = rpc.submit(
519 Bytes(data),
520 vec![Bytes(signer.encode())],
521 sig,
522 Bytes(signer.encode()),
523 TEST_SUBMIT_TS,
524 );
525 assert!(result.is_err());
526 let err = result.unwrap_err();
527 assert!(err.message().contains("authorization"), "got: {}", err.message());
528 }
529
530 #[test]
531 fn submit_success() {
532 let (rpc, pool, _dir) = setup(true);
533 let (pair, signer) = make_keypair();
534 let data = vec![1, 2, 3, 4, 5];
535 let sig = submit_sig(&pair, &data, TEST_SUBMIT_TS);
536
537 let result = rpc.submit(
538 Bytes(data),
539 vec![Bytes(signer.encode())],
540 sig,
541 Bytes(signer.encode()),
542 TEST_SUBMIT_TS,
543 );
544 assert!(result.is_ok(), "submit failed: {:?}", result.err());
545 let submit_result = result.unwrap();
546 assert_eq!(submit_result.pool_status.entry_count, 1);
547 assert_eq!(submit_result.pool_status.total_bytes, crate::types::entry_accounted_size(5, 1),);
549 assert_eq!(pool.status().entry_count, 1);
550 }
551
552 #[test]
553 fn submit_rejects_oversized_recipient_list() {
554 let (rpc, _, _dir) = setup(true);
555 let (pair, signer) = make_keypair();
556 let data = vec![1, 2, 3];
557 let sig = submit_sig(&pair, &data, TEST_SUBMIT_TS);
558
559 let oversized: Vec<Bytes> = std::iter::repeat_with(|| Bytes(signer.encode()))
560 .take(MAX_RECIPIENTS as usize + 1)
561 .collect();
562
563 let result =
564 rpc.submit(Bytes(data), oversized, sig, Bytes(signer.encode()), TEST_SUBMIT_TS);
565 assert!(result.is_err());
566 let err = result.unwrap_err();
567 assert!(err.message().contains("Too many recipients"), "got: {}", err.message());
568 }
569
570 #[test]
571 fn claim_invalid_hash_length() {
572 let (rpc, _, _dir) = setup(true);
573 let result = rpc.claim(Bytes(vec![0u8; 31]), Bytes(vec![0u8; 64]));
574 assert!(result.is_err());
575 let err = result.unwrap_err();
576 assert!(err.message().contains("expected 32 bytes"), "got: {}", err.message());
577 }
578
579 #[test]
580 fn claim_and_ack_through_rpc() {
581 let (rpc, _, _dir) = setup(true);
582 let (pair, signer) = make_keypair();
583 let data = vec![10, 20, 30];
584 let sig = submit_sig(&pair, &data, TEST_SUBMIT_TS);
585
586 rpc.submit(
587 Bytes(data.clone()),
588 vec![Bytes(signer.encode())],
589 sig,
590 Bytes(signer.encode()),
591 TEST_SUBMIT_TS,
592 )
593 .unwrap();
594
595 let hash = H256(blake2_256(&data));
596 let claimed = rpc.claim(Bytes(hash.0.to_vec()), claim_sig(&pair, &hash)).unwrap();
597 assert_eq!(claimed.0, data);
598
599 rpc.ack(Bytes(hash.0.to_vec()), ack_sig(&pair, &hash)).unwrap();
600
601 let status = rpc.pool_status().unwrap();
602 assert_eq!(status.entry_count, 0);
603 }
604
605 #[test]
606 fn pool_status_returns_correct_values() {
607 let (rpc, _, _dir) = setup(true);
608 let status = rpc.pool_status().unwrap();
609 assert_eq!(status.entry_count, 0);
610 assert_eq!(status.total_bytes, 0);
611 assert_eq!(status.max_bytes, 1024 * 1024);
612 }
613}