# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-of-a-delegation-chain-fail-its-attenuation-proof-when-the-confused-deputy-boundary-is-crossed-by-a-re-delegation-hop.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-of-a-delegation-chain-fail-its-attenuation-proof-when-the-confused-deputy-boundary-is-crossed-by-a-re-delegation-hop.warrant.json
# Markdown SHA-256:   257ee3b4db84b107fb0c338045e3e424f3bedba6d7a429bd409292b74a49a304
# Code AST SHA-256:   876eb4ae1b3f28f65f0117e9368f0e9cc65e06731f377c9b8cda513e2a494395
# Verified Classes:   AttenuationProofError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class AttenuationProofError(RuntimeError):
    """Raised when a delegation chain's terminal capability is not dominated
    by the root principal's signed authority, even though each pairwise hop
    passed a positive-set subset check."""
    pass


def validate_chain(root_authority, hops):
    """
    root_authority: signed set with .positive and .excluded (both frozensets)
    hops: list of (signed_set, signature) from immediate child to terminal
    Returns the terminal signed set if the full chain is dominated by root.
    """
    if not hops:
        raise AttenuationProofError("empty delegation chain")

    # Reconstruct signed authority from root, not from the immediate parent.
    current_positive = frozenset(root_authority.positive)
    current_excluded = frozenset(root_authority.excluded)

    for idx, (cap, sig) in enumerate(hops):
        if not verify_signature(cap, sig):
            raise AttenuationProofError(f"bad signature at hop {idx}")

        # Pairwise positive-subset check (the naive check).
        if not cap.positive <= current_positive:
            raise AttenuationProofError(
                f"hop {idx} claims positive scope outside parent"
            )

        # The check that naive validators omit: the child must not
        # re-introduce anything the parent excluded.
        leaked = cap.positive & current_excluded
        if leaked:
            raise AttenuationProofError(
                f"hop {idx} re-delegates excluded scope: {sorted(leaked)}"
            )

        # Child exclusions must be a superset of parent exclusions
        # restricted to the child's positive scope; otherwise the child
        # has silently dropped an ancestor exclusion.
        dropped = (current_excluded & cap.positive) - cap.excluded
        if dropped:
            raise AttenuationProofError(
                f"hop {idx} dropped ancestor exclusion: {sorted(dropped)}"
            )

        current_positive = frozenset(cap.positive)
        current_excluded = frozenset(cap.excluded)

    return current_positive, current_excluded
