Skip to content

Default strict byte checking #2786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,13 @@ def _wait_for_transaction(w3, txn_hash, timeout=120):

@pytest.fixture()
def w3():
provider = EthereumTesterProvider()
return Web3(provider)
return Web3(EthereumTesterProvider())


@pytest.fixture(scope="module")
def w3_strict_abi():
def w3_non_strict_abi():
w3 = Web3(EthereumTesterProvider())
w3.enable_strict_bytes_type_checking()
w3.strict_bytes_type_checking = False
return w3


Expand Down
46 changes: 30 additions & 16 deletions docs/abi_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,36 @@ All addresses must be supplied in one of three ways:
<https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md>`_ spec.
* A 20-byte binary address.

Strict Bytes Type Checking
--------------------------

.. note ::

In version 6, this will be the default behavior

There is a method on web3 that will enable stricter bytes type checking.
The default is to allow Python strings, and to allow bytestrings less
than the specified byte size. To enable stricter checks, use
``w3.enable_strict_bytes_type_checking()``. This method will cause the web3
instance to raise an error if a Python string is passed in without a "0x"
prefix. It will also raise an error if the byte string or hex string is not
the exact number of bytes specified by the ABI type. See the
:ref:`enable-strict-byte-check` section
for an example and more details.
Disabling Strict Bytes Type Checking
------------------------------------

There is a boolean flag on the ``Web3`` class and the ``ENS`` class that will disable
strict bytes type checking. This allows bytes values of Python strings and allows byte
strings less than the specified byte size, appropriately padding values that need
padding. To disable stricter checks, set the ``w3.strict_bytes_type_checking``
(or ``ns.strict_bytes_type_checking``) flag to ``False``. This will no longer cause
the ``Web3`` / ``ENS`` instance to raise an error if a Python string is passed in
without a "0x" prefix. It will also render valid byte strings or hex strings
that are below the exact number of bytes specified by the ABI type by padding the value
appropriately, according to the ABI type. See the :ref:`disable-strict-byte-check`
section for an example on using the flag and more details.

.. note::
If a standalone ``ENS`` instance is instantiated from a ``Web3`` instance, i.e.
``ns = ENS.from_web3(w3)``, it will inherit the value of the
``w3.strict_bytes_type_checking`` flag from the ``Web3`` instance at the time of
instantiation.

Also of note, all modules on the ``Web3`` class will inherit the value of this flag,
since all modules use the parent ``w3`` object reference under the hood. This means
that ``w3.eth.w3.strict_bytes_type_checking`` will always have the same value as
``w3.strict_bytes_type_checking``.


For more details on the ABI
specification, refer to the
`Solidity ABI Spec <https://docs.soliditylang.org/en/latest/abi-spec.html>`_.


Types by Example
----------------
Expand Down
76 changes: 74 additions & 2 deletions docs/ens_overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Create an :class:`~ens.ENS` object (named ``ns`` below) in one of three ways:
ns = ENS(provider)

# or, with a w3 instance
# Note: This inherits the w3 middlewares from the w3 instance and adds a stalecheck middleware to the middleware onion
# Note: This inherits the w3 middlewares from the w3 instance and adds a stalecheck middleware to the middleware onion.
# It also inherits the provider and codec from the w3 instance, as well as the ``strict_bytes_type_checking`` flag value.
from ens import ENS
w3 = Web3(...)
ns = ENS.from_web3(w3)
Expand All @@ -47,7 +48,7 @@ Asynchronous support is available via the ``AsyncENS`` module:
ns = AsyncENS(provider)


Note that an ``ens`` module instance is also avaliable on the ``w3`` instance.
Note that an ``ens`` module instance is also available on the ``w3`` instance.
The first time it's used, Web3.py will create the ``ens`` instance using
``ENS.from_web3(w3)`` or ``AsyncENS.from_web3(w3)`` as appropriate.

Expand All @@ -61,6 +62,77 @@ The first time it's used, Web3.py will create the ``ens`` instance using
w3.ens.address('ethereum.eth')


.. py:attribute:: ens.strict_bytes_type_checking

The ``ENS`` instance has a ``strict_bytes_type_checking`` flag that toggles the flag
with the same name on the ``Web3`` instance attached to the ``ENS`` instance.
You may disable the stricter bytes type checking that is loaded by default using
this flag. For more examples, see :ref:`disable-strict-byte-check`

If instantiating a standalone ENS instance using ``ENS.from_web3()``, the ENS
instance will inherit the value of the flag on the Web3 instance at time of
instantiation.

.. doctest::

>>> from web3 import Web3, EthereumTesterProvider
>>> from ens import ENS
>>> w3 = Web3(EthereumTesterProvider())

>>> assert w3.strict_bytes_type_checking # assert strict by default
>>> w3.is_encodable('bytes2', b'1')
False

>>> w3.strict_bytes_type_checking = False
>>> w3.is_encodable('bytes2', b'1') # zero-padded, so encoded to: b'1\x00'
True

>>> ns = ENS.from_web3(w3)
>>> # assert inherited from w3 at time of instantiation via ENS.from_web3()
>>> assert ns.strict_bytes_type_checking is False
>>> ns.w3.is_encodable('bytes2', b'1')
True

