Skip to content

Commit 9f5d31f

Browse files
authored
Merge pull request #23 from oasisprotocol/rube/19-rpc-can-fail
Rube/19 rpc can fail
2 parents 7fe3e6c + 601bd6e commit 9f5d31f

18 files changed

+1503
-165
lines changed

paymaster-relayer/.env.example

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@ ROFL_ADAPTER_ADDRESS=0xYourROFLAdapterOnSapphire
1414
# ===========================================
1515
# BASE SEPOLIA (Chain ID: 84532) -> relayer-base
1616
# ===========================================
17-
BASE_SOURCE_RPC_URL=https://base-sepolia.g.alchemy.com/v2/YOUR_KEY
17+
BASE_SOURCE_RPC_URLS=https://base-sepolia.g.alchemy.com/v2/YOUR_KEY
18+
# Add failover RPCs with commas: https://url1,https://url2,https://url3
1819
BASE_VAULT_ADDRESS=0xYourPaymasterVaultOnBase
1920

2021
# ===========================================
2122
# ETHEREUM SEPOLIA (Chain ID: 11155111) -> relayer-eth
2223
# ===========================================
23-
ETH_SOURCE_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
24+
ETH_SOURCE_RPC_URLS=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
25+
# Add failover RPCs with commas: https://url1,https://url2,https://url3
2426
ETH_VAULT_ADDRESS=0xYourPaymasterVaultOnEthereum
2527

2628
# ===========================================
2729
# ARBITRUM SEPOLIA (Chain ID: 421614) -> relayer-arb
2830
# ===========================================
29-
ARB_SOURCE_RPC_URL=https://arb-sepolia.g.alchemy.com/v2/YOUR_KEY
31+
ARB_SOURCE_RPC_URLS=https://arb-sepolia.g.alchemy.com/v2/YOUR_KEY
32+
# Add failover RPCs with commas: https://url1,https://url2,https://url3
3033
ARB_VAULT_ADDRESS=0xYourPaymasterVaultOnArbitrum
3134

3235
# ===========================================

paymaster-relayer/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ROFL deployment in one go.
4242

4343
| Variable | Required | Description |
4444
|----------|----------|-------------|
45-
| `SOURCE_RPC_URL` | Yes | Source chain RPC endpoint (HTTP) |
45+
| `SOURCE_RPC_URLS` | Yes | Source chain RPC endpoints (comma-separated for failover) |
4646
| `PAYMASTER_VAULT_ADDRESS` | Yes | PaymasterVault contract address on source chain |
4747
| `TARGET_RPC_URL` | Yes | Sapphire RPC endpoint |
4848
| `PAYMASTER_PROXY_ADDRESS` | Yes | CrossChainPaymaster proxy address on Sapphire |

paymaster-relayer/compose.local.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ x-relayer-common: &relayer-common
55
build:
66
context: .
77
dockerfile: Dockerfile
8-
image: ghcr.io/oasisprotocol/rofl-paymaster-relayer:multichain-test
8+
image: ghcr.io/oasisprotocol/rofl-paymaster-relayer:multirpc-test
99
platform: linux/amd64
1010
entrypoint: /bin/sh -c 'uv run python -m paymaster_relayer --local'
1111
restart: on-failure
@@ -31,7 +31,7 @@ services:
3131
environment:
3232
<<: *common-env
3333
CHAIN_NAME: base
34-
SOURCE_RPC_URL: ${BASE_SOURCE_RPC_URL}
34+
SOURCE_RPC_URLS: ${BASE_SOURCE_RPC_URLS}
3535
PAYMASTER_VAULT_ADDRESS: ${BASE_VAULT_ADDRESS}
3636
POLLING_INTERVAL: ${BASE_POLLING_INTERVAL:-6}
3737

@@ -43,7 +43,7 @@ services:
4343
environment:
4444
<<: *common-env
4545
CHAIN_NAME: ethereum
46-
SOURCE_RPC_URL: ${ETH_SOURCE_RPC_URL}
46+
SOURCE_RPC_URLS: ${ETH_SOURCE_RPC_URLS}
4747
PAYMASTER_VAULT_ADDRESS: ${ETH_VAULT_ADDRESS}
4848
POLLING_INTERVAL: ${ETH_POLLING_INTERVAL:-12}
4949

