Repository: ethereum/execution-spec-tests
Pull request #1355: chore: update hive ruleset and add osaka
Author: login: spencer-tb
Labels: name: type:chore
description: Type: Chore
name: scope:consume
description: Scope: Consume command suite
name: fork:osaka
description: Osaka hardfork
Created: 2025-03-26T12:45:23Z | Merged: 2025-03-31T15:58:35Z

## 🗒️ Description
Primarily adds Osaka to the hive ruleset file. Additional refactoring includes a blob schedule function to prevent things getting messy (a quick addition while adding Osaka).

### Before

Previously, you had to manually write out blob schedule keys like this:
```python
    "Prague": {
        "HIVE_FORK_HOMESTEAD": 0,
        ...
        "HIVE_CANCUN_TIMESTAMP": 0,
        "HIVE_CANCUN_BLOB_TARGET": 3,
        "HIVE_CANCUN_BLOB_MAX": 6,
        "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION": 3338477,
        "HIVE_PRAGUE_TIMESTAMP": 0,
        "HIVE_PRAGUE_BLOB_TARGET": 6,
        "HIVE_PRAGUE_BLOB_MAX": 9,
        "HIVE_PRAGUE_BLOB_BASE_FEE_UPDATE_FRACTION": 5007716,
    },
```

### After 

Now, you can simply write:
```python
    "Prague": {
        "HIVE_FORK_HOMESTEAD": 0,
        ...
        "HIVE_CANCUN_TIMESTAMP": 0,
        "HIVE_PRAGUE_TIMESTAMP": 0,
        **get_blob_schedule_entries(Prague),
    },
```

## 🔗 Related Issues
N/A

## ✅ Checklist
- [x] All: Set appropriate labels for the changes.
- [x] All: Considered squashing commits to improve commit history.
- [x] All: Added an entry to [CHANGELOG.md](/ethereum/execution-spec-tests/blob/main/docs/CHANGELOG.md).
- [x] All: Considered updating the online docs in the [./docs/](/ethereum/execution-spec-tests/blob/main/docs/) directory.


