diff --git a/tests/frontier/validation/test_transaction_rlp.py b/tests/frontier/validation/test_transaction_rlp.py index fb3fbbb53a..d1728686fc 100644 --- a/tests/frontier/validation/test_transaction_rlp.py +++ b/tests/frontier/validation/test_transaction_rlp.py @@ -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( @@ -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, @@ -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 @@ -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 @@ -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)) @@ -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 @@ -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 @@ -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( @@ -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}) @@ -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 @@ -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", @@ -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) @@ -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", @@ -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, @@ -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