A signature whose components are each individually in range but which
recovers no public key must be rejected.
test_bad_v_r_s covers the RANGE rules (v below 27/35, r or s at or
above secp256k1n, s above the EIP-2 halfway point). This is the distinct
failure that lies inside those ranges: r is read as the x-coordinate of
the ephemeral point R, and only about half of the values in [1, n) are
x-coordinates of a curve point at all. For the other half there is no R,
hence no public key and no sender -- with no range check violated anywhere.
A client that guards recovery by range-checking alone, or that treats the
two failures as different kinds of error, reaches this case through an
unintended path.
Source code in tests/frontier/validation/test_transaction.py
| @pytest.mark.inclusion_test
@pytest.mark.valid_from("Frontier")
@pytest.mark.exception_test
@pytest.mark.eels_base_coverage
@pytest.mark.parametrize(
"tx_type",
[
pytest.param(0, id="legacy"),
pytest.param(1, id="eip2930", marks=pytest.mark.valid_from("Berlin")),
pytest.param(2, id="eip1559", marks=pytest.mark.valid_from("London")),
],
)
def test_unrecoverable_signature(
state_test: StateTestFiller,
pre: Alloc,
tx_type: int,
) -> None:
"""
A signature whose components are each individually in range but which
recovers no public key must be rejected.
`test_bad_v_r_s` covers the RANGE rules (`v` below 27/35, `r` or `s` at or
above secp256k1n, `s` above the EIP-2 halfway point). This is the distinct
failure that lies inside those ranges: `r` is read as the x-coordinate of
the ephemeral point R, and only about half of the values in [1, n) are
x-coordinates of a curve point at all. For the other half there is no R,
hence no public key and no sender -- with no range check violated anywhere.
A client that guards recovery by range-checking alone, or that treats the
two failures as different kinds of error, reaches this case through an
unintended path.
"""
to = pre.fund_eoa(0xDEADBEEE)
tx = Transaction(
sender=pre.fund_eoa(),
to=to,
error=TransactionException.INVALID_SIGNATURE_VRS,
ty=tx_type,
value=1,
# Legacy encodes the (unprotected) recovery id in v; typed
# transactions carry the parity bit directly.
v=27 if tx_type == 0 else 0,
r=UNRECOVERABLE_R,
s=1,
)
state_test(
pre=pre,
# Transaction rejected: the recipient keeps exactly its funded balance.
post={to: Account(balance=0xDEADBEEE)},
tx=tx,
)
|