1#![allow(missing_docs)]
4
5use crate::{abi::abi_decode_calldata, provider::runtime_transport::RuntimeTransportBuilder};
6use alloy_json_abi::JsonAbi;
7use alloy_primitives::{B256, Selector, map::HashMap};
8use eyre::Context;
9use itertools::Itertools;
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11use std::{
12 fmt,
13 sync::{
14 Arc,
15 atomic::{AtomicBool, AtomicUsize, Ordering},
16 },
17 time::Duration,
18};
19
20const BASE_URL: &str = "https://api.4byte.sourcify.dev";
21const SELECTOR_LOOKUP_URL: &str = "https://api.4byte.sourcify.dev/signature-database/v1/lookup";
22const SELECTOR_IMPORT_URL: &str = "https://api.4byte.sourcify.dev/signature-database/v1/import";
23
24const SELECTOR_IMPORT_BODY_LIMIT: usize = 100 * 1024;
26
27const SELECTOR_IMPORT_SIGNATURE_LIMIT: usize = 1000;
29
30const REQ_TIMEOUT: Duration = Duration::from_secs(15);
32
33const MAX_TIMEDOUT_REQ: usize = 4usize;
35
36pub type OpenChainSignatures = Vec<String>;
38
39#[derive(Clone, Debug)]
41pub struct OpenChainClient {
42 inner: reqwest::Client,
43 spurious_connection: Arc<AtomicBool>,
45 timedout_requests: Arc<AtomicUsize>,
47 max_timedout_requests: usize,
49}
50
51impl OpenChainClient {
52 pub fn new() -> eyre::Result<Self> {
54 let inner = RuntimeTransportBuilder::new(BASE_URL.parse().unwrap())
55 .with_timeout(REQ_TIMEOUT)
56 .build()
57 .reqwest_client()
58 .wrap_err("failed to build OpenChain client")?;
59 Ok(Self {
60 inner,
61 spurious_connection: Default::default(),
62 timedout_requests: Default::default(),
63 max_timedout_requests: MAX_TIMEDOUT_REQ,
64 })
65 }
66
67 async fn get_text(&self, url: impl reqwest::IntoUrl + fmt::Display) -> reqwest::Result<String> {
68 trace!(%url, "GET");
69 self.inner
70 .get(url)
71 .send()
72 .await
73 .inspect_err(|err| self.on_reqwest_err(err))?
74 .error_for_status()
75 .inspect_err(|err| self.on_reqwest_err(err))?
76 .text()
77 .await
78 .inspect_err(|err| self.on_reqwest_err(err))
79 }
80
81 async fn post_json<T: Serialize + std::fmt::Debug, R: DeserializeOwned>(
83 &self,
84 url: &str,
85 body: &T,
86 ) -> reqwest::Result<R> {
87 trace!(%url, body=?serde_json::to_string(body), "POST");
88 self.inner
89 .post(url)
90 .json(body)
91 .send()
92 .await
93 .inspect_err(|err| self.on_reqwest_err(err))?
94 .error_for_status()
95 .inspect_err(|err| self.on_reqwest_err(err))?
96 .json()
97 .await
98 .inspect_err(|err| self.on_reqwest_err(err))
99 }
100
101 fn on_reqwest_err(&self, err: &reqwest::Error) {
102 fn is_connectivity_err(err: &reqwest::Error) -> bool {
103 if err.is_timeout() || err.is_connect() {
104 return true;
105 }
106 if let Some(status) = err.status() {
108 let code = status.as_u16();
109 if (500..600).contains(&code) {
110 return true;
111 }
112 }
113 false
114 }
115
116 if is_connectivity_err(err) {
117 warn!("spurious network detected for OpenChain");
118 let previous = self.timedout_requests.fetch_add(1, Ordering::SeqCst);
119 if previous + 1 >= self.max_timedout_requests {
120 self.set_spurious();
121 }
122 }
123 }
124
125 fn is_spurious(&self) -> bool {
127 self.spurious_connection.load(Ordering::Relaxed)
128 }
129
130 fn set_spurious(&self) {
132 self.spurious_connection.store(true, Ordering::Relaxed)
133 }
134
135 fn ensure_not_spurious(&self) -> eyre::Result<()> {
136 if self.is_spurious() {
137 eyre::bail!("Spurious connection detected");
138 }
139 Ok(())
140 }
141
142 pub async fn decode_selector(
144 &self,
145 selector: SelectorKind,
146 ) -> eyre::Result<OpenChainSignatures> {
147 Ok(self.decode_selectors(&[selector]).await?.pop().unwrap())
148 }
149
150 pub async fn decode_selectors(
152 &self,
153 selectors: &[SelectorKind],
154 ) -> eyre::Result<Vec<OpenChainSignatures>> {
155 if selectors.is_empty() {
156 return Ok(vec![]);
157 }
158
159 if enabled!(tracing::Level::TRACE) {
160 trace!(?selectors, "decoding selectors");
161 } else {
162 debug!(len = selectors.len(), "decoding selectors");
163 }
164
165 self.ensure_not_spurious()?;
167
168 let mut url: url::Url = SELECTOR_LOOKUP_URL.parse().unwrap();
170 {
171 let mut query = url.query_pairs_mut();
172 let functions = selectors.iter().filter_map(SelectorKind::as_function);
173 if functions.clone().next().is_some() {
174 query.append_pair("function", &functions.format(",").to_string());
175 }
176 let events = selectors.iter().filter_map(SelectorKind::as_event);
177 if events.clone().next().is_some() {
178 query.append_pair("event", &events.format(",").to_string());
179 }
180 let _ = query.finish();
181 }
182
183 let text = self.get_text(url).await?;
184 let SignatureResponse { ok, result } = match serde_json::from_str(&text) {
185 Ok(response) => response,
186 Err(err) => {
187 eyre::bail!("could not decode response: {err}: {text}");
188 }
189 };
190 if !ok {
191 eyre::bail!("OpenChain returned an error: {text}");
192 }
193
194 Ok(selectors
195 .iter()
196 .map(|selector| {
197 let signatures = match selector {
198 SelectorKind::Function(selector) | SelectorKind::Error(selector) => {
199 result.function.get(selector)
200 }
201 SelectorKind::Event(hash) => result.event.get(hash),
202 };
203 signatures
204 .map(Option::as_deref)
205 .unwrap_or_default()
206 .unwrap_or_default()
207 .iter()
208 .map(|sig| sig.name.clone())
209 .collect()
210 })
211 .collect())
212 }
213
214 pub async fn decode_function_selector(
216 &self,
217 selector: Selector,
218 ) -> eyre::Result<OpenChainSignatures> {
219 self.decode_selector(SelectorKind::Function(selector)).await
220 }
221
222 pub async fn decode_calldata(&self, calldata: &str) -> eyre::Result<OpenChainSignatures> {
224 let calldata = calldata.strip_prefix("0x").unwrap_or(calldata);
225 if calldata.len() < 8 {
226 eyre::bail!(
227 "Calldata too short: expected at least 8 characters (excluding 0x prefix), got {}.",
228 calldata.len()
229 );
230 }
231
232 let mut sigs = self.decode_function_selector(calldata[..8].parse()?).await?;
233 sigs.retain(|sig| abi_decode_calldata(sig, calldata, true, true).is_ok());
235 Ok(sigs)
236 }
237
238 pub async fn decode_event_topic(&self, topic: B256) -> eyre::Result<OpenChainSignatures> {
240 self.decode_selector(SelectorKind::Event(topic)).await
241 }
242
243 pub async fn pretty_calldata(
261 &self,
262 calldata: impl AsRef<str>,
263 offline: bool,
264 ) -> eyre::Result<PossibleSigs> {
265 let mut possible_info = PossibleSigs::new();
266 let calldata = calldata.as_ref().trim_start_matches("0x");
267
268 let selector =
269 calldata.get(..8).ok_or_else(|| eyre::eyre!("calldata cannot be less that 4 bytes"))?;
270
271 let sigs = if offline {
272 vec![]
273 } else {
274 let selector = selector.parse()?;
275 self.decode_function_selector(selector).await.unwrap_or_default().into_iter().collect()
276 };
277 let (_, data) = calldata.split_at(8);
278
279 if !data.len().is_multiple_of(64) {
280 eyre::bail!("\nInvalid calldata size");
281 }
282
283 let row_length = data.len() / 64;
284
285 for row in 0..row_length {
286 possible_info.data.push(data[64 * row..64 * (row + 1)].to_string());
287 }
288 if sigs.is_empty() {
289 possible_info.method = SelectorOrSig::Selector(selector.to_string());
290 } else {
291 possible_info.method = SelectorOrSig::Sig(sigs);
292 }
293 Ok(possible_info)
294 }
295
296 pub async fn import_selectors(
298 &self,
299 data: SelectorImportData,
300 ) -> eyre::Result<SelectorImportResponse> {
301 self.ensure_not_spurious()?;
302
303 let request = match data {
304 SelectorImportData::Abi(abis) => {
305 let functions_and_errors: OpenChainSignatures = abis
306 .iter()
307 .flat_map(|abi| {
308 abi.functions()
309 .map(|func| func.signature())
310 .chain(abi.errors().map(|error| error.signature()))
311 })
312 .unique()
313 .collect();
314
315 let events = abis
316 .iter()
317 .flat_map(|abi| abi.events().map(|event| event.signature()))
318 .unique()
319 .collect::<Vec<_>>();
320
321 SelectorImportRequest { function: functions_and_errors, event: events }
322 }
323 SelectorImportData::Raw(raw) => {
324 let function_and_error =
325 raw.function.iter().chain(raw.error.iter()).cloned().collect::<Vec<_>>();
326 SelectorImportRequest { function: function_and_error, event: raw.event }
327 }
328 };
329
330 let mut response: Option<SelectorImportResponse> = None;
331 for request in request.into_chunks()? {
332 let chunk: SelectorImportResponse =
333 self.post_json(SELECTOR_IMPORT_URL, &request).await?;
334 if let Some(response) = &mut response {
335 response.result.function.imported.extend(chunk.result.function.imported);
336 response.result.function.duplicated.extend(chunk.result.function.duplicated);
337 response.result.event.imported.extend(chunk.result.event.imported);
338 response.result.event.duplicated.extend(chunk.result.event.duplicated);
339 } else {
340 response = Some(chunk);
341 }
342 }
343 Ok(response.expect("selector import always contains at least one request"))
344 }
345}
346
347pub enum SelectorOrSig {
348 Selector(String),
349 Sig(OpenChainSignatures),
350}
351
352pub struct PossibleSigs {
353 method: SelectorOrSig,
354 data: OpenChainSignatures,
355}
356
357impl PossibleSigs {
358 fn new() -> Self {
359 Self { method: SelectorOrSig::Selector("0x00000000".to_string()), data: vec![] }
360 }
361}
362
363impl fmt::Display for PossibleSigs {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 match &self.method {
366 SelectorOrSig::Selector(selector) => {
367 writeln!(f, "\n Method: {selector}")?;
368 }
369 SelectorOrSig::Sig(sigs) => {
370 writeln!(f, "\n Possible methods:")?;
371 for sig in sigs {
372 writeln!(f, " - {sig}")?;
373 }
374 }
375 }
376
377 writeln!(f, " ------------")?;
378 for (i, row) in self.data.iter().enumerate() {
379 let row_label_decimal = i * 32;
380 let row_label_hex = format!("{row_label_decimal:03x}");
381 writeln!(f, " [{row_label_hex}]: {row}")?;
382 }
383 Ok(())
384 }
385}
386
387#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
389pub enum SelectorKind {
390 Function(Selector),
392 Error(Selector),
394 Event(B256),
396}
397
398impl SelectorKind {
399 pub const fn as_function(&self) -> Option<Selector> {
401 match *self {
402 Self::Function(selector) | Self::Error(selector) => Some(selector),
403 _ => None,
404 }
405 }
406
407 pub const fn as_event(&self) -> Option<B256> {
409 match *self {
410 Self::Event(hash) => Some(hash),
411 _ => None,
412 }
413 }
414}
415
416pub async fn decode_selector(selector: SelectorKind) -> eyre::Result<OpenChainSignatures> {
418 OpenChainClient::new()?.decode_selector(selector).await
419}
420
421pub async fn decode_selectors(
423 selectors: &[SelectorKind],
424) -> eyre::Result<Vec<OpenChainSignatures>> {
425 OpenChainClient::new()?.decode_selectors(selectors).await
426}
427
428pub async fn decode_function_selector(selector: Selector) -> eyre::Result<OpenChainSignatures> {
430 OpenChainClient::new()?.decode_function_selector(selector).await
431}
432
433pub async fn decode_calldata(calldata: &str) -> eyre::Result<OpenChainSignatures> {
435 OpenChainClient::new()?.decode_calldata(calldata).await
436}
437
438pub async fn decode_event_topic(topic: B256) -> eyre::Result<OpenChainSignatures> {
440 OpenChainClient::new()?.decode_event_topic(topic).await
441}
442
443pub async fn pretty_calldata(
459 calldata: impl AsRef<str>,
460 offline: bool,
461) -> eyre::Result<PossibleSigs> {
462 OpenChainClient::new()?.pretty_calldata(calldata, offline).await
463}
464
465#[derive(Debug, Default, PartialEq, Eq, Serialize)]
466pub struct RawSelectorImportData {
467 pub function: OpenChainSignatures,
468 pub event: OpenChainSignatures,
469 pub error: OpenChainSignatures,
470}
471
472impl RawSelectorImportData {
473 pub const fn is_empty(&self) -> bool {
474 self.function.is_empty() && self.event.is_empty() && self.error.is_empty()
475 }
476}
477
478#[derive(Serialize)]
479#[serde(untagged)]
480pub enum SelectorImportData {
481 Abi(Vec<JsonAbi>),
482 Raw(RawSelectorImportData),
483}
484
485#[derive(Debug, Default, Serialize)]
486struct SelectorImportRequest {
487 function: OpenChainSignatures,
488 event: OpenChainSignatures,
489}
490
491impl SelectorImportRequest {
492 fn into_chunks(self) -> eyre::Result<Vec<Self>> {
493 let empty_request_len = serde_json::to_vec(&Self::default()).unwrap().len();
494 let mut chunks = Vec::new();
495 let mut chunk = Self::default();
496 let mut chunk_len = empty_request_len;
497
498 for (is_event, signature) in self
499 .function
500 .into_iter()
501 .map(|signature| (false, signature))
502 .chain(self.event.into_iter().map(|signature| (true, signature)))
503 {
504 let signature_len = serde_json::to_vec(&signature).unwrap().len();
505 if empty_request_len + signature_len > SELECTOR_IMPORT_BODY_LIMIT {
506 eyre::bail!("selector signature exceeds the registry request body limit");
507 }
508 let needs_comma =
509 if is_event { !chunk.event.is_empty() } else { !chunk.function.is_empty() };
510 let signatures_full = if is_event {
511 chunk.event.len() == SELECTOR_IMPORT_SIGNATURE_LIMIT
512 } else {
513 chunk.function.len() == SELECTOR_IMPORT_SIGNATURE_LIMIT
514 };
515 if (signatures_full
516 || chunk_len + signature_len + usize::from(needs_comma)
517 > SELECTOR_IMPORT_BODY_LIMIT)
518 && chunk_len > empty_request_len
519 {
520 chunks.push(chunk);
521 chunk = Self::default();
522 chunk_len = empty_request_len;
523 }
524
525 let signatures = if is_event { &mut chunk.event } else { &mut chunk.function };
526 chunk_len += signature_len + usize::from(!signatures.is_empty());
527 signatures.push(signature);
528 }
529 chunks.push(chunk);
530 Ok(chunks)
531 }
532}
533
534#[derive(Debug, Deserialize)]
535struct SelectorImportEffect {
536 imported: HashMap<String, String>,
537 duplicated: HashMap<String, String>,
538}
539
540#[derive(Debug, Deserialize)]
541struct SelectorImportResult {
542 function: SelectorImportEffect,
543 event: SelectorImportEffect,
544}
545
546#[derive(Debug, Deserialize)]
547pub struct SelectorImportResponse {
548 result: SelectorImportResult,
549}
550
551impl SelectorImportResponse {
552 pub fn describe(&self) {
554 for (k, v) in &self.result.function.imported {
555 let _ = sh_println!("Imported: Function {k}: {v}");
556 }
557 for (k, v) in &self.result.event.imported {
558 let _ = sh_println!("Imported: Event {k}: {v}");
559 }
560 for (k, v) in &self.result.function.duplicated {
561 let _ = sh_println!("Duplicated: Function {k}: {v}");
562 }
563 for (k, v) in &self.result.event.duplicated {
564 let _ = sh_println!("Duplicated: Event {k}: {v}");
565 }
566
567 let _ = sh_println!("Selectors successfully uploaded to OpenChain");
568 }
569}
570
571pub async fn import_selectors(data: SelectorImportData) -> eyre::Result<SelectorImportResponse> {
573 OpenChainClient::new()?.import_selectors(data).await
574}
575
576#[derive(Debug, Default, PartialEq, Eq)]
577pub struct ParsedSignatures {
578 pub signatures: RawSelectorImportData,
579 pub abis: Vec<JsonAbi>,
580}
581
582#[derive(Deserialize)]
583struct Artifact {
584 abi: JsonAbi,
585}
586
587pub fn parse_signatures(tokens: Vec<String>) -> ParsedSignatures {
591 let abis = tokens
594 .iter()
595 .filter(|sig| sig.ends_with(".json"))
596 .filter_map(|filename| std::fs::read_to_string(filename).ok())
597 .filter_map(|file| serde_json::from_str(file.as_str()).ok())
598 .map(|artifact: Artifact| artifact.abi)
599 .collect();
600
601 let signatures = tokens.iter().filter(|sig| !sig.ends_with(".json")).fold(
604 RawSelectorImportData::default(),
605 |mut data, signature| {
606 let mut split = signature.split(' ');
607 #[allow(clippy::collapsible_match)]
608 match split.next() {
609 Some("function") => {
610 if let Some(sig) = split.next() {
611 data.function.push(sig.to_string())
612 }
613 }
614 Some("event") => {
615 if let Some(sig) = split.next() {
616 data.event.push(sig.to_string())
617 }
618 }
619 Some("error") => {
620 if let Some(sig) = split.next() {
621 data.error.push(sig.to_string())
622 }
623 }
624 Some(signature) => {
625 data.function.push(signature.to_string());
627 }
628 None => {}
629 }
630 data
631 },
632 );
633
634 ParsedSignatures { signatures, abis }
635}
636
637#[derive(Deserialize)]
639struct SignatureResponse {
640 ok: bool,
641 result: SignatureResult,
642}
643
644#[derive(Deserialize)]
645struct SignatureResult {
646 event: HashMap<B256, Option<Vec<Signature>>>,
647 function: HashMap<Selector, Option<Vec<Signature>>>,
648}
649
650#[derive(Deserialize)]
651struct Signature {
652 name: String,
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658 use axum::{
659 Router,
660 http::StatusCode,
661 routing::{get, post},
662 };
663
664 async fn spawn_status_server() -> (String, tokio::task::JoinHandle<()>) {
665 let app = Router::new()
666 .route("/unavailable", get(|| async { StatusCode::SERVICE_UNAVAILABLE }))
667 .route("/bad-request", get(|| async { StatusCode::BAD_REQUEST }))
668 .route("/post-unavailable", post(|| async { StatusCode::SERVICE_UNAVAILABLE }));
669 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
670 let addr = listener.local_addr().unwrap();
671 let handle = tokio::spawn(async move {
672 axum::serve(listener, app).await.unwrap();
673 });
674 (format!("http://{addr}"), handle)
675 }
676
677 #[test]
678 fn test_parse_signatures() {
679 let result = parse_signatures(vec!["transfer(address,uint256)".to_string()]);
680 assert_eq!(
681 result,
682 ParsedSignatures {
683 signatures: RawSelectorImportData {
684 function: vec!["transfer(address,uint256)".to_string()],
685 ..Default::default()
686 },
687 ..Default::default()
688 }
689 );
690
691 let result = parse_signatures(vec![
692 "transfer(address,uint256)".to_string(),
693 "function approve(address,uint256)".to_string(),
694 ]);
695 assert_eq!(
696 result,
697 ParsedSignatures {
698 signatures: RawSelectorImportData {
699 function: vec![
700 "transfer(address,uint256)".to_string(),
701 "approve(address,uint256)".to_string()
702 ],
703 ..Default::default()
704 },
705 ..Default::default()
706 }
707 );
708
709 let result = parse_signatures(vec![
710 "transfer(address,uint256)".to_string(),
711 "event Approval(address,address,uint256)".to_string(),
712 "error ERC20InsufficientBalance(address,uint256,uint256)".to_string(),
713 ]);
714 assert_eq!(
715 result,
716 ParsedSignatures {
717 signatures: RawSelectorImportData {
718 function: vec!["transfer(address,uint256)".to_string()],
719 event: vec!["Approval(address,address,uint256)".to_string()],
720 error: vec!["ERC20InsufficientBalance(address,uint256,uint256)".to_string()]
721 },
722 ..Default::default()
723 }
724 );
725
726 let result = parse_signatures(vec!["event".to_string()]);
728 assert_eq!(
729 result,
730 ParsedSignatures { signatures: Default::default(), ..Default::default() }
731 );
732 }
733
734 #[test]
735 fn selector_import_requests_stay_within_registry_limits() {
736 let suffix = "x".repeat(100);
737 let function = (0..3_000)
738 .map(|i| format!("functionName{i:04}{suffix}(address,uint256)"))
739 .collect::<Vec<_>>();
740 let expected = function.clone();
741 let chunks = SelectorImportRequest { function, event: Vec::new() }.into_chunks().unwrap();
742
743 assert!(chunks.len() > 3);
744 assert!(chunks.iter().all(|chunk| chunk.function.len() <= SELECTOR_IMPORT_SIGNATURE_LIMIT
745 && serde_json::to_vec(chunk).unwrap().len() <= SELECTOR_IMPORT_BODY_LIMIT));
746 assert_eq!(
747 chunks.into_iter().flat_map(|chunk| chunk.function).collect::<Vec<_>>(),
748 expected
749 );
750 }
751
752 #[tokio::test]
753 async fn spurious_marked_on_timeout_threshold() {
754 let client = OpenChainClient::new().expect("client must build");
756 let url = "http://127.0.0.1:9"; for i in 0..(MAX_TIMEDOUT_REQ - 1) {
760 let _ = client.get_text(url).await; assert!(!client.is_spurious(), "unexpected spurious after {} failed attempts", i + 1);
762 }
763
764 let _ = client.get_text(url).await;
766 assert!(client.is_spurious(), "expected spurious after threshold failures");
767 }
768
769 #[tokio::test]
770 async fn spurious_marked_on_http_5xx_threshold() {
771 let (base_url, server_task) = spawn_status_server().await;
772 let client = OpenChainClient::new().expect("client must build");
773
774 for i in 0..(MAX_TIMEDOUT_REQ - 1) {
775 let result = client.get_text(format!("{base_url}/unavailable")).await;
776 assert!(result.is_err(), "expected HTTP 503 on attempt {}", i + 1);
777 assert!(!client.is_spurious(), "unexpected spurious after {} failed attempts", i + 1);
778 }
779
780 let result = client.get_text(format!("{base_url}/unavailable")).await;
781 assert!(result.is_err());
782 assert!(client.is_spurious(), "expected spurious after threshold HTTP 503 responses");
783
784 server_task.abort();
785 }
786
787 #[tokio::test]
788 async fn post_http_5xx_counts_as_connectivity_failure() {
789 let (base_url, server_task) = spawn_status_server().await;
790 let client = OpenChainClient::new().expect("client must build");
791 let body = serde_json::json!({});
792
793 for _ in 0..MAX_TIMEDOUT_REQ {
794 let result = client
795 .post_json::<_, serde_json::Value>(&format!("{base_url}/post-unavailable"), &body)
796 .await;
797 assert!(result.is_err(), "expected HTTP 503");
798 }
799
800 assert!(client.is_spurious(), "expected HTTP 503 POSTs to mark the connection spurious");
801 server_task.abort();
802 }
803
804 #[tokio::test]
805 async fn http_4xx_does_not_count_as_connectivity_failure() {
806 let (base_url, server_task) = spawn_status_server().await;
807 let client = OpenChainClient::new().expect("client must build");
808
809 assert!(client.get_text(format!("{base_url}/bad-request")).await.is_err());
810 assert!(client.get_text(format!("{base_url}/missing")).await.is_err());
811 assert_eq!(client.timedout_requests.load(Ordering::SeqCst), 0);
812 assert!(!client.is_spurious());
813
814 server_task.abort();
815 }
816}