Diff:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 28b2b1c8be2..ccde9ec49ff 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -35,6 +35,7 @@ consume cache --help
 - 🐞 Fix the the hive command printed in test reports to reproduce tests in isolation by prefixing the `--sim.limit` flag value with `id:` ([#1333](https://github.com/ethereum/execution-spec-tests/pull/1333)).
 - 🐞 Improve index generation of [ethereum/tests](https://github.com/ethereum/tests) fixtures: Allow generation at any directory level and include `generatedTestHash` in the index file for the `fixture_hash` ([#1303](https://github.com/ethereum/execution-spec-tests/pull/1303)).
 - 🐞 Fix loading of [ethereum/tests](https://github.com/ethereum/tests) and [ethereum/legacytests](https://github.com/ethereum/legacytests) fixtures for the case of mixed `0x0` and `0x1` transaction types in multi-index (`data`, `gas`, `value`) state test fixtures ([#1330](https://github.com/ethereum/execution-spec-tests/pull/1330)).
+- ✨ Add Osaka to the hive ruleset, includes a small ruleset refactor ([#1355](https://github.com/ethereum/execution-spec-tests/pull/1355)).
 
 #### `fill`
 
diff --git a/src/pytest_plugins/consume/hive_simulators/ruleset.py b/src/pytest_plugins/consume/hive_simulators/ruleset.py
index 449241b81cf..d5920946643 100644
--- a/src/pytest_plugins/consume/hive_simulators/ruleset.py
+++ b/src/pytest_plugins/consume/hive_simulators/ruleset.py
@@ -5,6 +5,37 @@
 Remove this file afterwards.
 """
 
+from typing import Dict, List
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries: Dict = {}
+    forks_with_blobs: List[Fork] = []
+    current_fork = fork
+    while current_fork.supports_blobs():
+        forks_with_blobs.append(current_fork)
+        current_fork = current_fork.parent()  # type: ignore
+
+    for fork_to_process in forks_with_blobs:
+        prefix = fork_to_process.__name__.upper()
+        entries[f"HIVE_{prefix}_BLOB_TARGET"] = fork_to_process.target_blobs_per_block()
+        entries[f"HIVE_{prefix}_BLOB_MAX"] = fork_to_process.max_blobs_per_block()
+        entries[f"HIVE_{prefix}_BLOB_BASE_FEE_UPDATE_FRACTION"] = (
+            fork_to_process.blob_base_fee_update_fraction()
+        )
+    return entries
+
+
 ruleset = {
     "Frontier": {
         "HIVE_FORK_HOMESTEAD": 2000,
@@ -121,7 +152,6 @@
     },
     "HomesteadToEIP150At5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 5,
         "HIVE_FORK_SPURIOUS": 2000,
         "HIVE_FORK_BYZANTIUM": 2000,
@@ -145,7 +175,6 @@
     },
     "EIP158ToByzantiumAt5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 0,
         "HIVE_FORK_SPURIOUS": 0,
         "HIVE_FORK_BYZANTIUM": 5,
@@ -157,7 +186,6 @@
     },
     "ByzantiumToConstantinopleAt5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 0,
         "HIVE_FORK_SPURIOUS": 0,
         "HIVE_FORK_BYZANTIUM": 0,
@@ -169,7 +197,6 @@
     },
     "ByzantiumToConstantinopleFixAt5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 0,
         "HIVE_FORK_SPURIOUS": 0,
         "HIVE_FORK_BYZANTIUM": 0,
@@ -181,7 +208,6 @@
     },
     "ConstantinopleFixToIstanbulAt5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 0,
         "HIVE_FORK_SPURIOUS": 0,
         "HIVE_FORK_BYZANTIUM": 0,
@@ -193,7 +219,6 @@
     },
     "IstanbulToBerlinAt5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 0,
         "HIVE_FORK_SPURIOUS": 0,
         "HIVE_FORK_BYZANTIUM": 0,
@@ -205,7 +230,6 @@
     },
     "BerlinToLondonAt5": {
         "HIVE_FORK_HOMESTEAD": 0,
-        # "HIVE_FORK_DAO_BLOCK":      2000,
         "HIVE_FORK_TANGERINE": 0,
         "HIVE_FORK_SPURIOUS": 0,
         "HIVE_FORK_BYZANTIUM": 0,
@@ -306,9 +330,7 @@
         "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
         "HIVE_SHANGHAI_TIMESTAMP": 0,
         "HIVE_CANCUN_TIMESTAMP": 0,
-        "HIVE_CANCUN_BLOB_TARGET": 3,
-        "HIVE_CANCUN_BLOB_MAX": 6,
-        "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION": 3338477,
+        **get_blob_schedule_entries(Cancun),
     },
     "ShanghaiToCancunAtTime15k": {
         "HIVE_FORK_HOMESTEAD": 0,
@@ -324,9 +346,7 @@
         "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
         "HIVE_SHANGHAI_TIMESTAMP": 0,
         "HIVE_CANCUN_TIMESTAMP": 15000,
-        "HIVE_CANCUN_BLOB_TARGET": 3,
-        "HIVE_CANCUN_BLOB_MAX": 6,
-        "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION": 3338477,
+        **get_blob_schedule_entries(Cancun),
     },
     "Prague": {
         "HIVE_FORK_HOMESTEAD": 0,
@@ -342,13 +362,8 @@
         "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
         "HIVE_SHANGHAI_TIMESTAMP": 0,
         "HIVE_CANCUN_TIMESTAMP": 0,
-        "HIVE_CANCUN_BLOB_TARGET": 3,
-        "HIVE_CANCUN_BLOB_MAX": 6,
-        "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION": 3338477,
         "HIVE_PRAGUE_TIMESTAMP": 0,
-        "HIVE_PRAGUE_BLOB_TARGET": 6,
-        "HIVE_PRAGUE_BLOB_MAX": 9,
-        "HIVE_PRAGUE_BLOB_BASE_FEE_UPDATE_FRACTION": 5007716,
+        **get_blob_schedule_entries(Prague),
     },
     "CancunToPragueAtTime15k": {
         "HIVE_FORK_HOMESTEAD": 0,
@@ -364,12 +379,43 @@
         "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
         "HIVE_SHANGHAI_TIMESTAMP": 0,
         "HIVE_CANCUN_TIMESTAMP": 0,
-        "HIVE_CANCUN_BLOB_TARGET": 3,
-        "HIVE_CANCUN_BLOB_MAX": 6,
-        "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION": 3338477,
         "HIVE_PRAGUE_TIMESTAMP": 15000,
-        "HIVE_PRAGUE_BLOB_TARGET": 6,
-        "HIVE_PRAGUE_BLOB_MAX": 9,
-        "HIVE_PRAGUE_BLOB_BASE_FEE_UPDATE_FRACTION": 5007716,
+        **get_blob_schedule_entries(Prague),
+    },
+    "Osaka": {
+        "HIVE_FORK_HOMESTEAD": 0,
+        "HIVE_FORK_TANGERINE": 0,
+        "HIVE_FORK_SPURIOUS": 0,
+        "HIVE_FORK_BYZANTIUM": 0,
+        "HIVE_FORK_CONSTANTINOPLE": 0,
+        "HIVE_FORK_PETERSBURG": 0,
+        "HIVE_FORK_ISTANBUL": 0,
+        "HIVE_FORK_BERLIN": 0,
+        "HIVE_FORK_LONDON": 0,
+        "HIVE_FORK_MERGE": 0,
+        "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
+        "HIVE_SHANGHAI_TIMESTAMP": 0,
+        "HIVE_CANCUN_TIMESTAMP": 0,
+        "HIVE_PRAGUE_TIMESTAMP": 0,
+        "HIVE_OSAKA_TIMESTAMP": 0,
+        **get_blob_schedule_entries(Osaka),
+    },
+    "PragueToOsakaAtTime15k": {
+        "HIVE_FORK_HOMESTEAD": 0,
+        "HIVE_FORK_TANGERINE": 0,
+        "HIVE_FORK_SPURIOUS": 0,
+        "HIVE_FORK_BYZANTIUM": 0,
+        "HIVE_FORK_CONSTANTINOPLE": 0,
+        "HIVE_FORK_PETERSBURG": 0,
+        "HIVE_FORK_ISTANBUL": 0,
+        "HIVE_FORK_BERLIN": 0,
+        "HIVE_FORK_LONDON": 0,
+        "HIVE_FORK_MERGE": 0,
+        "HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
+        "HIVE_SHANGHAI_TIMESTAMP": 0,
+        "HIVE_CANCUN_TIMESTAMP": 0,
+        "HIVE_PRAGUE_TIMESTAMP": 0,
+        "HIVE_OSAKA_TIMESTAMP": 15000,
+        **get_blob_schedule_entries(Osaka),
     },
 }


Reviews:
user: login: marioevz
body: Great addition! Just one comment.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T22:02:28Z
user: login: spencer-tb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T09:59:39Z
user: login: spencer-tb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T12:26:53Z
user: login: marioevz
body: Thanks! Just one suggestion that could be applied.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T13:01:30Z
user: login: spencer-tb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T14:20:33Z
user: login: marioevz
body: Some more changes :)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T16:10:50Z
user: login: marioevz
body: LGTM, thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T15:58:15Z

Review comments:
diff_hunk: @@ -5,6 +5,49 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries = {}
+
+    def add_fork_entries(fork):
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: marioevz
body: Am I missing something or is `add_fork_entries` not called on the top level function?
created_at: 2025-03-26T22:02:09Z
author_association: MEMBER
diff_hunk: @@ -5,6 +5,49 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries = {}
+
+    def add_fork_entries(fork):
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: spencer-tb
body: Ahh yep! I initially allowed it to take a list of entries so this was called at the end of the entire function for each fork in the list. Will fix! Thanks :)
created_at: 2025-03-27T09:59:39Z
author_association: COLLABORATOR
diff_hunk: @@ -5,6 +5,49 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries = {}
+
+    def add_fork_entries(fork):
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: spencer-tb
body: Addressed in latest commit!
created_at: 2025-03-27T12:26:53Z
author_association: COLLABORATOR
diff_hunk: @@ -5,6 +5,45 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries: Dict = {}
+    # Only process if fork implements blobs
+    if not (
+        hasattr(fork, "blob_base_fee_update_fraction")
+        and callable(fork.blob_base_fee_update_fraction)
+    ):
+        return entries
+    # Process parent fork first (if exists AND implements blobs)
+    parent_method = getattr(fork, "parent", None)
+    if callable(parent_method):
+        parent_fork = parent_method()
+        if (
+            parent_fork is not None
+            and hasattr(parent_fork, "blob_base_fee_update_fraction")
+            and callable(parent_fork.blob_base_fee_update_fraction)
+        ):
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: marioevz
body: ```suggestion
        if fork.parent() is not None:
```
Seems to me like the two checks are duplicate because they are also done inside of the recursive call.

Also I think the parent method is available in every fork, because in the worst case it's there but returns `None`, so the callable check and the get attribute could be skipped and boiled down to the one line I suggest.
created_at: 2025-03-27T12:57:53Z
author_association: MEMBER
diff_hunk: @@ -5,6 +5,45 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries: Dict = {}
+    # Only process if fork implements blobs
+    if not (
+        hasattr(fork, "blob_base_fee_update_fraction")
+        and callable(fork.blob_base_fee_update_fraction)
+    ):
+        return entries
+    # Process parent fork first (if exists AND implements blobs)
+    parent_method = getattr(fork, "parent", None)
+    if callable(parent_method):
+        parent_fork = parent_method()
+        if (
+            parent_fork is not None
+            and hasattr(parent_fork, "blob_base_fee_update_fraction")
+            and callable(parent_fork.blob_base_fee_update_fraction)
+        ):
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: spencer-tb
body: Thanks! I combined it with the `if callable(parent_method):`
created_at: 2025-03-27T14:20:33Z
author_association: COLLABORATOR
diff_hunk: @@ -5,6 +5,42 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries: Dict = {}
+
+    # Only process if fork implements blobs
+    if not (
+        hasattr(fork, "blob_base_fee_update_fraction")
+        and callable(fork.blob_base_fee_update_fraction)
+    ):
+        return entries
+
+    # Process parent fork first (if exists AND implements blobs)
+    parent_method = getattr(fork, "parent", None)
+    if callable(parent_method) and parent_method() is not None:
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: marioevz
body: ```suggestion
    if fork.parent() is not None:
```
I think the method will always be available, so we can simply call it and check for None in the returned value.
created_at: 2025-03-27T15:30:24Z
author_association: MEMBER
diff_hunk: @@ -5,6 +5,42 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries: Dict = {}
+
+    # Only process if fork implements blobs
+    if not (
+        hasattr(fork, "blob_base_fee_update_fraction")
+        and callable(fork.blob_base_fee_update_fraction)
+    ):
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: marioevz
body: I think all forks have attribute `blob_base_fee_update_fraction` and it's always callable, but call will fail prior to `Cancun`, but we can use `supports_blobs` to check this beforehand.
```suggestion
    if not fork.supports_blobs():
```
created_at: 2025-03-27T16:09:00Z
author_association: MEMBER
diff_hunk: @@ -5,6 +5,42 @@
 Remove this file afterwards.
 """
 
+from typing import Dict
+
+from ethereum_test_forks import Cancun, Fork, Osaka, Prague
+
+
+def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
+    """
+    Generate blob schedule entries for each fork (and respective parent forks).
+
+    Adds the following entries to the ruleset for the given fork (and parent forks):
+        HIVE_{FORK}_BLOB_TARGET: target_blobs_per_block()
+        HIVE_{FORK}_BLOB_MAX: max_blobs_per_block()
+        HIVE_{FORK}_BLOB_BASE_FEE_UPDATE_FRACTION: blob_base_fee_update_fraction()
+    """
+    entries: Dict = {}
+
+    # Only process if fork implements blobs
+    if not (
+        hasattr(fork, "blob_base_fee_update_fraction")
+        and callable(fork.blob_base_fee_update_fraction)
+    ):
+        return entries
+
+    # Process parent fork first (if exists AND implements blobs)
+    parent_method = getattr(fork, "parent", None)
+    if callable(parent_method) and parent_method() is not None:
+        parent_entries = get_blob_schedule_entries(parent_method())
path: src/pytest_plugins/consume/hive_simulators/ruleset.py
user: login: marioevz
body: ```suggestion
        parent_entries = get_blob_schedule_entries(fork.parent())
```
created_at: 2025-03-27T16:10:05Z
author_association: MEMBER

PR comments:
user: login: spencer-tb
created_at: 2025-03-26T12:48:39Z
body: Feel free to try run consume for Cancun/Prague/Osaka when reviewing! ;)
author_association: COLLABORATOR
user: login: spencer-tb
created_at: 2025-03-30T10:52:57Z
body: I decided to remove the recursion as it wasn't necessary for such a simple function!
author_association: COLLABORATOR