@@ -55,6 +55,6 @@ services:
5555
environment:
5656
<<: *common-env
5757
CHAIN_NAME: arbitrum
58-
SOURCE_RPC_URL: ${ARB_SOURCE_RPC_URL}
58+
SOURCE_RPC_URLS: ${ARB_SOURCE_RPC_URLS}
5959
PAYMASTER_VAULT_ADDRESS: ${ARB_VAULT_ADDRESS}
6060
POLLING_INTERVAL: ${ARB_POLLING_INTERVAL:-3}

paymaster-relayer/compose.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ services:
3131
environment:
3232
<<: *common-env
3333
CHAIN_NAME: base
34-
SOURCE_RPC_URL: ${BASE_SOURCE_RPC_URL}
34+
SOURCE_RPC_URLS: ${BASE_SOURCE_RPC_URLS}
3535
PAYMASTER_VAULT_ADDRESS: ${BASE_VAULT_ADDRESS}
3636
POLLING_INTERVAL: ${BASE_POLLING_INTERVAL:-6}
3737

@@ -43,7 +43,7 @@ services:
4343
environment:
4444
<<: *common-env
4545
CHAIN_NAME: ethereum
46-
SOURCE_RPC_URL: ${ETH_SOURCE_RPC_URL}
46+
SOURCE_RPC_URLS: ${ETH_SOURCE_RPC_URLS}
4747
PAYMASTER_VAULT_ADDRESS: ${ETH_VAULT_ADDRESS}
4848
POLLING_INTERVAL: ${ETH_POLLING_INTERVAL:-12}
4949

@@ -55,6 +55,6 @@ services:
5555
environment:
5656
<<: *common-env
5757
CHAIN_NAME: arbitrum
58-
SOURCE_RPC_URL: ${ARB_SOURCE_RPC_URL}
58+
SOURCE_RPC_URLS: ${ARB_SOURCE_RPC_URLS}
5959
PAYMASTER_VAULT_ADDRESS: ${ARB_VAULT_ADDRESS}
6060
POLLING_INTERVAL: ${ARB_POLLING_INTERVAL:-3}

paymaster-relayer/paymaster_relayer/__main__.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ async def main():
1919
)
2020
args = parser.parse_args()
2121

22-
logger.info(f"=== Paymaster Relayer Starting {'(LOCAL MODE)' if args.local else ''} ===")
22+
logger.info(
23+
f"=== Paymaster Relayer Starting {'(LOCAL MODE)' if args.local else ''} ==="
24+
)
2325

2426
relayer = None
2527

@@ -29,11 +31,19 @@ async def main():
2931
except ValueError as e:
3032
logger.error(f"Configuration error: {e}")
3133
logger.error("Required environment variables:")
32-
logger.error(" - SOURCE_RPC_URL: Source chain RPC endpoint (e.g., Ethereum)")
34+
logger.error(
35+
" - SOURCE_RPC_URLS: Source chain RPC endpoints (comma-separated)"
36+
)
3337
logger.error(" - TARGET_RPC_URL: Target chain RPC endpoint (e.g., Sapphire)")
34-
logger.error(" - PAYMASTER_VAULT_ADDRESS: PaymasterVault contract address (source)")
35-
logger.error(" - PAYMASTER_PROXY_ADDRESS: CrossChainPaymaster contract address (target)")
36-
logger.error(" - ROFL_ADAPTER_ADDRESS: ROFLAdapter contract address (target, HashStored)")
38+
logger.error(
39+
" - PAYMASTER_VAULT_ADDRESS: PaymasterVault contract address (source)"
40+
)
41+
logger.error(
42+
" - PAYMASTER_PROXY_ADDRESS: CrossChainPaymaster contract address (target)"
43+
)
44+
logger.error(
45+
" - ROFL_ADAPTER_ADDRESS: ROFLAdapter contract address (target, HashStored)"
46+
)
3747
if args.local:
3848
logger.error(" - PRIVATE_KEY: Private key for signing transactions")
3949
sys.exit(1)

