Repository: ethereum/web3.py

Issue:
Add new predefined block identifiers
### What was wrong?

There are [new predefined block identifiers](https://github.com/ethereum/execution-apis/blob/830fcd8768ef46353c8a0e3b8df85af421026a98/src/schemas/block.yaml#L97-L98), "finalized" and "safe" that should be added to the current {"latest", "pending", "earliest"} set.


### How can it be fixed?

Search the codebase and add them in where the other predefined block numbers are. Should also add tests to make sure everything works as expected.

Discussion:
fselmo: > I'm not sure if you want feedback on this right at the moment, but it looks like there is a predefined block number test [here](https://github.com/ethereum/web3.py/blob/master/tests/core/utilities/test_is_predefined_block_number.py#L18) that could use the new predefined block names. I also dug in a little bit more, and I'm pretty sure that we do want to accept these block names to the filter method. Looks good otherwise!

Yeah it looks like event filters may or may not take `safe` and `finalized` in the `fromBlock` and `toBlock`. Since they aren't yet in the specifications, I added support for them but I updated the documentation with notes that they may or may not yield expected results.

Patch:
diff --git a/docs/filters.rst b/docs/filters.rst
index 57fef002..b5c42bb2 100644
--- a/docs/filters.rst
+++ b/docs/filters.rst
@@ -15,7 +15,7 @@ Filtering
 
         from web3.auto import w3
 
-The :meth:`web3.eth.Eth.filter` method can be used to setup filters for:
+The :meth:`web3.eth.Eth.filter` method can be used to set up filters for:
 
 * Pending Transactions: ``web3.eth.filter('pending')``
 
@@ -107,6 +107,11 @@ will return a new :class:`BlockFilter` object.
         new_block_filter = w3.eth.filter('latest')
         new_block_filter.get_new_entries()
 
+    .. note::
+
+        ``"safe"`` and ``"finalized"`` block identifiers are not yet supported for
+        ``eth_newBlockFilter``.
+
 .. py:class:: TransactionFilter(...)
 
 ``TransactionFilter`` is a subclass of :class:`Filter`.
@@ -159,7 +164,13 @@ In addition to being order-dependent, there are a few more points to recognize w
     - [A, B] "A in first position AND B in second position (and anything after)"
     - [[A, B], [A, B]] "(A OR B) in first position AND (A OR B) in second position (and anything after)"
 
-See the JSON-RPC documentation for `eth_newFilter <https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter>`_ more information on the standard filter parameters.
+See the JSON-RPC documentation for `eth_newFilter <https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_newfilter>`_ more information on the standard filter parameters.
+
+    .. note::
+
+        Though ``"latest"`` and ``"safe"`` block identifiers are not yet part of the
+        specifications for ``eth_newFilter``, they are supported by web3.py and may or
+        may not yield expected results depending on the node being accessed.
 
 Creating a log filter by either of the above methods will return a :class:`LogFilter` instance.
 
diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst
index 9950b11e..9ee8a8cf 100644
--- a/docs/web3.eth.rst
+++ b/docs/web3.eth.rst
@@ -432,8 +432,9 @@ The following methods are available on the ``web3.eth`` namespace.
 
     Returns the block specified by ``block_identifier``.  Delegates to
     ``eth_getBlockByNumber`` if ``block_identifier`` is an integer or one of
-    the predefined block parameters ``'latest', 'earliest', 'pending'``,
-    otherwise delegates to ``eth_getBlockByHash``. Throws ``BlockNotFound`` error if the block is not found.
+    the predefined block parameters ``'latest', 'earliest', 'pending',
+    'safe', 'finalized'`` - otherwise delegates to ``eth_getBlockByHash``.
+    Throws ``BlockNotFound`` error if the block is not found.
 
     If ``full_transactions`` is ``True`` then the ``'transactions'`` key will
     contain full transactions objects.  Otherwise it will be an array of
@@ -478,7 +479,9 @@ The following methods are available on the ``web3.eth`` namespace.
     ``block_identifier``.  Delegates to
     ``eth_getBlockTransactionCountByNumber`` if ``block_identifier`` is an
     integer or one of the predefined block parameters ``'latest', 'earliest',
-    'pending'``, otherwise delegates to ``eth_getBlockTransactionCountByHash``. Throws ``BlockNotFoundError`` if transactions are not found.
+    'pending', 'safe', 'finalized'``,
+    otherwise delegates to ``eth_getBlockTransactionCountByHash``.
+    Throws ``BlockNotFoundError`` if transactions are not found.
 
     .. code-block:: python
 
@@ -636,7 +639,7 @@ The following methods are available on the ``web3.eth`` namespace.
     from the block specified by ``block_identifier``.  Delegates to
     ``eth_getTransactionByBlockNumberAndIndex`` if ``block_identifier`` is an
     integer or one of the predefined block parameters ``'latest', 'earliest',
-    'pending'``, otherwise delegates to
+    'pending', 'safe', 'finalized'``, otherwise delegates to
     ``eth_getTransactionByBlockHashAndIndex``.
     If a transaction is not found at specified arguments, throws :class:`web3.exceptions.TransactionNotFound`.
 
@@ -689,7 +692,7 @@ The following methods are available on the ``web3.eth`` namespace.
     from the block specified by ``block_identifier``.  Delegates to
     ``eth_getRawTransactionByBlockNumberAndIndex`` if ``block_identifier`` is an
     integer or one of the predefined block parameters ``'latest', 'earliest',
-    'pending'``, otherwise delegates to
+    'pending', 'safe', 'finalized'``, otherwise delegates to
     ``eth_getRawTransactionByBlockHashAndIndex``.
     If a transaction is not found at specified arguments, throws :class:`web3.exceptions.TransactionNotFound`.
 
@@ -1230,11 +1233,11 @@ with the filtering API.
     dictionary with the following keys.
 
     * ``fromBlock``: ``integer/tag`` - (optional, default: "latest") Integer
-      block number, or "latest" for the last mined block or "pending",
-      "earliest" for not yet mined transactions.
+      block number, or one of predefined block identifiers
+      "latest", "pending", "earliest", "safe", or "finalized".
     * ``toBlock``: ``integer/tag`` - (optional, default: "latest") Integer
-      block number, or "latest" for the last mined block or "pending",
-      "earliest" for not yet mined transactions.
+      block number, or one of predefined block identifiers
+      "latest", "pending", "earliest", "safe", or "finalized".
     * ``address``: ``string`` or list of ``strings``, each 20 Bytes -
       (optional) Contract address or a list of addresses from which logs should
       originate.
@@ -1243,6 +1246,11 @@ with the filtering API.
       This parameter can also be a list of topic lists in which case filtering
       will match any of the provided topic arrays.
 
+    .. note::
+
+        Though ``"latest"`` and ``"safe"`` block identifiers are not yet part of the
+        specifications for ``eth_newFilter``, they are supported by web3.py and may or
+        may not yield expected results depending on the node being accessed.
 
     See :doc:`./filters` for more information about filtering.
 
@@ -1264,8 +1272,8 @@ with the filtering API.
 
     .. code-block:: python
 
-        >>> filt = web3.eth.filter()
-        >>> web3.eth.get_filter_changes(filt.filter_id)
+        >>> filter = web3.eth.filter()
+        >>> web3.eth.get_filter_changes(filter.filter_id)
         [
             {
                 'address': '0xDc3A9Db694BCdd55EBaE4A89B22aC6D12b3F0c24',
@@ -1297,8 +1305,8 @@ with the filtering API.
 
     .. code-block:: python
 
-        >>> filt = web3.eth.filter()
-        >>> web3.eth.get_filter_logs(filt.filter_id)
+        >>> filter = web3.eth.filter()
+        >>> web3.eth.get_filter_logs(filter.filter_id)
         [
             {
                 'address': '0xDc3A9Db694BCdd55EBaE4A89B22aC6D12b3F0c24',
@@ -1331,10 +1339,10 @@ with the filtering API.
 
     .. code-block:: python
 
-        >>> filt = web3.eth.filter()
-        >>> web3.eth.uninstall_filter(filt.filter_id)
+        >>> filter = web3.eth.filter()
+        >>> web3.eth.uninstall_filter(filter.filter_id)
         True
-        >>> web3.eth.uninstall_filter(filt.filter_id)
+        >>> web3.eth.uninstall_filter(filter.filter_id)
         False  # already uninstalled.
 
 .. py:method:: Eth.uninstallFilter(self, filter_id)
diff --git a/newsfragments/2652.feature.rst b/newsfragments/2652.feature.rst
new file mode 100644
index 00000000..75e1e48f
--- /dev/null
+++ b/newsfragments/2652.feature.rst
@@ -0,0 +1,1 @@
+Add new predefined block identifiers ``safe`` and ``finalized``.
diff --git a/web3/_utils/blocks.py b/web3/_utils/blocks.py
index 46ee84be..b2ae0f7e 100644
--- a/web3/_utils/blocks.py
+++ b/web3/_utils/blocks.py
@@ -24,7 +24,7 @@ def is_predefined_block_number(value: Any) -> bool:
         value_text = value
     elif is_bytes(value):
         # `value` could either be random bytes or the utf-8 encoding of
-        # one of the words in: {"latest", "pending", "earliest"}
+        # one of the words in: {"latest", "pending", "earliest", "safe", "finalized"}
         # We cannot decode the bytes as utf8, because random bytes likely won't be
         # valid. So we speculatively decode as 'latin-1', which cannot fail.
         value_text = value.decode("latin-1")
@@ -33,7 +33,7 @@ def is_predefined_block_number(value: Any) -> bool:
     else:
         raise TypeError(f"unrecognized block reference: {value!r}")
 
-    return value_text in {"latest", "pending", "earliest"}
+    return value_text in {"latest", "pending", "earliest", "safe", "finalized"}
 
 
 def is_hex_encoded_block_hash(value: Any) -> bool:
diff --git a/web3/contract.py b/web3/contract.py
index 5f99c185..bc18affb 100644
--- a/web3/contract.py
+++ b/web3/contract.py
@@ -2120,7 +2120,7 @@ def parse_block_identifier(
 ) -> BlockIdentifier:
     if isinstance(block_identifier, int):
         return parse_block_identifier_int(w3, block_identifier)
-    elif block_identifier in ["latest", "earliest", "pending"]:
+    elif block_identifier in {"latest", "earliest", "pending", "safe", "finalized"}:
         return block_identifier
     elif isinstance(block_identifier, bytes) or is_hex_encoded_block_hash(
         block_identifier
@@ -2135,7 +2135,7 @@ async def async_parse_block_identifier(
 ) -> Awaitable[BlockIdentifier]:
     if isinstance(block_identifier, int):
         return await async_parse_block_identifier_int(w3, block_identifier)
-    elif block_identifier in ["latest", "earliest", "pending"]:
+    elif block_identifier in {"latest", "earliest", "pending", "safe", "finalized"}:
         return block_identifier  # type: ignore
     elif isinstance(block_identifier, bytes) or is_hex_encoded_block_hash(
         block_identifier
diff --git a/web3/eth.py b/web3/eth.py
index cdb08fd3..041c6eec 100644
--- a/web3/eth.py
+++ b/web3/eth.py
@@ -915,7 +915,7 @@ class Eth(BaseEth):
         if isinstance(filter_params, dict):
             return [filter_params]
         elif is_string(filter_params):
-            if filter_params in ["latest", "pending"]:
+            if filter_params in {"latest", "pending"}:
                 return [filter_params]
             else:
                 raise ValueError(
diff --git a/web3/types.py b/web3/types.py
index 4bbc1b38..d3d4da1c 100644
--- a/web3/types.py
+++ b/web3/types.py
@@ -44,7 +44,7 @@ TReturn = TypeVar("TReturn")
 TParams = TypeVar("TParams")
 TValue = TypeVar("TValue")
 
-BlockParams = Literal["latest", "earliest", "pending"]
+BlockParams = Literal["latest", "earliest", "pending", "safe", "finalized"]
 BlockIdentifier = Union[BlockParams, BlockNumber, Hash32, HexStr, HexBytes, int]
 LatestBlockParam = Literal["latest"]
 


Test patch:
diff --git a/tests/core/contracts/test_contract_util_functions.py b/tests/core/contracts/test_contract_util_functions.py
index 19e28900..ce003152 100644
--- a/tests/core/contracts/test_contract_util_functions.py
+++ b/tests/core/contracts/test_contract_util_functions.py
@@ -22,6 +22,8 @@ from web3.exceptions import (
         ("latest", "latest"),
         ("earliest", "earliest"),
         ("pending", "pending"),
+        ("safe", "safe"),
+        ("finalized", "finalized"),
     ),
 )
 def test_parse_block_identifier_int_and_string(w3, block_identifier, expected_output):
@@ -83,6 +85,8 @@ def test_validate_payable(value):
         ("latest", "latest"),
         ("earliest", "earliest"),
         ("pending", "pending"),
+        ("safe", "safe"),
+        ("finalized", "finalized"),
     ),
 )
 async def test_async_parse_block_identifier_int_and_string(
diff --git a/tests/core/core/block-utils/test_select_method_for_block_identifier.py b/tests/core/core/block-utils/test_select_method_for_block_identifier.py
index 45d02325..484b9049 100644
--- a/tests/core/core/block-utils/test_select_method_for_block_identifier.py
+++ b/tests/core/core/block-utils/test_select_method_for_block_identifier.py
@@ -22,6 +22,8 @@ selector_fn = partial(
         ("latest", "test_predefined"),
         ("pending", "test_predefined"),
         ("earliest", "test_predefined"),
+        ("safe", "test_predefined"),
+        ("finalized", "test_predefined"),
         (-1, ValueError),
         (0, "test_number"),
         (1, "test_number"),
diff --git a/tests/core/utilities/test_is_predefined_block_number.py b/tests/core/utilities/test_is_predefined_block_number.py
index e266146a..9b534c11 100644
--- a/tests/core/utilities/test_is_predefined_block_number.py
+++ b/tests/core/utilities/test_is_predefined_block_number.py
@@ -10,6 +10,8 @@ from web3._utils.blocks import (
     (
         ("earliest", True),
         ("latest", True),
+        ("finalized", True),
+        ("safe", True),
         ("pending", True),
         (1, False),
         ("0x1", False),
diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py
index 7afffba7..3ab472a5 100644
--- a/web3/_utils/module_testing/eth_module.py
+++ b/web3/_utils/module_testing/eth_module.py
@@ -573,6 +573,24 @@ class AsyncEthModuleTest:
         assert block["number"] == 0
         assert block["hash"] == genesis_block["hash"]
 
+    @pytest.mark.asyncio
+    @pytest.mark.xfail(reason="Integration test suite not yet set up for PoS")
+    async def test_eth_getBlockByNumber_safe(
+        self, async_w3: "Web3", empty_block: BlockData
+    ) -> None:
+        block = await async_w3.eth.get_block("safe")  # type: ignore
+        assert block is not None
+        assert isinstance(block["number"], int)
+
+    @pytest.mark.asyncio
+    @pytest.mark.xfail(reason="Integration test suite not yet set up for PoS")
+    async def test_eth_getBlockByNumber_finalized(
+        self, async_w3: "Web3", empty_block: BlockData
+    ) -> None:
+        block = await async_w3.eth.get_block("finalized")  # type: ignore
+        assert block is not None
+        assert isinstance(block["number"], int)
+
     @pytest.mark.asyncio
     async def test_eth_getBlockByNumber_full_transactions(
         self, async_w3: "Web3", block_with_txn: BlockData
@@ -2934,6 +2952,22 @@ class EthModuleTest:
         assert block["number"] == 0
         assert block["hash"] == genesis_block["hash"]
 
+    @pytest.mark.xfail(reason="Integration test suite not yet set up for PoS")
+    def test_eth_getBlockByNumber_safe(
+        self, w3: "Web3", empty_block: BlockData
+    ) -> None:
+        block = w3.eth.get_block("safe")
+        assert block is not None
+        assert isinstance(block["number"], int)
+
+    @pytest.mark.xfail(reason="Integration test suite not yet set up for PoS")
+    def test_eth_getBlockByNumber_finalized(
+        self, w3: "Web3", empty_block: BlockData
+    ) -> None:
+        block = w3.eth.get_block("finalized")
+        assert block is not None
+        assert isinstance(block["number"], int)
+
     def test_eth_getBlockByNumber_full_transactions(
         self, w3: "Web3", block_with_txn: BlockData
     ) -> None:
diff --git a/web3/providers/eth_tester/middleware.py b/web3/providers/eth_tester/middleware.py
index 51d83a73..9db386c0 100644
--- a/web3/providers/eth_tester/middleware.py
+++ b/web3/providers/eth_tester/middleware.py
@@ -58,7 +58,7 @@ if TYPE_CHECKING:
 
 
 def is_named_block(value: Any) -> bool:
-    return value in {"latest", "earliest", "pending"}
+    return value in {"latest", "earliest", "pending", "safe", "finalized"}
 
 
 def is_hexstr(value: Any) -> bool: