# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-equivocating-pre-commit-evidence-as-non-transferable-across-view-changes-in-byzantine-consensus-for-multi-age.md
# Remnant Warrant:    https://corpusfetch.com/c/in-what-scenarios-must-an-instance-treat-equivocating-pre-commit-evidence-as-non-transferable-across-view-changes-in-byzantine-consensus-for-multi-age.warrant.json
# Markdown SHA-256:   6b585edc7e1942a759fc0116178175a53162605daf63ef5bcbdd68eb9c023542
# Code AST SHA-256:   bad450eede1974d10c4f545849cbb2d12bd0c1a4a4407ff89611a6b6393e3c08
# Verified Classes:   NonTransferableEvidenceError, Vote, EquivocationBundle, QuorumBuilder
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import FrozenSet, Optional


class NonTransferableEvidenceError(RuntimeError):
    """Raised when equivocation evidence from a prior view is used to
    authorize vote reuse toward a new quorum."""
    pass


@dataclass(frozen=True)
class Vote:
    view: int
    seq: int
    digest: str
    signer: str


@dataclass
class EquivocationBundle:
    accused: str
    view: int
    seq: int
    conflicting_votes: FrozenSet[Vote]
    first_hand: bool


@dataclass
class QuorumBuilder:
    current_view: int
    quorum_size: int
    tainted: set = field(default_factory=set)  # (signer, seq) pairs

    def register_evidence(self, bundle: EquivocationBundle) -> None:
        # Taint every vote referenced by the bundle, on either branch.
        for vote in bundle.conflicting_votes:
            self.tainted.add((vote.signer, vote.seq))

    def admit_vote(self, vote: Vote) -> None:
        if (vote.signer, vote.seq) in self.tainted:
            raise NonTransferableEvidenceError(
                f"vote from {vote.signer} for seq {vote.seq} is tainted by "
                f"equivocation evidence; may not count toward quorum in view "
                f"{self.current_view}"
            )
        if vote.view < self.current_view:
            raise NonTransferableEvidenceError(
                f"vote from view {vote.view} cannot be reused in view "
                f"{self.current_view} without independent re-attestation"
            )


def recover_after_view_change(
    builder: QuorumBuilder,
    bundle: Optional[EquivocationBundle],
    candidate_votes: list,
) -> list:
    if bundle is not None:
        builder.register_evidence(bundle)
    admitted = []
    for vote in candidate_votes:
        try:
            builder.admit_vote(vote)
            admitted.append(vote)
        except NonTransferableEvidenceError:
            # Drop tainted/stale votes; force re-collection for this slot.
            continue
    if len(admitted) < builder.quorum_size:
        raise NonTransferableEvidenceError(
            f"only {len(admitted)} admissible votes for seq "
            f"{candidate_votes[0].seq if candidate_votes else '?'}; "
            f"quorum of {builder.quorum_size} not reachable without "
            f"re-collection from the honest set"
        )
    return admitted