>>> # assert these are now separate instances
>>> ns.strict_bytes_type_checking = True
>>> ns.w3.is_encodable('bytes2', b'1')
False

>>> # assert w3 flag value remains
>>> assert w3.strict_bytes_type_checking is False
>>> w3.is_encodable('bytes2', b'1')
True

However, if accessing the ``ENS`` class via the ``Web3`` instance as a module
(``w3.ens``), since all modules use the same ``Web3`` object reference
under the the hood (the parent ``w3`` object), changing the
``strict_bytes_type_checking`` flag value on ``w3`` also changes the flag state
for ``w3.ens.w3`` and all modules.

.. doctest::

>>> from web3 import Web3, EthereumTesterProvider
>>> w3 = Web3(EthereumTesterProvider())

>>> assert w3.strict_bytes_type_checking # assert strict by default
>>> w3.is_encodable('bytes2', b'1')
False

>>> w3.strict_bytes_type_checking = False
>>> w3.is_encodable('bytes2', b'1') # zero-padded, so encoded to: b'1\x00'
True

>>> assert w3 == w3.ens.w3 # assert same object
>>> assert not w3.ens.w3.strict_bytes_type_checking
>>> w3.ens.w3.is_encodable('bytes2', b'1')
True

>>> # sanity check on eth module as well
>>> assert not w3.eth.w3.strict_bytes_type_checking
>>> w3.eth.w3.is_encodable('bytes2', b'1')
True


Usage
-----

Expand Down
16 changes: 16 additions & 0 deletions docs/v6_migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ project depends on Web3.py v6, then you'll probably need to make some changes.

Breaking Changes:

Strict Bytes Checking by Default
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Web3.py v6 moved to requiring strict bytes checking by default. This means that if an
ABI specifies a ``bytes4`` argument, web3.py will invalidate any entry that is not
encodable as a bytes type with length of 4. This means only 0x-prefixed hex strings with
a length of 4 and bytes types with a length of 4 will be considered valid. This removes
doubt that comes from inferring values and assuming they should be padded.

This behavior was previously available in via the ``w3.enable_strict_bytes_checking()``
method. This is now, however, a toggleable flag on the ``Web3`` instance via the
``w3.strict_bytes_type_checking`` property. As outlined above, this property is set to
``True`` by default but can be toggled on and off via the property's setter
(e.g. ``w3.strict_bytes_type_checking = False``).


Snake Case
~~~~~~~~~~

Expand Down
156 changes: 79 additions & 77 deletions docs/web3.contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -444,100 +444,76 @@ and the arguments are ambiguous.
1


.. _enable-strict-byte-check:
.. _disable-strict-byte-check:

Enabling Strict Checks for Bytes Types
--------------------------------------
Disabling Strict Checks for Bytes Types
---------------------------------------

By default, web3 is not very strict when it comes to hex and bytes values.
A bytes type will take a hex string, a bytestring, or a regular python
string that can be decoded as a hex.
Additionally, if an abi specifies a byte size, but the value that gets
passed in is less than the specified size, web3 will automatically pad the value.
For example, if an abi specifies a type of ``bytes4``, web3 will handle all of the following values:
By default, web3 is strict when it comes to hex and bytes values, as of ``v6``.
If an abi specifies a byte size, but the value that gets passed in is not the specified
size, web3 will invalidate the value. For example, if an abi specifies a type of
``bytes4``, web3 will invalidate the following values:

.. list-table:: Valid byte and hex strings for a bytes4 type
:widths: 25 75
:header-rows: 1

* - Input
- Normalizes to
* - ``''``
- ``b'\x00\x00\x00\x00'``
* - ``'0x'``
- ``b'\x00\x00\x00\x00'``
* - ``b''``
- ``b'\x00\x00\x00\x00'``
* - ``b'ab'``
- ``b'ab\x00\x00'``
* - ``'0xab'``
- ``b'\xab\x00\x00\x00'``
* - ``'1234'``
- ``b'\x124\x00\x00'``
* - ``'0x61626364'``
- ``b'abcd'``
* - ``'1234'``
- ``b'1234'``


The following values will raise an error by default:

.. list-table:: Invalid byte and hex strings for a bytes4 type
.. list-table:: Invalid byte and hex strings with strict (default) bytes4 type checking
:widths: 25 75
:header-rows: 1

* - Input
- Reason
* - ``b'abcde'``
- Bytestring with more than 4 bytes
* - ``'0x6162636423'``
- Hex string with more than 4 bytes
* - ``''``
- Needs to be prefixed with a "0x" to be interpreted as an empty hex string
* - ``2``
- Wrong type
* - ``'ah'``
- String is not valid hex
* - ``'1234'``
- Needs to either be a bytestring (b'1234') or be a hex value of the right size, prefixed with 0x (in this case: '0x31323334')
* - ``b''``
- Needs to have exactly 4 bytes
* - ``b'ab'``
- Needs to have exactly 4 bytes
* - ``'0xab'``
- Needs to have exactly 4 bytes
* - ``'0x6162636464'``
- Needs to have exactly 4 bytes

