From 9125330eec90c293f8186138d49baab46dce5c9d Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:10:29 +0000 Subject: [PATCH 1/3] feat: adding support for rpc api key --- .env.example | 1 + configuration/config.py | 6 +++++- configuration/types.py | 1 + observer/observer.py | 6 ++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 3dcdb80..b074577 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ IDENTITY_ADDRESS=address RPC_BASE_URL=url-without-/ext/bc/C +RPC_API_KEY="foo" NOTIFICATION_DISCORD_WEBHOOK=https://discord.com/api/webhooks/secret/secret NOTIFICATION_TELEGRAM_BOT_TOKEN=secret NOTIFICATION_TELEGRAM_CHAT_ID=secret diff --git a/configuration/config.py b/configuration/config.py index 6ccaff8..6cc409e 100644 --- a/configuration/config.py +++ b/configuration/config.py @@ -148,7 +148,10 @@ def get_config() -> Configuration: rpc_url = rpc_base_url + "/ext/bc/C/rpc" p_chain_rpc_url = rpc_base_url + "/ext/bc/P" - w = Web3(Web3.HTTPProvider(rpc_url)) + rpc_api_key = os.environ.get("RPC_API_KEY") + rpc_headers = {"x-apikey": rpc_api_key} if rpc_api_key else {} + + w = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"headers": rpc_headers})) if not w.is_connected(): raise ConfigError(f"Unable to connect to rpc with provided {rpc_url=}") @@ -176,6 +179,7 @@ def get_config() -> Configuration: fee_threshold=fee_threshold, metrics=get_metrics_config(), log_level=log_level, + rpc_api_key=rpc_api_key ) return config diff --git a/configuration/types.py b/configuration/types.py index 40a8956..da1a5ad 100644 --- a/configuration/types.py +++ b/configuration/types.py @@ -269,3 +269,4 @@ class Configuration: fee_threshold: int metrics: MetricsConfig log_level: str + rpc_api_key: str | None = None diff --git a/observer/observer.py b/observer/observer.py index 492328f..9888dc4 100644 --- a/observer/observer.py +++ b/observer/observer.py @@ -377,8 +377,9 @@ def _record_submit_metrics( async def observer_loop(config: Configuration) -> None: logging.getLogger().setLevel(config.log_level) + rpc_headers = {"x-apikey": config.rpc_api_key} if config.rpc_api_key else {} w = AsyncWeb3( - AsyncWeb3.AsyncHTTPProvider(config.rpc_url), + AsyncWeb3.AsyncHTTPProvider(config.rpc_url, request_kwargs={"headers": rpc_headers}), middleware=[ExtraDataToPOAMiddleware], ) @@ -967,8 +968,9 @@ async def observer_loop(config: Configuration) -> None: and len(node_ids) > 0 ): try: + p_chain_headers = {"x-apikey": config.rpc_api_key} if config.rpc_api_key else {} response = requests.post( - config.p_chain_rpc_url, json=payload, timeout=10 + config.p_chain_rpc_url, json=payload, headers=p_chain_headers, timeout=10 ) response.raise_for_status() result = response.json() From 3acc16188bf6b56c2aa79c6f91a49083b9049336 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:13:51 +0000 Subject: [PATCH 2/3] feat: testnet readiness and performance improvements --- .../artifacts/FlareSystemsCalculator.json | 4 +- .../FlareSystemsCalculator_stable.json | 59 ++++++++++++ configuration/artifacts/VoterRegistry.json | 55 ++++++++--- .../artifacts/VoterRegistry_stable.json | 78 +++++++++++++++ configuration/config.py | 4 +- observer/observer.py | 63 ++++++++++-- observer/reward_epoch_manager.py | 95 +++++++++++++------ observer/types.py | 9 +- observer/validation/ftso.py | 4 + observer/validation/validation.py | 5 +- observer/voting_round.py | 29 +++++- 11 files changed, 343 insertions(+), 62 deletions(-) create mode 100644 configuration/artifacts/FlareSystemsCalculator_stable.json create mode 100644 configuration/artifacts/VoterRegistry_stable.json diff --git a/configuration/artifacts/FlareSystemsCalculator.json b/configuration/artifacts/FlareSystemsCalculator.json index fab9be7..ef481c9 100644 --- a/configuration/artifacts/FlareSystemsCalculator.json +++ b/configuration/artifacts/FlareSystemsCalculator.json @@ -144,9 +144,9 @@ }, { "indexed": true, - "internalType": "uint24", + "internalType": "uint32", "name": "rewardEpochId", - "type": "uint24" + "type": "uint32" }, { "indexed": false, diff --git a/configuration/artifacts/FlareSystemsCalculator_stable.json b/configuration/artifacts/FlareSystemsCalculator_stable.json new file mode 100644 index 0000000..e37e031 --- /dev/null +++ b/configuration/artifacts/FlareSystemsCalculator_stable.json @@ -0,0 +1,59 @@ +{ + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "voter", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint24", + "name": "rewardEpochId", + "type": "uint24" + }, + { + "indexed": false, + "internalType": "address", + "name": "delegationAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "delegationFeeBIPS", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wNatWeight", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wNatCappedWeight", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes20[]", + "name": "nodeIds", + "type": "bytes20[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "nodeWeights", + "type": "uint256[]" + } + ], + "name": "VoterRegistrationInfo", + "type": "event" + } + ] +} diff --git a/configuration/artifacts/VoterRegistry.json b/configuration/artifacts/VoterRegistry.json index 3be8b52..44f4af4 100644 --- a/configuration/artifacts/VoterRegistry.json +++ b/configuration/artifacts/VoterRegistry.json @@ -200,9 +200,9 @@ }, { "indexed": true, - "internalType": "uint24", + "internalType": "uint32", "name": "rewardEpochId", - "type": "uint24" + "type": "uint32" }, { "indexed": true, @@ -223,22 +223,51 @@ "type": "address" }, { + "components": [ + { + "internalType": "bytes32", + "name": "x", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "y", + "type": "bytes32" + } + ], "indexed": false, - "internalType": "bytes32", - "name": "publicKeyPart1", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "bytes32", - "name": "publicKeyPart2", - "type": "bytes32" + "internalType": "struct PublicKey", + "name": "publicKey", + "type": "tuple" }, { "indexed": false, "internalType": "uint256", "name": "registrationWeight", "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct Signature", + "name": "signature", + "type": "tuple" } ], "name": "VoterRegistered", @@ -255,9 +284,9 @@ }, { "indexed": true, - "internalType": "uint256", + "internalType": "uint32", "name": "rewardEpochId", - "type": "uint256" + "type": "uint32" } ], "name": "VoterRemoved", diff --git a/configuration/artifacts/VoterRegistry_stable.json b/configuration/artifacts/VoterRegistry_stable.json new file mode 100644 index 0000000..3629465 --- /dev/null +++ b/configuration/artifacts/VoterRegistry_stable.json @@ -0,0 +1,78 @@ +{ + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "voter", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint24", + "name": "rewardEpochId", + "type": "uint24" + }, + { + "indexed": true, + "internalType": "address", + "name": "signingPolicyAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "submitAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "submitSignaturesAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "publicKeyPart1", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "publicKeyPart2", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "registrationWeight", + "type": "uint256" + } + ], + "name": "VoterRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "voter", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "rewardEpochId", + "type": "uint256" + } + ], + "name": "VoterRemoved", + "type": "event" + } + ] +} diff --git a/configuration/config.py b/configuration/config.py index 6cc409e..0f1cb0e 100644 --- a/configuration/config.py +++ b/configuration/config.py @@ -149,7 +149,9 @@ def get_config() -> Configuration: p_chain_rpc_url = rpc_base_url + "/ext/bc/P" rpc_api_key = os.environ.get("RPC_API_KEY") - rpc_headers = {"x-apikey": rpc_api_key} if rpc_api_key else {} + rpc_headers = {"Content-Type": "application/json"} + if rpc_api_key: + rpc_headers["x-apikey"] = rpc_api_key w = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"headers": rpc_headers})) if not w.is_connected(): diff --git a/observer/observer.py b/observer/observer.py index 9888dc4..25fd72f 100644 --- a/observer/observer.py +++ b/observer/observer.py @@ -30,6 +30,7 @@ from configuration.types import ( Configuration, + Contract, un_prefix_0x, ) from observer.contract_manager import ContractManager @@ -230,6 +231,10 @@ async def get_signing_policy_events( end_block: int, ) -> SigningPolicy: # reads logs for given blocks for the informations about the signing policy + LOGGER.info( + f"Fetching signing policy events: reward_epoch={reward_epoch.id}" + f" | blocks={start_block}..{end_block}" + ) builder = SigningPolicy.builder().for_epoch(reward_epoch) @@ -240,6 +245,24 @@ async def get_signing_policy_events( config.contracts.FlareSystemsManager, ] + # Stable contract ABIs cover Songbird/Flare where VoterRegistered/VoterRemoved + # use uint24/uint256 rewardEpochId and flat publicKeyPart1/2 fields, and + # VoterRegistrationInfo uses uint24 rewardEpochId. Coston/Coston2 have received + # an upgraded version of these contracts. Both sets of topic hashes are registered + # so a single binary works on all chains without configuration. + _stable_contracts = [ + Contract( + "VoterRegistry_stable", + config.contracts.VoterRegistry.address, + "configuration/artifacts/VoterRegistry_stable.json", + ), + Contract( + "FlareSystemsCalculator_stable", + config.contracts.FlareSystemsCalculator.address, + "configuration/artifacts/FlareSystemsCalculator_stable.json", + ), + ] + event_names = { # relay "SigningPolicyInitialized", @@ -253,7 +276,7 @@ async def get_signing_policy_events( } event_signatures = { e.signature: e - for c in contracts + for c in contracts + _stable_contracts for e in c.events.values() if e.name in event_names } @@ -283,6 +306,10 @@ async def get_signing_policy_events( } ) block_logs.extend(_relay_patch_sps) + LOGGER.info( + f"Fetched {len(block_logs)} logs for signing policy " + f"(reward_epoch={reward_epoch.id}, blocks={start_block}..{end_block})" + ) for log in block_logs: sig = log["topics"][0] @@ -377,7 +404,9 @@ def _record_submit_metrics( async def observer_loop(config: Configuration) -> None: logging.getLogger().setLevel(config.log_level) - rpc_headers = {"x-apikey": config.rpc_api_key} if config.rpc_api_key else {} + rpc_headers = {"Content-Type": "application/json"} + if config.rpc_api_key: + rpc_headers["x-apikey"] = config.rpc_api_key w = AsyncWeb3( AsyncWeb3.AsyncHTTPProvider(config.rpc_url, request_kwargs={"headers": rpc_headers}), middleware=[ExtraDataToPOAMiddleware], @@ -466,13 +495,22 @@ async def observer_loop(config: Configuration) -> None: f" | nodes={_init_node_ids}" ) await _init_entity.check_addresses(config, w) - _unclaimed_init = await RewardManager().get_unclaimed_rewards( - _init_entity, config, w - ) + try: + _unclaimed_init = await asyncio.wait_for( + RewardManager().get_unclaimed_rewards(_init_entity, config, w), + timeout=60, + ) + except asyncio.TimeoutError: + LOGGER.warning("Unclaimed rewards check timed out after 60s — skipping") + _unclaimed_init = [] for m in _unclaimed_init: log_message(config, m) else: - LOGGER.warning(f"Entity {tia} NOT found in current signing policy!") + raise KeyError( + f"Identity address {tia} is not registered as a voter in signing policy " + f"for reward epoch {reward_epoch.id} — cannot observe. " + f"Registered entities: {list(signing_policy.entity_mapper.by_identity_address.keys())}" + ) if config.metrics.enabled: metrics.start_metrics_server(config.metrics.port, config.metrics.address) @@ -503,7 +541,7 @@ async def observer_loop(config: Configuration) -> None: while True: latest_block = await w.eth.block_number if block_number == latest_block: - time.sleep(2) + await asyncio.sleep(2) continue block_number += 1 @@ -579,7 +617,7 @@ async def observer_loop(config: Configuration) -> None: while True: latest_block = await w.eth.block_number if block_number == latest_block: - time.sleep(2) + await asyncio.sleep(2) continue for block in range(block_number, latest_block): @@ -636,7 +674,14 @@ async def observer_loop(config: Configuration) -> None: minimal_conditions.reward_epoch_id = signing_policy.reward_epoch.id entity = signing_policy.entity_mapper.by_identity_address[tia] - unclaimed_rewards = await rm.get_unclaimed_rewards(entity, config, w) + try: + unclaimed_rewards = await asyncio.wait_for( + rm.get_unclaimed_rewards(entity, config, w), + timeout=60, + ) + except asyncio.TimeoutError: + LOGGER.warning("Unclaimed rewards check timed out after 60s — skipping") + unclaimed_rewards = [] for m in unclaimed_rewards: log_message(config, m) diff --git a/observer/reward_epoch_manager.py b/observer/reward_epoch_manager.py index 80b46fc..5fdb88c 100644 --- a/observer/reward_epoch_manager.py +++ b/observer/reward_epoch_manager.py @@ -1,3 +1,5 @@ +import asyncio +import logging from collections.abc import Sequence from typing import Self @@ -8,8 +10,11 @@ from configuration.types import Configuration from observer import metrics + from observer.address import AddressChecker +LOGGER = logging.getLogger(__name__) + from .message import Message, MessageLevel from .types import ( RandomAcquisitionStarted, @@ -206,6 +211,15 @@ def build(self) -> SigningPolicy: for i, voter in enumerate(self.signing_policy_initialized.voters): weight = self.signing_policy_initialized.weights[i] + if voter not in spa: + LOGGER.error( + f"Signing policy voter {voter} has no VoterRegistered event " + f"(reward_epoch={rid}, registered_spa={list(spa.keys())})" + ) + raise KeyError( + f"No VoterRegistered event found for signing policy address {voter} " + f"in reward epoch {rid}" + ) vre = vres[spa[voter]] vrie = vries[spa[voter]] @@ -257,36 +271,59 @@ async def get_unclaimed_rewards( entity.submit_address, entity.submit_signatures_address, ] - claimable_func = w.eth.contract( + reward_manager_contract = w.eth.contract( abi=config.contracts.RewardManager.abi, address=config.contracts.RewardManager.address, - ).functions["getRewardEpochIdsWithClaimableRewards"] - min_re, max_re = await claimable_func().call() + ) + min_re, max_re = await reward_manager_contract.functions[ + "getRewardEpochIdsWithClaimableRewards" + ]().call() + unclaimed_func = reward_manager_contract.functions["getUnclaimedRewardState"] + + nr_epochs = max_re - min_re + 1 + nr_tasks = len(addresses) * nr_epochs * 4 + LOGGER.info( + f"Checking unclaimed rewards: epochs={min_re}..{max_re}" + f" | addresses={len(addresses)} | total_calls={nr_tasks}" + ) + + semaphore = asyncio.Semaphore(20) + + async def _check(address: str, re: int, claim_type: int): + async with semaphore: + state = await unclaimed_func(address, re, claim_type).call() + return address, re, claim_type, state + + tasks = [ + _check(address, re, claim_type) + for address in addresses + for re in range(min_re, max_re + 1) + for claim_type in range(4) + ] + results = await asyncio.gather(*tasks, return_exceptions=True) + metrics.UNCLAIMED_REWARDS.clear() - for address in addresses: - for re in range(min_re, max_re + 1): - for claim_type in range(4): - unclaimed_func = w.eth.contract( - abi=config.contracts.RewardManager.abi, - address=config.contracts.RewardManager.address, - ).functions["getUnclaimedRewardState"] - state = await unclaimed_func(address, re, claim_type).call() - # we are looking for claims that are not initialised - # and with non-zero amount - if not state[0] and state[1] > 0: - metrics.UNCLAIMED_REWARDS.labels( - identity_address=metrics.identity_address, - address=address, - reward_epoch=str(re), - claim_type=str(claim_type), - ).set(state[1]) - messages.append( - mb.build( - MessageLevel.WARNING, - ( - f"Unclaimed rewards in reward epoch {re}," - f" claim type {claim_type}, amount {state[1]}" - ), - ) - ) + for result in results: + if isinstance(result, Exception): + LOGGER.warning(f"Error checking unclaimed rewards: {result}") + continue + address, re, claim_type, state = result + # we are looking for claims that are not initialised + # and with non-zero amount + if not state[0] and state[1] > 0: + metrics.UNCLAIMED_REWARDS.labels( + identity_address=metrics.identity_address, + address=address, + reward_epoch=str(re), + claim_type=str(claim_type), + ).set(state[1]) + messages.append( + mb.build( + MessageLevel.WARNING, + ( + f"Unclaimed rewards in reward epoch {re}," + f" claim type {claim_type}, amount {state[1]}" + ), + ) + ) return messages diff --git a/observer/types.py b/observer/types.py index 77c6ff5..1bd5a4e 100644 --- a/observer/types.py +++ b/observer/types.py @@ -79,13 +79,20 @@ class VoterRegistered: @classmethod def from_dict(cls, d: dict[str, Any]) -> Self: + pk = d.get("publicKey") + if pk is not None: + # Coston/Coston2 upgraded contract: publicKey is a struct {x, y} + public_key = pk["x"].hex() + pk["y"].hex() + else: + # Songbird/Flare stable contract: two flat fields publicKeyPart1 / publicKeyPart2 + public_key = d["publicKeyPart1"].hex() + d["publicKeyPart2"].hex() return cls( reward_epoch_id=int(d["rewardEpochId"]), voter=d["voter"], signing_policy_address=d["signingPolicyAddress"], submit_address=d["submitAddress"], submit_signatures_address=d["submitSignaturesAddress"], - public_key=d["publicKeyPart1"].hex() + d["publicKeyPart2"].hex(), + public_key=public_key, registration_weight=int(d["registrationWeight"]), ) diff --git a/observer/validation/ftso.py b/observer/validation/ftso.py index 11ae7a1..7fe5baf 100644 --- a/observer/validation/ftso.py +++ b/observer/validation/ftso.py @@ -175,6 +175,10 @@ def check_submit_2( none_indices.append(str(i)) continue + if m is None: + # No valid votes for this feed in this round; skip validation + continue + # as per https://proposals.flare.network/FIP/FIP_10.html mcb_low = m.value * 0.995 mcb_high = m.value * 1.005 diff --git a/observer/validation/validation.py b/observer/validation/validation.py index 483593c..5da0848 100644 --- a/observer/validation/validation.py +++ b/observer/validation/validation.py @@ -10,7 +10,7 @@ SubmitSignatures, ) -from configuration.config import Protocol +from configuration.config import ChainId, Protocol from configuration.types import Configuration from ..message import Message @@ -56,7 +56,8 @@ def validate_round( config: Configuration, ) -> Sequence[Message]: # TODO:(matej) move this somewhere else - round.ftso.calculate_medians(round.voting_epoch, signing_policy) + _is_testnet = config.chain_id in (ChainId.COSTON, ChainId.COSTON2) + round.ftso.calculate_medians(round.voting_epoch, signing_policy, is_testnet=_is_testnet) issues: list[Message] = [] diff --git a/observer/voting_round.py b/observer/voting_round.py index 40fca90..6e82208 100644 --- a/observer/voting_round.py +++ b/observer/voting_round.py @@ -1,3 +1,4 @@ +import logging from collections import defaultdict from typing import Self @@ -22,10 +23,11 @@ from .reward_epoch_manager import Entity, SigningPolicy from .types import AttestationRequest, ProtocolMessageRelayed +LOGGER = logging.getLogger(__name__) + @frozen class WTxData: - wrapped: TxData hash: HexBytes to_address: ChecksumAddress | None input: HexBytes @@ -54,7 +56,6 @@ def from_tx_data(cls, tx_data: TxData, block_data: BlockData) -> Self: assert "timestamp" in block_data return cls( - wrapped=tx_data, hash=tx_data["hash"], to_address=tx_data.get("to"), input=tx_data["input"], @@ -146,12 +147,13 @@ def insert_submit_signatures( class FtsoVotingRoundProtocol( VotingRoundProtocol[FtsoSubmit1, FtsoSubmit2, SubmitSignatures] ): - medians: list[FtsoMedian] = field(factory=list) + medians: list[FtsoMedian | None] = field(factory=list) def calculate_medians( self, epoch: VotingEpoch, signing_policy: SigningPolicy, + is_testnet: bool = False, ): next = epoch.next rd = next.reveal_deadline() @@ -188,8 +190,13 @@ def calculate_medians( votes_to_consider.append((entity, submit_1, submit_2)) + if not number_of_feeds: + return + nr_feed = max(number_of_feeds.items(), key=lambda x: x[1])[0] + empty_feed_indices: list[int] = [] + for i in range(nr_feed): ftso_votes = [] @@ -214,10 +221,22 @@ def calculate_medians( ftso_votes.sort(key=lambda x: x.value) median = calculate_median(ftso_votes) - assert median is not None - + if median is None: + if not is_testnet: + raise AssertionError( + f"voting_epoch={epoch.id}: feed index {i} has no valid votes " + f"— all {len(votes_to_consider)} voter(s) submitted None or " + f"commit/reveal didn't match" + ) + empty_feed_indices.append(i) self.medians.append(median) + if empty_feed_indices: + LOGGER.debug( + f"voting_epoch={epoch.id}: no valid votes for feed indices " + f"{empty_feed_indices} (out of {nr_feed} feeds)" + ) + @define class AttestationRequestMapper: From a3e1923efbd1c99b23322eaf84804a8be63789e1 Mon Sep 17 00:00:00 2001 From: Sam Clusker <9279784+samclusker@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:34:07 +0000 Subject: [PATCH 3/3] chore: update readme and comment --- README.md | 1 + observer/observer.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7d2131d..fed0141 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ RPC_BASE_URL="https://flare-api.flare.network" \ | `METRICS_PORT` | no | `8000` | Prometheus metrics server port | | `METRICS_ADDRESS` | no | `0.0.0.0` | Prometheus metrics server bind address | | `LOG_LEVEL` | no | `INFO` | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | +| `RPC_API_KEY` | no | - | Use RPC API key where header is `x-apikey` | ## Prometheus metrics diff --git a/observer/observer.py b/observer/observer.py index 25fd72f..1280b35 100644 --- a/observer/observer.py +++ b/observer/observer.py @@ -245,11 +245,8 @@ async def get_signing_policy_events( config.contracts.FlareSystemsManager, ] - # Stable contract ABIs cover Songbird/Flare where VoterRegistered/VoterRemoved - # use uint24/uint256 rewardEpochId and flat publicKeyPart1/2 fields, and - # VoterRegistrationInfo uses uint24 rewardEpochId. Coston/Coston2 have received - # an upgraded version of these contracts. Both sets of topic hashes are registered - # so a single binary works on all chains without configuration. + # Coston/Coston2 run upgraded contracts with a different event ABI than Songbird/Flare. + # Registering both sets of topic hashes lets the same binary decode events on all chains. _stable_contracts = [ Contract( "VoterRegistry_stable",