Skip to content

Commit

Permalink
Update disabling strict byte check dynamics and add docs
Browse files Browse the repository at this point in the history
- Make disabling / enabling strict bytes checking a flag that can be toggled on and off.
- Update documentation based on these recent changes and fix tests.
  • Loading branch information
fselmo committed Jan 27, 2023
1 parent 184796a commit dd4c369
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 151 deletions.
2 changes: 1 addition & 1 deletion conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def w3():
@pytest.fixture(scope="module")
def w3_non_strict_abi():
w3 = Web3(EthereumTesterProvider())
w3.disable_strict_bytes_type_checking()
w3.strict_bytes_type_checking = False
return w3


Expand Down
21 changes: 8 additions & 13 deletions docs/abi_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,17 @@ 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
--------------------------
Disabling 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
There is a method on web3 that will disable strict bytes type checking.
This allows bytes values of Python strings and allows bytestrings less
than the specified byte size. To disable stricter checks, set the
``w3.strict_bytes_type_checking`` flag to ``False``. This will no longer 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
prefix. It will also render valid byte strings or hex strings that are below
the exact number of bytes specified by the ABI type. See the
:ref:`enable-strict-byte-check` section
for an example and more details.
:ref:`disable-strict-byte-check` section for an example and more details.

Types by Example
----------------
Expand Down
196 changes: 78 additions & 118 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.
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 respects to the respective 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.

- A Python string that is not prefixed with ``0x`` will throw an error.
- A bytestring whose length is not exactly the specified byte size
will raise an error.
- 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 strict bytes4 type
.. 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,69 +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_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'b\x00']
>>> array_contract.functions.setBytes2Value([b'a']).transact({'gas': 420000, 'gasPrice': Web3.to_wei(1, 'gwei')})
HexBytes('0xc5377ba25224bd763ceedc0ee455cc14fc57b23dbc6b6409f40a557a009ff5f4')
>>> array_contract.functions.getBytes2Value().call()
[b'a\x00']
>>> w3.disable_strict_bytes_type_checking()
>>> array_contract.functions.setBytes2Value([b'a']).transact()
Traceback (most recent call last):
...
ValidationError:
Could not identify the intended function with name

>>> 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
... )

>>> 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')
>>> array_contract.functions.getBytes2Value().call()
[b'a\x00']
>>> w3.disable_strict_bytes_type_checking()
>>> array_contract.functions.setBytes2Value([b'a']).transact()
Traceback (most recent call last):
...
ValidationError:
Could not identify the intended function with name
>>> 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'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'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']
>>> array_contract.functions.setBytes2Value([b'a']).transact({'gas': 420000, 'gasPrice': Web3.to_wei(1, 'gwei')})
HexBytes('0xc5377ba25224bd763ceedc0ee455cc14fc57b23dbc6b6409f40a557a009ff5f4')
>>> array_contract.functions.getBytes2Value().call()
[b'a\x00']
>>> w3.enable_strict_bytes_type_checking()

>>> # 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
35 changes: 23 additions & 12 deletions docs/web3.main.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,36 +316,47 @@ Check Encodability
>>> from web3.auto.gethdev import w3
>>> w3.is_encodable('bytes2', b'12')
True
>>> w3.is_encodable('bytes2', b'1')
True
>>> w3.is_encodable('bytes2', '0x1234')
True
>>> w3.is_encodable('bytes2', b'123')
>>> w3.is_encodable('bytes2', '1234') # not 0x-prefixed, no assumptions will be made
False
>>> w3.is_encodable('bytes2', b'1') # does not match specified bytes size
False
>>> w3.is_encodable('bytes2', b'123') # does not match specified bytes size
False
.. py:method:: w3.enable_strict_bytes_type_checking()
.. py:attribute:: w3.strict_bytes_type_checking
Enables stricter bytes type checking. For more examples see :ref:`enable-strict-byte-check`
Disable the stricter bytes type checking that is loaded by default. For more
examples, see :ref:`disable-strict-byte-check`

.. doctest::

>>> from web3.auto.gethdev import w3
>>> w3.disable_strict_bytes_type_checking()
>>> w3.is_encodable('bytes2', b'12')
True
>>> w3.is_encodable('bytes2', b'1')
False
>>> w3.enable_strict_bytes_type_checking()

>>> w3.is_encodable('bytes2', b'12')
True

>>> # not of exact size bytes2
>>> w3.is_encodable('bytes2', b'1')
False

>>> w3.strict_bytes_type_checking = False

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

>>> # re-enable it
>>> w3.strict_bytes_type_checking = True
>>> w3.is_encodable('bytes2', b'1')
False


RPC API Modules
~~~~~~~~~~~~~~~

Each ``Web3`` instance also exposes these namespaced API modules.
Each ``Web3`` instance also exposes these name-spaced API modules.


.. py:attribute:: Web3.eth
Expand Down
1 change: 1 addition & 0 deletions newsfragments/2788.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Strict bytes type checking is now default for ``web3.py``. This change also adds a boolean flag for turning this feature on and off.
2 changes: 1 addition & 1 deletion tests/core/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,6 @@ async def async_w3():
async def async_w3_non_strict_abi():
provider = AsyncEthereumTesterProvider()
w3 = Web3(provider, modules={"eth": [AsyncEth]}, middlewares=provider.middlewares)
w3.disable_strict_bytes_type_checking()
w3.strict_bytes_type_checking = False
w3.eth.default_account = await w3.eth.coinbase
return w3
2 changes: 1 addition & 1 deletion tests/core/utilities/test_abi_is_encodable.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@ def test_is_encodable(w3, value, _type, expected):
),
)
def test_is_encodable_non_strict(w3, value, _type, expected):
w3.disable_strict_bytes_type_checking()
w3.strict_bytes_type_checking = False
actual = w3.is_encodable(_type, value)
assert actual is expected
3 changes: 3 additions & 0 deletions tests/core/web3-module/test_strict_bytes_type_checking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def test_strict_bytes_type_checking_turns_on_and_off(w3):
# TODO: write this test
pass
2 changes: 1 addition & 1 deletion web3/_utils/abi.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,7 @@ def strip_abi_type(elements: Any) -> Any:
return elements


def build_default_registry() -> ABIRegistry:
def build_non_strict_registry() -> ABIRegistry:
# We make a copy here just to make sure that eth-abi's default registry is not
# affected by our custom encoder subclasses
registry = default_registry.copy()
Expand Down
Loading

0 comments on commit dd4c369

Please sign in to comment.