However, you may want to be stricter with acceptable values for bytes types.
For this you can use the :meth:`w3.enable_strict_bytes_type_checking()` method,
which is available on the web3 instance. A web3 instance which has had this method
invoked will enforce a stricter set of rules on which values are accepted.

- A Python string that is not prefixed with ``0x`` will throw an error.
- A bytestring whose length not exactly the specified byte size
will raise an error.
However, you may want to be less strict with acceptable values for bytes types.
This may prove useful if you trust that values coming through are what they are
meant to be with respect to the ABI. In this case, the automatic padding might be
convenient for inferred types. For this, you can set the
:meth:`w3.strict_bytes_type_checking` flag to ``False``, which is available on the
Web3 instance. A Web3 instance which has this flag set to ``False`` will have a less
strict set of rules on which values are accepted. A ``bytes`` type will allow values as
a hex string, a bytestring, or a regular Python string that can be decoded as a hex.
0x-prefixed hex strings are also not required.

.. list-table:: Valid byte and hex strings for a strict bytes4 type
- A Python string that is not prefixed with ``0x`` is valid.
- A bytestring whose length is less than the specified byte size is valid.

.. list-table:: Valid byte and hex strings for a non-strict bytes4 type
:widths: 25 75
:header-rows: 1

* - Input
- Normalizes to
* - ``''``
- ``b'\x00\x00\x00\x00'``
* - ``'0x'``
- ``b'\x00\x00\x00\x00'``
* - ``b''``
- ``b'\x00\x00\x00\x00'``
* - ``b'ab'``
- ``b'ab\x00\x00'``
* - ``'0xab'``
- ``b'\xab\x00\x00\x00'``
* - ``'1234'``
- ``b'\x124\x00\x00'``
* - ``'0x61626364'``
- ``b'abcd'``
* - ``'1234'``
- ``b'1234'``

.. list-table:: Invalid byte and hex strings with strict bytes4 type checking
:widths: 25 75
:header-rows: 1

* - Input
- Reason
* - ``''``
- Needs to be prefixed with a "0x" to be interpreted as an empty hex string
* - ``'1234'``
- Needs to either be a bytestring (b'1234') or be a hex value of the right size, prefixed with 0x (in this case: '0x31323334')
* - ``b''``
- Needs to have exactly 4 bytes
* - ``b'ab'``
- Needs to have exactly 4 bytes
* - ``'0xab'``
- Needs to have exactly 4 bytes
* - ``'0x6162636464'``
- Needs to have exactly 4 bytes


Taking the following contract code as an example:

Expand Down Expand Up @@ -636,27 +612,53 @@ Taking the following contract code as an example:

>>> ArraysContract = w3.eth.contract(abi=abi, bytecode=bytecode)

>>> tx_hash = ArraysContract.constructor([b'b']).transact()
>>> tx_hash = ArraysContract.constructor([b'bb']).transact()
>>> tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

>>> array_contract = w3.eth.contract(
... address=tx_receipt.contractAddress,
... abi=abi
... )
>>> array_contract.functions.getBytes2Value().call()
[b'bb']

>>> # set value with appropriate byte size
>>> array_contract.functions.setBytes2Value([b'aa']).transact({'gas': 420000, "maxPriorityFeePerGas": 10 ** 9, "maxFeePerGas": 10 ** 9})
HexBytes('0xcb95151142ea56dbf2753d70388aef202a7bb5a1e323d448bc19f1d2e1fe3dc9')
>>> # check value
>>> array_contract.functions.getBytes2Value().call()
[b'b\x00']
>>> array_contract.functions.setBytes2Value([b'a']).transact({'gas': 420000, 'gasPrice': Web3.to_wei(1, 'gwei')})
HexBytes('0xc5377ba25224bd763ceedc0ee455cc14fc57b23dbc6b6409f40a557a009ff5f4')
[b'aa']

>>> # trying to set value without appropriate size (bytes2) is not valid
>>> array_contract.functions.setBytes2Value([b'b']).transact()
Traceback (most recent call last):
...
web3.exceptions.Web3ValidationError:
Could not identify the intended function with name
>>> # check value is still b'aa'
>>> array_contract.functions.getBytes2Value().call()
[b'a\x00']
>>> w3.enable_strict_bytes_type_checking()
[b'aa']

>>> # disabling strict byte checking...
>>> w3.strict_bytes_type_checking = False

>>> tx_hash = ArraysContract.constructor([b'b']).transact()
>>> tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
>>> array_contract = w3.eth.contract(
... address=tx_receipt.contractAddress,
... abi=abi
... )
>>> # check value is zero-padded... i.e. b'b\x00'
>>> array_contract.functions.getBytes2Value().call()
[b'b\x00']

>>> # set the flag back to True
>>> w3.strict_bytes_type_checking = True

>>> array_contract.functions.setBytes2Value([b'a']).transact()
Traceback (most recent call last):
...
Web3ValidationError:
Could not identify the intended function with name `setBytes2Value`

web3.exceptions.Web3ValidationError:
Could not identify the intended function with name

.. _contract-functions:

Expand Down
Loading