paymaster-relayer/paymaster_relayer/config.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,35 @@
99
import os
1010
from dataclasses import dataclass, field
1111

12+
from .utils.multi_rpc_provider import sanitize_url
13+
14+
15+
def parse_rpc_urls() -> list[str]:
16+
"""
17+
Parse SOURCE_RPC_URLS env var into a list of RPC endpoint URLs.
18+
19+
Splits on commas, strips whitespace, and filters empty entries.
20+
21+
Returns:
22+
List of non-empty, trimmed URLs
23+
24+
Raises:
25+
ValueError: If SOURCE_RPC_URLS is missing or contains no valid URLs
26+
"""
27+
raw = os.environ.get("SOURCE_RPC_URLS", "")
28+
urls = [url.strip() for url in raw.split(",") if url.strip()]
29+
30+
if not urls:
31+
raise ValueError("SOURCE_RPC_URLS environment variable is missing or empty")
32+
33+
return urls
34+
1235

1336
@dataclass(frozen=True, slots=True)
1437
class SourceChainConfig:
1538
"""Configuration for the source chain (e.g., Base/Sepolia)."""
1639

17-
rpc_url: str
40+
rpc_urls: list[str]
1841
paymaster_vault_address: str
1942

2043

@@ -37,7 +60,9 @@ class MonitoringConfig:
3760
retry_count: int = 3
3861
lookback_blocks: int = 9
3962
process_batch_size: int = 10 # max events to process in one batch
40-
max_block_range: int = 10 # max blocks per get_logs request (Alchemy free tier limit)
63+
max_block_range: int = (
64+
10 # max blocks per get_logs request (Alchemy free tier limit)
65+
)
4166

4267
def __post_init__(self) -> None:
4368
"""Validate monitoring configuration."""
@@ -69,7 +94,6 @@ def __post_init__(self) -> None:
6994
f"Lookback blocks too high (max 1000), got {self.lookback_blocks}"
7095
)
7196

72-
7397
# Validate batch size
7498
if self.process_batch_size <= 0:
7599
raise ValueError(
@@ -90,6 +114,7 @@ def __post_init__(self) -> None:
90114
f"Max block range too large (max 10000), got {self.max_block_range}"
91115
)
92116

