Skip to content

Commit b2ebfd7

Browse files
dvilelafclaude
andcommitted
test: verify GS013 triggers RPC rotation in SafeTransactionExecutor
Two new tests confirm that GS013 errors call _handle_rpc_error so the next attempt uses a fresh RPC node instead of retrying against the same stale provider. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9ee4576 commit b2ebfd7

1 file changed

Lines changed: 40 additions & 0 deletions

File tree

src/tests/test_safe_executor.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,3 +1284,43 @@ def test_extract_revert_hex_matches_standalone_hex(executor):
12841284
error = ValueError("execution reverted 0x08c379a0deadbeef")
12851285
result = SafeTransactionExecutor._extract_revert_hex(error)
12861286
assert result == "0x08c379a0deadbeef"
1287+
1288+
1289+
def test_gs013_triggers_rpc_rotation(executor, mock_chain_interface, mock_safe_tx, mock_safe):
1290+
"""GS013 must call _handle_rpc_error to rotate away from the stale node.
1291+
1292+
Root cause: GS013 from Gnosis RPC is a stale-state error from the provider,
1293+
not a real contract failure. Rotating immediately lets the next attempt hit
1294+
a fresh node instead of waiting through exponential backoff (up to 63s).
1295+
"""
1296+
with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
1297+
mock_safe_tx.call.side_effect = ValueError("execution reverted: GS013")
1298+
1299+
with patch("time.sleep"):
1300+
executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
1301+
1302+
# _handle_rpc_error must have been called at least once to rotate the RPC
1303+
assert mock_chain_interface._handle_rpc_error.call_count >= 1
1304+
assert SAFE_TX_STATS["rpc_rotations"] >= 1
1305+
1306+
1307+
def test_gs013_rpc_rotation_counted_separately_from_plain_rpc_errors(
1308+
executor, mock_chain_interface, mock_safe_tx, mock_safe
1309+
):
1310+
"""GS013 rotations are tracked in rpc_rotations counter, not only on is_rpc_error."""
1311+
initial_rotations = SAFE_TX_STATS["rpc_rotations"]
1312+
with patch.object(executor, "_recreate_safe_client", return_value=mock_safe):
1313+
mock_safe_tx.call.side_effect = [
1314+
ValueError("execution reverted: GS013"),
1315+
None, # succeeds on next attempt
1316+
]
1317+
mock_safe_tx.execute.return_value = {"transactionHash": b"\x01" * 32}
1318+
mock_chain_interface.web3.eth.wait_for_transaction_receipt.return_value = (
1319+
MagicMock(status=1)
1320+
)
1321+
1322+
with patch("time.sleep"):
1323+
success, _, _ = executor.execute_with_retry("0xSafe", mock_safe_tx, ["key1"])
1324+
1325+
assert success is True
1326+
assert SAFE_TX_STATS["rpc_rotations"] > initial_rotations

0 commit comments

Comments
 (0)