use std::net::{Ipv4Addr, SocketAddr};
use chia_protocol::{Message, ProtocolMessageTypes};
use chia_traits::Streamable;
use dig_peer_protocol::Bytes;
use dig_peer_protocol::{DigLink, DigMessage, DigMessageType, LinkOptions};
use futures_util::{SinkExt, StreamExt};
use tokio::io::DuplexStream;
use tokio_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
const REGISTER_PEER: u8 = DigMessageType::RegisterPeer as u8;
async fn linked_pair() -> (
DigLink,
tokio::sync::mpsc::Receiver<DigMessage>,
DigLink,
tokio::sync::mpsc::Receiver<DigMessage>,
) {
let (left, right) = tokio::io::duplex(64 * 1024);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 8444));
let client: WebSocketStream<DuplexStream> =
WebSocketStream::from_raw_socket(left, Role::Client, None).await;
let server: WebSocketStream<DuplexStream> =
WebSocketStream::from_raw_socket(right, Role::Server, None).await;
let (a, a_rx) = DigLink::from_server_websocket(client, addr, LinkOptions::default());
let (b, b_rx) = DigLink::from_server_websocket(server, addr, LinkOptions::default());
(a, a_rx, b, b_rx)
}
async fn link_with_raw_peer() -> (
DigLink,
tokio::sync::mpsc::Receiver<DigMessage>,
WebSocketStream<DuplexStream>,
) {
let (left, right) = tokio::io::duplex(64 * 1024);
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 8444));
let client: WebSocketStream<DuplexStream> =
WebSocketStream::from_raw_socket(left, Role::Client, None).await;
let raw: WebSocketStream<DuplexStream> =
WebSocketStream::from_raw_socket(right, Role::Server, None).await;
let (link, link_rx) = DigLink::from_server_websocket(client, addr, LinkOptions::default());
(link, link_rx, raw)
}
const MALFORMED_FRAME: [u8; 3] = [0xFF, 0x01, 0x00];
async fn next(rx: &mut tokio::sync::mpsc::Receiver<DigMessage>) -> DigMessage {
tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
.await
.expect("timed out waiting for an inbound message")
.expect("the link closed instead of delivering a message")
}
#[test]
fn the_pre_migration_decoder_rejects_218_but_accepts_the_same_frame_with_a_chia_opcode() {
let payload = Bytes::new(b"registration".to_vec());
let dig_frame = DigMessage::new(REGISTER_PEER, Some(7), payload.clone()).to_bytes();
assert!(
Message::from_bytes(&dig_frame).is_err(),
"chia_protocol accepted opcode 218 — the fork's premise no longer holds"
);
let chia_opcode = *ProtocolMessageTypes::RequestPeers
.to_bytes()
.expect("encode")
.first()
.expect("one byte");
let chia_frame = DigMessage::new(chia_opcode, Some(7), payload).to_bytes();
assert!(
Message::from_bytes(&chia_frame).is_ok(),
"the control frame failed to decode, so the rejection above is about framing, not opcodes"
);
}
#[tokio::test]
async fn opcode_218_round_trips_and_leaves_the_link_alive() {
let (sender_link, _sender_rx, _receiver_link, mut receiver_rx) = linked_pair().await;
sender_link
.send_dig(REGISTER_PEER, Bytes::new(b"registration".to_vec()))
.await
.expect("send 218");
let received = next(&mut receiver_rx).await;
assert_eq!(received.msg_type, REGISTER_PEER);
assert_eq!(received.data.as_ref(), b"registration");
let chia_opcode = *ProtocolMessageTypes::RequestPeers
.to_bytes()
.expect("encode")
.first()
.expect("one byte");
sender_link
.send_dig(chia_opcode, Bytes::new(b"after".to_vec()))
.await
.expect("send the follow-up");
let after = next(&mut receiver_rx).await;
assert_eq!(
after.data.as_ref(),
b"after",
"the link died on the DIG frame — decoding it is not enough"
);
}
#[tokio::test]
async fn an_inbound_request_id_is_delivered_rather_than_matched_against_our_own_waiters() {
let (sender_link, _sender_rx, _receiver_link, mut receiver_rx) = linked_pair().await;
sender_link
.send_message(DigMessage::new(
REGISTER_PEER,
Some(0),
Bytes::new(b"request".to_vec()),
))
.await
.expect("send an inbound request");
let received = next(&mut receiver_rx).await;
assert_eq!(received.msg_type, REGISTER_PEER);
assert_eq!(
received.id,
Some(0),
"the correlation id must survive, so the reply can echo it"
);
assert_eq!(received.data.as_ref(), b"request");
}
#[tokio::test]
async fn a_dig_request_correlates_with_its_response() {
let (requester, _requester_rx, responder, mut responder_rx) = linked_pair().await;
let responder_task = tokio::spawn(async move {
let request = responder_rx.recv().await.expect("a request");
responder
.send_message(DigMessage::new(
DigMessageType::RespondStatus as u8,
request.id,
Bytes::new(b"pong".to_vec()),
))
.await
.expect("send the response");
});
let response = tokio::time::timeout(
std::time::Duration::from_secs(5),
requester.request_dig(
DigMessageType::RequestStatus as u8,
Bytes::new(b"ping".to_vec()),
),
)
.await
.expect("timed out waiting for the response")
.expect("the request resolves");
assert_eq!(response.msg_type, DigMessageType::RespondStatus as u8);
assert_eq!(response.data.as_ref(), b"pong");
responder_task.await.expect("responder finished");
}
#[tokio::test]
async fn a_malformed_frame_is_skipped_and_the_next_frame_still_routes() {
assert!(
DigMessage::from_bytes_owned(MALFORMED_FRAME.to_vec()).is_none(),
"the fixture decoded — this test would prove nothing about malformed frames"
);
let (requester, _requester_rx, mut raw) = link_with_raw_peer().await;
let peer = tokio::spawn(async move {
let request = loop {
match raw
.next()
.await
.expect("the link sent nothing")
.expect("ws error")
{
tungstenite::Message::Binary(bytes) => {
break DigMessage::from_bytes_owned(bytes).expect("the link framed a request")
}
_ => continue,
}
};
raw.send(tungstenite::Message::Binary(MALFORMED_FRAME.to_vec()))
.await
.expect("send the malformed frame");
raw.send(tungstenite::Message::Binary(
DigMessage::new(
DigMessageType::RespondStatus as u8,
request.id,
Bytes::new(b"pong".to_vec()),
)
.to_bytes(),
))
.await
.expect("send the reply that follows the malformed frame");
std::future::pending::<()>().await;
});
let response = tokio::time::timeout(
std::time::Duration::from_secs(5),
requester.request_dig(
DigMessageType::RequestStatus as u8,
Bytes::new(b"ping".to_vec()),
),
)
.await
.expect("the request hung after the malformed frame")
.expect("the link died on a malformed frame instead of skipping it");
assert_eq!(
response.data.as_ref(),
b"pong",
"the frame after the malformed one was not routed"
);
peer.abort();
}