use std::error::Error;
use std::path::Path;
use std::sync::Arc;
use liminal::durability::bridge::block_on;
use liminal::durability::{DurableStore, StoredEntry};
use liminal_protocol::lifecycle::ConversationEvent;
use liminal_protocol::wire::{
AttachAttemptToken, AttachSecret, ClientRequest, CredentialAttachRequest, DetachAttemptToken,
DetachRequest, EnrollmentRequest, EnrollmentToken, Generation, ParticipantAck,
ParticipantFrame, ParticipantId, RecordAdmission, RecordAdmissionAttemptToken, ServerValue,
encode, encoded_len,
};
use liminal_sdk::{
ConnectionPoolConfig, ParticipantResumeStore, RemoteConfig, RemoteOperationRecordOutcome,
RemoteParticipantHandle, RemoteParticipantInbound, RemoteParticipantSendOutcome, SdkError,
};
use liminal_server::config::types::ParticipantConfig;
use liminal_server::config::{LimitsConfig, ServerConfig, ServicesConfig};
use liminal_server::server::connection::{
ConnectionServices, ConnectionSupervisor, LiminalConnectionServices,
};
use liminal_server::server::embedded::EmbeddedServer;
use liminal_server::server::listener::ServerListener;
const PARITY_CONVERSATION: u64 = 0x50_05;
const TRANSITION_LOG_PREFIX: &str = "liminal:participant-production:";
const TRANSITION_LOG_PAGE: usize = 256;
const EXEMPT_FIELDS: &[&str] = &[
"attach_secret (32 bytes of /dev/urandom entropy, minted per enrollment and per rotation)",
"receipt_expires_at / provenance_expires_at / admitted_now_ms (wall-clock reads plus TTL)",
"unsolicited-push interleave position and repeat count (at-least-once publication \
scheduling, observed on BOTH mounts; push content is still compared byte for byte)",
];
const fn participant_config() -> ParticipantConfig {
ParticipantConfig {
wire_frame_limit: 65_536,
attach_receipt_ttl_ms: 60_000,
receipt_provenance_ttl_ms: 600_000,
max_live_attach_receipts_server: 1_024,
max_live_attach_receipts_per_participant: 8,
max_receipt_provenance_server: 4_096,
max_receipt_provenance_per_conversation: 256,
max_receipt_provenance_per_participant: 64,
max_retired_identity_slots_server: 1_024,
identity_slots: 4,
observer_recovery_max_entries: 64,
max_semantic_conversations_per_connection: 32,
max_ordinary_record_entries: 1,
max_ordinary_record_bytes: 131_072,
max_generated_marker_entries: 1,
max_generated_marker_bytes: 4_096,
mandatory_transaction_bound_entries: 4,
mandatory_transaction_bound_bytes: 16_384,
full_recovery_claim_entries: 4,
full_recovery_claim_bytes: 16_384,
retained_capacity_entries: 2_048,
retained_capacity_bytes: 16_777_216,
max_retained_record_rows: 1_024,
closure_episode_churn_limit: 1_024,
}
}
#[derive(Debug, Default)]
struct MemoryResumeStore {
canonical: Vec<u8>,
}
impl ParticipantResumeStore for MemoryResumeStore {
fn persist(&mut self, canonical_lpcr: &[u8]) -> Result<(), SdkError> {
self.canonical.clear();
self.canonical.extend_from_slice(canonical_lpcr);
Ok(())
}
}
type SdkParticipant = RemoteParticipantHandle<MemoryResumeStore>;
struct DriveOutcome {
rows: Vec<Vec<u8>>,
responses: Vec<Vec<u8>>,
pushes: Vec<Vec<u8>>,
secrets: Vec<[u8; 32]>,
deadlines: Vec<u128>,
}
fn server_config(store_dir: &Path) -> Result<ServerConfig, Box<dyn Error>> {
Ok(ServerConfig {
listen_address: "127.0.0.1:0".parse()?,
health_listen_address: "127.0.0.1:0".parse()?,
drain_timeout_ms: 30_000,
channels: Vec::new(),
routing_rules: Vec::new(),
persistence_path: Some(store_dir.to_path_buf()),
cluster: None,
auth: None,
services: ServicesConfig::default(),
limits: LimitsConfig::default(),
websocket: None,
participant: Some(participant_config()),
})
}
type ProductionStack = (Arc<LiminalConnectionServices>, Arc<dyn DurableStore>);
fn production_services(store_dir: &Path) -> Result<ProductionStack, Box<dyn Error>> {
std::fs::create_dir_all(store_dir)?;
let config = server_config(store_dir)?;
let services = Arc::new(LiminalConnectionServices::from_config(&config)?);
let store = services.durable_store();
Ok((services, store))
}
const fn pool() -> ConnectionPoolConfig {
ConnectionPoolConfig::new(1, 1, 8)
}
fn send_operation(
participant: &SdkParticipant,
request: ClientRequest,
) -> Result<(), Box<dyn Error>> {
let operation = match participant.record_operation(request)? {
RemoteOperationRecordOutcome::Recorded(operation)
| RemoteOperationRecordOutcome::Continuous(operation) => operation,
RemoteOperationRecordOutcome::Refused { request, reason } => {
return Err(format!("SDK refused outbound request {request:?}: {reason:?}").into());
}
};
match participant.send_operation(operation)? {
RemoteParticipantSendOutcome::Sent { .. } => Ok(()),
RemoteParticipantSendOutcome::TransportLost { error, .. } => {
Err(format!("SDK transport lost while sending participant operation: {error}").into())
}
}
}
fn frame_bytes(frame: &ParticipantFrame) -> Result<Vec<u8>, Box<dyn Error>> {
let mut bytes = vec![0_u8; encoded_len(frame).map_err(|error| format!("{error:?}"))?];
let written = encode(frame, &mut bytes).map_err(|error| format!("{error:?}"))?;
bytes.truncate(written);
Ok(bytes)
}
const MAX_DEMUX_FRAMES: usize = 64;
fn step(
participant: &SdkParticipant,
responses: &mut Vec<Vec<u8>>,
pushes: &mut Vec<Vec<u8>>,
request: ClientRequest,
) -> Result<ServerValue, Box<dyn Error>> {
send_operation(participant, request)?;
for _ in 0..MAX_DEMUX_FRAMES {
match participant.receive()? {
RemoteParticipantInbound::Applied { value, .. } => {
responses.push(frame_bytes(&ParticipantFrame::ServerValue(value.clone()))?);
return Ok(value);
}
RemoteParticipantInbound::Push { value, .. } => {
pushes.push(frame_bytes(&ParticipantFrame::ServerPush(value))?);
}
refused @ RemoteParticipantInbound::Refused { .. } => {
return Err(format!("expected an applied server value, got {refused:?}").into());
}
}
}
Err(format!("no response arrived within {MAX_DEMUX_FRAMES} inbound frames").into())
}
fn run_scenario(
participant: &SdkParticipant,
peer: &SdkParticipant,
store: &Arc<dyn DurableStore>,
) -> Result<DriveOutcome, Box<dyn Error>> {
let mut responses = Vec::new();
let mut pushes = Vec::new();
let mut secrets = Vec::new();
let mut deadlines = Vec::new();
let enrolled = step(
participant,
&mut responses,
&mut pushes,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: PARITY_CONVERSATION,
enrollment_token: EnrollmentToken::new([0x5A; 16]),
}),
)?;
let ServerValue::EnrollBound(bound) = enrolled else {
return Err(format!("enrollment did not bind: {enrolled:?}").into());
};
let participant_id = bound.participant_id();
let enrollment_secret = bound.attach_secret();
secrets.push(enrollment_secret.into_bytes());
deadlines.push(bound.receipt_expires_at());
deadlines.push(bound.provenance_expires_at());
let peer_enrolled = step(
peer,
&mut responses,
&mut pushes,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: PARITY_CONVERSATION,
enrollment_token: EnrollmentToken::new([0x6A; 16]),
}),
)?;
let ServerValue::EnrollBound(peer_bound) = peer_enrolled else {
return Err(format!("the peer enrollment did not bind: {peer_enrolled:?}").into());
};
secrets.push(peer_bound.attach_secret().into_bytes());
deadlines.push(peer_bound.receipt_expires_at());
deadlines.push(peer_bound.provenance_expires_at());
let detached = step(
participant,
&mut responses,
&mut pushes,
ClientRequest::Detach(DetachRequest {
conversation_id: PARITY_CONVERSATION,
participant_id,
capability_generation: Generation::ONE,
detach_attempt_token: DetachAttemptToken::new([0x5B; 16]),
}),
)?;
if !matches!(detached, ServerValue::DetachCommitted(_)) {
return Err(format!("the origin detach did not commit: {detached:?}").into());
}
let attached = step(
participant,
&mut responses,
&mut pushes,
ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: PARITY_CONVERSATION,
participant_id,
capability_generation: Generation::ONE,
attach_secret: enrollment_secret,
attach_attempt_token: AttachAttemptToken::new([0x5C; 16]),
accept_marker_delivery_seq: None,
}),
)?;
let ServerValue::AttachBound(attach_bound) = attached else {
return Err(format!("the credential attach did not bind: {attached:?}").into());
};
secrets.push(attach_bound.attach_secret().into_bytes());
deadlines.push(attach_bound.receipt_expires_at());
deadlines.push(attach_bound.provenance_expires_at());
let rotated = Generation::new(2).ok_or("generation two is nonzero")?;
drive_records_ack_and_detach(
participant,
&mut responses,
&mut pushes,
participant_id,
rotated,
)?;
let rows = read_transition_log(store)?;
Ok(DriveOutcome {
rows,
responses,
pushes,
secrets,
deadlines,
})
}
fn drive_records_ack_and_detach(
participant: &SdkParticipant,
responses: &mut Vec<Vec<u8>>,
pushes: &mut Vec<Vec<u8>>,
participant_id: ParticipantId,
rotated: Generation,
) -> Result<(), Box<dyn Error>> {
for (token, payload) in [
([0x5D_u8; 16], vec![0x00_u8, 0xFF, 0x50, 0x05, 0xA5]),
([0x5E_u8; 16], vec![0x11_u8, 0x22, 0x33, 0x44, 0x55, 0x66]),
] {
let committed = step(
participant,
responses,
pushes,
ClientRequest::RecordAdmission(RecordAdmission {
conversation_id: PARITY_CONVERSATION,
participant_id,
capability_generation: rotated,
record_admission_attempt_token: RecordAdmissionAttemptToken::new(token),
payload,
}),
)?;
if !matches!(committed, ServerValue::RecordCommitted(_)) {
return Err(format!("an ordinary record did not commit: {committed:?}").into());
}
}
let acked = step(
participant,
responses,
pushes,
ClientRequest::ParticipantAck(ParticipantAck {
conversation_id: PARITY_CONVERSATION,
participant_id,
capability_generation: rotated,
through_seq: 2,
}),
)?;
if !matches!(acked, ServerValue::AckCommitted(_)) {
return Err(format!("the acknowledgement did not commit: {acked:?}").into());
}
let final_detach = step(
participant,
responses,
pushes,
ClientRequest::Detach(DetachRequest {
conversation_id: PARITY_CONVERSATION,
participant_id,
capability_generation: rotated,
detach_attempt_token: DetachAttemptToken::new([0x5F; 16]),
}),
)?;
if !matches!(final_detach, ServerValue::DetachCommitted(_)) {
return Err(format!("the final detach did not commit: {final_detach:?}").into());
}
Ok(())
}
fn read_transition_log(store: &Arc<dyn DurableStore>) -> Result<Vec<Vec<u8>>, Box<dyn Error>> {
let stream_key = format!("{TRANSITION_LOG_PREFIX}{PARITY_CONVERSATION}");
let mut rows: Vec<Vec<u8>> = Vec::new();
loop {
let head = u64::try_from(rows.len())?;
let page: Vec<StoredEntry> =
block_on(store.read_from(&stream_key, head, TRANSITION_LOG_PAGE))??;
let read = page.len();
for (offset, entry) in page.into_iter().enumerate() {
let expected = head + u64::try_from(offset)?;
if entry.sequence != expected {
return Err(format!(
"the transition-input log is not contiguous: expected sequence {expected}, \
read {}",
entry.sequence
)
.into());
}
rows.push(entry.payload);
}
if read < TRANSITION_LOG_PAGE {
break;
}
}
if rows.is_empty() {
return Err(format!(
"no durable rows were found at {stream_key} — a parity assertion over an empty \
stream would prove nothing"
)
.into());
}
Ok(rows)
}
fn drive_loopback(store_dir: &Path) -> Result<DriveOutcome, Box<dyn Error>> {
let (services, store) = production_services(store_dir)?;
let server = Arc::new(EmbeddedServer::with_services(
services as Arc<dyn ConnectionServices>,
)?);
let config = RemoteConfig::new(
"in-process",
"loopback-parity",
PARITY_CONVERSATION.to_string(),
pool(),
)?
.connect_loopback(Arc::clone(&server))?;
let peer_config = RemoteConfig::new(
"in-process",
"loopback-parity-peer",
PARITY_CONVERSATION.to_string(),
pool(),
)?
.connect_loopback(Arc::clone(&server))?;
let participant = RemoteParticipantHandle::new(&config, MemoryResumeStore::default())?;
let peer = RemoteParticipantHandle::new(&peer_config, MemoryResumeStore::default())?;
let outcome = run_scenario(&participant, &peer, &store)?;
drop(participant);
drop(peer);
drop(config);
drop(peer_config);
Ok(outcome)
}
fn drive_tcp(store_dir: &Path) -> Result<DriveOutcome, Box<dyn Error>> {
let (services, store) = production_services(store_dir)?;
let config = server_config(store_dir)?;
let supervisor = ConnectionSupervisor::with_services(services as Arc<dyn ConnectionServices>)?;
let listener = ServerListener::bind(&config, supervisor.clone())?;
let address = listener.local_addr();
let remote = RemoteConfig::new(
address.to_string(),
"loopback-parity",
PARITY_CONVERSATION.to_string(),
pool(),
)?
.connect_tcp()?;
let peer_remote = RemoteConfig::new(
address.to_string(),
"loopback-parity-peer",
PARITY_CONVERSATION.to_string(),
pool(),
)?
.connect_tcp()?;
let participant = RemoteParticipantHandle::new(&remote, MemoryResumeStore::default())?;
let peer = RemoteParticipantHandle::new(&peer_remote, MemoryResumeStore::default())?;
let outcome = run_scenario(&participant, &peer, &store)?;
drop(participant);
drop(peer);
drop(remote);
drop(peer_remote);
listener.shutdown()?;
supervisor.shutdown();
Ok(outcome)
}
const EXEMPT_ROW_FIELDS: &[(&str, &str)] = &[
(
"attach_secret",
"32 bytes read from /dev/urandom per enrollment and per credential rotation \
(production/facts.rs, mint_secret_bytes); a predictable attach secret must never \
be issued, so this is nondeterministic on purpose",
),
(
"receipt_expires_at",
"a wall-clock read plus the configured attach-receipt TTL \
(production/facts.rs, now_unix_millis)",
),
(
"provenance_expires_at",
"a wall-clock read plus the configured receipt-provenance TTL",
),
(
"admitted_now_ms",
"the wall-clock millisecond the attach was admitted at",
),
];
#[derive(Debug, Default)]
struct RowComparison {
exempt_paths: Vec<String>,
events: Vec<(String, Vec<u8>, Vec<u8>)>,
}
fn compare_row_structure(
path: &str,
left: &serde_json::Value,
right: &serde_json::Value,
report: &mut RowComparison,
) -> Result<(), Box<dyn Error>> {
use serde_json::Value;
match (left, right) {
(Value::Object(left_map), Value::Object(right_map)) => {
let mut left_keys: Vec<&String> = left_map.keys().collect();
let mut right_keys: Vec<&String> = right_map.keys().collect();
left_keys.sort_unstable();
right_keys.sort_unstable();
if left_keys != right_keys {
return Err(format!(
"{path}: the two mounts wrote different row FIELDS — loopback \
{left_keys:?}, socket {right_keys:?}"
)
.into());
}
for (key, left_value) in left_map {
let child = format!("{path}/{key}");
let right_value = right_map
.get(key)
.ok_or_else(|| format!("{child}: missing on the socket mount"))?;
if key == "event" {
let left_bytes: Vec<u8> = serde_json::from_value(left_value.clone())?;
let right_bytes: Vec<u8> = serde_json::from_value(right_value.clone())?;
report.events.push((child, left_bytes, right_bytes));
continue;
}
if let Some((_, reason)) = EXEMPT_ROW_FIELDS.iter().find(|(name, _)| name == key) {
assert_eq!(
shape_of(left_value),
shape_of(right_value),
"{child}: an exempt field must still be EQUAL-SHAPED across the two \
mounts ({reason})"
);
report.exempt_paths.push(child);
continue;
}
compare_row_structure(&child, left_value, right_value, report)?;
}
Ok(())
}
(Value::Array(left_items), Value::Array(right_items)) => {
if left_items.len() != right_items.len() {
return Err(format!(
"{path}: the two mounts wrote arrays of different length ({} vs {})",
left_items.len(),
right_items.len()
)
.into());
}
for (index, (left_item, right_item)) in
left_items.iter().zip(right_items.iter()).enumerate()
{
compare_row_structure(&format!("{path}/{index}"), left_item, right_item, report)?;
}
Ok(())
}
_ => {
if left == right {
return Ok(());
}
Err(format!(
"{path}: the two mounts wrote different values — loopback {left}, socket \
{right}. This field is NOT on the exempt list, so it is a genuine \
divergence between the in-process and socket record paths."
)
.into())
}
}
}
fn shape_of(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => "null".to_owned(),
serde_json::Value::Bool(_) => "bool".to_owned(),
serde_json::Value::Number(_) => "number".to_owned(),
serde_json::Value::String(text) => format!("string[{}]", text.len()),
serde_json::Value::Array(items) => format!("array[{}]", items.len()),
serde_json::Value::Object(map) => format!("object[{}]", map.len()),
}
}
type ExemptPair = (Vec<u8>, Vec<u8>);
fn substitute(image: &[u8], pairs: &[ExemptPair]) -> (Vec<u8>, usize) {
let mut out = image.to_vec();
let mut count = 0;
for (from, to) in pairs {
assert_eq!(
from.len(),
to.len(),
"an exempt substitution pair must be equal-width"
);
let mut cursor = 0;
while let Some(found) = out
.get(cursor..)
.and_then(|tail| tail.windows(from.len()).position(|w| w == from.as_slice()))
{
let at = cursor + found;
if let Some(slot) = out.get_mut(at..at + to.len()) {
slot.copy_from_slice(to);
}
cursor = at + to.len();
count += 1;
}
}
(out, count)
}
fn assert_parity_after_substitution(what: &str, loopback: &[u8], tcp: &[u8], pairs: &[ExemptPair]) {
assert_eq!(
loopback.len(),
tcp.len(),
"{what}: the two mounts produced different image LENGTHS ({} vs {}), which no \
fixed-width exempt field can explain",
loopback.len(),
tcp.len()
);
if loopback == tcp {
return;
}
let (substituted, replacements) = substitute(loopback, pairs);
assert_eq!(
substituted,
tcp.to_vec(),
"{what}: the two mounts differ in bytes that are NOT one of the {} exempt \
server-minted secrets ({replacements} substitution(s) applied). \
loopback={loopback:02x?} tcp={tcp:02x?}",
pairs.len()
);
}
fn row_operation(row: &[u8]) -> Result<String, Box<dyn Error>> {
let json: serde_json::Value = serde_json::from_slice(row)?;
json["operation"]["operation"]
.as_str()
.map(str::to_owned)
.ok_or_else(|| "a durable row carried no operation tag".into())
}
fn compare_durable_rows(
loopback: &DriveOutcome,
tcp: &DriveOutcome,
) -> Result<RowComparison, Box<dyn Error>> {
assert_eq!(
loopback.rows.len(),
tcp.rows.len(),
"the two mounts wrote different numbers of durable rows"
);
let census: Vec<String> = loopback
.rows
.iter()
.map(|row| row_operation(row))
.collect::<Result<_, _>>()?;
assert_eq!(
census,
vec![
"genesis".to_owned(),
"enrolled".to_owned(),
"enrolled".to_owned(),
"detached".to_owned(),
"attached".to_owned(),
"record_admission".to_owned(),
"record_admission".to_owned(),
"zero_debt_ack".to_owned(),
"detached".to_owned(),
],
"the scenario did not write the operations it claims to exercise, so a parity \
assertion over these rows would not cover the record path it names"
);
let mut report = RowComparison::default();
for (index, (loopback_row, tcp_row)) in loopback.rows.iter().zip(tcp.rows.iter()).enumerate() {
assert_eq!(
row_operation(loopback_row)?,
row_operation(tcp_row)?,
"durable row {index} is a different operation on the two mounts"
);
let loopback_json: serde_json::Value = serde_json::from_slice(loopback_row)?;
let tcp_json: serde_json::Value = serde_json::from_slice(tcp_row)?;
compare_row_structure(
&format!("row[{index}]"),
&loopback_json,
&tcp_json,
&mut report,
)?;
}
assert!(
report
.exempt_paths
.iter()
.any(|path| path.ends_with("/attach_secret")),
"no attach-secret exemption fired, so the exempt path was never taken and the \
rows above may not contain the nondeterminism this test claims to handle"
);
assert!(
report
.exempt_paths
.iter()
.any(|path| path.ends_with("/receipt_expires_at")),
"no wall-clock exemption fired"
);
Ok(report)
}
fn assert_shell_event_parity(
report: &RowComparison,
pairs: &[ExemptPair],
) -> Result<(), Box<dyn Error>> {
assert!(
report.events.len() >= 4,
"only {} canonical shell events were compared; the scenario mints one per \
lifecycle operation, so a low count means the walk did not reach them",
report.events.len()
);
for (path, loopback_event, tcp_event) in &report.events {
assert_parity_after_substitution(
&format!("canonical shell event bytes at {path}"),
loopback_event,
tcp_event,
pairs,
);
let loopback_decoded = ConversationEvent::decode_canonical(loopback_event)
.map_err(|error| format!("loopback shell event did not decode: {error:?}"))?;
let tcp_decoded = ConversationEvent::decode_canonical(tcp_event)
.map_err(|error| format!("socket shell event did not decode: {error:?}"))?;
assert_eq!(
loopback_decoded.conversation_id(),
tcp_decoded.conversation_id(),
"the shell event at {path} names a different conversation on the two mounts"
);
assert_eq!(
loopback_decoded.ordinal(),
tcp_decoded.ordinal(),
"the shell event at {path} carries a different ordinal on the two mounts"
);
}
Ok(())
}
#[test]
fn a_loopback_drive_and_a_socket_drive_leave_byte_identical_records() -> Result<(), Box<dyn Error>>
{
let home = tempfile::tempdir()?;
let loopback = drive_loopback(&home.path().join("loopback"))?;
let tcp = drive_tcp(&home.path().join("socket"))?;
assert_eq!(
loopback.secrets.len(),
tcp.secrets.len(),
"the two drives minted different numbers of attach secrets"
);
assert_eq!(
loopback.secrets.len(),
3,
"the scenario mints three secrets: two enrollments and one rotation"
);
assert_eq!(
loopback.deadlines.len(),
tcp.deadlines.len(),
"the two drives stamped different numbers of deadlines"
);
assert_eq!(
loopback.deadlines.len(),
6,
"the scenario stamps a receipt and a provenance deadline on each of the three \
bound responses"
);
let mut pairs: Vec<ExemptPair> = loopback
.secrets
.iter()
.zip(tcp.secrets.iter())
.map(|(left, right)| (left.to_vec(), right.to_vec()))
.collect();
for (loopback_secret, tcp_secret) in &pairs {
assert_ne!(
loopback_secret, tcp_secret,
"the two drives minted the SAME attach secret, so its substitution would be \
a no-op and would prove nothing"
);
}
pairs.extend(
loopback
.deadlines
.iter()
.zip(tcp.deadlines.iter())
.map(|(left, right)| (left.to_be_bytes().to_vec(), right.to_be_bytes().to_vec())),
);
let report = compare_durable_rows(&loopback, &tcp)?;
assert_shell_event_parity(&report, &pairs)?;
assert_eq!(
loopback.responses.len(),
tcp.responses.len(),
"the two mounts answered with different numbers of response frames"
);
assert_eq!(
loopback.responses.len(),
8,
"the scenario expects exactly one response per request: two enrollments, \
detach, attach, two records, ack, detach"
);
for (index, (loopback_frame, tcp_frame)) in loopback
.responses
.iter()
.zip(tcp.responses.iter())
.enumerate()
{
assert_parity_after_substitution(
&format!("response frame {index}"),
loopback_frame,
tcp_frame,
&pairs,
);
}
assert_push_parity(&loopback.pushes, &tcp.pushes, &pairs);
assert_eq!(
EXEMPT_FIELDS.len(),
3,
"the exempted-field list changed without the parity assertions changing with it"
);
Ok(())
}
fn assert_push_parity(loopback: &[Vec<u8>], tcp: &[Vec<u8>], pairs: &[ExemptPair]) {
fn distinct(pushes: &[Vec<u8>]) -> Vec<Vec<u8>> {
let mut out: Vec<Vec<u8>> = Vec::new();
for push in pushes {
if !out.iter().any(|seen| seen == push) {
out.push(push.clone());
}
}
out
}
let loopback_distinct = distinct(loopback);
let tcp_distinct = distinct(tcp);
assert_eq!(
loopback_distinct.len(),
1,
"the loopback drive received {} distinct pushes; the scenario delivers exactly \
one (the peer's arrival at delivery sequence 2), so anything else means the \
two drives did not run the same scenario",
loopback_distinct.len()
);
assert_eq!(
tcp_distinct.len(),
1,
"the socket drive received {} distinct pushes; the scenario delivers exactly one",
tcp_distinct.len()
);
for (index, (loopback_push, tcp_push)) in loopback_distinct
.iter()
.zip(tcp_distinct.iter())
.enumerate()
{
assert_parity_after_substitution(
&format!("distinct push {index}"),
loopback_push,
tcp_push,
pairs,
);
}
}
#[test]
fn the_parity_substitution_does_not_absorb_an_unrelated_difference() {
let left = [1_u8, 2, 3, 4, 5];
let mut right = left;
right[3] = 0xFF;
let pairs: [ExemptPair; 1] = [([0xAA_u8; 32].to_vec(), [0xBB_u8; 32].to_vec())];
let (substituted, replacements) = substitute(&left, &pairs);
assert_eq!(replacements, 0, "no exempt value occurs in this image");
assert_ne!(
substituted,
right.to_vec(),
"substitution must leave an unrelated differing byte differing"
);
let mut exempt_left = vec![9_u8, 9];
exempt_left.extend_from_slice(&[0xAA_u8; 32]);
exempt_left.push(7);
let mut exempt_right = vec![9_u8, 9];
exempt_right.extend_from_slice(&[0xBB_u8; 32]);
exempt_right.push(7);
let (reconciled, replaced) = substitute(&exempt_left, &pairs);
assert_eq!(replaced, 1, "the exempt value occurs exactly once");
assert_eq!(
reconciled, exempt_right,
"substituting the exempt value must reconcile the two images exactly"
);
}
#[test]
fn the_exempt_attach_secret_is_thirty_two_bytes_wide() {
let secret = AttachSecret::new([0x11; 32]);
assert_eq!(
secret.into_bytes().len(),
32,
"the substitution window must match the protocol's attach-secret width"
);
}