Skip to content
174 changes: 158 additions & 16 deletions tests/frontier/validation/test_transaction_rlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,39 @@ def tx_fields(tx: Transaction) -> Dict[str, bytes]:
}


def signed_tx(pre: Alloc, fork: Fork) -> Transaction:
"""Build and sign the base type-0 transaction."""
def signed_tx(
pre: Alloc,
fork: Fork,
nonce: int = 0,
data: bytes = b"",
funded: bool = True,
) -> Transaction:
"""
Build and sign the base type-0 transaction.

Pass `funded=False` when the transaction is only used as an
encoding source and never sent, so that the execute mode does not
defer funding of a sender that never sends a transaction.
"""
return Transaction(
sender=pre.fund_eoa(),
sender=pre.fund_eoa() if funded else pre.fund_eoa(amount=0),
to=pre.fund_eoa(amount=0),
nonce=nonce,
gas_price=10,
gas_limit=30_000,
value=1,
data=data,
protected=fork.supports_protected_txs(),
).with_signature_and_sender()


def signed_tx_fields(pre: Alloc, fork: Fork) -> Dict[str, bytes]:
def signed_tx_fields(
pre: Alloc, fork: Fork, nonce: int = 0, data: bytes = b""
) -> Dict[str, bytes]:
"""Build a signed type-0 transaction and decompose it."""
return tx_fields(signed_tx(pre, fork))
return tx_fields(
signed_tx(pre, fork, nonce=nonce, data=data, funded=False)
)