117+
93118
@dataclass(frozen=True, slots=True)
94119
class RelayerConfig:
95120
"""Main configuration class for the ROFL Relayer."""
@@ -110,13 +135,14 @@ def from_env(cls, local_mode: bool = False) -> "RelayerConfig":
110135
Raises:
111136
ValueError: If required environment variables are missing
112137
"""
113-
# Source chain configuration
114-
source_rpc_url = os.environ.get("SOURCE_RPC_URL")
115-
if not source_rpc_url:
138+
# Source chain configuration - parse comma-delimited RPC URLs
139+
try:
140+
source_rpc_urls = parse_rpc_urls()
141+
except ValueError:
116142
raise ValueError(
117-
"SOURCE_RPC_URL environment variable is required. "
118-
"Example: https://ethereum-sepolia.publicnode.com"
119-
)
143+
"SOURCE_RPC_URLS environment variable is required (comma-separated). "
144+
"Example: SOURCE_RPC_URLS=https://rpc1.example.com,https://rpc2.example.com"
145+
) from None
120146

121147
paymaster_vault_address = os.environ.get("PAYMASTER_VAULT_ADDRESS")
122148
if not paymaster_vault_address:
@@ -173,7 +199,7 @@ def from_env(cls, local_mode: bool = False) -> "RelayerConfig":
173199

174200
# Create configuration objects
175201
source_chain = SourceChainConfig(
176-
rpc_url=source_rpc_url,
202+
rpc_urls=source_rpc_urls,
177203
paymaster_vault_address=paymaster_vault_address,
178204
)
179205

@@ -197,11 +223,13 @@ def log_config(self) -> None:
197223
print(f"Mode: {'LOCAL' if self.local_mode else 'ROFL'}")
198224

199225
print("\n[Source Chain]")
200-
print(f" RPC URL: {self.source_chain.rpc_url}")
226+
print(f" RPC URLs ({len(self.source_chain.rpc_urls)} configured):")
227+
for i, url in enumerate(self.source_chain.rpc_urls, 1):
228+
print(f" [{i}] {sanitize_url(url)}")
201229
print(f" PaymasterVault: {self.source_chain.paymaster_vault_address}")
202230

203231
print("\n[Target Chain]")
204-
print(f" RPC URL: {self.target_chain.rpc_url}")
232+
print(f" RPC URL: {sanitize_url(self.target_chain.rpc_url)}")
205233
print(f" CrossChainPaymaster: {self.target_chain.paymaster_address}")
206234
print(f" ROFLAdapter: {self.target_chain.rofl_adapter_address}")
207235
print(

paymaster-relayer/paymaster_relayer/event_processor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ async def process_hash_stored(self, event: EventData) -> tuple[int, str] | None:
182182
self.stored_hashes.popitem(last=False)
183183
self.stored_hashes[block_id] = block_hash
184184

185-
logger.info(f"Hash stored - Chain {domain} Block {block_id}: {block_hash[:10]}...")
185+
logger.info(
186+
f"Hash stored - Chain {domain} Block {block_id}: {block_hash[:10]}..."
187+
)
186188

187189
matching_payments: list[PaymentEvent] = self.pending_payments.get(
188190
block_id, []
@@ -264,7 +266,9 @@ async def process_matched_payment(self, payment_event: PaymentEvent) -> bool:
264266
# This prevents race between HashStored handler and retry task
265267
if not self._remove_from_pending(payment_event):
266268
# Already removed by another task - skip to avoid duplicate submission
267-
logger.debug(f"Payment {payment_event.tx_hash[:10]}... already being processed")
269+
logger.debug(
270+
f"Payment {payment_event.tx_hash[:10]}... already being processed"
271+
)
268272
return False
269273

270274
paymaster_address = self.config.target_chain.paymaster_address

paymaster-relayer/paymaster_relayer/proof_manager.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
if TYPE_CHECKING:
2222
from .utils.contract_utility import ContractUtility
23+
from .utils.multi_rpc_provider import MultiRpcProvider
2324
from .utils.rofl_utility import ROFLUtility
2425

2526
logger = logging.getLogger(__name__)
@@ -38,19 +39,19 @@ class ProofManager:
3839

3940
def __init__(
4041
self,
41-
w3_source: Web3,
42+
source_provider: "MultiRpcProvider",
4243
contract_util: "ContractUtility",
4344
rofl_util: "ROFLUtility | None" = None,
4445
):
4546
"""
4647
Initialize the ProofManager.
4748
4849
Args:
49-
w3_source: Web3 instance for the source chain
50+
source_provider: MultiRpcProvider for source chain with failover
5051
contract_util: Utility for contract interactions
5152
rofl_util: ROFL utility for transaction submission (optional)
5253
"""
53-
self.w3_source = w3_source
54+
self.source_provider = source_provider
5455
self.contract_util = contract_util
5556
self.rofl_util = rofl_util
5657

@@ -72,7 +73,9 @@ def _get_transaction_local_index(self, payment_event: PaymentEvent) -> int:
7273
Returns:
7374
Transaction-local index (position within transaction's logs)
7475
"""
75-
receipt = self.w3_source.eth.get_transaction_receipt(HexStr(payment_event.tx_hash))
76+
receipt = self.source_provider.execute_with_failover(
77+
lambda w3: w3.eth.get_transaction_receipt(HexStr(payment_event.tx_hash))
78+
)
7679
if not receipt or "logs" not in receipt:
7780
logger.warning(f"No logs found in transaction {payment_event.tx_hash}")
7881
return 0
@@ -90,7 +93,9 @@ def _get_transaction_local_index(self, payment_event: PaymentEvent) -> int:
9093
return i
9194

