# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-byzantine-equivocation-from-benign-message-reordering-during-deterministic-multi-agent-replay.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-byzantine-equivocation-from-benign-message-reordering-during-deterministic-multi-agent-replay.warrant.json
# Markdown SHA-256:   22a9acbb8c0aa6e31b14a6944a767879ed6a7b4ce4b312ebb715572adce6e47a
# Code AST SHA-256:   f1ba7e34535b4f4bd14bd2bd68ab1d756f0fd1af4006eb265fd16c210853b786
# Verified Classes:   AttributionError, SignedMessage, ReplayResult
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Dict, List, Set, Tuple


class AttributionError(RuntimeError):
    """Raised when replay cannot support sound Byzantine attribution."""
    pass


@dataclass
class SignedMessage:
    sender: str
    slot: int
    recipient: str
    payload: str
    signature_valid: bool


@dataclass
class ReplayResult:
    messages: List[SignedMessage]
    scheduler_causally_faithful: bool
    diverged_from_recording: bool


def attribute_byzantine(replay: ReplayResult) -> Dict[str, List[int]]:
    """
    Return {sender: [slots]} for equivocating senders.
    Raises AttributionError if the replay cannot support attribution.
    """
    if not replay.scheduler_causally_faithful:
        raise AttributionError(
            "Replay scheduler is not causally faithful; divergence is "
            "uninformative and attribution must be aborted."
        )

    # Per (sender, slot, recipient) -> set of distinct payloads
    seen: Dict[Tuple[str, int, str], Set[str]] = {}
    for m in replay.messages:
        if not m.signature_valid:
            raise AttributionError(
                f"Invalid signature from {m.sender} at slot {m.slot}; "
                "multiplicity rule cannot be applied to unauthenticated payloads."
            )
        key = (m.sender, m.slot, m.recipient)
        seen.setdefault(key, set()).add(m.payload)

    faults: Dict[str, List[int]] = {}
    for (sender, slot, _recipient), payloads in seen.items():
        if len(payloads) > 1:
            faults.setdefault(sender, []).append(slot)

    if not faults and replay.diverged_from_recording:
        # Divergence without multiplicity is a scheduler artifact, not a fault.
        # Classify as benign reordering; do not attribute.
        return {}

    return faults