def encode_tx(
Expand All @@ -100,7 +118,7 @@ def invalid_tx(
) -> Transaction:
"""Return a transaction whose serialization is the given raw bytes."""
tx = Transaction(
sender=pre.fund_eoa(),
sender=pre.fund_eoa(amount=0),
to=0,
gas_price=10,
gas_limit=30_000,
Expand Down Expand Up @@ -134,6 +152,16 @@ def test_valid_reencoded_transaction(
f"{LEGACY_TX_TESTS}/ttRSValue/TransactionWithRvaluePrefixed00Filler.json",
f"{LEGACY_TX_TESTS}/ttRSValue/TransactionWithSvaluePrefixed00Filler.json",
f"{LEGACY_TX_TESTS}/ttRSValue/RightVRSTestVPrefixedBy0Filler.json",
f"{LEGACY_TX_TESTS}/ttNonce/TransactionWithLeadingZerosNonceFiller.json",
f"{LEGACY_TX_TESTS}/ttNonce/TransactionWithZerosBigIntFiller.json",
f"{LEGACY_TX_TESTS}/ttGasPrice/"
"TransactionWithLeadingZerosGasPriceFiller.json",
f"{LEGACY_TX_TESTS}/ttGasLimit/"
"TransactionWithLeadingZerosGasLimitFiller.json",
f"{LEGACY_TX_TESTS}/ttValue/TransactionWithLeadingZerosValueFiller.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_gasLimit_Prefixed0000Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_rvalue_Prefixed0000Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_svalue_Prefixed0000Copier.json",
],
)
@pytest.mark.exception_test
Expand Down Expand Up @@ -167,12 +195,76 @@ def test_field_leading_zeros(
transaction_test(pre=pre, tx=invalid_tx(pre, rlp, error))


@pytest.mark.ported_from(
[
f"{LEGACY_TX_TESTS}/ttWrongRLP/RLPIncorrectByteEncoding00Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/RLPIncorrectByteEncoding01Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/RLPIncorrectByteEncoding127Copier.json",
],
)
@pytest.mark.exception_test
def test_non_canonical_single_byte(
transaction_test: TransactionTestFiller,
pre: Alloc,
fork: Fork,
) -> None:
"""
Encode the single-byte nonce payload behind a one-byte string
header (0x8101) instead of as the byte itself; the non-canonical
encoding must be rejected.
"""
fields = signed_tx_fields(pre, fork, nonce=1)
payload = fields["nonce"]
assert len(payload) == 1 and payload[0] < 0x80
rlp = encode_tx(fields, {"nonce": b"\x81" + payload})
transaction_test(
pre=pre,
tx=invalid_tx(
pre, rlp, TransactionException.RLP_LEADING_ZEROS_NONCE_SIZE
),
)


@pytest.mark.ported_from(
[
f"{LEGACY_TX_TESTS}/ttWrongRLP/"
"RLPArrayLengthWithFirstZerosCopier.json",
],
)
@pytest.mark.exception_test
def test_data_size_leading_zeros(
transaction_test: TransactionTestFiller,
pre: Alloc,
fork: Fork,
) -> None:
"""
Encode the size of the data field's long-form string header with a
leading zero byte; the non-canonical encoding must be rejected.
"""
fields = signed_tx_fields(pre, fork, data=b"\xff" * 64)
payload = fields["data"]
# The corruption assumes the long-form string header.
assert len(payload) >= 56
size = len(payload).to_bytes(2, "big")
assert size[0] == 0
corrupted = bytes([0x80 + 55 + len(size)]) + size + payload
rlp = encode_tx(fields, {"data": corrupted})
transaction_test(
pre=pre,
tx=invalid_tx(
pre, rlp, TransactionException.RLP_LEADING_ZEROS_DATA_SIZE
),
)


@pytest.mark.ported_from(
[
f"{LEGACY_TX_TESTS}/ttNonce/TransactionWithNonceOverflowFiller.json",
f"{LEGACY_TX_TESTS}/ttValue/TransactionWithHighValueOverflowFiller.json",
f"{LEGACY_TX_TESTS}/ttRSValue/TransactionWithRvalueOverflowFiller.json",
f"{LEGACY_TX_TESTS}/ttRSValue/TransactionWithSvalueOverflowFiller.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_rvalue_TooLargeCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_svalue_TooLargeCopier.json",
],
)
@pytest.mark.exception_test
Expand All @@ -194,11 +286,16 @@ def test_field_overflow(
) -> None:
"""
Encode one integer field as a 33-byte value (2**256), exceeding the
field's width (64 bits for the nonce, 256 bits for the others).
256-bit width of the field. The spec decodes the nonce as a 256-bit
scalar as well; its 64-bit bound is a validation rule (EIP-2681),
not a decoding one, and clients that store the nonce in 64 bits
reject the oversized encoding all the same.

The gas limit and gas price are unbounded scalars in the spec, so
oversized values there are not a decoding error and are rejected
only in block context.
only in block context. The signature v is also a bounded 256-bit
field, but has no field-specific decoding exception, so its
oversized encoding is not covered here.
"""
fields = signed_tx_fields(pre, fork)
corrupted = rlp_bytes(int_payload(2**256))
Expand All @@ -212,6 +309,10 @@ def test_field_overflow(
f"{LEGACY_TX_TESTS}/ttAddress/AddressLessThan20Filler.json",
f"{LEGACY_TX_TESTS}/ttAddress/AddressMoreThan20PrefixedBy0Filler.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/RLPAddressWithFirstZerosCopier.json",
f"{LEGACY_TX_TESTS}/ttAddress/AddressMoreThan20Filler.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_to_Prefixed0000Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_to_TooLargeCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_to_TooShortCopier.json",
],
)
@pytest.mark.exception_test
Expand Down Expand Up @@ -248,6 +349,11 @@ def test_to_address_size(
"RLPElementIsListWhenItShouldntBeCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/"
"RLPElementIsListWhenItShouldntBe2Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_rvalue_GivenAsListCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_svalue_GivenAsListCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_data_GivenAsListCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_gasLimit_GivenAsListCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_to_GivenAsListCopier.json",
],
)
@pytest.mark.exception_test
Expand All @@ -259,6 +365,8 @@ def test_to_address_size(
("to", TransactionException.RLP_INVALID_TO),
("value", TransactionException.RLP_INVALID_VALUE),
("data", TransactionException.RLP_INVALID_DATA),
("r", TransactionException.RLP_INVALID_SIGNATURE_R),
("s", TransactionException.RLP_INVALID_SIGNATURE_S),
],
)
def test_field_as_list(
Expand All @@ -268,7 +376,12 @@ def test_field_as_list(
field: str,
error: TransactionException,
) -> None:
"""Encode one field as an RLP list instead of a byte string."""
"""
Encode one field as an RLP list instead of a byte string.

The gas price and v fields are equally rejected but have no
field-specific decoding exception, so they are not covered.
"""
fields = signed_tx_fields(pre, fork)
corrupted = rlp_list([rlp_bytes(fields[field])])
rlp = encode_tx(fields, {field: corrupted})
Expand All @@ -282,6 +395,7 @@ def test_field_as_list(
f"{LEGACY_TX_TESTS}/ttWrongRLP/TRANSCT_HeaderGivenAsArray_0Copier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/RLPListLengthWithFirstZerosCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/aMaliciousRLPCopier.json",
f"{LEGACY_TX_TESTS}/ttWrongRLP/RLPTransactionGivenAsArrayCopier.json",
],
)
@pytest.mark.exception_test
Expand All @@ -293,7 +407,13 @@ def test_field_as_list(
("too_few_elements", TransactionException.RLP_TOO_FEW_ELEMENTS),
("too_many_elements", TransactionException.RLP_TOO_MANY_ELEMENTS),
("header_declares_more", TransactionException.RLP_ERROR_EOF),
("header_declares_less", TransactionException.RLP_ERROR_EOF),
(
"header_declares_less",
[
TransactionException.RLP_ERROR_EOF,
TransactionException.RLP_ERROR_SIZE,
],
),
("tx_as_byte_string", TransactionException.RLP_INVALID_HEADER),
(
"list_size_leading_zeros",
Expand All @@ -306,9 +426,15 @@ def test_invalid_structure(
pre: Alloc,
fork: Fork,
mutation: str,
error: TransactionException,
error: TransactionException | list[TransactionException],
) -> None:
"""Corrupt the RLP structure of the whole transaction."""
"""
Corrupt the RLP structure of the whole transaction.

A list header that declares less than the actual payload leaves
both a truncated final field and a trailing byte at the top level,
so clients report it as either an EOF or a size error.
"""
fields = signed_tx_fields(pre, fork)
items = [rlp_bytes(fields[name]) for name in fields]
payload = b"".join(items)
Expand Down Expand Up @@ -350,7 +476,22 @@ def test_invalid_structure(
[
("r", b"", TransactionException.INVALID_SIGNATURE_VRS),
("s", b"", TransactionException.INVALID_SIGNATURE_VRS),
("v", b"\x1d", TransactionException.INVALID_SIGNATURE_VRS),
(
"v",
b"",
[
TransactionException.INVALID_SIGNATURE_VRS,
TransactionException.INVALID_CHAINID,
],
),
(
"v",
b"\x1d",
[
TransactionException.INVALID_SIGNATURE_VRS,
TransactionException.INVALID_CHAINID,
],
),
(
"v",
b"\xff",
Expand All @@ -360,7 +501,7 @@ def test_invalid_structure(
],
),
],
ids=["r_zero", "s_zero", "v_29", "v_255"],
ids=["r_zero", "s_zero", "v_zero", "v_29", "v_255"],
)
def test_invalid_signature_values(
transaction_test: TransactionTestFiller,
Expand All @@ -374,8 +515,9 @@ def test_invalid_signature_values(
Replace a signature field with a well-encoded but invalid value:
zero r or s, or a v that is neither 27, 28 nor an EIP-155 value.

Before EIP-155 a v of 255 is a plain invalid v; afterwards it is
interpreted as a chain id that does not match the chain.
Before EIP-155 any v other than 27 or 28 is a plain invalid v;
afterwards clients may instead derive a chain id from the invalid
v and reject the transaction for the chain id mismatch.
"""
fields = signed_tx_fields(pre, fork)
fields[field] = payload
Expand Down
Loading