9295
# If not found (shouldn't happen), default to 0
93-
logger.warning("PaymentInitiated not found in transaction logs, defaulting to index 0")
96+
logger.warning(
97+
"PaymentInitiated not found in transaction logs, defaulting to index 0"
98+
)
9499
return 0
95100

96101
async def generate_proof(self, payment_event: PaymentEvent) -> list[Any]:
@@ -109,18 +114,28 @@ async def generate_proof(self, payment_event: PaymentEvent) -> list[Any]:
109114
ValueError: If receipt or block not found, or proof generation fails
110115
"""
111116
# Calculate transaction-local log index from event content
112-
log_index = self._get_transaction_local_index(payment_event)
117+
log_index = await asyncio.to_thread(
118+
self._get_transaction_local_index, payment_event
119+
)
113120
logger.info(
114121
f"Generating proof for tx {payment_event.tx_hash}, transaction-local log index {log_index}"
115122
)
116123

117124
# 1. Fetch receipt and block
118-
receipt = self.w3_source.eth.get_transaction_receipt(HexStr(payment_event.tx_hash))
125+
receipt = await asyncio.to_thread(
126+
self.source_provider.execute_with_failover,
127+
lambda w3: w3.eth.get_transaction_receipt(HexStr(payment_event.tx_hash)),
128+
)
119129
if not receipt:
120-
raise ValueError(f"Transaction receipt not found for {payment_event.tx_hash}")
130+
raise ValueError(
131+
f"Transaction receipt not found for {payment_event.tx_hash}"
132+
)
121133

122134
block_number = receipt["blockNumber"]
123-
block = self.w3_source.eth.get_block(block_number, full_transactions=True)
135+
block = await asyncio.to_thread(
136+
self.source_provider.execute_with_failover,
137+
lambda w3: w3.eth.get_block(block_number, full_transactions=True),
138+
)
124139
if not block:
125140
raise ValueError(f"Block not found for block number {block_number}")
126141

@@ -129,7 +144,7 @@ async def generate_proof(self, payment_event: PaymentEvent) -> list[Any]:
129144
)
130145

131146
# 2. Get all receipts in block
132-
receipts = self._get_block_receipts(block_number)
147+
receipts = await asyncio.to_thread(self._get_block_receipts, block_number)
133148

134149
logger.info(f"Fetched {len(receipts)} receipts from block")
135150

@@ -166,7 +181,10 @@ async def generate_proof(self, payment_event: PaymentEvent) -> list[Any]:
166181
# 6. Encode block header
167182
encoded_block_header = BlockchainEncoder.encode_block_header(block)
168183

169-
chain_id = int(self.w3_source.eth.chain_id)
184+
chain_id = await asyncio.to_thread(
185+
self.source_provider.execute_with_failover,
186+
lambda w3: int(w3.eth.chain_id),
187+
)
170188

171189
# 7. Create proof structure for Hashi
172190
proof = [
@@ -185,7 +203,9 @@ async def generate_proof(self, payment_event: PaymentEvent) -> list[Any]:
185203
)
186204
return proof
187205

188-
async def submit_proof(self, proof: list[Any], paymaster_address: str) -> str | None:
206+
async def submit_proof(
207+
self, proof: list[Any], paymaster_address: str
208+
) -> str | None:
189209
"""
190210
Submit proof to CrossChainPaymaster contract.
191211
@@ -314,7 +334,9 @@ def _get_block_receipts(self, block_number: int) -> list[TxReceipt]:
314334
ValueError: If block receipts cannot be fetched
315335
"""
316336
try:
317-
receipts = self.w3_source.eth.get_block_receipts(block_number)
337+
receipts = self.source_provider.execute_with_failover(
338+
lambda w3: w3.eth.get_block_receipts(block_number)
339+
)
318340
except Exception as e:
319341
logger.error(f"Failed to fetch receipts for block {block_number}: {e}")
320342
raise ValueError(

0 commit comments

Comments
 (0)