# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-coalition-quorum-equivocation-from-benign-view-change-overlap-in-bft-agent-consensus.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-coalition-quorum-equivocation-from-benign-view-change-overlap-in-bft-agent-consensus.warrant.json
# Markdown SHA-256:   3e5c5a262886ad2f02d07a6f257850b3292e4db089984164fd386b91e5a1900c
# Code AST SHA-256:   608f5a0d56c468fb0eb2a51016ef033968353226073824902e225f815d0569b5
# Verified Classes:   EquivocationAmbiguityError, Vote
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class EquivocationAmbiguityError(RuntimeError):
    """Raised when a vote set cannot be classified because the signed
    preimage does not bind the view, making same-view double-signs
    indistinguishable from benign cross-view overlap."""
    pass


@dataclass(frozen=True)
class Vote:
    signer: str
    height: int
    view: int
    digest: str
    view_bound_in_preimage: bool


def classify_pair(a: Vote, b: Vote) -> str:
    if a.signer != b.signer or a.height != b.height:
        return "unrelated"
    if not (a.view_bound_in_preimage and b.view_bound_in_preimage):
        raise EquivocationAmbiguityError(
            f"votes from {a.signer} at height {a.height} lack view binding; "
            "cannot distinguish equivocation from benign overlap"
        )
    if a.view == b.view and a.digest != b.digest:
        return "equivocation"
    if a.view != b.view:
        return "benign_view_overlap"
    return "duplicate"


def find_byzantine(votes: list[Vote]) -> list[str]:
    guilty: list[str] = []
    for i in range(len(votes)):
        for j in range(i + 1, len(votes)):
            verdict = classify_pair(votes[i], votes[j])
            if verdict == "equivocation":
                guilty.append(votes[i].signer)
    if not guilty:
        # No same-view conflict: absence of evidence, not proof of honesty.
        raise EquivocationAmbiguityError(
            "no same-view conflict found; cannot certify coalition honest "
            "without full vote coverage for every height"
        )
    return sorted(set(guilty))
